FixLandStatusCommand.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. <?php
  2. namespace App\Module\Farm\Commands;
  3. use App\Module\Farm\Enums\GROWTH_STAGE;
  4. use App\Module\Farm\Enums\LAND_STATUS;
  5. use App\Module\Farm\Models\FarmCrop;
  6. use App\Module\Farm\Models\FarmLand;
  7. use Illuminate\Console\Command;
  8. use Illuminate\Support\Facades\DB;
  9. use Illuminate\Support\Facades\Log;
  10. /**
  11. * 修复农场土地状态命令
  12. *
  13. * 修复土地状态与作物生长阶段不一致的问题
  14. * php artisan farm:fix-land-status --dry-run
  15. */
  16. class FixLandStatusCommand extends Command
  17. {
  18. /**
  19. * 命令签名
  20. *
  21. * @var string
  22. */
  23. protected $signature = 'farm:fix-land-status
  24. {--dry-run : 仅显示需要修复的数据,不执行修复}
  25. {--user= : 指定用户ID,只修复该用户的数据}
  26. {--limit=100 : 每批处理的数量}';
  27. /**
  28. * 命令描述
  29. *
  30. * @var string
  31. */
  32. protected $description = '修复农场土地状态与作物生长阶段不一致的问题';
  33. /**
  34. * 执行命令
  35. *
  36. * @return int
  37. */
  38. public function handle(): int
  39. {
  40. $dryRun = $this->option('dry-run');
  41. $userId = $this->option('user');
  42. $limit = (int) $this->option('limit');
  43. $this->info('开始检查土地状态与作物生长阶段的一致性...');
  44. if ($dryRun) {
  45. $this->warn('运行在模拟模式,不会实际修改数据');
  46. }
  47. try {
  48. // 获取需要修复的数据
  49. $inconsistentData = $this->getInconsistentData($userId, $limit);
  50. if ($inconsistentData->isEmpty()) {
  51. $this->info('没有发现需要修复的数据');
  52. return 0;
  53. }
  54. $this->info("发现 {$inconsistentData->count()} 条需要修复的数据");
  55. // 按问题类型分组
  56. $groupedData = $inconsistentData->groupBy('problem_type');
  57. foreach ($groupedData as $problemType => $items) {
  58. $this->info("\n=== {$problemType} ({$items->count()} 条) ===");
  59. if ($dryRun) {
  60. $this->displayProblems($items);
  61. } else {
  62. $this->fixProblems($items, $problemType);
  63. }
  64. }
  65. if (!$dryRun) {
  66. $this->info("\n修复完成!");
  67. }
  68. return 0;
  69. } catch (\Exception $e) {
  70. $this->error("修复过程中发生错误: {$e->getMessage()}");
  71. Log::error('土地状态修复失败', [
  72. 'error' => $e->getMessage(),
  73. 'trace' => $e->getTraceAsString()
  74. ]);
  75. return 1;
  76. }
  77. }
  78. /**
  79. * 获取状态不一致的数据
  80. *
  81. * @param string|null $userId
  82. * @param int $limit
  83. * @return \Illuminate\Support\Collection
  84. */
  85. private function getInconsistentData(?string $userId, int $limit): \Illuminate\Support\Collection
  86. {
  87. $query = DB::table('farm_land_users as fl')
  88. ->join('farm_crops as fc', 'fl.id', '=', 'fc.land_id')
  89. ->select([
  90. 'fl.id as land_id',
  91. 'fl.user_id',
  92. 'fl.status as land_status',
  93. 'fc.id as crop_id',
  94. 'fc.growth_stage',
  95. 'fc.disasters'
  96. ])
  97. ->whereNull('fc.deleted_at') // 排除软删除的作物
  98. ->where(function ($query) {
  99. $query
  100. // 作物成熟但土地状态不是可收获
  101. ->where(function ($q) {
  102. $q->where('fc.growth_stage', GROWTH_STAGE::MATURE->value)
  103. ->where('fl.status', '!=', LAND_STATUS::HARVESTABLE->value);
  104. })
  105. // 作物枯萎但土地状态不是枯萎
  106. ->orWhere(function ($q) {
  107. $q->where('fc.growth_stage', GROWTH_STAGE::WITHERED->value)
  108. ->where('fl.status', '!=', LAND_STATUS::WITHERED->value);
  109. })
  110. // 作物未成熟但土地状态是可收获
  111. ->orWhere(function ($q) {
  112. $q->whereNotIn('fc.growth_stage', [GROWTH_STAGE::MATURE->value, GROWTH_STAGE::WITHERED->value])
  113. ->where('fl.status', LAND_STATUS::HARVESTABLE->value);
  114. })
  115. // 作物未枯萎但土地状态是枯萎
  116. ->orWhere(function ($q) {
  117. $q->where('fc.growth_stage', '!=', GROWTH_STAGE::WITHERED->value)
  118. ->where('fl.status', LAND_STATUS::WITHERED->value);
  119. });
  120. });
  121. if ($userId) {
  122. $query->where('fl.user_id', $userId);
  123. }
  124. $results = $query->limit($limit)->get();
  125. // 添加问题类型标识
  126. return $results->map(function ($item) {
  127. $item->problem_type = $this->getProblemType($item);
  128. $item->active_disasters_count = $this->countActiveDisasters($item->disasters);
  129. return $item;
  130. });
  131. }
  132. /**
  133. * 获取问题类型
  134. *
  135. * @param object $item
  136. * @return string
  137. */
  138. private function getProblemType(object $item): string
  139. {
  140. if ($item->growth_stage == GROWTH_STAGE::MATURE->value && $item->land_status != LAND_STATUS::HARVESTABLE->value) {
  141. return '作物成熟但土地非可收获';
  142. }
  143. if ($item->growth_stage == GROWTH_STAGE::WITHERED->value && $item->land_status != LAND_STATUS::WITHERED->value) {
  144. return '作物枯萎但土地非枯萎';
  145. }
  146. if (!in_array($item->growth_stage, [GROWTH_STAGE::MATURE->value, GROWTH_STAGE::WITHERED->value]) && $item->land_status == LAND_STATUS::HARVESTABLE->value) {
  147. return '作物未成熟但土地可收获';
  148. }
  149. if ($item->growth_stage != GROWTH_STAGE::WITHERED->value && $item->land_status == LAND_STATUS::WITHERED->value) {
  150. return '作物未枯萎但土地枯萎';
  151. }
  152. return '其他不一致';
  153. }
  154. /**
  155. * 统计活跃灾害数量
  156. *
  157. * @param string|null $disasters
  158. * @return int
  159. */
  160. private function countActiveDisasters(?string $disasters): int
  161. {
  162. if (empty($disasters)) {
  163. return 0;
  164. }
  165. $disasterArray = json_decode($disasters, true);
  166. if (!is_array($disasterArray)) {
  167. return 0;
  168. }
  169. return count(array_filter($disasterArray, function ($disaster) {
  170. return ($disaster['status'] ?? '') === 'active';
  171. }));
  172. }
  173. /**
  174. * 显示问题数据
  175. *
  176. * @param \Illuminate\Support\Collection $items
  177. * @return void
  178. */
  179. private function displayProblems(\Illuminate\Support\Collection $items): void
  180. {
  181. $headers = ['土地ID', '用户ID', '土地状态', '作物ID', '生长阶段', '活跃灾害数'];
  182. $rows = [];
  183. foreach ($items as $item) {
  184. $rows[] = [
  185. $item->land_id,
  186. $item->user_id,
  187. $this->getLandStatusName($item->land_status),
  188. $item->crop_id,
  189. $this->getGrowthStageName($item->growth_stage),
  190. $item->active_disasters_count
  191. ];
  192. }
  193. $this->table($headers, $rows);
  194. }
  195. /**
  196. * 修复问题数据
  197. *
  198. * @param \Illuminate\Support\Collection $items
  199. * @param string $problemType
  200. * @return void
  201. */
  202. private function fixProblems(\Illuminate\Support\Collection $items, string $problemType): void
  203. {
  204. $fixedCount = 0;
  205. $failedCount = 0;
  206. foreach ($items as $item) {
  207. try {
  208. DB::transaction(function () use ($item, $problemType) {
  209. $this->fixSingleItem($item, $problemType);
  210. });
  211. $fixedCount++;
  212. $this->info("✓ 修复土地 {$item->land_id} (用户 {$item->user_id})");
  213. } catch (\Exception $e) {
  214. $failedCount++;
  215. $this->error("✗ 修复土地 {$item->land_id} 失败: {$e->getMessage()}");
  216. Log::error('单个土地状态修复失败', [
  217. 'land_id' => $item->land_id,
  218. 'user_id' => $item->user_id,
  219. 'problem_type' => $problemType,
  220. 'error' => $e->getMessage()
  221. ]);
  222. }
  223. }
  224. $this->info("修复完成: 成功 {$fixedCount} 条,失败 {$failedCount} 条");
  225. }
  226. /**
  227. * 修复单个数据项
  228. *
  229. * @param object $item
  230. * @param string $problemType
  231. * @return void
  232. */
  233. private function fixSingleItem(object $item, string $problemType): void
  234. {
  235. $land = FarmLand::lockForUpdate()->find($item->land_id);
  236. $crop = FarmCrop::lockForUpdate()->find($item->crop_id);
  237. if (!$land || !$crop) {
  238. throw new \Exception('土地或作物不存在');
  239. }
  240. $newStatus = $this->calculateCorrectLandStatus($crop);
  241. if ($land->status != $newStatus) {
  242. $oldStatus = $land->status;
  243. $land->status = $newStatus;
  244. $land->updateHasCrop();
  245. $land->save();
  246. Log::info('土地状态修复', [
  247. 'land_id' => $land->id,
  248. 'user_id' => $land->user_id,
  249. 'crop_id' => $crop->id,
  250. 'old_status' => $oldStatus,
  251. 'new_status' => $newStatus,
  252. 'growth_stage' => $crop->growth_stage,
  253. 'problem_type' => $problemType,
  254. 'active_disasters' => $this->countActiveDisasters($crop->disasters)
  255. ]);
  256. }
  257. }
  258. /**
  259. * 计算正确的土地状态
  260. *
  261. * @param FarmCrop $crop
  262. * @return int
  263. */
  264. private function calculateCorrectLandStatus(FarmCrop $crop): int
  265. {
  266. // 根据作物生长阶段确定土地状态
  267. switch ($crop->growth_stage) {
  268. case GROWTH_STAGE::MATURE->value:
  269. // 作物成熟,土地状态为可收获
  270. return LAND_STATUS::HARVESTABLE->value;
  271. case GROWTH_STAGE::WITHERED->value:
  272. // 作物枯萎,土地状态为枯萎
  273. return LAND_STATUS::WITHERED->value;
  274. default:
  275. // 其他阶段,检查是否有活跃灾害
  276. $activeDisasters = $this->countActiveDisasters($crop->disasters);
  277. if ($activeDisasters > 0) {
  278. return LAND_STATUS::DISASTER->value;
  279. } else {
  280. return LAND_STATUS::PLANTING->value;
  281. }
  282. }
  283. }
  284. /**
  285. * 获取土地状态名称
  286. *
  287. * @param int $status
  288. * @return string
  289. */
  290. private function getLandStatusName(int $status): string
  291. {
  292. return match ($status) {
  293. LAND_STATUS::IDLE->value => '空闲',
  294. LAND_STATUS::PLANTING->value => '种植中',
  295. LAND_STATUS::DISASTER->value => '灾害',
  296. LAND_STATUS::HARVESTABLE->value => '可收获',
  297. LAND_STATUS::WITHERED->value => '枯萎',
  298. default => '未知'
  299. };
  300. }
  301. /**
  302. * 获取生长阶段名称
  303. *
  304. * @param int $stage
  305. * @return string
  306. */
  307. private function getGrowthStageName(int $stage): string
  308. {
  309. return match ($stage) {
  310. GROWTH_STAGE::SEED->value => '种子期',
  311. GROWTH_STAGE::SPROUT->value => '发芽期',
  312. GROWTH_STAGE::GROWTH->value => '生长期',
  313. GROWTH_STAGE::FRUIT->value => '果实期',
  314. GROWTH_STAGE::MATURE->value => '成熟期',
  315. GROWTH_STAGE::WITHERED->value => '枯萎期',
  316. default => '未知'
  317. };
  318. }
  319. }