Преглед изворни кода

refactor(farm): 重构灾害生成逻辑并优化性能

- 新增 DisasterLogic 类,将灾害生成相关逻辑移至专门的类中
- 在 FarmCrop 模型中添加 last_disaster_check_time 和 can_disaster 字段
- 修改 GenerateDisastersCommand,使用新的批量生成方法
- 更新 GenerateDisasterListener,只在作物进入发芽期或生长期时允许生成灾害
- 在 CropLogic 中初始化新种植作物的灾害检查时间和可生成灾害状态
- 优化 DisasterService,添加灾害类型键名和检查间隔时间的获取方法
- 调整定时任务,每小时执行一次灾害生成命令
notfff пре 7 месеци
родитељ
комит
88c1961828

+ 8 - 105
app/Module/Farm/Commands/GenerateDisastersCommand.php

@@ -2,14 +2,7 @@
 
 namespace App\Module\Farm\Commands;
 
-use App\Module\Farm\Enums\BUFF_TYPE;
-use App\Module\Farm\Enums\DISASTER_TYPE;
-use App\Module\Farm\Enums\GROWTH_STAGE;
-use App\Module\Farm\Enums\LAND_STATUS;
-use App\Module\Farm\Events\DisasterGeneratedEvent;
-use App\Module\Farm\Models\FarmCrop;
-use App\Module\Farm\Models\FarmGodBuff;
-use App\Module\Farm\Services\DisasterService;
+use App\Module\Farm\Logics\DisasterLogic;
 use Illuminate\Console\Command;
 use Illuminate\Support\Facades\Log;
 
@@ -42,91 +35,16 @@ class GenerateDisastersCommand extends Command
         $this->info('开始生成作物灾害...');
 
         try {
-            // 获取处于发芽期或生长期的作物
-            $crops = FarmCrop::whereIn('growth_stage', [GROWTH_STAGE::SPROUT, GROWTH_STAGE::GROWTH])
-                ->get();
+            $disasterLogic = new DisasterLogic();
 
-            $this->info("找到 {$crops->count()} 个可能受灾的作物");
+            // 使用优化后的批量生成方法
+            $result = $disasterLogic->batchGenerateDisasters();
 
-            $generatedCount = 0;
+            $this->info("检查了 {$result['total']} 个作物");
+            $this->info("跳过了 {$result['skipped']} 个已有灾害的作物");
+            $this->info("成功生成 {$result['generated']} 个作物灾害");
 
-            foreach ($crops as $crop) {
-                $userId = $crop->user_id;
-                $land = $crop->land;
-                $seed = $crop->seed;
-
-                // 跳过已经有灾害的土地
-                if ($land->status === LAND_STATUS::DISASTER) {
-                    continue;
-                }
-
-                // 获取种子的灾害抵抗属性
-                $disasterResistance = $seed->disaster_resistance ?? [];
-
-                // 获取土地的灾害抵抗属性
-                $landDisasterResistance = $land->landType->disaster_resistance ?? 0;
-
-                // 检查用户是否有有效的神灵加持
-                $activeBuffs = FarmGodBuff::where('user_id', $userId)
-                    ->where('expire_time', '>', now())
-                    ->pluck('buff_type')
-                    ->toArray();
-
-                // 灾害类型及其基础概率
-                $disasterTypes = DisasterService::getAllDisasters();
-                // 随机选择一种灾害类型
-                $randomDisasterType = array_rand($disasterTypes);
-                $baseProb = $disasterTypes[$randomDisasterType];
-
-                // 计算最终概率,考虑种子抵抗、土地抵抗和神灵加持
-                $seedResistance = $disasterResistance[$this->getDisasterKey($randomDisasterType)] ?? 0;
-                $finalProb = $baseProb - $seedResistance - $landDisasterResistance;
-
-                // 如果有对应的神灵加持,则不生成该类型的灾害
-                $buffType = DISASTER_TYPE::getPreventBuffType($randomDisasterType);
-                if ($buffType && in_array($buffType, $activeBuffs)) {
-                    $finalProb = 0;
-                }
-
-                // 确保概率在有效范围内
-                $finalProb = max(0, min(1, $finalProb));
-
-                // 随机决定是否生成灾害
-                if (mt_rand(1, 100) <= $finalProb * 100) {
-                    // 生成灾害
-                    $disasterInfo = [
-                        'type' => $randomDisasterType,
-                        'generated_at' => now()->toDateTimeString(),
-                        'status' => 'active'
-                    ];
-
-                    // 更新作物灾害信息
-                    $disasters = $crop->disasters ?? [];
-                    $disasters[] = $disasterInfo;
-                    $crop->disasters = $disasters;
-
-                    // 更新土地状态为灾害
-                    $land->status = LAND_STATUS::DISASTER;
-
-                    // 保存更改
-                    $crop->save();
-                    $land->save();
-
-                    // 触发灾害生成事件
-                    event(new DisasterGeneratedEvent($userId, $crop, $randomDisasterType, $disasterInfo));
-
-                    $generatedCount++;
-
-                    $this->info("作物 ID: {$crop->id}, 用户 ID: {$userId}, 灾害类型: {$randomDisasterType}");
-                }
-            }
-
-            $this->info("成功生成 {$generatedCount} 个作物灾害");
-
-            Log::info('作物灾害生成成功', [
-                'total' => $crops->count(),
-                'generated' => $generatedCount
-            ]);
+            Log::info('作物灾害生成成功', $result);
 
             return 0;
         } catch (\Exception $e) {
@@ -141,20 +59,5 @@ class GenerateDisastersCommand extends Command
         }
     }
 
