TransferCleanCommand.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. <?php
  2. namespace App\Module\Transfer\Commands;
  3. use App\Module\Transfer\Models\TransferOrder;
  4. use App\Module\Transfer\Enums\TransferStatus;
  5. use Illuminate\Console\Command;
  6. use Illuminate\Support\Facades\DB;
  7. use Illuminate\Support\Facades\Log;
  8. use Carbon\Carbon;
  9. /**
  10. * 划转数据清理命令
  11. */
  12. class TransferCleanCommand extends Command
  13. {
  14. /**
  15. * 命令签名
  16. *
  17. * @var string
  18. */
  19. protected $signature = 'transfer:clean
  20. {--days=30 : 清理多少天前的数据}
  21. {--status= : 指定要清理的状态}
  22. {--limit=1000 : 每次处理的数量限制}
  23. {--dry-run : 仅显示将要清理的数据,不实际删除}
  24. {--force : 强制清理,跳过确认}';
  25. /**
  26. * 命令描述
  27. *
  28. * @var string
  29. */
  30. protected $description = '清理过期的划转订单数据';
  31. /**
  32. * 执行命令
  33. *
  34. * @return int
  35. */
  36. public function handle(): int
  37. {
  38. $this->info('开始清理划转订单数据...');
  39. try {
  40. // 获取命令参数
  41. $days = (int) $this->option('days');
  42. $status = $this->option('status');
  43. $limit = (int) $this->option('limit');
  44. $dryRun = $this->option('dry-run');
  45. $force = $this->option('force');
  46. if ($days <= 0) {
  47. $this->error('天数必须大于0');
  48. return 1;
  49. }
  50. // 计算截止日期
  51. $cutoffDate = Carbon::now()->subDays($days);
  52. $this->info("将清理 {$cutoffDate->format('Y-m-d H:i:s')} 之前的数据");
  53. // 构建查询
  54. $query = TransferOrder::where('created_at', '<', $cutoffDate);
  55. if ($status) {
  56. $statusEnum = TransferStatus::tryFrom((int) $status);
  57. if (!$statusEnum) {
  58. $this->error("无效的状态值: {$status}");
  59. $this->showStatusOptions();
  60. return 1;
  61. }
  62. $query->where('status', $statusEnum);
  63. $this->info("只清理状态为 {$statusEnum->getDescription()} 的订单");
  64. } else {
  65. // 默认只清理已完成或失败的订单
  66. $query->whereIn('status', [TransferStatus::COMPLETED, TransferStatus::FAILED]);
  67. $this->info('只清理已完成或失败的订单');
  68. }
  69. // 获取统计信息
  70. $totalCount = $query->count();
  71. if ($totalCount === 0) {
  72. $this->info('没有找到需要清理的数据');
  73. return 0;
  74. }
  75. $this->info("找到 {$totalCount} 条记录需要清理");
  76. // 显示详细统计
  77. $this->showStatistics($cutoffDate, $status);
  78. if ($dryRun) {
  79. $this->info('这是预览模式,不会实际删除数据');
  80. return 0;
  81. }
  82. // 确认操作
  83. if (!$force) {
  84. if (!$this->confirm("确定要删除这 {$totalCount} 条记录吗?此操作不可恢复!")) {
  85. $this->info('操作已取消');
  86. return 0;
  87. }
  88. }
  89. // 分批删除
  90. $deletedCount = 0;
  91. $batchCount = 0;
  92. while (true) {
  93. $batch = $query->limit($limit)->get();
  94. if ($batch->isEmpty()) {
  95. break;
  96. }
  97. $batchCount++;
  98. $this->info("处理第 {$batchCount} 批,共 {$batch->count()} 条记录...");
  99. DB::transaction(function () use ($batch, &$deletedCount) {
  100. foreach ($batch as $order) {
  101. // 记录删除日志
  102. Log::info('Transfer order cleaned', [
  103. 'order_id' => $order->id,
  104. 'type' => $order->type->value,
  105. 'status' => $order->status->value,
  106. 'amount' => $order->amount,
  107. 'created_at' => $order->created_at->toISOString()
  108. ]);
  109. $order->delete();
  110. $deletedCount++;
  111. }
  112. });
  113. $this->line("已删除 {$deletedCount} / {$totalCount} 条记录");
  114. // 避免内存泄漏
  115. unset($batch);
  116. // 短暂休息
  117. usleep(100000); // 0.1秒
  118. }
  119. $this->info("清理完成!共删除 {$deletedCount} 条记录");
  120. // 记录清理操作
  121. Log::info('Transfer clean command completed', [
  122. 'deleted_count' => $deletedCount,
  123. 'cutoff_date' => $cutoffDate->toISOString(),
  124. 'status_filter' => $status,
  125. 'days' => $days
  126. ]);
  127. return 0;
  128. } catch (\Exception $e) {
  129. $this->error("清理失败: {$e->getMessage()}");
  130. Log::error('Transfer clean command failed', [
  131. 'error' => $e->getMessage(),
  132. 'trace' => $e->getTraceAsString()
  133. ]);
  134. return 1;
  135. }
  136. }
  137. /**
  138. * 显示统计信息
  139. *
  140. * @param Carbon $cutoffDate
  141. * @param string|null $status
  142. * @return void
  143. */
  144. protected function showStatistics(Carbon $cutoffDate, ?string $status): void
  145. {
  146. $this->info('数据统计:');
  147. $baseQuery = TransferOrder::where('created_at', '<', $cutoffDate);
  148. if ($status) {
  149. $statusEnum = TransferStatus::tryFrom((int) $status);
  150. $baseQuery->where('status', $statusEnum);
  151. } else {
  152. $baseQuery->whereIn('status', [TransferStatus::COMPLETED, TransferStatus::FAILED]);
  153. }
  154. // 按状态统计
  155. $statusStats = [];
  156. foreach (TransferStatus::cases() as $statusCase) {
  157. $count = (clone $baseQuery)->where('status', $statusCase)->count();
  158. if ($count > 0) {
  159. $statusStats[] = " {$statusCase->getDescription()}: {$count} 条";
  160. }
  161. }
  162. if (!empty($statusStats)) {
  163. $this->line(implode("\n", $statusStats));
  164. }
  165. // 按类型统计
  166. $typeStats = (clone $baseQuery)
  167. ->select('type', DB::raw('count(*) as count'))
  168. ->groupBy('type')
  169. ->get();
  170. if ($typeStats->isNotEmpty()) {
  171. $this->line('按类型统计:');
  172. foreach ($typeStats as $stat) {
  173. $typeEnum = \App\Module\Transfer\Enums\TransferType::from($stat->type);
  174. $this->line(" {$typeEnum->getDescription()}: {$stat->count} 条");
  175. }
  176. }
  177. }
  178. /**
  179. * 显示状态选项
  180. *
  181. * @return void
  182. */
  183. protected function showStatusOptions(): void
  184. {
  185. $this->info('可用的状态值:');
  186. foreach (TransferStatus::cases() as $status) {
  187. $this->line(" {$status->value} - {$status->getDescription()}");
  188. }
  189. }
  190. }