From acb2b05c263bb053b6571e3c630cff1516f817ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=99=AF=E7=AB=8B?= Date: Sun, 17 Aug 2025 22:15:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=9B=E4=B8=80=E6=AD=A5=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=95=B0=E6=8D=AE=E5=BA=93=E5=90=8C=E6=AD=A5=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: 修正代码注释 This reverts commit 373427a539459a7abe17479e89ed9d9d353fff67. --- plugin/think-plugs-helper/composer.json | 4 +- .../think-plugs-helper/src/DbBackupStruct.php | 162 ++++++++++++ .../think-plugs-helper/src/DbIndexStruct.php | 84 ++++++ .../src/{ModelGen.php => DbModelStruct.php} | 2 +- .../src/DbRestoreStruct.php | 250 ++++++++++++++++++ plugin/think-plugs-helper/src/Service.php | 7 +- 6 files changed, 506 insertions(+), 3 deletions(-) create mode 100644 plugin/think-plugs-helper/src/DbBackupStruct.php create mode 100644 plugin/think-plugs-helper/src/DbIndexStruct.php rename plugin/think-plugs-helper/src/{ModelGen.php => DbModelStruct.php} (98%) create mode 100644 plugin/think-plugs-helper/src/DbRestoreStruct.php diff --git a/plugin/think-plugs-helper/composer.json b/plugin/think-plugs-helper/composer.json index 8f21740c0..263f62526 100644 --- a/plugin/think-plugs-helper/composer.json +++ b/plugin/think-plugs-helper/composer.json @@ -11,8 +11,10 @@ } ], "require": { - "php": ">7.1", + "php": "^8.1", "ext-json": "*", + "ext-zlib": "*", + "doctrine/dbal": "^4.2", "topthink/think-orm": "^2.0|^3.0|^4.0", "topthink/think-ide-helper": "*" }, diff --git a/plugin/think-plugs-helper/src/DbBackupStruct.php b/plugin/think-plugs-helper/src/DbBackupStruct.php new file mode 100644 index 000000000..6b83d55a4 --- /dev/null +++ b/plugin/think-plugs-helper/src/DbBackupStruct.php @@ -0,0 +1,162 @@ + +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-helper +// | github 代码仓库:https://github.com/zoujingli/think-plugs-helper +// +---------------------------------------------------------------------- + +declare (strict_types=1); + +namespace plugin\helper; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\DriverManager; +use think\admin\Library; +use think\admin\service\SystemService; +use think\console\Command; +use think\console\input\Option; + +/** + * 数据库备份. + */ +class DbBackupStruct extends Command +{ + /** + * 配置命令参数。 + */ + public function configure(): void + { + $this->setName("xadmin:helper:backup"); + $this->addOption('all', 'a', Option::VALUE_NONE, 'Backup All Tables'); + $this->setDescription("恢复数据前是否强制清空所有表数据"); + } + + /** + * 指令执行入口. + */ + public function handle(): void + { + $this->backupSchema() && $this->backupTables(); + } + + /** + * 检查是否允许执行任务 + * @return bool 返回true表示允许执行 + */ + public function isEnabled(): bool + { + return SystemService::isDebug(); + } + + /** + * 备份数据库结构,使用 gzip 压缩保存. + */ + protected function backupSchema(): bool + { + try { + $outputFile = $this->getSchemaPath(); + if (!($gz = gzopen($outputFile, 'w9'))) { + $this->output->error("❌ 无法打开压缩文件写入数据库结构:{$outputFile}"); + return false; + } + $schema = $this->makeConnect()->createSchemaManager(); + gzwrite($gz, serialize($schema->introspectSchema())); + gzclose($gz); + $this->output->info("✅ 数据库结构已压缩保存至:{$outputFile}"); + return true; + } catch (\Throwable $throwable) { + $this->output->error("❌ 数据库结构导出失败:{$throwable->getMessage()}"); + return false; + } + } + + /** + * 备份数据表数据,gzip 压缩写入. + */ + protected function backupTables(): bool + { + $backupPath = $this->getBackupPath(); + is_dir(dirname($backupPath)) || mkdir(dirname($backupPath), 0755, true); + if (!($gz = gzopen($backupPath, 'w9'))) { + $this->output->error("❌ 无法打开压缩文件写入数据表数据:{$backupPath}"); + return false; + } + $force = (bool)$this->input->getOption('all'); + foreach ($this->getBkTables($force) as $table) { + $total = 0; + if (!empty($fields = $this->app->db->getFields($table))) { + $query = $this->app->db->table($table)->order(in_array('id', $fields) ? 'id' : array_values($fields)[0]); + in_array('ssid', $fields) && $query = $query->where('ssid', '0'); + in_array('deleted_at', $fields) && $query = $query->whereNull('deleted_at'); + $query->chunk(10000, function ($rows) use ($gz, $table, &$total) { + foreach ($rows as $row) { + $record = ['table' => $table, 'data' => (array)$row]; + gzwrite($gz, json_encode($record, JSON_UNESCAPED_UNICODE) . "\n"); + ++$total; + } + }); + } + $this->output->writeln("✅ 表 {$table} 备份完成,共 {$total} 行"); + } + + gzclose($gz); + $this->output->info("📂 表数据已压缩写入:{$backupPath}"); + return true; + } + + /** + * 获取需要备份的表. + * @return array + */ + protected function getBkTables(bool $all = true): array + { + // 接收指定打包数据表 + if ($all) { + [$tables] = SystemService::getTables(); + } elseif (empty($tables = Library::$sapp->config->get('phinx.tables', []))) { + $this->output->error("❌ 配置文件未定义数据表列表,请检查配置项:phinx.tables"); + return []; + } + return $tables; + } + + /** + * 创建连接对接. + */ + protected function makeConnect(): Connection + { + $config = $this->app->db->connect()->getConfig(); + $config['host'] = $config['hostname'] ?? ''; + $config['user'] = $config['username'] ?? ''; + $config['dbname'] = $config['database'] ?? ''; + if (in_array($config['type'], ['mysql', 'sqlite', 'oci'])) { + $config['driver'] = 'pdo_' . $config['type']; + } + return DriverManager::getConnection($config); + } + + /** + * 结构文件路径,压缩格式. + */ + protected function getSchemaPath(): string + { + return syspath("database/backup.schema.gz"); + } + + /** + * 数据备份文件路径,压缩格式. + */ + protected function getBackupPath(): string + { + return syspath("database/backup.data.gz"); + } +} diff --git a/plugin/think-plugs-helper/src/DbIndexStruct.php b/plugin/think-plugs-helper/src/DbIndexStruct.php new file mode 100644 index 000000000..b4b073478 --- /dev/null +++ b/plugin/think-plugs-helper/src/DbIndexStruct.php @@ -0,0 +1,84 @@ + +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-helper +// | github 代码仓库:https://github.com/zoujingli/think-plugs-helper +// +---------------------------------------------------------------------- + +declare (strict_types=1); + +namespace plugin\helper; + +use think\admin\service\SystemService; +use think\console\Command; +use think\facade\Db; + +class DbIndexStruct extends Command +{ + + /** + * 配置命令参数。 + */ + public function configure(): void + { + $this->setName("xadmin:helper:index"); + $this->setDescription("刷新数据库的结构索引"); + } + + /** + * 检查是否允许执行任务 + * @return bool 返回true表示允许执行 + */ + public function isEnabled(): bool + { + return SystemService::isDebug(); + } + + /** + * 命令执行入口 + * 遍历数据库表并重命名索引. + */ + public function handle(): void + { + [$tables, $total, $count] = SystemService::getTables(); + foreach ($tables as $table) { + $this->output->writeln(sprintf("[%s/%s] 开始处理表 {$table}", $count++, $total)); + foreach (Db::query(sprintf('SHOW INDEX FROM `%s`', $table)) as $index) { + $keyName = $index['Key_name'] ?? ''; + if ($keyName === 'PRIMARY') { + continue; + } + $newName = $this->genIndexName($table, (array)$index); + if ($keyName === $newName) { + continue; + } + Db::execute(sprintf('ALTER TABLE `%s` RENAME INDEX `%s` TO `%s`', $table, $keyName, $newName)); + ++$count; + } + } + $this->output->writeln("✅ 完成 {$count} 个索引重命名"); + } + + /** + * 生成索引名称. + * @param string $table 表名 + * @param array $index 索引信息 + * @return string 生成的索引名称 + */ + private function genIndexName(string $table, array $index): string + { + $abbr = implode('', array_map(function ($word) { + return $word[0]; + }, explode('_', $table))); + return ($index['Non_unique'] ? 'idx_' : 'uni_') . $abbr . '_' . substr(md5($table), -4) . '_' . $index['Column_name']; + } +} diff --git a/plugin/think-plugs-helper/src/ModelGen.php b/plugin/think-plugs-helper/src/DbModelStruct.php similarity index 98% rename from plugin/think-plugs-helper/src/ModelGen.php rename to plugin/think-plugs-helper/src/DbModelStruct.php index 8d407883c..7cc67ade5 100644 --- a/plugin/think-plugs-helper/src/ModelGen.php +++ b/plugin/think-plugs-helper/src/DbModelStruct.php @@ -30,7 +30,7 @@ use think\ide\console\ModelCommand; * @class ModelGen * @package plugin\helper */ -class ModelGen extends ModelCommand +class DbModelStruct extends ModelCommand { protected function configure() { diff --git a/plugin/think-plugs-helper/src/DbRestoreStruct.php b/plugin/think-plugs-helper/src/DbRestoreStruct.php new file mode 100644 index 000000000..ac3b34c87 --- /dev/null +++ b/plugin/think-plugs-helper/src/DbRestoreStruct.php @@ -0,0 +1,250 @@ + +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-helper +// | github 代码仓库:https://github.com/zoujingli/think-plugs-helper +// +---------------------------------------------------------------------- + +declare (strict_types=1); + +namespace plugin\helper; + +use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Schema\Comparator; +use Doctrine\DBAL\Schema\Schema; +use think\admin\model\SystemUser; +use think\admin\service\RuntimeService; +use think\admin\service\SystemService; +use think\console\input\Option; + +/** + * 数据库结构与数据恢复命令。 + * 支持恢复 Doctrine Schema 结构及按行压缩备份的数据。 + */ +class DbRestoreStruct extends DbBackupStruct +{ + + /** + * 配置命令参数。 + */ + public function configure(): void + { + $this->setName("xadmin:helper:restore"); + $this->addOption('force', 'f', Option::VALUE_NONE, 'Force All Update'); + $this->setDescription("恢复数据前是否强制清空所有表数据"); + } + + /** + * 命令执行入口。 + * @throws Exception + */ + public function handle(): void + { + $this->restoreSchema() && $this->restoreBackup(); + } + + /** + * 检查是否允许执行任务 + * @return bool 返回true表示允许执行 + */ + public function isEnabled(): bool + { + return SystemService::isDebug(); + } + + /** + * 恢复数据库结构。 + * @throws Exception + */ + protected function restoreSchema(): bool + { + if (!is_file($gzPath = self::getSchemaPath())) { + $this->output->error("❌ 结构文件不存在:{$gzPath}"); + return false; + } + + $content = @gzdecode(file_get_contents($gzPath)); + if (empty($content) || !($backupSchema = @unserialize($content)) instanceof Schema) { + $this->output->error("❌ 解压或反序列化失败:{$gzPath}"); + return false; + } + + $connect = self::makeConnect(); + $platform = $connect->getDatabasePlatform(); + $diff = (new Comparator($platform))->compareSchemas( + $connect->createSchemaManager()->introspectSchema(), + $backupSchema + ); + + $sqls = []; + foreach ($diff->getCreatedTables() as $t) { + $sqls = [...$sqls, ...$platform->getCreateTableSQL($t)]; + } + foreach ($diff->getAlteredTables() as $t) { + $sqls = [...$sqls, ...$platform->getAlterTableSQL($t)]; + } + foreach ($diff->getDroppedTables() as $t) { + $sqls[] = $platform->getDropTableSQL($t->getName()); + } + + if (!$sqls) { + $this->output->info('✅ 数据库结构已一致,无需变更。'); + return true; + } + + try { + foreach ($sqls as $sql) { + $this->output->writeln("🔧 执行 SQL:{$sql}"); + $connect->executeStatement($sql); + } + $this->output->info('✅ 数据库结构同步完成。'); + return true; + } catch (\Throwable $throwable) { + $this->output->error('❌ 结构同步失败:' . $throwable->getMessage()); + return false; + } + } + + /** + * 恢复业务数据。 + * @throws Exception + */ + protected function restoreBackup(): bool + { + $force = (bool)$this->input->getOption('force'); + if (empty($tables = $this->getBkTables($force))) { + $this->output->error('❌ 未定义需恢复的数据表'); + return false; + } + + if (!file_exists($path = $this->getBackupPath())) { + $this->output->error("❌ 备份数据文件不存在:{$path}"); + return false; + } + + copy($path, $tmp = syspath('runtime/backup_data_tmp.gz')); + $schemaManager = $this->makeConnect()->createSchemaManager(); + + // 先清空 forceCleanTables 表,无需 count 检查 + $forceCleanTables = ['system_menu', 'system_dict_data', 'system_dict_type']; + foreach (array_intersect($forceCleanTables, $tables) as $table) { + _query($table)->empty(); + $this->output->writeln("🧹 强制清空表:{$table}"); + } + + if ($force) { + // 如果是强制恢复,需要清空所有表(非 forceCleanTables 表) + foreach ($schemaManager->listTableNames() as $table) { + if (!in_array($table, $forceCleanTables, true)) { + _query($table)->empty(); + $this->output->writeln("✅ 已经清空表:{$table}"); + } + } + } + + // 计算需要恢复的数据表 + $restoreTableFlags = []; + foreach ($tables as $table) { + $restoreTableFlags[$table] = $force || in_array($table, $forceCleanTables, true) || $this->app->db->table($table)->count() === 0; + } + + if (!($fp = gzopen($tmp, 'rb'))) { + $this->output->writeln("❌ 无法打开备份文件:{$tmp}"); + return false; + } + + $totalLines = 0; + $currentLine = 0; + $batchInsert = []; + $insertCount = array_fill_keys($tables, 0); + + try { + while (!gzeof($fp)) { + ++$totalLines; + $row = json_decode(trim(gzgets($fp)), true); + $table = $row['table'] ?? '-'; + + // 判断是否跳过恢复,非强制恢复时,根据 tableFlags 决定是否插入 + if (empty($restoreTableFlags[$table]) || empty($row['data']) || !is_array($row['data'])) { + continue; + } + + ++$currentLine; + ++$insertCount[$table]; + $batchInsert[$table][] = $row['data']; + if (count($batchInsert[$table]) >= 1000) { + $this->flushBatchInsert($table, $batchInsert[$table]); + $this->output->writeln("📥 表 {$table} 批量插入 1000 行,已读取 {$totalLines} 行"); + } + } + + // 插入剩余数据 + foreach ($batchInsert as $table => $rows) { + if ($count = count($rows)) { + $this->flushBatchInsert($table, $rows); + $this->output->writeln("📥 表 {$table} 批量插入 {$count} 行,已读取 {$totalLines} 行"); + } + } + $this->output->writeln("✅ 数据恢复完成,共插入 {$currentLine} 行(读取 {$totalLines} 行)"); + foreach ($insertCount as $table => $count) { + $count > 0 && $this->output->writeln("✅ 表 {$table} 插入 {$count} 行"); + } + @unlink($tmp); + + // 恢复管理员数据 + $this->insertSuperUser(); + + // 清理系统运行缓存 + return RuntimeService::clear(false); + } catch (\Throwable $throwable) { + trace_file($throwable); + $this->output->error('❌ 数据恢复失败:' . $throwable->getMessage()); + return false; + } finally { + gzclose($fp); + } + } + + /** + * 批量插入数据。 + */ + private function flushBatchInsert(string $table, array &$rows): void + { + if (!$rows) { + return; + } + + try { + $this->app->db->table($table)->insertAll($rows); + } catch (\Throwable $throwable) { + $this->output->writeln("⚠️ 表 {$table} 插入失败:{$throwable->getMessage()}"); + } finally { + $rows = []; + } + } + + /** + * 插入默认管理员。 + */ + private function insertSuperUser(): void + { + $model = SystemUser::mk()->whereRaw('1=1')->findOrEmpty(); + $model->isEmpty() && $model->save([ + 'id' => '10000', + 'username' => 'admin', + 'nickname' => '超级管理员', + 'password' => '21232f297a57a5a743894a0e4a801fc3', + 'headimg' => 'https://thinkadmin.top/static/img/head.png', + ], true); + $this->output->writeln('✅ 管理员账号恢复成功'); + } +} \ No newline at end of file diff --git a/plugin/think-plugs-helper/src/Service.php b/plugin/think-plugs-helper/src/Service.php index 0d2766fc0..2e0b2ec8b 100644 --- a/plugin/think-plugs-helper/src/Service.php +++ b/plugin/think-plugs-helper/src/Service.php @@ -22,6 +22,11 @@ class Service extends \think\Service { public function boot() { - $this->commands([ModelGen::class]); + $this->commands([ + DbModelStruct::class, + DbIndexStruct::class, + DbBackupStruct::class, + DbRestoreStruct::class, + ]); } }