-    /**
-     * 获取灾害类型对应的键名
-     *
-     * @param int $disasterType
-     * @return string
-     */
-    private function getDisasterKey(int $disasterType): string
-    {
-        $keys = [
-            DISASTER_TYPE::DROUGHT => 'drought',
-            DISASTER_TYPE::PEST => 'pest',
-            DISASTER_TYPE::WEED => 'weed',
-        ];
 
-        return $keys[$disasterType] ?? '';
-    }
 }

+ 4 - 0
app/Module/Farm/Databases/GenerateSql/farm_crops.sql

@@ -15,6 +15,8 @@ CREATE TABLE `kku_farm_crops` (
   `stage_end_time` timestamp NULL DEFAULT NULL COMMENT '当前阶段结束时间',
   `disasters` json DEFAULT NULL COMMENT '灾害情况',
   `fertilized` tinyint(1) NOT NULL DEFAULT '0' COMMENT '当前阶段是否已使用化肥',
+  `last_disaster_check_time` timestamp NULL DEFAULT NULL COMMENT '上次灾害检查时间',
+  `can_disaster` tinyint(1) NOT NULL DEFAULT '1' COMMENT '当前阶段是否可以产生灾害:0否,1是',
   `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (`id`) USING BTREE,
@@ -23,5 +25,7 @@ CREATE TABLE `kku_farm_crops` (
   KEY `idx_seed_id` (`seed_id`) USING BTREE,
   KEY `idx_growth_stage` (`growth_stage`) USING BTREE,
   KEY `idx_stage_end_time` (`stage_end_time`) USING BTREE,
+  KEY `idx_disaster_check` (`can_disaster`, `last_disaster_check_time`) USING BTREE,
+  KEY `idx_can_disaster` (`can_disaster`) USING BTREE,
   CONSTRAINT `fk_farm_crops_land_id` FOREIGN KEY (`land_id`) REFERENCES `kku_farm_land_users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作物信息表';

+ 166 - 0
app/Module/Farm/Docs/灾害系统重构说明.md

@@ -0,0 +1,166 @@
+# 农场灾害系统重构说明
+
+## 重构背景
+
+原有的灾害生成系统存在以下问题:
+
+1. **代码重复**:灾害生成逻辑在 `DisasterLogic`、`GenerateDisasterListener`、`GenerateDisastersCommand` 三个地方重复实现
+2. **性能问题**:`GenerateDisastersCommand` 每分钟执行时会扫描所有发芽期和生长期的作物,没有状态标识
+3. **维护困难**:相同逻辑分散在多个地方,修改时容易遗漏
+
+## 重构内容
+
+### 1. 数据库优化
+
+#### 新增字段
+在 `farm_crops` 表中添加两个字段:
+
+1. **last_disaster_check_time**:
+   - **类型**:`timestamp NULL`
+   - **用途**:记录上次灾害检查时间,控制检查频率
+
+2. **can_disaster**:
+   - **类型**:`tinyint(1) NOT NULL DEFAULT '1'`
+   - **用途**:标识当前阶段是否可以产生灾害,每个阶段只能产生一次灾害
+
+#### 索引优化
+- **复合索引**:`idx_disaster_check (can_disaster, last_disaster_check_time)`
+- **单独索引**:`idx_can_disaster (can_disaster)`
+
+### 2. 服务层优化
+
+#### DisasterService 增强
+- 添加 `getDisasterKey()` 公共方法,统一灾害类型键名映射
+- 添加 `getCheckInterval()` 方法,配置灾害检查间隔(默认5分钟)
+
+#### DisasterLogic 重构
+- **新增方法**:
+  - `tryGenerateDisasterForCrop()` - 核心灾害生成逻辑
+  - `applyDisasterToCrop()` - 将灾害应用到作物
+  - `batchGenerateDisasters()` - 批量生成灾害(优化版)
+
+- **优化特性**:
+  - 统一的灾害生成逻辑
+  - 支持批量处理,减少数据库查询
+  - 使用 `with()` 预加载关联数据
+  - 基于时间戳的智能过滤
+
+### 3. 命令优化
+
+#### GenerateDisastersCommand 重构
+- **性能提升**:
+  - 只处理需要检查的作物(基于 `last_disaster_check_time`)
+  - 使用预加载减少 N+1 查询问题
+  - 批量处理替代逐个处理
+
+- **代码简化**:
+  - 移除重复的灾害生成逻辑
+  - 直接调用 `DisasterLogic::batchGenerateDisasters()`
+
+### 4. 事件监听器优化
+
+#### GenerateDisasterListener 重构
+- **逻辑统一**:移除重复代码,调用统一的 `DisasterLogic::generateDisaster()`
+- **状态管理**:在作物生长阶段变化时重置 `last_disaster_check_time`,确保能立即检查
+
+### 5. 模型更新
+
+#### FarmCrop 模型
+- 添加 `last_disaster_check_time` 字段到 `$fillable` 和 `$casts`
+- 更新字段注释
+
+## 性能改进
+
+### 执行效率提升
+
+#### 原有方式
+```php
+// 每次都查询所有发芽期和生长期的作物
+$crops = FarmCrop::whereIn('growth_stage', [GROWTH_STAGE::SPROUT, GROWTH_STAGE::GROWTH])->get();
+
+// 对每个作物都要查询关联数据
+foreach ($crops as $crop) {
+    $land = $crop->land;
+    $seed = $crop->seed;
+    $activeBuffs = FarmGodBuff::where('user_id', $userId)->get();
+    // ... 重复的灾害生成逻辑
+}
+```
+
+#### 优化后方式
+```php
+// 只查询需要检查的作物(基于时间戳)
+$crops = FarmCrop::whereIn('growth_stage', [GROWTH_STAGE::SPROUT, GROWTH_STAGE::GROWTH])
+    ->where(function ($query) use ($checkTime) {
+        $query->whereNull('last_disaster_check_time')
+              ->orWhere('last_disaster_check_time', '<', $checkTime);
+    })
+    ->with(['land.landType', 'seed', 'user.buffs'])  // 预加载关联数据
+    ->get();
+```
+
+### 查询优化
+- **减少查询次数**:使用 `with()` 预加载,避免 N+1 查询
+- **智能过滤**:只处理需要检查的作物
+- **索引优化**:为 `last_disaster_check_time` 添加索引
+
+## 配置参数
+
+### 检查间隔
+- **默认值**:5分钟
+- **配置位置**:`DisasterService::getCheckInterval()`
+- **说明**:可根据服务器性能和游戏需求调整
+
+### 灾害概率
+- **基础概率**:90%(所有灾害类型)
+- **影响因素**:
+  - 种子抵抗属性
+  - 土地抵抗属性
+  - 神灵加持效果
+
+## 使用方式
+
+### 命令执行
+```bash
+# 手动执行灾害生成
+php artisan farm:generate-disasters
+```
+
+### 定时任务
+```php
+// 在 Kernel.php 中配置
+$schedule->command('farm:generate-disasters')->everyMinute();
+```
+
+### 代码调用
+```php
+// 单个作物灾害生成
+$disasterLogic = new DisasterLogic();
+$disasterInfo = $disasterLogic->generateDisaster($cropId);
+
+// 批量灾害生成
+$result = $disasterLogic->batchGenerateDisasters();
+```
+
+## 兼容性说明
+
+- **向后兼容**:现有的灾害数据结构保持不变
+- **API 兼容**:公共方法签名保持一致
+- **数据迁移**:新字段为可空,不影响现有数据
+
+## 监控和日志
+
+### 性能监控
+- 记录每次批量处理的统计信息
+- 监控处理时间和作物数量
+
+### 错误处理
+- 统一的异常处理和日志记录
+- 详细的错误信息便于调试
+
+## 后续优化建议
+
+1. **缓存优化**:考虑缓存神灵加持信息,减少重复查询
+2. **配置化**:将灾害概率等参数移到配置文件
+3. **队列处理**:对于大量作物的情况,考虑使用队列异步处理
+4. **监控告警**:添加性能监控和异常告警机制

+ 21 - 87
app/Module/Farm/Listeners/GenerateDisasterListener.php

@@ -2,14 +2,8 @@
 
 namespace App\Module\Farm\Listeners;
 
-use App\Module\Farm\Enums\DISASTER_TYPE;
 use App\Module\Farm\Enums\GROWTH_STAGE;
-use App\Module\Farm\Enums\LAND_STATUS;
 use App\Module\Farm\Events\CropGrowthStageChangedEvent;
-use App\Module\Farm\Events\DisasterGeneratedEvent;
-
-
-use App\Module\Farm\Services\DisasterService;
 use Illuminate\Support\Facades\Log;
 
 /**
@@ -30,81 +24,36 @@ class GenerateDisasterListener
     public function handle(CropGrowthStageChangedEvent $event)
     {
         try {
-            // 只在发芽期和生长期生成灾害
-            if (!in_array($event->newStage, [GROWTH_STAGE::SPROUT, GROWTH_STAGE::GROWTH])) {
-                return;
-            }
-
             $crop = $event->crop;
-            $land = $crop->land;
-            $seed = $crop->seed;
-
-            // 获取种子的灾害抵抗属性
-            $disasterResistance = $seed->disaster_resistance ?? [];
-
-            // 获取土地的灾害抵抗属性
-            $landDisasterResistance = $land->landType->disaster_resistance ?? 0;
-
-            // 检查用户是否有有效的神灵加持
-            $activeBuffs = $crop->user->buffs()
-                ->where('expire_time', '>', now())
-                ->pluck('buff_type')
-                ->toArray();
-
-            // 灾害类型及其基础概率
-            $disasterTypes =DisasterService::getRate();
-
-            // 随机选择一种灾害类型
-            $randomDisasterType = array_rand($disasterTypes);
-            $baseProb = $disasterTypes[$randomDisasterType];
-
-            // 计算最终概率,考虑种子抵抗、土地抵抗和神灵加持
-            $seedResistance = $disasterResistance[$this->getDisasterKey($randomDisasterType)] ?? 0;
-            $finalProb = $baseProb - $seedResistance - $landDisasterResistance;
-
-            // 如果有对应的神灵加持,则不生成该类型的灾害
-            $buffType = DISASTER_TYPE::getPreventBuffType($randomDisasterType);
-            if ($buffType && in_array($buffType, $activeBuffs)) {
-                $finalProb = 0;
-            }
-
-            // 确保概率在有效范围内
-            $finalProb = max(0, min(1, $finalProb));
 
-            // 随机决定是否生成灾害
-            if (mt_rand(1, 100) <= $finalProb * 100) {
-                // 生成灾害
-                $disasterInfo = [
-                    'type' => $randomDisasterType,
-                    'generated_ts' => time(),
-                    'status' => 'active'
-                ];
+            // 根据新的生长阶段设置是否可以产生灾害
+            if (in_array($event->newStage, [GROWTH_STAGE::SPROUT, GROWTH_STAGE::GROWTH])) {
+                // 进入发芽期或生长期,允许产生灾害并重置检查时间
+                $crop->can_disaster = true;
+                $crop->last_disaster_check_time = null;
 
-                // 更新作物灾害信息
-                $disasters = $crop->disasters ?? [];
-                $disasters[] = $disasterInfo;
-                $crop->disasters = $disasters;
-
-                // 更新土地状态为灾害
-                $land->status = LAND_STATUS::DISASTER;
-
-                // 保存更改
-                $crop->save();
-                $land->save();
-
-                // 触发灾害生成事件
-                event(new DisasterGeneratedEvent($event->userId, $crop, $randomDisasterType, $disasterInfo));
+                Log::info('作物进入可产生灾害阶段', [
+                    'user_id' => $event->userId,
+                    'crop_id' => $crop->id,
+                    'old_stage' => $event->oldStage->value,
+                    'new_stage' => $event->newStage->value
+                ]);
+            } else {
+                // 其他阶段不能产生灾害
+                $crop->can_disaster = false;
 
-                Log::info('灾害生成成功', [
+                Log::info('作物离开可产生灾害阶段', [
                     'user_id' => $event->userId,
                     'crop_id' => $crop->id,
-                    'land_id' => $land->id,
-                    'disaster_type' => $randomDisasterType,
-                    'disaster_info' => $disasterInfo
+                    'old_stage' => $event->oldStage->value,
+                    'new_stage' => $event->newStage->value
                 ]);
             }
+
+            $crop->save();
+
         } catch (\Exception $e) {
-            Log::error('灾害生成失败', [
+            Log::error('作物生长阶段变化时更新灾害状态失败', [
                 'user_id' => $event->userId,
                 'crop_id' => $event->crop->id ?? null,
                 'error' => $e->getMessage(),
@@ -113,20 +62,5 @@ class GenerateDisasterListener
         }
     }
 
-    /**
-     * 获取灾害类型对应的键名
-     *
-     * @param int $disasterType
-     * @return string
-     */
-    private function getDisasterKey(int $disasterType): string
-    {
-        $keys = [
-            DISASTER_TYPE::DROUGHT => 'drought',
-            DISASTER_TYPE::PEST => 'pest',
-            DISASTER_TYPE::WEED => 'weed',
-        ];
 
-        return $keys[$disasterType] ?? '';
-    }
 }

