// +---------------------------------------------------------------------- namespace ywxapp\command; use think\console\Command; use think\console\Input; use think\console\input\Option; use think\console\Output; use think\facade\App; use ywxapp\service\FrameworkService; use ywxapp\service\AddonService; /** * 一键离线升级命令(手动执行升级) * * 用于绕过在线下载,直接对本地打包产物执行升级: * - 框架核心包 / 框架整站包 → 复用 FrameworkService::apply(解压、备份、跑 upgrade.sql、写版本号) * - 插件包 → 复用 AddonService::local(解压覆盖、导入 install.sql、重导菜单) * * 用法: * # 框架核心补丁包(剥离 ywxapp/ 前缀,解压到 ywxapp/) * php think ywxapp:upgrade --file=runtime/framework/ywxapp-1.0.9.zip --ver=1.0.9 --type=framework --patch * # 框架整站包(不解前缀,解压到项目根) * php think ywxapp:upgrade --file=runtime/framework/ywxapp-1.0.9.zip --ver=1.0.9 --type=framework * # 插件升级(zip 根已是插件内容) * php think ywxapp:upgrade --file=runtime/market/forum-1.0.1.zip --type=addon * * 说明: * - 框架 --ver 必填(写入 config/ywxapp.php);插件版本从 zip 内 info.php 自动读取。 * - 命令执行前会自动备份(FrameworkService 备份到 runtime/framework/backup;AddonService 备份到 runtime/addon)。 */ class Upgrade extends Command { protected function configure() { $this->setName('ywxapp:upgrade') ->addOption('file', 'f', Option::VALUE_REQUIRED, '升级包本地路径(zip)') ->addOption('type', 't', Option::VALUE_REQUIRED, '类型: framework | addon') ->addOption('ver', 'r', Option::VALUE_OPTIONAL, '目标版本号(framework 必填;addon 自动读取)') ->addOption('patch', 'p', Option::VALUE_NONE, '框架补丁包(核心包,剥离 ywxapp/ 前缀;整站包勿加)') ->setDescription('本地离线升级:框架核心/整站包或插件包一键手动执行'); } protected function execute(Input $input, Output $output): int { $file = trim((string) $input->getOption('file')); $type = strtolower(trim((string) $input->getOption('type'))); $version = trim((string) $input->getOption('ver')); $isPatch = (bool) $input->getOption('patch'); if ($file === '' || !is_file($file)) { $output->writeln('请通过 --file 指定存在的 zip 路径'); return 1; } if (!in_array($type, ['framework', 'addon'], true)) { $output->writeln('--type 必须是 framework 或 addon'); return 1; } try { if ($type === 'framework') { if ($version === '') { $output->writeln('framework 升级需通过 --version 指定目标版本号'); return 1; } $output->writeln("开始升级框架到 {$version}(" . ($isPatch ? '补丁包' : '整站包') . ")..."); $svc = App::getInstance()->make(FrameworkService::class); $res = $svc->apply($file, $version, $isPatch ? 1 : 0); $output->writeln('框架升级完成:' . ($res['msg'] ?? 'ok') . ''); } else { $output->writeln("开始升级插件({$file})..."); // 从 zip 内 info.php 解析插件名,用于实例化 AddonService(需先确定 addonDir) $addonName = $this->readAddonNameFromZip($file); if ($addonName === '') { $output->writeln('无法从升级包内 info.php 解析插件名'); return 1; } // 构造 think\File 对象(real 路径,足够 validate/upload 使用) $fileObj = new \think\File($file); $svc = new AddonService($addonName); $res = $svc->localUpgrade($fileObj); $output->writeln('插件升级完成:' . $addonName . ' ' . ($res['from'] ?? '') . ' -> ' . ($res['to'] ?? '') . ''); } } catch (\Throwable $e) { $output->writeln('升级失败:' . $e->getMessage() . ''); return 1; } $output->writeln('建议随后清理缓存:删除 runtime/cache、runtime/temp'); return 0; } /** * 从升级包 zip 内 info.php 解析插件名 * * @param string $zipFile * @return string */ private function readAddonNameFromZip(string $zipFile): string { $zip = new \ZipArchive(); if ($zip->open($zipFile) !== true) { return ''; } try { // 寻找包内 info.php(可能位于根目录或 /info.php) $name = ''; for ($i = 0; $i < $zip->numFiles; $i++) { $entry = $zip->getNameIndex($i); if (basename($entry) === 'info.php' && substr($entry, -strlen('info.php')) === 'info.php') { $content = $zip->getFromIndex($i); if ($content !== false && preg_match("/'name'\s*=>\s*'([^']+)'/", $content, $m)) { $name = $m[1]; break; } } } return $name; } finally { $zip->close(); } } }