FixExcessiveDisastersCommand.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. <?php
  2. namespace App\Module\Farm\Commands;
  3. use App\Module\Farm\Models\FarmCrop;
  4. use Illuminate\Console\Command;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\Log;
  7. /**
  8. * 修复作物异常灾害数据命令
  9. *
  10. * 清理由于事务问题导致的作物灾害数量异常数据
  11. * php artisan farm:fix-excessive-disasters --dry-run
  12. * php artisan farm:fix-excessive-disasters
  13. */
  14. class FixExcessiveDisastersCommand extends Command
  15. {
  16. /**
  17. * 命令名称
  18. *
  19. * @var string
  20. */
  21. protected $signature = 'farm:fix-excessive-disasters {--dry-run : 仅显示需要修复的数据,不实际执行修复}';
  22. /**
  23. * 命令描述
  24. *
  25. * @var string
  26. */
  27. protected $description = '修复作物异常灾害数据(清理超过3个灾害的异常数据)';
  28. /**
  29. * 执行命令
  30. *
  31. * @return int
  32. */
  33. public function handle()
  34. {
  35. $isDryRun = $this->option('dry-run');
  36. if ($isDryRun) {
  37. $this->info('=== 干运行模式:仅显示需要修复的数据 ===');
  38. } else {
  39. $this->info('=== 开始修复作物异常灾害数据 ===');
  40. }
  41. try {
  42. // 查找有异常灾害数量的作物(超过9个灾害为异常)
  43. // 只处理未成熟的作物:发芽期(20)、生长期(30)、果实期(35)
  44. $excessiveCrops = FarmCrop::whereRaw('JSON_LENGTH(disasters) > 9')
  45. ->where('deleted_at', null)
  46. ->whereIn('growth_stage', [20, 30, 35]) // 只处理未成熟的作物
  47. ->get();
  48. $totalCount = $excessiveCrops->count();
  49. $this->info("发现 {$totalCount} 个作物有异常灾害数据");
  50. if ($totalCount === 0) {
  51. $this->info('没有发现需要修复的数据');
  52. return 0;
  53. }
  54. // 显示统计信息
  55. $this->displayStatistics($excessiveCrops);
  56. if ($isDryRun) {
  57. $this->info('=== 干运行完成,未执行实际修复 ===');
  58. return 0;
  59. }
  60. // 确认是否继续
  61. if (!$this->confirm('是否继续执行修复?这将清理异常的灾害数据')) {
  62. $this->info('用户取消操作');
  63. return 0;
  64. }
  65. // 执行修复
  66. $fixedCount = $this->fixExcessiveDisasters($excessiveCrops);
  67. $this->info("=== 修复完成 ===");
  68. $this->info("总共修复了 {$fixedCount} 个作物的异常灾害数据");
  69. Log::info('作物异常灾害数据修复完成', [
  70. 'total_crops' => $totalCount,
  71. 'fixed_crops' => $fixedCount
  72. ]);
  73. return 0;
  74. } catch (\Exception $e) {
  75. $this->error('修复失败: ' . $e->getMessage());
  76. Log::error('作物异常灾害数据修复失败', [
  77. 'error' => $e->getMessage(),
  78. 'trace' => $e->getTraceAsString()
  79. ]);
  80. return 1;
  81. }
  82. }
  83. /**
  84. * 显示统计信息
  85. *
  86. * @param \Illuminate\Support\Collection $crops
  87. */
  88. private function displayStatistics($crops)
  89. {
  90. $this->info('=== 异常数据统计 ===');
  91. // 按灾害数量分组统计
  92. $disasterCounts = $crops->groupBy(function ($crop) {
  93. return count($crop->disasters ?? []);
  94. })->map(function ($group) {
  95. return $group->count();
  96. })->sortKeys();
  97. $this->table(
  98. ['灾害数量', '作物数量'],
  99. $disasterCounts->map(function ($count, $disasters) {
  100. return [$disasters, $count];
  101. })->toArray()
  102. );
  103. // 显示最严重的几个案例
  104. $worstCases = $crops->sortByDesc(function ($crop) {
  105. return count($crop->disasters ?? []);
  106. })->take(5);
  107. $this->info('=== 最严重的5个案例 ===');
  108. $this->table(
  109. ['作物ID', '用户ID', '生长阶段', '灾害数量', '最后检查时间'],
  110. $worstCases->map(function ($crop) {
  111. return [
  112. $crop->id,
  113. $crop->user_id,
  114. is_object($crop->growth_stage) ? $crop->growth_stage->value : $crop->growth_stage,
  115. count($crop->disasters ?? []),
  116. $crop->last_disaster_check_time ?? 'NULL'
  117. ];
  118. })->toArray()
  119. );
  120. }
  121. /**
  122. * 修复异常灾害数据
  123. *
  124. * @param \Illuminate\Support\Collection $crops
  125. * @return int 修复的作物数量
  126. */
  127. private function fixExcessiveDisasters($crops): int
  128. {
  129. $fixedCount = 0;
  130. $progressBar = $this->output->createProgressBar($crops->count());
  131. $progressBar->start();
  132. foreach ($crops as $crop) {
  133. try {
  134. // 为每个作物使用独立事务
  135. DB::transaction(function () use ($crop, &$fixedCount) {
  136. // 重新获取作物数据,确保数据一致性
  137. $lockedCrop = FarmCrop::where('id', $crop->id)
  138. ->lockForUpdate()
  139. ->first();
  140. if (!$lockedCrop || $lockedCrop->trashed()) {
  141. return; // 作物已被删除,跳过
  142. }
  143. $disasters = $lockedCrop->disasters ?? [];
  144. if (count($disasters) > 9) {
  145. // 策略:保留每个阶段每种类型的最新一个灾害,最多9个
  146. $fixedDisasters = $this->getOptimalDisasters($disasters);
  147. // 更新作物数据
  148. $lockedCrop->disasters = $fixedDisasters;
  149. $lockedCrop->can_disaster = false; // 设置为不能再产生灾害
  150. $lockedCrop->save();
  151. $fixedCount++;
  152. Log::info('修复作物异常灾害数据', [
  153. 'crop_id' => $lockedCrop->id,
  154. 'user_id' => $lockedCrop->user_id,
  155. 'growth_stage' => $lockedCrop->growth_stage,
  156. 'original_disaster_count' => count($disasters),
  157. 'fixed_disaster_count' => count($fixedDisasters),
  158. 'original_disasters' => array_column($disasters, 'type'),
  159. 'fixed_disasters' => array_column($fixedDisasters, 'type')
  160. ]);
  161. }
  162. });
  163. } catch (\Exception $e) {
  164. Log::error('修复单个作物失败', [
  165. 'crop_id' => $crop->id,
  166. 'error' => $e->getMessage(),
  167. 'trace' => $e->getTraceAsString()
  168. ]);
  169. }
  170. $progressBar->advance();
  171. }
  172. $progressBar->finish();
  173. $this->newLine();
  174. return $fixedCount;
  175. }
  176. /**
  177. * 获取最优的灾害组合
  178. *
  179. * 策略:每种灾害类型保留最新的几个,最多保留9个
  180. * 理论上:3个阶段(发芽期、生长期、果实期) × 3种灾害类型 = 9个灾害
  181. *
  182. * @param array $disasters
  183. * @return array
  184. */
  185. private function getOptimalDisasters(array $disasters): array
  186. {
  187. // 按灾害类型分组
  188. $disastersByType = [];
  189. foreach ($disasters as $disaster) {
  190. $type = $disaster['type'] ?? 0;
  191. if (!isset($disastersByType[$type])) {
  192. $disastersByType[$type] = [];
  193. }
  194. $disastersByType[$type][] = $disaster;
  195. }
  196. $fixedDisasters = [];
  197. // 对每种类型,保留最新的几个(最多3个,对应3个生长阶段)
  198. foreach ($disastersByType as $type => $typeDisasters) {
  199. // 按生成时间排序,取最新的几个
  200. usort($typeDisasters, function ($a, $b) {
  201. return strcmp($b['generated_ts'] ?? '', $a['generated_ts'] ?? '');
  202. });
  203. // 每种类型最多保留3个(对应3个可产生灾害的阶段)
  204. $keepCount = min(3, count($typeDisasters));
  205. for ($i = 0; $i < $keepCount; $i++) {
  206. $fixedDisasters[] = $typeDisasters[$i];
  207. }
  208. }
  209. // 如果还是超过9个,按生成时间保留最新的9个
  210. if (count($fixedDisasters) > 9) {
  211. usort($fixedDisasters, function ($a, $b) {
  212. return strcmp($b['generated_ts'] ?? '', $a['generated_ts'] ?? '');
  213. });
  214. $fixedDisasters = array_slice($fixedDisasters, 0, 9);
  215. }
  216. return $fixedDisasters;
  217. }
  218. }