+ 2 - 0
app/Module/Farm/Logics/CropLogic.php

@@ -130,6 +130,8 @@ class CropLogic
             $crop->stage_end_time = now()->addSeconds($seed->seed_time);
             $crop->disasters = [];
             $crop->fertilized = false;
+            $crop->last_disaster_check_time = null; // 初始化灾害检查时间
+            $crop->can_disaster = false; // 种子期不能产生灾害
             $crop->save();
 
             // 创建种植日志

+ 147 - 54
app/Module/Farm/Logics/DisasterLogic.php

@@ -3,7 +3,6 @@
 namespace App\Module\Farm\Logics;
 
 use App\Module\Farm\Dtos\DisasterInfoDto;
-use App\Module\Farm\Enums\BUFF_TYPE;
 use App\Module\Farm\Enums\DISASTER_TYPE;
 use App\Module\Farm\Enums\GROWTH_STAGE;
 use App\Module\Farm\Enums\LAND_STATUS;
@@ -129,58 +128,18 @@ class DisasterLogic
                 ->pluck('buff_type')
                 ->toArray();
 
-            // 灾害类型及其基础概率
-            $disasterTypes =DisasterService::getAllDisasters();
-
-            // 随机选择一种灾害类型
-            $randomDisasterType = array_rand($disasterTypes);
-            $baseProb = $disasterTypes[$randomDisasterType];
-
-            // 计算最终概率,考虑种子抵抗、土地抵抗和神灵加持
-            $seedResistance = $disasterResistance[$this->getDisasterKey($randomDisasterType)] ?? 0;
-            $finalProb = $baseProb - $seedResistance - $landDisasterResistance;
-
-            // 如果有对应的神灵加持,则不生成该类型的灾害
-            $buffType = DISASTER_TYPE::getPreventBuffType($randomDisasterType);
-            if ($buffType && in_array($buffType, $activeBuffs)) {
-                $finalProb = 0;
-            }
-
-            // 确保概率在有效范围内
-            $finalProb = max(0, min(1, $finalProb));
-
-            // 随机决定是否生成灾害
-            if (mt_rand(1, 100) <= $finalProb * 100) {
-                // 生成灾害
-                $disasterInfo = [
-                    'type' => $randomDisasterType,
-                    'generated_ts' => now()->toDateTimeString(),
-                    'status' => 'active'
-                ];
-
-                // 更新作物灾害信息
-                $disasters = $crop->disasters ?? [];
-                $disasters[] = $disasterInfo;
-                $crop->disasters = $disasters;
-
-                // 更新土地状态为灾害
-                $land->status = LAND_STATUS::DISASTER;
-
-                // 保存更改
-                $crop->save();
-                $land->save();
-
-                // 触发灾害生成事件
-                event(new DisasterGeneratedEvent($userId, $crop, $randomDisasterType, $disasterInfo));
+            // 尝试生成灾害
+            $disasterInfo = $this->tryGenerateDisasterForCrop($crop, $disasterResistance, $landDisasterResistance, $activeBuffs);
 
+            if ($disasterInfo) {
                 Log::info('灾害生成成功', [
                     'user_id' => $userId,
                     'crop_id' => $crop->id,
                     'land_id' => $land->id,
-                    'disaster_type' => $randomDisasterType
+                    'disaster_type' => $disasterInfo->type
                 ]);
 
-                return DisasterInfoDto::fromArray($disasterInfo);
+                return $disasterInfo;
             }
 
             return null;
@@ -196,19 +155,153 @@ class DisasterLogic
     }
 
     /**
-     * 获取灾害类型对应的键名
+     * 尝试为作物生成灾害
      *
+     * @param FarmCrop $crop
+     * @param array $disasterResistance
+     * @param float $landDisasterResistance
+     * @param array $activeBuffs
+     * @return DisasterInfoDto|null
+     */
+    private function tryGenerateDisasterForCrop(FarmCrop $crop, array $disasterResistance, float $landDisasterResistance, array $activeBuffs): ?DisasterInfoDto
+    {
+        // 灾害类型及其基础概率
+        $disasterTypes = DisasterService::getRate();
+
+        // 随机选择一种灾害类型
+        $randomDisasterType = array_rand($disasterTypes);
+        $baseProb = $disasterTypes[$randomDisasterType];
+
+        // 计算最终概率,考虑种子抵抗、土地抵抗和神灵加持
+        $seedResistance = $disasterResistance[DisasterService::getDisasterKey($randomDisasterType)] ?? 0;
+        $finalProb = $baseProb - $seedResistance - $landDisasterResistance;
+
+        // 如果有对应的神灵加持,则不生成该类型的灾害
+        $buffType = DISASTER_TYPE::getPreventBuffType($randomDisasterType);
+        if ($buffType && in_array($buffType, $activeBuffs)) {
+            $finalProb = 0;
+        }
+
+        // 确保概率在有效范围内
+        $finalProb = max(0, min(1, $finalProb));
+
+        // 随机决定是否生成灾害
+        if (mt_rand(1, 100) <= $finalProb * 100) {
+            return $this->applyDisasterToCrop($crop, $randomDisasterType);
+        }
+
+        return null;
+    }
+
+    /**
+     * 将灾害应用到作物上
+     *
+     * @param FarmCrop $crop
      * @param int $disasterType
-     * @return string
+     * @return DisasterInfoDto
      */
