CleanupDataCommand.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. <?php
  2. namespace App\Module\Cleanup\Commands;
  3. use App\Module\Cleanup\Services\CleanupService;
  4. use App\Module\Cleanup\Models\CleanupPlan;
  5. use App\Module\Cleanup\Models\CleanupTask;
  6. use App\Module\Cleanup\Enums\PLAN_TYPE;
  7. use App\Module\Cleanup\Enums\TASK_STATUS;
  8. use Illuminate\Console\Command;
  9. /**
  10. * 数据清理命令
  11. *
  12. * 提供完整的数据清理命令行接口
  13. */
  14. class CleanupDataCommand extends Command
  15. {
  16. /**
  17. * 命令签名
  18. */
  19. protected $signature = 'cleanup:data
  20. {action : 操作类型: create-plan|show-plan|create-task|execute|status|preview}
  21. {id? : 计划ID或任务ID}
  22. {--name= : 计划或任务名称}
  23. {--type= : 计划类型: 1=全量,2=模块,3=分类,4=自定义,5=混合}
  24. {--modules= : 目标模块列表(逗号分隔)}
  25. {--categories= : 目标分类列表(逗号分隔)}
  26. {--tables= : 目标表列表(逗号分隔)}
  27. {--exclude-tables= : 排除表列表(逗号分隔)}
  28. {--exclude-modules= : 排除模块列表(逗号分隔)}
  29. {--execute : 创建任务后立即执行}
  30. {--dry-run : 预演模式,不实际执行}
  31. {--force : 强制执行,跳过确认}';
  32. /**
  33. * 命令描述
  34. */
  35. protected $description = '数据清理管理命令';
  36. /**
  37. * 执行命令
  38. */
  39. public function handle(): int
  40. {
  41. $action = $this->argument('action');
  42. try {
  43. switch ($action) {
  44. case 'create-plan':
  45. return $this->createPlan();
  46. case 'show-plan':
  47. return $this->showPlan();
  48. case 'create-task':
  49. return $this->createTask();
  50. case 'execute':
  51. return $this->executeTask();
  52. case 'status':
  53. return $this->showStatus();
  54. case 'preview':
  55. return $this->previewCleanup();
  56. default:
  57. $this->error("未知的操作类型: {$action}");
  58. $this->showHelp();
  59. return Command::FAILURE;
  60. }
  61. } catch (\Exception $e) {
  62. $this->error('❌ 操作失败: ' . $e->getMessage());
  63. if ($this->getOutput()->isVerbose()) {
  64. $this->error($e->getTraceAsString());
  65. }
  66. return Command::FAILURE;
  67. }
  68. }
  69. /**
  70. * 创建清理计划
  71. */
  72. private function createPlan(): int
  73. {
  74. $this->info('📋 创建清理计划');
  75. $this->newLine();
  76. // 获取计划参数
  77. $planName = $this->option('name') ?: $this->ask('请输入计划名称');
  78. $planType = $this->option('type') ?: $this->choice(
  79. '请选择计划类型',
  80. PLAN_TYPE::getOptions(),
  81. PLAN_TYPE::CUSTOM->value
  82. );
  83. $planData = [
  84. 'plan_name' => $planName,
  85. 'plan_type' => (int)$planType,
  86. 'description' => '通过命令行创建的清理计划',
  87. ];
  88. // 根据计划类型配置目标选择
  89. $targetSelection = $this->buildTargetSelection((int)$planType);
  90. if ($targetSelection) {
  91. $planData['target_selection'] = $targetSelection;
  92. }
  93. // 创建计划
  94. $result = CleanupService::createCleanupPlan($planData);
  95. if ($result['success']) {
  96. $this->info("✅ 清理计划创建成功!");
  97. $this->info("计划ID: {$result['plan_id']}");
  98. $this->info("计划名称: {$planName}");
  99. // 生成计划内容
  100. $this->info('正在生成计划内容...');
  101. $contentResult = CleanupService::generatePlanContents($result['plan_id']);
  102. if ($contentResult['success']) {
  103. $this->info("✅ 计划内容生成成功!包含 {$contentResult['total_tables']} 个表");
  104. } else {
  105. $this->warn("⚠️ 计划内容生成失败: " . $contentResult['message']);
  106. }
  107. } else {
  108. $this->error("❌ 计划创建失败: " . $result['message']);
  109. return Command::FAILURE;
  110. }
  111. return Command::SUCCESS;
  112. }
  113. /**
  114. * 显示计划详情
  115. */
  116. private function showPlan(): int
  117. {
  118. $planId = $this->argument('id');
  119. if (!$planId) {
  120. $this->error('请提供计划ID');
  121. return Command::FAILURE;
  122. }
  123. $plan = CleanupPlan::with(['contents', 'tasks'])->find($planId);
  124. if (!$plan) {
  125. $this->error("计划 {$planId} 不存在");
  126. return Command::FAILURE;
  127. }
  128. $this->info("📋 计划详情 (ID: {$planId})");
  129. $this->newLine();
  130. // 基本信息
  131. $this->table(
  132. ['属性', '值'],
  133. [
  134. ['计划名称', $plan->plan_name],
  135. ['计划类型', $plan->plan_type_name],
  136. ['状态', $plan->enabled_text],
  137. ['创建时间', $plan->created_at->format('Y-m-d H:i:s')],
  138. ['内容数量', $plan->contents_count],
  139. ['任务数量', $plan->tasks_count],
  140. ]
  141. );
  142. // 目标选择
  143. if ($plan->target_selection) {
  144. $this->newLine();
  145. $this->info('🎯 目标选择:');
  146. $this->line(json_encode($plan->target_selection, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
  147. }
  148. // 计划内容
  149. if ($plan->contents->isNotEmpty()) {
  150. $this->newLine();
  151. $this->info('📝 计划内容:');
  152. $contentData = [];
  153. foreach ($plan->contents as $content) {
  154. $contentData[] = [
  155. $content->table_name,
  156. $content->cleanup_type_name,
  157. $content->priority,
  158. $content->enabled_text,
  159. $content->backup_text,
  160. ];
  161. }
  162. $this->table(
  163. ['表名', '清理类型', '优先级', '状态', '备份'],
  164. $contentData
  165. );
  166. }
  167. return Command::SUCCESS;
  168. }
  169. /**
  170. * 创建清理任务
  171. */
  172. private function createTask(): int
  173. {
  174. $planId = $this->argument('id');
  175. if (!$planId) {
  176. $this->error('请提供计划ID');
  177. return Command::FAILURE;
  178. }
  179. $plan = CleanupPlan::find($planId);
  180. if (!$plan) {
  181. $this->error("计划 {$planId} 不存在");
  182. return Command::FAILURE;
  183. }
  184. $this->info("🚀 基于计划 '{$plan->plan_name}' 创建清理任务");
  185. $this->newLine();
  186. $taskOptions = [
  187. 'task_name' => $this->option('name') ?: $plan->plan_name . '_' . date('Ymd_His'),
  188. 'execute_immediately' => $this->option('execute'),
  189. ];
  190. $result = CleanupService::createCleanupTask($planId, $taskOptions);
  191. if ($result['success']) {
  192. $this->info("✅ 清理任务创建成功!");
  193. $this->info("任务ID: {$result['task_id']}");
  194. $this->info("任务名称: {$taskOptions['task_name']}");
  195. if ($this->option('execute')) {
  196. $this->info('正在执行清理任务...');
  197. return $this->executeTaskById($result['task_id']);
  198. }
  199. } else {
  200. $this->error("❌ 任务创建失败: " . $result['message']);
  201. return Command::FAILURE;
  202. }
  203. return Command::SUCCESS;
  204. }
  205. /**
  206. * 执行清理任务
  207. */
  208. private function executeTask(): int
  209. {
  210. $taskId = $this->argument('id');
  211. if (!$taskId) {
  212. $this->error('请提供任务ID');
  213. return Command::FAILURE;
  214. }
  215. return $this->executeTaskById($taskId);
  216. }
  217. /**
  218. * 执行指定的清理任务
  219. */
  220. private function executeTaskById(int $taskId): int
  221. {
  222. $task = CleanupTask::find($taskId);
  223. if (!$task) {
  224. $this->error("任务 {$taskId} 不存在");
  225. return Command::FAILURE;
  226. }
  227. $this->info("🚀 执行清理任务: {$task->task_name}");
  228. $this->newLine();
  229. $dryRun = $this->option('dry-run');
  230. $force = $this->option('force');
  231. if ($dryRun) {
  232. $this->warn('⚠️ 预演模式:不会实际执行清理操作');
  233. }
  234. if (!$force && !$dryRun) {
  235. if (!$this->confirm('确认要执行此清理任务吗?此操作不可撤销!')) {
  236. $this->info('操作已取消');
  237. return Command::SUCCESS;
  238. }
  239. }
  240. $result = CleanupService::executeCleanupTask($taskId, $dryRun);
  241. if ($result['success']) {
  242. $this->info("✅ 任务执行成功!");
  243. $this->displayExecutionResult($result);
  244. } else {
  245. $this->error("❌ 任务执行失败: " . $result['message']);
  246. return Command::FAILURE;
  247. }
  248. return Command::SUCCESS;
  249. }
  250. /**
  251. * 显示任务状态
  252. */
  253. private function showStatus(): int
  254. {
  255. $taskId = $this->argument('id');
  256. if ($taskId) {
  257. return $this->showTaskStatus($taskId);
  258. } else {
  259. return $this->showOverallStatus();
  260. }
  261. }
  262. /**
  263. * 显示特定任务状态
  264. */
  265. private function showTaskStatus(int $taskId): int
  266. {
  267. $task = CleanupTask::with('plan')->find($taskId);
  268. if (!$task) {
  269. $this->error("任务 {$taskId} 不存在");
  270. return Command::FAILURE;
  271. }
  272. $this->info("📊 任务状态 (ID: {$taskId})");
  273. $this->newLine();
  274. $this->table(
  275. ['属性', '值'],
  276. [
  277. ['任务名称', $task->task_name],
  278. ['关联计划', $task->plan->plan_name],
  279. ['状态', $task->status_name],
  280. ['进度', $task->progress_text],
  281. ['当前步骤', $task->current_step ?: '-'],
  282. ['总表数', $task->total_tables],
  283. ['已处理表数', $task->processed_tables],
  284. ['总记录数', number_format($task->total_records)],
  285. ['已删除记录数', number_format($task->deleted_records)],
  286. ['执行时间', $task->execution_time_formatted],
  287. ['开始时间', $task->started_at ? $task->started_at->format('Y-m-d H:i:s') : '-'],
  288. ['完成时间', $task->completed_at ? $task->completed_at->format('Y-m-d H:i:s') : '-'],
  289. ]
  290. );
  291. if ($task->error_message) {
  292. $this->newLine();
  293. $this->error('错误信息: ' . $task->error_message);
  294. }
  295. return Command::SUCCESS;
  296. }
  297. /**
  298. * 显示整体状态
  299. */
  300. private function showOverallStatus(): int
  301. {
  302. $this->info('📊 系统整体状态');
  303. $this->newLine();
  304. $stats = CleanupService::getCleanupStats();
  305. $health = CleanupService::getSystemHealth();
  306. // 显示健康状态
  307. $healthColor = match($health['health']) {
  308. 'good' => 'green',
  309. 'warning' => 'yellow',
  310. 'critical' => 'red',
  311. default => 'white',
  312. };
  313. $this->line("<fg={$healthColor}>系统健康状态: " . strtoupper($health['health']) . "</>");
  314. if (!empty($health['issues'])) {
  315. $this->newLine();
  316. $this->warn('⚠️ 发现的问题:');
  317. foreach ($health['issues'] as $issue) {
  318. $this->line("• {$issue}");
  319. }
  320. }
  321. // 显示统计信息
  322. $this->newLine();
  323. $this->info('📈 统计信息:');
  324. $this->table(
  325. ['类型', '总数', '详细信息'],
  326. [
  327. ['计划', $stats['plans']['total'], "启用: {$stats['plans']['enabled']}, 禁用: {$stats['plans']['disabled']}"],
  328. ['任务', array_sum(array_column($stats['tasks'], 'count')), $this->formatTaskStats($stats['tasks'])],
  329. ['备份', array_sum(array_column($stats['backups'], 'count')), $this->formatBackupStats($stats['backups'])],
  330. ]
  331. );
  332. return Command::SUCCESS;
  333. }
  334. /**
  335. * 预览清理结果
  336. */
  337. private function previewCleanup(): int
  338. {
  339. $id = $this->argument('id');
  340. if (!$id) {
  341. $this->error('请提供计划ID或任务ID');
  342. return Command::FAILURE;
  343. }
  344. // 尝试作为计划ID
  345. $plan = CleanupPlan::find($id);
  346. if ($plan) {
  347. $result = CleanupService::previewPlanCleanup($id);
  348. $this->info("📋 计划预览: {$plan->plan_name}");
  349. } else {
  350. // 尝试作为任务ID
  351. $task = CleanupTask::find($id);
  352. if ($task) {
  353. $result = CleanupService::previewTaskCleanup($id);
  354. $this->info("🚀 任务预览: {$task->task_name}");
  355. } else {
  356. $this->error("计划或任务 {$id} 不存在");
  357. return Command::FAILURE;
  358. }
  359. }
  360. $this->newLine();
  361. $this->displayPreviewResult($result);
  362. return Command::SUCCESS;
  363. }
  364. /**
  365. * 构建目标选择配置
  366. */
  367. private function buildTargetSelection(int $planType): ?array
  368. {
  369. $planTypeEnum = PLAN_TYPE::from($planType);
  370. if (!$planTypeEnum->needsTargetSelection()) {
  371. return null;
  372. }
  373. $selection = [];
  374. switch ($planTypeEnum) {
  375. case PLAN_TYPE::MODULE:
  376. $modules = $this->option('modules');
  377. if ($modules) {
  378. $selection = [
  379. 'selection_type' => 'module',
  380. 'modules' => explode(',', $modules),
  381. ];
  382. }
  383. break;
  384. case PLAN_TYPE::CATEGORY:
  385. $categories = $this->option('categories');
  386. if ($categories) {
  387. $selection = [
  388. 'selection_type' => 'category',
  389. 'categories' => array_map('intval', explode(',', $categories)),
  390. ];
  391. }
  392. break;
  393. case PLAN_TYPE::CUSTOM:
  394. $tables = $this->option('tables');
  395. if ($tables) {
  396. $selection = [
  397. 'selection_type' => 'custom',
  398. 'tables' => explode(',', $tables),
  399. ];
  400. }
  401. break;
  402. case PLAN_TYPE::MIXED:
  403. $selection = ['selection_type' => 'mixed'];
  404. if ($modules = $this->option('modules')) {
  405. $selection['modules'] = explode(',', $modules);
  406. }
  407. if ($categories = $this->option('categories')) {
  408. $selection['categories'] = array_map('intval', explode(',', $categories));
  409. }
  410. if ($tables = $this->option('tables')) {
  411. $selection['tables'] = explode(',', $tables);
  412. }
  413. break;
  414. }
  415. // 添加排除选项
  416. if ($excludeTables = $this->option('exclude-tables')) {
  417. $selection['exclude_tables'] = explode(',', $excludeTables);
  418. }
  419. if ($excludeModules = $this->option('exclude-modules')) {
  420. $selection['exclude_modules'] = explode(',', $excludeModules);
  421. }
  422. return empty($selection) ? null : $selection;
  423. }
  424. /**
  425. * 显示执行结果
  426. */
  427. private function displayExecutionResult(array $result): void
  428. {
  429. $this->table(
  430. ['项目', '值'],
  431. [
  432. ['处理表数', $result['processed_tables'] ?? 0],
  433. ['删除记录数', number_format($result['deleted_records'] ?? 0)],
  434. ['执行时间', $result['execution_time'] ?? '0 秒'],
  435. ['备份大小', $result['backup_size'] ?? '0 B'],
  436. ]
  437. );
  438. }
  439. /**
  440. * 显示预览结果
  441. */
  442. private function displayPreviewResult(array $result): void
  443. {
  444. $this->table(
  445. ['项目', '值'],
  446. [
  447. ['总表数', $result['total_tables'] ?? 0],
  448. ['总记录数', number_format($result['total_records'] ?? 0)],
  449. ['预计删除记录数', number_format($result['estimated_deleted_records'] ?? 0)],
  450. ['预计释放空间', $result['estimated_size_mb'] ?? '0 MB'],
  451. ['预计执行时间', $result['estimated_time_seconds'] ?? '0 秒'],
  452. ]
  453. );
  454. if (!empty($result['tables_preview'])) {
  455. $this->newLine();
  456. $this->info('📋 表详细预览:');
  457. $tableData = [];
  458. foreach ($result['tables_preview'] as $table) {
  459. $tableData[] = [
  460. $table['table_name'],
  461. number_format($table['current_records']),
  462. number_format($table['records_to_delete']),
  463. $table['size_to_free_mb'] . ' MB',
  464. $table['cleanup_type'],
  465. ];
  466. }
  467. $this->table(
  468. ['表名', '当前记录数', '将删除记录数', '释放空间', '清理类型'],
  469. $tableData
  470. );
  471. }
  472. }
  473. /**
  474. * 格式化任务统计
  475. */
  476. private function formatTaskStats(array $stats): string
  477. {
  478. $parts = [];
  479. foreach ($stats as $status => $info) {
  480. if ($info['count'] > 0) {
  481. $parts[] = "{$info['name']}: {$info['count']}";
  482. }
  483. }
  484. return implode(', ', $parts);
  485. }
  486. /**
  487. * 格式化备份统计
  488. */
  489. private function formatBackupStats(array $stats): string
  490. {
  491. $parts = [];
  492. foreach ($stats as $status => $info) {
  493. if ($info['count'] > 0) {
  494. $parts[] = "{$info['name']}: {$info['count']}";
  495. }
  496. }
  497. return implode(', ', $parts);
  498. }
  499. /**
  500. * 显示帮助信息
  501. */
  502. private function showHelp(): void
  503. {
  504. $this->newLine();
  505. $this->info('📖 使用示例:');
  506. $this->line('');
  507. $this->line('# 创建清理计划');
  508. $this->line('php artisan cleanup:data create-plan --name="农场模块清理" --type=2 --modules=Farm');
  509. $this->line('');
  510. $this->line('# 查看计划详情');
  511. $this->line('php artisan cleanup:data show-plan 1');
  512. $this->line('');
  513. $this->line('# 创建并执行任务');
  514. $this->line('php artisan cleanup:data create-task 1 --execute');
  515. $this->line('');
  516. $this->line('# 预览清理结果');
  517. $this->line('php artisan cleanup:data preview 1');
  518. $this->line('');
  519. $this->line('# 查看任务状态');
  520. $this->line('php artisan cleanup:data status 1');
  521. }
  522. }