mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-01 15:32:48 +08:00
207 lines
7.0 KiB
PHP
207 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace base\common\command\scheme;
|
|
|
|
use app\common\console\Command;
|
|
use app\common\service\scheme\SchemeToDbService;
|
|
use think\console\Input;
|
|
use think\console\Input\Option;
|
|
use think\console\Output;
|
|
use ReflectionClass;
|
|
use app\common\scheme\attribute\Table;
|
|
use think\facade\Config;
|
|
use think\facade\Db;
|
|
|
|
/**
|
|
* scheme:sync 命令基类
|
|
*/
|
|
class Sync extends Command
|
|
{
|
|
protected function configure()
|
|
{
|
|
parent::configure();
|
|
|
|
$this->setName('scheme:sync')
|
|
->addOption('skip-data', null, Option::VALUE_NONE, 'Skip data migration')
|
|
->setDescription('Synchronize Scheme classes to Database');
|
|
}
|
|
|
|
protected function execute(Input $input, Output $output)
|
|
{
|
|
$skipData = $input->getOption('skip-data');
|
|
|
|
$service = new SchemeToDbService();
|
|
$schemeDir = app()->getAppPath() . 'admin/scheme/';
|
|
$ignoreTables = Config::get('scheme.ignore_tables', []);
|
|
$connection = Config::get('database.default', 'mysql');
|
|
$prefix = Config::get('database.connections.' . $connection . '.prefix', '');
|
|
$backupPrefix = Config::get('scheme.backup_prefix', 'backup');
|
|
|
|
if (!is_dir($schemeDir)) {
|
|
$error = "Scheme directory not found: $schemeDir";
|
|
$output->writeln("<error>$error</error>");
|
|
return;
|
|
}
|
|
|
|
$pendingSync = [];
|
|
$files = glob($schemeDir . '*.php');
|
|
|
|
foreach ($files as $file) {
|
|
require_once $file;
|
|
$className = 'app\\admin\\scheme\\' . basename($file, '.php');
|
|
|
|
if (class_exists($className)) {
|
|
$tableName = $this->getTableNameFromScheme($className);
|
|
if (empty($tableName)) {
|
|
$output->writeln("Skipping $className (missing table name)");
|
|
continue;
|
|
}
|
|
|
|
$fullTableName = $this->getFullTableName($tableName, $prefix);
|
|
if ($this->isBackupTable($fullTableName, $prefix, $backupPrefix)) {
|
|
$output->writeln("Skipping $className (backup table: $fullTableName)");
|
|
continue;
|
|
}
|
|
|
|
if ($this->isIgnoredTable($fullTableName, $ignoreTables, $prefix)) {
|
|
$output->writeln("Skipping $className (ignored table: $fullTableName)");
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$diffs = $service->diff($className);
|
|
} catch (\Throwable $e) {
|
|
$output->error("Check failed for $className: " . $e->getMessage());
|
|
continue;
|
|
}
|
|
|
|
if (count($diffs) === 1 && str_starts_with($diffs[0], '无法读取数据库表结构')) {
|
|
$output->error("Check failed for $className: {$diffs[0]}");
|
|
continue;
|
|
}
|
|
|
|
if (empty($diffs)) {
|
|
continue;
|
|
}
|
|
|
|
$pendingSync[] = [
|
|
'className' => $className,
|
|
'tableName' => $fullTableName,
|
|
'diffs' => $diffs
|
|
];
|
|
}
|
|
}
|
|
|
|
if (empty($pendingSync)) {
|
|
$output->writeln('<info>未检测到 Scheme 变更。</info>');
|
|
return;
|
|
}
|
|
|
|
// 显示所有待同步的变更
|
|
$output->writeln('');
|
|
$output->writeln('<comment>========================================</comment>');
|
|
$output->writeln('<comment>即将执行以下 Scheme 变更:</comment>');
|
|
$output->writeln('<comment>========================================</comment>');
|
|
foreach ($pendingSync as $item) {
|
|
$output->writeln("<comment>表名: {$item['tableName']} ({$item['className']})</comment>");
|
|
foreach ($item['diffs'] as $line) {
|
|
$output->writeln(" $line");
|
|
}
|
|
$output->writeln('');
|
|
}
|
|
|
|
// 确认(全局 -ff 可跳过)
|
|
if (!$output->confirm($input, '确认要将这些变更应用到数据库吗?', true)) {
|
|
$output->comment('操作已取消。');
|
|
return;
|
|
}
|
|
|
|
// 执行同步
|
|
foreach ($pendingSync as $item) {
|
|
$output->writeln("正在同步 {$item['className']}...");
|
|
try {
|
|
$backup = $service->sync($item['className'], $skipData);
|
|
$output->writeln("<info>成功!</info>");
|
|
if ($backup) {
|
|
$output->writeln("已创建备份: $backup");
|
|
}
|
|
} catch (\Exception $e) {
|
|
$output->writeln("<error>失败: " . $e->getMessage() . "</error>");
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function getTableNameFromScheme(string $className): string
|
|
{
|
|
try {
|
|
$ref = new ReflectionClass($className);
|
|
$tableAttrs = $ref->getAttributes(Table::class);
|
|
if (empty($tableAttrs)) {
|
|
return '';
|
|
}
|
|
|
|
$tableAttr = $tableAttrs[0]->newInstance();
|
|
return (string)($tableAttr->name ?? '');
|
|
} catch (\Throwable $e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
protected function isIgnoredTable(string $tableName, array $ignoreTables, string $prefix): bool
|
|
{
|
|
if (empty($ignoreTables)) {
|
|
return false;
|
|
}
|
|
|
|
$shortName = $tableName;
|
|
if ($prefix && str_starts_with($tableName, $prefix)) {
|
|
$shortName = substr($tableName, strlen($prefix));
|
|
}
|
|
|
|
return in_array($shortName, $ignoreTables, true) || in_array($tableName, $ignoreTables, true);
|
|
}
|
|
|
|
protected function normalizeCode(string $code): string
|
|
{
|
|
$code = str_replace(["\r\n", "\r"], "\n", $code);
|
|
$code = preg_replace('/[ \t]+$/m', '', $code);
|
|
return trim((string)$code);
|
|
}
|
|
|
|
protected function checkTableExists(string $connection, string $tableName): bool
|
|
{
|
|
$database = (string)Config::get('database.connections.' . $connection . '.database', '');
|
|
if ($database !== '') {
|
|
$rows = Db::connect($connection)->query(
|
|
'SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? LIMIT 1',
|
|
[$database, $tableName]
|
|
);
|
|
return !empty($rows);
|
|
}
|
|
|
|
$escaped = addcslashes($tableName, "\\_%");
|
|
$tables = Db::connect($connection)->query("SHOW TABLES LIKE '$escaped'");
|
|
return !empty($tables);
|
|
}
|
|
|
|
protected function getFullTableName(string $tableName, string $prefix): string
|
|
{
|
|
if ($prefix && !str_starts_with($tableName, $prefix)) {
|
|
return $prefix . $tableName;
|
|
}
|
|
|
|
return $tableName;
|
|
}
|
|
|
|
protected function isBackupTable(string $tableName, string $prefix, string $backupPrefix): bool
|
|
{
|
|
$basePrefix = $prefix . $backupPrefix;
|
|
if ($basePrefix === '') {
|
|
return false;
|
|
}
|
|
|
|
$pattern = '/^' . preg_quote($basePrefix, '/') . '_?\\d{14}(?:_.*)?$/';
|
|
return preg_match($pattern, $tableName) === 1;
|
|
}
|
|
}
|