-    private function getDisasterKey(int $disasterType): string
+    private function applyDisasterToCrop(FarmCrop $crop, int $disasterType): DisasterInfoDto
     {
-        $keys = [
-            DISASTER_TYPE::DROUGHT => 'drought',
-            DISASTER_TYPE::PEST => 'pest',
-            DISASTER_TYPE::WEED => 'weed',
+        // 生成灾害信息
+        $disasterInfo = [
+            'type' => $disasterType,
+            'generated_ts' => now()->toDateTimeString(),
+            'status' => 'active'
         ];
 
-        return $keys[$disasterType] ?? '';
+        // 更新作物灾害信息
+        $disasters = $crop->disasters ?? [];
+        $disasters[] = $disasterInfo;
+        $crop->disasters = $disasters;
+
+        // 更新土地状态为灾害
+        $land = $crop->land;
+        $land->status = LAND_STATUS::DISASTER;
+
+        // 保存更改
+        $crop->save();
+        $land->save();
+
+        // 触发灾害生成事件
+        event(new DisasterGeneratedEvent($crop->user_id, $crop, $disasterType, $disasterInfo));
+
+        return DisasterInfoDto::fromArray($disasterInfo);
+    }
+
+    /**
+     * 批量检查并生成灾害
+     *
+     * @param int|null $checkIntervalMinutes 检查间隔(分钟)
+     * @return array 生成结果统计
+     */
+    public function batchGenerateDisasters(?int $checkIntervalMinutes = null): array
+    {
+        if ($checkIntervalMinutes === null) {
+            $checkIntervalMinutes = DisasterService::getCheckInterval();
+        }
+
+        $checkTime = now()->subMinutes($checkIntervalMinutes);
+
+        // 获取需要检查灾害的作物:
+        // 1. 必须在发芽期或生长期
+        // 2. 当前阶段可以产生灾害 (can_disaster = 1)
+        // 3. 满足时间检查条件(首次检查或超过检查间隔)
+        $crops = FarmCrop::whereIn('growth_stage', [GROWTH_STAGE::SPROUT, GROWTH_STAGE::GROWTH])
+            ->where('can_disaster', true)
+            ->where(function ($query) use ($checkTime) {
+                $query->whereNull('last_disaster_check_time')
+                      ->orWhere('last_disaster_check_time', '<', $checkTime);
+            })
+            ->with(['land.landType', 'seed', 'user.buffs' => function ($query) {
+                $query->where('expire_time', '>', now());
+            }])
+            ->get();
+
+        $totalCount = $crops->count();
+        $generatedCount = 0;
+        $skippedCount = 0;
+
+        foreach ($crops as $crop) {
+            try {
+                // 更新检查时间
+                $crop->last_disaster_check_time = now();
+
+                // 跳过已经有灾害的土地
+                if ($crop->land->status === LAND_STATUS::DISASTER) {
+                    $crop->save(); // 仍需更新检查时间
+                    $skippedCount++;
+                    continue;
+                }
+
+                // 获取相关数据
+                $disasterResistance = $crop->seed->disaster_resistance ?? [];
+                $landDisasterResistance = $crop->land->landType->disaster_resistance ?? 0;
+                $activeBuffs = $crop->user->buffs->pluck('buff_type')->toArray();
+
+                // 尝试生成灾害
+                $disasterInfo = $this->tryGenerateDisasterForCrop($crop, $disasterResistance, $landDisasterResistance, $activeBuffs);
+
+                if ($disasterInfo) {
+                    // 生成灾害后,设置当前阶段不能再产生灾害
+                    $crop->can_disaster = false;
+                    $generatedCount++;
+                }
+
+                // 保存更改
+                $crop->save();
+
+            } catch (\Exception $e) {
+                Log::error('批量生成灾害失败', [
+                    'crop_id' => $crop->id,
+                    'error' => $e->getMessage()
+                ]);
+            }
+        }
+
+        return [
+            'total' => $totalCount,
+            'generated' => $generatedCount,
+            'skipped' => $skippedCount
+        ];
     }
 }

