GenerateModelAnnotation.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. <?php
  2. namespace UCore\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Database\Eloquent\Model;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\File;
  7. use Illuminate\Support\Facades\Log;
  8. use UCore\ModelCore;
  9. /**
  10. * 自动生成Eloquent模型属性注释和表创建SQL
  11. *
  12. * 通过分析数据库表结构,自动生成模型属性的PHPDoc注释,
  13. * 并提供fillable字段自动维护功能。同时,为每个表生成创建SQL文件。
  14. *
  15. * 功能特性:
  16. * 1. 自动识别字段类型并生成对应property注释
  17. * 2. 特殊时间字段自动识别为Carbon类型
  18. * 3. 支持自定义Casts类型,自动识别并使用正确的类型
  19. * 4. 自动维护模型$attrlist属性包含所有字段
  20. * 5. 在模块的Databases/createsql目录下为每个表生成创建SQL文件(不包含当前自增值,但保留字段的自增属性)
  21. * 6. 支持指定单一模块进行处理,提高效率
  22. *
  23. * 使用说明:
  24. * 1. 在模型中添加标记注释:
  25. * - 属性注释区域标记:在模型文件中添加 field start 和 field end 注释块
  26. * - fillable字段标记:在模型文件中添加 attrlist start 和 attrlist end 注释块
  27. * 2. 对于使用自定义Cast类的字段,会自动识别并使用Cast类作为类型
  28. * 3. 可以通过指定模块名称参数,只处理特定模块的模型
  29. * 4. 生成的SQL文件中不包含当前AUTO_INCREMENT值,但保留字段的自增属性,便于在不同环境中使用
  30. *
  31. * @package UCore\Commands
  32. *
  33. * @example 处理所有模块: php artisan ucore:generate-model-annotation
  34. * @example 只处理特定模块: php artisan ucore:generate-model-annotation GameItems
  35. * @example 开启调试信息: php artisan ucore:generate-model-annotation --debug-info
  36. * @example 跳过SQL生成: php artisan ucore:generate-model-annotation --skip-sql
  37. */
  38. class GenerateModelAnnotation extends Command
  39. {
  40. /**
  41. * The name and signature of the console command.
  42. *
  43. * @var string
  44. */
  45. protected $signature = 'ucore:generate-model-annotation
  46. {module? : 要处理的模块名称,不提供则处理所有模块}
  47. {--skip-sql : 跳过生成表创建SQL文件}
  48. {--debug-info : 输出详细的调试信息}';
  49. /**
  50. * The console command description.
  51. *
  52. * @var string
  53. */
  54. protected $description = '生成模型property注释和表创建SQL文件(不含当前自增值,但保留字段自增属性),支持自定义Casts类型,可指定单一模块处理';
  55. private $fillable = [];
  56. /**
  57. * 已处理的表名,用于避免重复生成SQL
  58. *
  59. * @var array
  60. */
  61. private $processedTables = [];
  62. /**
  63. * 成功处理的模型计数
  64. *
  65. * @var int
  66. */
  67. private $successCount = 0;
  68. /**
  69. * 跳过的模型计数
  70. *
  71. * @var int
  72. */
  73. private $skippedCount = 0;
  74. /**
  75. * 失败的模型计数
  76. *
  77. * @var int
  78. */
  79. private $failedCount = 0;
  80. /**
  81. * 主执行方法
  82. *
  83. * 扫描指定目录下的模型文件并生成注释和表创建SQL
  84. *
  85. * @return void
  86. */
  87. public function handle()
  88. {
  89. // 获取要处理的模块名称
  90. $targetModule = $this->argument('module');
  91. // 显示处理模式
  92. if ($targetModule) {
  93. $this->simpleOutput("仅处理模块: {$targetModule}");
  94. } else {
  95. $this->simpleOutput("处理所有模块");
  96. }
  97. // 显示是否跳过SQL生成的选项状态
  98. if ($this->option('skip-sql')) {
  99. $this->debug('已设置跳过生成表创建SQL文件', 'warning');
  100. } else {
  101. $this->debug('将为每个表生成创建SQL文件到模块的Databases/createsql目录');
  102. }
  103. // 如果没有指定模块或指定的是UCore,则扫描核心模型目录
  104. if (!$targetModule || strtolower($targetModule) === 'ucore') {
  105. $ucoreModelsDir = app_path('../UCore/Models');
  106. if (is_dir($ucoreModelsDir)) {
  107. $this->debug("扫描核心模型目录: $ucoreModelsDir");
  108. $this->call1($ucoreModelsDir, '\UCore\Models\\');
  109. } else {
  110. $this->simpleOutput("UCore Models 目录不存在: $ucoreModelsDir", 'warning');
  111. }
  112. }
  113. // 扫描模块下的Models目录
  114. $modulesPath = app_path('Module');
  115. if (is_dir($modulesPath)) {
  116. $modules = scandir($modulesPath);
  117. foreach ($modules as $module) {
  118. if ($module === '.' || $module === '..') {
  119. continue;
  120. }
  121. // 如果指定了模块,则只处理该模块
  122. if ($targetModule && strtolower($targetModule) !== strtolower($module)) {
  123. continue;
  124. }
  125. $modelsDir = "$modulesPath/$module/Models";
  126. if (is_dir($modelsDir)) {
  127. // 添加模块扫描日志
  128. $this->debug("扫描模块目录: $modelsDir");
  129. // 修复模块命名空间格式
  130. $namespace = "App\\Module\\$module\\Models\\";
  131. // 统一目录分隔符
  132. $namespace = str_replace('/', '\\', $namespace);
  133. // 添加自动加载提示
  134. $this->debug("请确保已配置composer自动加载: \"App\\Module\\$module\\Models\\\": \"app/Module/$module/Models/\"", 'warning');
  135. $this->call1($modelsDir, $namespace);
  136. } else if ($targetModule) {
  137. // 如果指定了模块但目录不存在,则输出警告
  138. $this->simpleOutput("模块 {$module} 的 Models 目录不存在: $modelsDir", 'warning');
  139. }
  140. }
  141. // 如果指定了模块但未找到,则输出错误
  142. if ($targetModule && $this->successCount === 0 && $this->skippedCount === 0 && $this->failedCount === 0) {
  143. $this->simpleOutput("未找到指定的模块: {$targetModule}", 'error');
  144. return;
  145. }
  146. }
  147. // 显示处理结果
  148. $this->simpleOutput("成功: {$this->successCount} | 跳过: {$this->skippedCount} | 失败: {$this->failedCount}" .
  149. (!$this->option('skip-sql') ? " | SQL文件: " . count($this->processedTables) : ""));
  150. }
  151. // ... 保持原有call1、call2、getAnnotation方法不变 ...
  152. /**
  153. * 扫描模型目录
  154. *
  155. * @param string $dir 模型文件目录路径
  156. * @param string $ns 模型类命名空间
  157. */
  158. public function call1($dir, $ns)
  159. {
  160. $this->debug("扫描目录: $dir");
  161. $list = scandir($dir);
  162. foreach ($list as $item) {
  163. if ($item === '.' || $item === '..') {
  164. continue;
  165. }
  166. $fullPath = "{$dir}/{$item}";
  167. // 递归处理子目录
  168. if (is_dir($fullPath)) {
  169. // 跳过Validator目录
  170. if (basename($fullPath) === 'Validators' || basename($fullPath) === 'Validator') {
  171. $this->debug("跳过Validator目录: $fullPath", 'warning');
  172. continue;
  173. }
  174. $this->call1($fullPath, "{$ns}{$item}\\");
  175. continue;
  176. }
  177. $p = strpos($item, '.php');
  178. if ($p !== false) {
  179. $model = substr($item, 0, $p);
  180. // 修复命名空间拼接逻辑
  181. $modelClass = rtrim($ns, '\\') . '\\' . $model;
  182. // 修复文件路径拼接
  183. $file = $fullPath;
  184. $this->call2($model, $modelClass, $file);
  185. }
  186. }
  187. }
  188. /**
  189. * 处理单个模型文件
  190. *
  191. * @param string $model 模型类名
  192. * @param string $modelClass 完整模型类名
  193. * @param string $file 模型文件路径
  194. */
  195. public function call2($model,$modelClass,$file)
  196. {
  197. if(class_exists($modelClass)){
  198. // 检查类是否是抽象类
  199. $reflectionClass = new \ReflectionClass($modelClass);
  200. if ($reflectionClass->isAbstract()) {
  201. $this->debug(" model $modelClass 是抽象类,跳过", 'warning');
  202. $this->skippedCount++;
  203. return;
  204. }
  205. // 检查类是否是模型类
  206. if (!$reflectionClass->isSubclassOf(Model::class)) {
  207. $this->debug(" model $modelClass 不是模型类,跳过", 'warning');
  208. $this->skippedCount++;
  209. return;
  210. }
  211. // 检查构造函数是否需要参数
  212. $constructor = $reflectionClass->getConstructor();
  213. if ($constructor && $constructor->getNumberOfRequiredParameters() > 0) {
  214. $this->debug(" model $modelClass 需要构造函数参数,跳过", 'warning');
  215. $this->skippedCount++;
  216. return;
  217. }
  218. /**
  219. * @var ModelCore $model
  220. */
  221. try {
  222. $model = new $modelClass();
  223. } catch (\Throwable $e) {
  224. $this->output->writeln("<fg=red>失败: " . $e->getMessage() . "</>");
  225. $this->failedCount++;
  226. return;
  227. }
  228. if($model instanceof Model){
  229. $co = $model->getConnection();
  230. $tTablePrefix = $co->getTablePrefix();
  231. $table = $tTablePrefix.$model->getTable();
  232. $this->output->write("表: $table ... ");
  233. // 提取模块名称
  234. $moduleName = $this->extractModuleNameFromNamespace($modelClass);
  235. // 如果不跳过SQL生成且找到了模块名称,则生成表的创建SQL
  236. if (!$this->option('skip-sql') && $moduleName) {
  237. // 获取无前缀的表名
  238. $tableWithoutPrefix = $model->getTable();
  239. $this->generateTableSQLFile($table, $moduleName, $co, $tableWithoutPrefix, $modelClass);
  240. }
  241. $annotation = $this->getAnnotation($table, $co, $model);
  242. $string = file_get_contents($file);
  243. // 输出文件内容的前200个字符,帮助调试
  244. $this->debug("文件内容前200字符: " . substr($string, 0, 200) . "...");
  245. // 检查文件是否包含 field start/end 标识符
  246. $hasFieldStart = strpos($string, 'field start') !== false;
  247. $hasFieldEnd = strpos($string, 'field end') !== false;
  248. $this->debug("包含 field start: " . ($hasFieldStart ? "是" : "否"));
  249. $this->debug("包含 field end: " . ($hasFieldEnd ? "是" : "否"));
  250. // 使用正则表达式匹配 field start/end 标识符
  251. $pattern = '/field\s+start[\s\S]+?field\s+end/';
  252. $this->debug("使用正则表达式匹配 field start/end: {$pattern}");
  253. // 尝试匹配
  254. $matches = [];
  255. $matchResult = preg_match($pattern, $string, $matches);
  256. $this->debug("正则匹配结果: " . ($matchResult ? "成功" : "失败"));
  257. if ($matchResult) {
  258. $this->debug("匹配到的内容长度: " . strlen($matches[0]));
  259. $this->debug("匹配到的内容前50字符: " . substr($matches[0], 0, 50) . "...");
  260. }
  261. // 替换 field start/end 标识符
  262. $replacement = "field start {$annotation} * field end";
  263. $result = preg_replace($pattern, $replacement, $string);
  264. // 检查替换是否成功
  265. $this->debug("field start/end 替换结果: 成功");
  266. // 过滤系统默认字段
  267. $filteredFillable = array_filter($this->fillable, fn($field) =>
  268. !in_array($field, ['created_at', 'updated_at', 'deleted_at'])
  269. );
  270. // 格式化数组输出
  271. $fillableContent = " protected \$fillable = [\n";
  272. foreach ($filteredFillable as $field) {
  273. $fillableContent .= " '{$field}',\n";
  274. }
  275. $fillableContent .= " ];";
  276. // 检查文件是否包含 attrlist start/end 标识符
  277. $hasAttrlistStart = strpos($string, 'attrlist start') !== false;
  278. $hasAttrlistEnd = strpos($string, 'attrlist end') !== false;
  279. $this->debug("包含 attrlist start: " . ($hasAttrlistStart ? "是" : "否"));
  280. $this->debug("包含 attrlist end: " . ($hasAttrlistEnd ? "是" : "否"));
  281. // 使用正则表达式匹配 attrlist start/end 标识符
  282. $pattern2 = '/\/\/\s*attrlist\s+start[\s\S]+?\/\/\s*attrlist\s+end/';
  283. $this->debug("使用正则表达式匹配 attrlist start/end: {$pattern2}");
  284. // 尝试匹配
  285. $matches2 = [];
  286. $matchResult2 = preg_match($pattern2, $result, $matches2);
  287. $this->debug("正则匹配结果: " . ($matchResult2 ? "成功" : "失败"));
  288. if ($matchResult2) {
  289. $this->debug("匹配到的内容长度: " . strlen($matches2[0]));
  290. $this->debug("匹配到的内容前50字符: " . substr($matches2[0], 0, 50) . "...");
  291. }
  292. // 替换 attrlist start/end 标识符
  293. $replacement2 = "// attrlist start \n{$fillableContent}\n // attrlist end";
  294. $result = preg_replace($pattern2, $replacement2, $result);
  295. // 检查替换是否成功
  296. $this->debug("attrlist start/end 替换结果: 成功");
  297. // 强制写入文件
  298. file_put_contents($file,$result);
  299. $this->successCount++;
  300. $this->output->writeln("<fg=green>完成</>");
  301. }else{
  302. $this->output->writeln("<fg=yellow>跳过: 不是继承 ModelBase</>");
  303. $this->skippedCount++;
  304. }
  305. }else{
  306. $this->debug(" model $model 不存在", 'warning');
  307. $this->skippedCount++;
  308. }
  309. }
  310. /**
  311. * 从模型命名空间中提取模块名称
  312. *
  313. * @param string $modelClass 模型类名
  314. * @return string|null 模块名称,如果不是模块内的模型则返回null
  315. */
  316. protected function extractModuleNameFromNamespace($modelClass)
  317. {
  318. // 匹配 App\Module\{ModuleName}\Models 格式的命名空间
  319. if (preg_match('/^App\\\\Module\\\\([^\\\\]+)\\\\Models/', $modelClass, $matches)) {
  320. return $matches[1];
  321. }
  322. return null;
  323. }
  324. /**
  325. * 输出调试信息并写入日志
  326. *
  327. * @param string $message 调试信息
  328. * @param string $type 信息类型 (info, warning, error)
  329. * @return void
  330. */
  331. protected function debug($message, $type = 'info')
  332. {
  333. // 始终写入日志
  334. $logPrefix = '[GenerateModelAnnotation] ';
  335. switch ($type) {
  336. case 'warning':
  337. Log::warning("{$logPrefix}{$message}");
  338. break;
  339. case 'error':
  340. Log::error("{$logPrefix}{$message}");
  341. break;
  342. default:
  343. Log::info("{$logPrefix}{$message}");
  344. }
  345. // 仅在启用调试信息时输出到控制台
  346. if ($this->option('debug-info')) {
  347. switch ($type) {
  348. case 'warning':
  349. $this->output->warning($message);
  350. break;
  351. case 'error':
  352. $this->output->error($message);
  353. break;
  354. default:
  355. $this->output->info($message);
  356. }
  357. }
  358. }
  359. /**
  360. * 输出简洁信息并写入日志
  361. *
  362. * @param string $message 信息内容
  363. * @param string $type 信息类型 (info, warning, error)
  364. * @return void
  365. */
  366. protected function simpleOutput($message, $type = 'info')
  367. {
  368. // 检查消息是否为空
  369. if (empty(trim($message))) {
  370. return;
  371. }
  372. // 始终写入日志
  373. $logPrefix = '[GenerateModelAnnotation] ';
  374. switch ($type) {
  375. case 'warning':
  376. Log::warning("{$logPrefix}{$message}");
  377. $this->output->writeln("<fg=yellow>{$message}</>");
  378. break;
  379. case 'error':
  380. Log::error("{$logPrefix}{$message}");
  381. $this->output->writeln("<fg=red>{$message}</>");
  382. break;
  383. default:
  384. Log::info("{$logPrefix}{$message}");
  385. $this->output->writeln($message);
  386. }
  387. }
  388. /**
  389. * 生成属性注释字符串
  390. *
  391. * @param string $tableName 数据库表名
  392. * @param \Illuminate\Database\Connection $con 数据库连接
  393. * @param Model|null $model 模型实例,用于获取 $casts 属性
  394. * @return string 生成的注释字符串
  395. * @throws \Exception 当数据库查询失败时
  396. */
  397. public function getAnnotation($tableName, \Illuminate\Database\Connection $con, $model = null)
  398. {
  399. $db = $con->getDatabaseName();
  400. $fillable = [];
  401. $sql = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS
  402. WHERE table_name = '{$tableName}' AND TABLE_SCHEMA = '{$db}'
  403. ORDER BY ORDINAL_POSITION ASC";
  404. $columns = $con->select($sql);
  405. $annotation = "";
  406. // 获取模型的 $casts 属性
  407. $casts = [];
  408. if ($model instanceof Model && method_exists($model, 'getCasts')) {
  409. $casts = $model->getCasts();
  410. $this->debug("模型 Casts: " . json_encode($casts, JSON_UNESCAPED_UNICODE));
  411. }
  412. foreach ($columns as $column) {
  413. $type = $this->getColumnType($column->DATA_TYPE);
  414. $columnName = $column->COLUMN_NAME;
  415. $fillable[] = $columnName;
  416. // 检查是否有自定义 Cast
  417. if (isset($casts[$columnName])) {
  418. $castType = $casts[$columnName];
  419. $this->debug("字段 {$columnName} 有自定义 Cast: {$castType}");
  420. $type = $this->getCastType($castType, $type);
  421. } else {
  422. $type = $this->handleSpecialColumns($columnName, $type);
  423. }
  424. $annotation .= sprintf("\n * @property %s \$%s %s",
  425. $type,
  426. $columnName,
  427. $column->COLUMN_COMMENT);
  428. }
  429. $this->fillable = $fillable;
  430. return "{$annotation}\n";
  431. }
  432. private function getColumnType($dataType)
  433. {
  434. return match($dataType) {
  435. 'int', 'tinyint', 'smallint', 'mediumint', 'bigint' => 'int',
  436. 'float', 'double', 'decimal' => 'float',
  437. 'json' => 'object|array',
  438. default => 'string'
  439. };
  440. }
  441. /**
  442. * 处理特殊列名的类型
  443. *
  444. * @param string $columnName 列名
  445. * @param string $type 默认类型
  446. * @return string 处理后的类型
  447. */
  448. private function handleSpecialColumns($columnName, $type)
  449. {
  450. if (in_array($columnName, ['created_at', 'updated_at', 'deleted_at'])) {
  451. return '\\Carbon\\Carbon';
  452. }
  453. return $type;
  454. }
  455. /**
  456. * 获取 Cast 类型对应的 PHP 类型
  457. *
  458. * @param string $castType Cast 类型
  459. * @param string $defaultType 默认类型
  460. * @return string PHP 类型
  461. */
  462. private function getCastType($castType, $defaultType)
  463. {
  464. // 处理基本类型
  465. $basicTypes = [
  466. 'int' => 'int',
  467. 'integer' => 'int',
  468. 'real' => 'float',
  469. 'float' => 'float',
  470. 'double' => 'float',
  471. 'decimal' => 'float',
  472. 'string' => 'string',
  473. 'bool' => 'bool',
  474. 'boolean' => 'bool',
  475. 'object' => 'object',
  476. 'array' => 'array',
  477. 'json' => 'array',
  478. 'collection' => 'Collection',
  479. 'date' => '\\Carbon\\Carbon',
  480. 'datetime' => '\\Carbon\\Carbon',
  481. 'timestamp' => '\\Carbon\\Carbon',
  482. 'immutable_date' => '\\Carbon\\CarbonImmutable',
  483. 'immutable_datetime' => '\\Carbon\\CarbonImmutable',
  484. 'immutable_timestamp' => '\\Carbon\\CarbonImmutable',
  485. ];
  486. if (isset($basicTypes[$castType])) {
  487. return $basicTypes[$castType];
  488. }
  489. // 处理自定义 Cast 类
  490. if (class_exists($castType)) {
  491. $this->debug("尝试解析自定义 Cast 类: {$castType}");
  492. // 检查是否是 UCore\Model\CastsAttributes 的子类
  493. if (is_subclass_of($castType, 'UCore\Model\CastsAttributes')) {
  494. $this->debug("{$castType} 是 UCore\Model\CastsAttributes 的子类");
  495. // 创建一个实例并获取其属性
  496. try {
  497. // 使用反射获取类的公共属性
  498. $reflectionClass = new \ReflectionClass($castType);
  499. $properties = $reflectionClass->getProperties(\ReflectionProperty::IS_PUBLIC);
  500. if (count($properties) > 0) {
  501. $this->debug("Cast 类 {$castType} 有 " . count($properties) . " 个公共属性");
  502. return "\\". get_class(new $castType());
  503. } else {
  504. $this->debug("Cast 类 {$castType} 没有公共属性,使用类名作为类型");
  505. return $castType;
  506. }
  507. } catch (\Throwable $e) {
  508. $this->debug("无法分析 Cast 类 {$castType}: " . $e->getMessage(), 'warning');
  509. }
  510. } else if (in_array('Illuminate\Contracts\Database\Eloquent\CastsAttributes', class_implements($castType))) {
  511. // 检查是否是 Illuminate\Contracts\Database\Eloquent\CastsAttributes 的实现
  512. $this->debug("{$castType} 实现了 Illuminate\Contracts\Database\Eloquent\CastsAttributes 接口");
  513. return "\\". $castType;
  514. } else if (is_subclass_of($castType, '\UnitEnum')){
  515. // 枚举
  516. $this->debug("{$castType} 实现了 \UnitEnum 枚举");
  517. return "\\".$castType;
  518. }
  519. }
  520. // 如果无法确定类型,返回默认类型
  521. $this->debug("无法确定 Cast 类型 {$castType},使用默认类型 {$defaultType}", 'warning');
  522. return $defaultType;
  523. }
  524. /**
  525. * 获取表的创建SQL语句,并移除表的当前自增值
  526. *
  527. * @param string $tableName 表名
  528. * @param \Illuminate\Database\Connection $connection 数据库连接
  529. * @return string 创建表的SQL语句(不包含当前自增值,但保留字段的自增属性)
  530. */
  531. protected function getCreateTableSQL($tableName, $connection)
  532. {
  533. try {
  534. $result = $connection->select("SHOW CREATE TABLE `{$tableName}`");
  535. if (!empty($result) && isset($result[0]->{'Create Table'})) {
  536. $createTableSQL = $result[0]->{'Create Table'};
  537. // 只移除表的当前自增值 AUTO_INCREMENT=xxx 部分,保留字段的自增属性
  538. $createTableSQL = preg_replace('/\s+AUTO_INCREMENT=\d+/', '', $createTableSQL);
  539. $this->debug("已移除表的当前自增值: " . ($createTableSQL !== $result[0]->{'Create Table'} ? "是" : "否"));
  540. return $createTableSQL;
  541. }
  542. } catch (\Exception $e) {
  543. $this->output->writeln("<fg=red>获取表 {$tableName} 的创建SQL失败: " . $e->getMessage() . "</>");
  544. }
  545. return null;
  546. }
  547. /**
  548. * 为表生成创建SQL文件
  549. *
  550. * @param string $tableName 表名(带前缀)
  551. * @param string $moduleName 模块名
  552. * @param \Illuminate\Database\Connection $connection 数据库连接
  553. * @param string $tableNameWithoutPrefix 无前缀的表名(必传)
  554. * @param string $modelClass 模型类名
  555. * @return void
  556. */
  557. protected function generateTableSQLFile($tableName, $moduleName, $connection, $tableNameWithoutPrefix, $modelClass = null)
  558. {
  559. // 标记该表已处理
  560. $this->processedTables[] = $tableName;
  561. // 获取表的创建SQL
  562. $createSQL = $this->getCreateTableSQL($tableName, $connection);
  563. if (empty($createSQL)) {
  564. $modelInfo = $modelClass ? " (Model: {$modelClass})" : "";
  565. $this->output->writeln("<fg=yellow>SQL生成失败{$modelInfo}</>");
  566. return;
  567. }
  568. // 创建模块的Databases/GenerateSql 目录
  569. $sqlDir = app_path("Module/{$moduleName}/Databases/GenerateSql");
  570. if (!File::exists($sqlDir)) {
  571. $this->debug("创建目录: {$sqlDir}");
  572. File::makeDirectory($sqlDir, 0755, true);
  573. }
  574. // 创建或更新README.md文件,声明该目录是自动生成的,不能修改
  575. $readmePath = "{$sqlDir}/README.md";
  576. $readmeContent = "# 自动生成的SQL文件目录\n\n";
  577. $readmeContent .= "**警告:这是自动生成的目录,请勿手动修改此目录下的任何文件!**\n\n";
  578. $readmeContent .= "此目录下的SQL文件由系统自动生成,用于记录数据库表结构。\n";
  579. $readmeContent .= "如需修改表结构,请修改对应的模型文件,然后重新运行生成命令。\n";
  580. // 只有当README.md不存在或内容不同时才写入文件
  581. if (!File::exists($readmePath) || File::get($readmePath) !== $readmeContent) {
  582. File::put($readmePath, $readmeContent);
  583. $this->debug("已创建/更新README.md文件: {$readmePath}");
  584. }
  585. // 生成SQL文件,使用无前缀的表名作为文件名
  586. $sqlFile = "{$sqlDir}/{$tableNameWithoutPrefix}.sql";
  587. // 构建SQL文件内容,添加Model信息和禁止修改声明
  588. $sqlContent = "-- ******************************************************************\n";
  589. $sqlContent .= "-- 表 {$tableName} 的创建SQL\n";
  590. if ($modelClass) {
  591. $sqlContent .= "-- 对应的Model: {$modelClass}\n";
  592. }
  593. $sqlContent .= "-- 警告: 此文件由系统自动生成,禁止修改!\n";
  594. $sqlContent .= "-- ******************************************************************\n\n";
  595. // $sqlContent .= "DROP TABLE IF EXISTS `{$tableName}`;\n";
  596. $sqlContent .= "{$createSQL};\n";
  597. File::put($sqlFile, $sqlContent);
  598. $this->debug("已生成SQL: {$sqlFile} (无前缀表名: {$tableNameWithoutPrefix})");
  599. }
  600. }