option('dry-run'); $specificModel = $this->option('model'); if ($dryRun) { $this->warn('=== 预览模式(--dry-run):不会实际修改数据库 ==='); } $this->line(''); $this->info("目标域名: {$this->targetDomain}"); $this->info("匹配规则: 字段值包含 aliyuncs.com 即替换其域名部分"); $this->line(''); // ============ Step 1: 发现模型 ============ $this->info('━━━ Step 1: 发现模型 ━━━'); if ($specificModel) { $models = $this->resolveModels([$specificModel]); } else { $models = $this->discoverAllModels(); } if (empty($models)) { $this->error('未找到任何模型,请检查 app/Models 目录'); return 1; } $this->info("发现 " . count($models) . " 个 Eloquent 模型"); $this->line(''); // ============ Step 2: 扫描每张表 ============ $this->info('━━━ Step 2: 扫描数据表 ━━━'); $bar = $this->output->createProgressBar(count($models)); $bar->setFormat('%current%/%max% [%bar%] %percent:3s%% -- %message%'); $bar->setMessage('开始扫描...'); $bar->start(); foreach ($models as $modelClass) { $shortName = (new \ReflectionClass($modelClass))->getShortName(); $bar->setMessage($shortName); $this->processModel($modelClass, $dryRun); $bar->advance(); } $bar->finish(); // ============ Step 3: 汇总 ============ $this->line(''); $this->line(''); $this->info('━━━ 扫描汇总 ━━━'); $this->info("模型总数: " . count($models)); $this->info("已扫表数: {$this->scannedTables}"); $this->info("已扫字段: {$this->scannedColumns}"); $this->info("命中表数: {$this->hitTables}"); $this->info("命中记录: {$this->totalRecords}"); if (!$dryRun && $this->totalUpdated > 0) { $this->info("已更新记录: {$this->totalUpdated}"); } } private function processModel($modelClass, $dryRun) { try { if (!class_exists($modelClass)) { $this->line(''); $this->warn(" [跳过] 类不存在: {$modelClass}"); return; } /** @var \Illuminate\Database\Eloquent\Model $instance */ $instance = new $modelClass; $table = $instance->getTable(); $connection = $instance->getConnectionName() ?: config('database.default'); // 跳过视图 if ($table === 'member_perfor_view') { return; } // 获取该表所有文本类型字段 $textColumns = $this->getTextColumns($table, $connection); if (empty($textColumns)) { $this->line(''); $this->warn(" [{$connection}:{$table}] 无文本字段或查询失败"); return; } $this->scannedTables++; $this->scannedColumns += count($textColumns); $tableHit = false; foreach ($textColumns as $column) { $count = DB::connection($connection) ->table($table) ->where($column, 'like', '%aliyuncs.com%') ->count(); if ($count === 0) { continue; } if (!$tableHit) { $tableHit = true; $this->hitTables++; $this->line(''); } $this->totalRecords += $count; $this->info(" [{$connection}:{$table}.{$column}] 发现 {$count} 条"); if ($dryRun) { $samples = DB::connection($connection) ->table($table) ->select('id', $column) ->where($column, 'like', '%aliyuncs.com%') ->limit(3) ->get(); foreach ($samples as $sample) { $original = $sample->$column; $replaced = $this->replaceOssDomain($original); $this->line(" #{$sample->id}"); $this->line(" 旧: " . mb_strcut($original, 0, 150)); $this->line(" 新: " . mb_strcut($replaced, 0, 150)); } } else { $updated = DB::connection($connection) ->table($table) ->where($column, 'like', '%aliyuncs.com%') ->update([ $column => DB::raw(" CASE WHEN {$column} LIKE '%aliyuncs.com/%' THEN CONCAT('{$this->targetDomain}/', SUBSTRING_INDEX({$column}, 'aliyuncs.com/', -1)) ELSE '{$this->targetDomain}' END ") ]); $this->totalUpdated += $updated; $this->line(" → 已更新 {$updated} 条"); } } } catch (\Exception $e) { $this->line(''); $this->error(" [异常] {$modelClass}: " . $e->getMessage()); } } /** * 获取指定表的所有文本类型字段 * * 注意:information_schema 存的是物理表名(含前缀 x_), * 而 getTable() 返回的是不含前缀的逻辑表名,查询时必须补上。 */ private function getTextColumns($table, $connection) { try { $db = DB::connection($connection); $database = $db->getDatabaseName(); $prefix = $db->getTablePrefix(); $physicalTable = $prefix . $table; $rows = $db->select( "SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND DATA_TYPE IN ('varchar', 'char', 'text', 'mediumtext', 'longtext', 'json')", [$database, $physicalTable] ); return array_column($rows, 'COLUMN_NAME'); } catch (\Exception $e) { $this->line(''); $this->error(" [information_schema查询失败] connection={$connection} table={$table}: " . $e->getMessage()); return []; } } /** * PHP层面的域名替换(预览用) */ private function replaceOssDomain($value) { if (empty($value) || !is_string($value)) { return $value; } return preg_replace( '#https?://[^/]*aliyuncs\.com#i', $this->targetDomain, $value ); } /** * 解析用户指定的模型类名 */ private function resolveModels($input) { $models = []; foreach ($input as $name) { // 完整类名 if (strpos($name, 'App\\Models\\') === 0 && class_exists($name)) { $models[] = $name; continue; } // 相对写法 App\Models\ShopModels\Shop $fullName = 'App\\Models\\' . ltrim($name, '\\'); if (class_exists($fullName)) { $models[] = $fullName; continue; } $this->error("模型类不存在: {$name}"); } return $models; } /** * 自动发现 app/Models 下所有 Eloquent 模型 */ private function discoverAllModels() { $models = []; $basePath = app_path('Models'); if (!is_dir($basePath)) { $this->error("目录不存在: {$basePath}"); return $models; } $this->line(" 扫描目录: {$basePath}"); $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($basePath, \RecursiveDirectoryIterator::SKIP_DOTS) ); $fileCount = 0; foreach ($iterator as $file) { if ($file->getExtension() !== 'php') { continue; } $fileCount++; $pathname = $file->getPathname(); // 统一处理 Windows / Linux 路径分隔符 $relative = str_replace([$basePath . DIRECTORY_SEPARATOR, $basePath . '/', $basePath . '\\'], '', $pathname); $relative = str_replace(['/', '\\', '.php'], ['\\', '\\', ''], $relative); $class = 'App\\Models\\' . $relative; if (class_exists($class) && is_subclass_of($class, \Illuminate\Database\Eloquent\Model::class)) { $models[] = $class; } } $this->line(" 扫描到 {$fileCount} 个 PHP 文件,其中 " . count($models) . " 个是 Eloquent 模型"); // 打印前5个模型作为样例 $samples = array_slice($models, 0, 5); foreach ($samples as $m) { $instance = new $m; $conn = $instance->getConnectionName() ?: config('database.default'); $this->line(" - {$m} [{$conn}:{$instance->getTable()}]"); } if (count($models) > 5) { $this->line(" ... 还有 " . (count($models) - 5) . " 个"); } return $models; } }