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"); 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('未检测到 Scheme 变更。'); return; } // 显示所有待同步的变更 $output->writeln(''); $output->writeln('========================================'); $output->writeln('即将执行以下 Scheme 变更:'); $output->writeln('========================================'); foreach ($pendingSync as $item) { $output->writeln("表名: {$item['tableName']} ({$item['className']})"); 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("成功!"); if ($backup) { $output->writeln("已创建备份: $backup"); } } catch (\Exception $e) { $output->writeln("失败: " . $e->getMessage() . ""); } } } 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; } }