+ 12 - 7
app/Module/Farm/Models/FarmCrop.php

@@ -21,6 +21,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  * @property  string $stage_end_time  当前阶段结束时间
  * @property  array $disasters  灾害情况
  * @property  bool $fertilized  当前阶段是否已使用化肥
+ * @property  \Carbon\Carbon $last_disaster_check_time  上次灾害检查时间
+ * @property  bool $can_disaster  当前阶段是否可以产生灾害
  * @property  \Carbon\Carbon $created_at  创建时间
  * @property  \Carbon\Carbon $updated_at  更新时间
  * field end
@@ -52,6 +54,8 @@ class FarmCrop extends Model
         'stage_end_time',
         'disasters',
         'fertilized',
+        'last_disaster_check_time',
+        'can_disaster',
     ];
 
 
@@ -61,13 +65,14 @@ class FarmCrop extends Model
      * @var array
      */
     protected $casts = [
-        'disasters'        => 'json',
-        'growth_stage'     => GROWTH_STAGE::class,
-        'fertilized'       => 'boolean',
-        'plant_time'       => 'datetime',
-        'stage_start_time' => 'datetime',
-        'stage_end_time'   => 'datetime',
-
+        'disasters'                => 'json',
+        'growth_stage'             => GROWTH_STAGE::class,
+        'fertilized'               => 'boolean',
+        'plant_time'               => 'datetime',
+        'stage_start_time'         => 'datetime',
+        'stage_end_time'           => 'datetime',
+        'last_disaster_check_time' => 'datetime',
+        'can_disaster'             => 'boolean',
     ];
 
     /**

+ 26 - 0
app/Module/Farm/Services/DisasterService.php

@@ -37,7 +37,33 @@ class DisasterService
         ];
 
         return $disasterTypes;
+    }
+
+    /**
+     * 获取灾害类型对应的键名
+     *
+     * @param int $disasterType
+     * @return string
+     */
+    public static function getDisasterKey(int $disasterType): string
+    {
+        $keys = [
+            DISASTER_TYPE::DROUGHT->valueInt() => 'drought',
+            DISASTER_TYPE::PEST->valueInt() => 'pest',
+            DISASTER_TYPE::WEED->valueInt() => 'weed',
+        ];
 
+        return $keys[$disasterType] ?? '';
+    }
+
+    /**
+     * 灾害检查间隔时间(分钟)
+     *
+     * @return int
+     */
+    public static function getCheckInterval(): int
+    {
+        return 5; // 每5分钟检查一次
     }
 
 }

+ 2 - 0
routes/console.php

@@ -12,3 +12,5 @@ Artisan::command('inspire', function () {
 \Illuminate\Support\Facades\Schedule::command('farm:check-house-downgrade')->dailyAt('02:00');
 // 每分钟更新作物生长状态
 \Illuminate\Support\Facades\Schedule::command('farm:update-crop-growth')->everyMinute();
+// 每小时随机生成灾害
+\Illuminate\Support\Facades\Schedule::command(\App\Module\Farm\Commands\GenerateDisastersCommand::class)->everyMinute();