/ * ├── template.json # 元数据 * ├── view/<插件>/<区>/*.html # 覆盖视图(与插件 view 同构) * ├── static/ # 皮肤专属静态资源(安装时部署到 public/static/templates//) * └── preview.png # 商店预览图 * * zip 内部两段前缀: * - templates// 放 template.json / view/ / preview.png * - static/ 放皮肤静态资源 → 安装时落到 public 侧 * 客户端 TemplateInstaller::install() 按此前缀还原。 * * 用法: * php scripts/package_template.php [version | major|minor|patch] * - version 缺省时按 patch 自动递增 templates//template.json 的版本号并写回 * * 例: * php scripts/package_template.php myblog * php scripts/package_template.php myblog minor * php scripts/package_template.php myblog 1.2.0 */ declare(strict_types=1); $root = dirname(__DIR__) . DIRECTORY_SEPARATOR; // d:/wwwroot/ywxapp_dev/ $name = $argv[1] ?? ''; if ($name === '' || !preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) { fwrite(STDERR, "用法: php scripts/package_template.php [version|major|minor|patch]\n"); exit(1); } $tplDir = $root . 'templates' . DIRECTORY_SEPARATOR . $name . DIRECTORY_SEPARATOR; if (!is_dir($tplDir)) { fwrite(STDERR, "模板目录不存在: {$tplDir}\n"); exit(1); } $jsonFile = $tplDir . 'template.json'; if (!is_file($jsonFile)) { fwrite(STDERR, "缺少 template.json: {$jsonFile}\n"); exit(1); } $meta = json_decode(file_get_contents($jsonFile), true); if (!is_array($meta)) { fwrite(STDERR, "template.json 解析失败\n"); exit(1); } $curVersion = $meta['version'] ?? ''; if (!$curVersion) { fwrite(STDERR, "template.json 缺少 version\n"); exit(1); } // ===================== 确定新版本号(支持自动递增) ===================== $verArg = $argv[2] ?? ''; if ($verArg === '') { $newVersion = bumpVersion($curVersion, 'patch'); echo "[版本] 未指定版本号,按 patch 自动递增: {$curVersion} -> {$newVersion}\n"; } elseif (preg_match('/^\d+\.\d+\.\d+$/', $verArg)) { $newVersion = $verArg; } elseif (in_array($verArg, ['major', 'minor', 'patch'], true)) { $newVersion = bumpVersion($curVersion, $verArg); echo "[版本] 按 {$verArg} 递增: {$curVersion} -> {$newVersion}\n"; } else { fwrite(STDERR, "无效的版本参数: {$verArg}\n"); exit(1); } // 写回 template.json(保持元数据与产出一致) $meta['version'] = $newVersion; file_put_contents($jsonFile, json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"); echo "[版本] 已写回 templates/{$name}/template.json: {$newVersion}\n"; // ===================== 打包 ===================== $outDir = $root . 'build' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR; if (!is_dir($outDir)) { mkdir($outDir, 0755, true); } $zipFile = $outDir . $name . '-' . $newVersion . '.zip'; if (!class_exists('ZipArchive')) { fwrite(STDERR, "ZipArchive 扩展不可用\n"); exit(1); } $zip = new ZipArchive(); if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { fwrite(STDERR, "无法创建 zip: {$zipFile}\n"); exit(1); } // 模板代码(排除 static/,static 单独以 static/ 前缀打包) addDirToZip($zip, $tplDir, 'templates/' . $name . '/', ['static']); // 皮肤静态资源 → 安装时部署到 public/static/templates// $staticDir = $tplDir . 'static' . DIRECTORY_SEPARATOR; if (is_dir($staticDir)) { addDirToZip($zip, $staticDir, 'static/'); } $zip->close(); $hash = hash_file('sha256', $zipFile); $size = filesize($zipFile); $absPath = realpath($zipFile); // ===================== 测试记录 SQL(Phase 3 接入 appmall 时 type='template') ===================== $esc = static function (string $v): string { return str_replace(["\\", "'"], ["\\\\", "\\'"], $v); }; $title = $meta['title'] ?? $name; $author = $meta['author'] ?? ''; $description = $meta['description'] ?? ''; $price = 0.00; $sql = "INSERT INTO `wxapp_appmall_addon_list` " . "(`name`,`type`,`title`,`version`,`price`,`author`,`description`,`logo`,`file_path`,`file_hash`,`status`,`create_at`,`update_at`) VALUES (" . "'" . $esc($name) . "'," . "'template'," . "'" . $esc($title) . "'," . "'" . $esc($newVersion) . "'," . floatval($price) . "," . "'" . $esc($author) . "'," . "'" . $esc($description) . "'," . "''," . "'" . $esc($absPath) . "'," . "'" . $hash . "'," . "1,UNIX_TIMESTAMP(),UNIX_TIMESTAMP());"; $sqlDir = $root . 'build' . DIRECTORY_SEPARATOR . 'sql' . DIRECTORY_SEPARATOR; if (!is_dir($sqlDir)) { mkdir($sqlDir, 0755, true); } $sqlFile = $sqlDir . $name . '-' . $newVersion . '-template.sql'; file_put_contents($sqlFile, "-- 模板市场测试记录(由 scripts/package_template.php 生成,type='template')\n" . "-- zip: {$absPath}\n-- sha256: {$hash}\n" . $sql . "\n"); echo "=== 打包完成 ===\n"; echo "name : {$name}\n"; echo "title : {$title}\n"; echo "zip : {$absPath}\n"; echo "size : {$size} bytes\n"; echo "sha256: {$hash}\n"; echo "目标插件: " . implode(',', $meta['target_addons'] ?? []) . "\n\n"; echo "=== 测试记录 SQL(已写入 {$sqlFile})===\n"; echo $sql . "\n"; /** * 在 x.y.z 基础上递增指定段 */ function bumpVersion(string $ver, string $level = 'patch'): string { if (!preg_match('/^(\d+)\.(\d+)\.(\d+)$/', $ver, $m)) { return $ver; } $major = (int) $m[1]; $minor = (int) $m[2]; $patch = (int) $m[3]; switch ($level) { case 'major': $major++; $minor = 0; $patch = 0; break; case 'minor': $minor++; $patch = 0; break; case 'patch': default: $patch++; break; } return $major . '.' . $minor . '.' . $patch; } /** * 将目录递归加入 zip(结构对齐 AddonService::addDirectoryToZip) * @param string[] $exclude 需排除的子目录名(相对 $dir 的一级目录) */ function addDirToZip(ZipArchive $zip, string $dir, string $prefix, array $exclude = []): void { $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($files as $file) { $filePath = $file->getRealPath(); $rel = ltrim(str_replace(DIRECTORY_SEPARATOR, '/', substr($filePath, strlen($dir))), '/'); // 排除指定一级子目录(如 static) $top = explode('/', $rel)[0] ?? ''; if ($top !== '' && in_array($top, $exclude, true)) { continue; } if ($file->isDir()) { $zip->addEmptyDir($prefix . $rel); } else { $zip->addFile($filePath, $prefix . $rel); } } }