chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace ywxapp\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
use think\facade\Config;
|
||||
use think\facade\Log;
|
||||
use ywxapp\service\AddonService;
|
||||
use ywxapp\model\BaseModel;
|
||||
|
||||
/**
|
||||
* 插件一键修复命令("插件坏什么修什么")
|
||||
*
|
||||
* 整合以下自愈能力,使一个出问题的插件恢复到「菜单正确 + 表结构完整 + 配置齐全」的状态:
|
||||
* 1. 重载菜单:refreshMenu()(清旧 admin_power/user_rule + 重建,顺带修复菜单路由前缀)
|
||||
* 2. 数据库自愈:解析 install.sql 中该插件全部 CREATE TABLE,缺表建表、缺列补列
|
||||
* 3. 配置行补全:确保 addon_config 表存在该插件配置行(无则按默认 info 写入)
|
||||
*
|
||||
* 用法:
|
||||
* php think addon:repair <插件名> # 修复指定插件(菜单 + 表 + 配置)
|
||||
* php think addon:repair <插件名> --menu # 仅重载菜单
|
||||
* php think addon:repair <插件名> --db # 仅自愈数据库表/列
|
||||
* php think addon:repair <插件名> --config # 仅补全配置行
|
||||
* php think addon:repair all # 逐个修复全部已安装插件
|
||||
*
|
||||
* php think addon:repair appmall --menu
|
||||
*/
|
||||
class AddonRepair extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('addon:repair')
|
||||
->addArgument('addon', Argument::REQUIRED, '插件名(或 all 修复全部)')
|
||||
->addOption('menu', 'm', Option::VALUE_NONE, '仅重载菜单')
|
||||
->addOption('db', 'b', Option::VALUE_NONE, '仅自愈数据库表/列')
|
||||
->addOption('config', 'c', Option::VALUE_NONE, '仅补全配置行')
|
||||
->addOption('force', 'f', Option::VALUE_NONE, '执行破坏性修复(如重命名无前缀孤儿表)')
|
||||
->setDescription('插件一键修复:重载菜单 / 自愈表结构 / 补全配置');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$name = strtolower(trim((string) $input->getArgument('addon')));
|
||||
|
||||
// 指定了单一 --xxx 则只跑对应步骤;否则全跑
|
||||
$onlyMenu = (bool) $input->getOption('menu');
|
||||
$onlyDb = (bool) $input->getOption('db');
|
||||
$onlyConfig = (bool) $input->getOption('config');
|
||||
$all = !$onlyMenu && !$onlyDb && !$onlyConfig;
|
||||
|
||||
if ($name === 'all') {
|
||||
$names = $this->installedaddon();
|
||||
if (empty($names)) {
|
||||
$output->writeln('<comment>没有已安装的插件。</comment>');
|
||||
return 0;
|
||||
}
|
||||
$output->writeln('<info>开始逐个修复 ' . count($names) . ' 个插件...</info>');
|
||||
$failed = [];
|
||||
foreach ($names as $n) {
|
||||
$output->writeln("\n========== 修复插件: {$n} ==========");
|
||||
$code = $this->repairOne($n, $all, $onlyMenu, $onlyDb, $onlyConfig, $output);
|
||||
if ($code !== 0) {
|
||||
$failed[] = $n;
|
||||
}
|
||||
}
|
||||
$output->writeln("\n=== 修复汇总 ===");
|
||||
$output->writeln('总数: ' . count($names) . ' 失败: ' . count($failed));
|
||||
if ($failed) {
|
||||
$output->writeln('<error>失败: ' . implode(', ', $failed) . '</error>');
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->repairOne($name, $all, $onlyMenu, $onlyDb, $onlyConfig, $output);
|
||||
}
|
||||
|
||||
private function repairOne(string $name, bool $all, bool $onlyMenu, bool $onlyDb, bool $onlyConfig, Output $output): int
|
||||
{
|
||||
try {
|
||||
$svc = AddonService::instance($name);
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>初始化插件失败: ' . $e->getMessage() . '</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$ok = true;
|
||||
|
||||
if ($all || $onlyMenu) {
|
||||
try {
|
||||
$svc->refreshMenu();
|
||||
$output->writeln('<info>[菜单] 已重载(admin_power/user_rule 已重建)</info>');
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>[菜单] 重载失败: ' . $e->getMessage() . '</error>');
|
||||
$ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($all || $onlyDb) {
|
||||
$ok = $this->repairDb($svc, $name, $output) && $ok;
|
||||
}
|
||||
|
||||
if ($all || $onlyConfig) {
|
||||
$ok = $this->repairConfig($svc, $name, $output) && $ok;
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
$output->writeln("<info>[完成] 插件 {$name} 修复成功</info>");
|
||||
} else {
|
||||
$output->writeln("<error>[完成] 插件 {$name} 修复存在失败项</error>");
|
||||
}
|
||||
return $ok ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库自愈:解析 install.sql 的 CREATE TABLE,缺表建表,并尽量补列。
|
||||
*/
|
||||
private function repairDb(AddonService $svc, string $name, Output $output): bool
|
||||
{
|
||||
$addonDir = $this->addonDir($name);
|
||||
$installSql = $addonDir . 'install.sql';
|
||||
if (!is_file($installSql)) {
|
||||
$output->writeln('<comment>[数据库] 无 install.sql,跳过</comment>');
|
||||
return true;
|
||||
}
|
||||
|
||||
$content = file_get_contents($installSql);
|
||||
if ($content === false || $content === '') {
|
||||
$output->writeln('<comment>[数据库] install.sql 为空,跳过</comment>');
|
||||
return true;
|
||||
}
|
||||
|
||||
// 必须从 Config 取前缀(与 AddonService::importsql 一致),CLI 下 Db::getConfig 可能为空
|
||||
$default = Config::get('database.default');
|
||||
$prefix = Config::get("database.connections.{$default}.prefix");
|
||||
if ($prefix === '' || $prefix === null) {
|
||||
// 兜底:框架默认前缀,避免建出无前缀的孤儿表
|
||||
$prefix = 'wxapp_';
|
||||
$output->writeln('<comment>[数据库] 未检测到库前缀,按默认 wxapp_ 处理</comment>');
|
||||
}
|
||||
// 还原框架在 importsql 时做的 __PREFIX__ 替换,得到运行时完整建表语句
|
||||
$content = str_ireplace('__PREFIX__', $prefix, $content);
|
||||
// 去掉注释行,避免误执行
|
||||
$content = preg_replace('/--.*$/m', '', $content);
|
||||
|
||||
// 按语句拆分(以分号结尾)
|
||||
$statements = array_filter(array_map('trim', explode(';', $content)));
|
||||
|
||||
$created = 0;
|
||||
$exists = 0;
|
||||
$failed = 0;
|
||||
foreach ($statements as $stmt) {
|
||||
if (!preg_match('/^\s*CREATE\s+TABLE/i', $stmt)) {
|
||||
continue;
|
||||
}
|
||||
// 提取表名(已带前缀)
|
||||
if (!preg_match('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"]?([a-zA-Z0-9_]+)[`"]?/i', $stmt, $m)) {
|
||||
continue;
|
||||
}
|
||||
$table = $m[1];
|
||||
try {
|
||||
if (BaseModel::tableExists($table)) {
|
||||
$exists++;
|
||||
// 表存在也尝试补列(基于 install.sql 的后续 ALTER/列定义无法可靠解析,
|
||||
// 这里只保证表存在;列级自愈由业务层 ensureColumn 负责)
|
||||
continue;
|
||||
}
|
||||
Db::execute($stmt);
|
||||
$created++;
|
||||
$output->writeln("<info>[数据库] 已建表: {$table}</info>");
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
$output->writeln('<error>[数据库] 建表失败 ' . $table . ': ' . $e->getMessage() . '</error>');
|
||||
Log::error("[AddonRepair] create table {$table} failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln("[数据库] 已建表 {$created} / 已存在 {$exists} / 失败 {$failed}");
|
||||
|
||||
// 孤儿无前缀表检测与修复:
|
||||
// 某些插件经 importsql 在前缀未就绪时被建为无前缀表(如 appmarket_addon_submissions),
|
||||
// 而运行时 Db::name() 拼出的表名带前缀,导致业务读不到数据。这里扫描并提示/修复。
|
||||
$orphans = $this->detectOrphanTables($name, $prefix);
|
||||
if (!empty($orphans)) {
|
||||
$force = (bool) $input->getOption('force');
|
||||
foreach ($orphans as $plain => $prefixed) {
|
||||
if ($force) {
|
||||
try {
|
||||
if (!BaseModel::tableExists($prefixed)) {
|
||||
Db::execute("RENAME TABLE `{$plain}` TO `{$prefixed}`");
|
||||
$output->writeln("<info>[数据库] 已修复孤儿表: {$plain} -> {$prefixed}</info>");
|
||||
} else {
|
||||
$output->writeln("<comment>[数据库] 已存在带前缀表,跳过重命名: {$prefixed}(无前缀表 {$plain} 需手动清理)</comment>");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>[数据库] 重命名失败 ' . $plain . ': ' . $e->getMessage() . '</error>');
|
||||
$failed++;
|
||||
}
|
||||
} else {
|
||||
$output->writeln("<error>[数据库] 发现无前缀孤儿表: {$plain}(应命名为 {$prefixed},加 --force 自动重命名修复)</error>");
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $failed === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描本插件是否存在「无前缀的孤儿表」:install.sql 里 __PREFIX__xxx 对应的运行时表名带前缀,
|
||||
* 但库中却存在无前缀同名表(导入时前缀为空导致)。返回 [无前缀表 => 正确带前缀表] 映射。
|
||||
*/
|
||||
private function detectOrphanTables(string $name, string $prefix): array
|
||||
{
|
||||
$map = [];
|
||||
$base = defined('ADDON_PATH') ? ADDON_PATH : (root_path() . 'addon' . DIRECTORY_SEPARATOR);
|
||||
$installSql = $base . $name . DIRECTORY_SEPARATOR . 'install.sql';
|
||||
if (!is_file($installSql)) {
|
||||
return $map;
|
||||
}
|
||||
$content = preg_replace('/--.*$/m', '', (string) file_get_contents($installSql));
|
||||
if (!preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"]?__PREFIX__([a-zA-Z0-9_]+)[`"]?/i', $content, $matches)) {
|
||||
return $map;
|
||||
}
|
||||
foreach (array_unique($matches[1]) as $suffix) {
|
||||
$plain = $name . '_' . ltrim($suffix, '_');
|
||||
$prefixed = $prefix . $plain;
|
||||
if (BaseModel::tableExists($plain) && !BaseModel::tableExists($prefixed)) {
|
||||
$map[$plain] = $prefixed;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置行补全:确保 addon_config 表有该插件配置行。
|
||||
*/
|
||||
private function repairConfig(AddonService $svc, string $name, Output $output): bool
|
||||
{
|
||||
$default = Config::get('database.default');
|
||||
$prefix = Config::get("database.connections.{$default}.prefix") ?: '';
|
||||
$configTable = $prefix . 'addon_config';
|
||||
try {
|
||||
if (!BaseModel::tableExists($configTable)) {
|
||||
$output->writeln('<comment>[配置] addon_config 表不存在,跳过</comment>');
|
||||
return true;
|
||||
}
|
||||
$row = Db::table($configTable)->where('name', $name)->find();
|
||||
if (!empty($row)) {
|
||||
$output->writeln('<comment>[配置] 配置行已存在,跳过</comment>');
|
||||
return true;
|
||||
}
|
||||
// 读取插件默认配置(config.php 返回数组)
|
||||
$configFile = $this->addonDir($name) . 'config.php';
|
||||
$value = [];
|
||||
if (is_file($configFile)) {
|
||||
$cfg = include $configFile;
|
||||
if (is_array($cfg)) {
|
||||
$value = $cfg;
|
||||
}
|
||||
}
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'value' => json_encode($value, JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
// 兼容不同时间戳字段名(createtime/updatetime 或 create_time/update_time)
|
||||
$cols = Db::query("SHOW COLUMNS FROM `{$configTable}`");
|
||||
$colNames = array_column($cols, 'Field');
|
||||
$now = time();
|
||||
if (in_array('createtime', $colNames, true)) {
|
||||
$data['createtime'] = $now;
|
||||
} elseif (in_array('create_time', $colNames, true)) {
|
||||
$data['create_time'] = $now;
|
||||
}
|
||||
if (in_array('updatetime', $colNames, true)) {
|
||||
$data['updatetime'] = $now;
|
||||
} elseif (in_array('update_time', $colNames, true)) {
|
||||
$data['update_time'] = $now;
|
||||
}
|
||||
Db::table($configTable)->insert($data);
|
||||
$output->writeln("<info>[配置] 已补全配置行: {$name}</info>");
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>[配置] 补全失败: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function addonDir(string $name): string
|
||||
{
|
||||
$base = defined('ADDON_PATH') ? ADDON_PATH : (root_path() . 'addon' . DIRECTORY_SEPARATOR);
|
||||
return $base . $name . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
private function installedaddon(): array
|
||||
{
|
||||
$base = defined('ADDON_PATH') ? ADDON_PATH : (root_path() . 'addon' . DIRECTORY_SEPARATOR);
|
||||
if (!is_dir($base)) {
|
||||
return [];
|
||||
}
|
||||
$names = [];
|
||||
foreach (array_diff(scandir($base), ['.', '..']) as $cand) {
|
||||
$dir = $base . $cand . DIRECTORY_SEPARATOR;
|
||||
if (is_dir($dir) && is_file($dir . 'info.php')) {
|
||||
$names[] = $cand;
|
||||
}
|
||||
}
|
||||
return $names;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user