浏览代码

feat(farm): 重构灾害系统并支持多灾害

- 改进灾害生成逻辑,支持同时生成多种灾害
- 优化灾害抵抗属性的处理方式,使用百分比格式
- 新增灾害详细日志记录,便于调试和分析- 重构代码结构,提高可维护性和扩展性
notfff 7 月之前
父节点
当前提交
9c94f0dc82

+ 13 - 14
app/Module/AppGame/Handler/Land/PesticideHandler.php

@@ -14,6 +14,8 @@ use UCore\Exception\LogicException;
 
 /**
  * 处理除虫操作请求
+ * Pesticide
+ *
  */
 class PesticideHandler extends BaseHandler
 {
@@ -37,22 +39,14 @@ class PesticideHandler extends BaseHandler
         try {
             // 获取请求参数
             $landId = $data->getLandId();
-            $userItemId = $data->getUserItemId();
+            $userItemId = $data->getItemId();
             $userId = $this->user_id;
 
             // 验证土地是否存在且属于当前用户
             // 获取用户所有土地
-            $userLands = LandService::getUserLands($userId);
-            $landInfo = null;
-            
-            // 查找指定ID的土地
-            foreach ($userLands as $land) {
-                if ($land->id == $landId) {
-                    $landInfo = $land;
-                    break;
-                }
-            }
-            
+            $landInfo = LandService::getUserLand($userId,$landId);
+
+
             if (!$landInfo) {
                 throw new LogicException("土地不存在或不属于当前用户");
             }
@@ -62,6 +56,11 @@ class PesticideHandler extends BaseHandler
             if ($hasItem->isEmpty()) {
                 throw new LogicException("您没有该除虫物品");
             }
+            // 除虫概率 百分比 ,100 = 100%
+            $rate  =ItemService::getItemNumericAttribute($userItemId,'fram_pesticide_rate');
+            if($rate<=0){
+                throw new LogicException("不是除虫物品");
+            }
 
             // 调用除虫服务
             $result = CropService::clearDisaster($userId, $landId, 2); // 2表示虫害灾害类型
@@ -90,7 +89,7 @@ class PesticideHandler extends BaseHandler
             // 设置错误响应
             $this->response->setCode(400);
             $this->response->setMsg($e->getMessage());
-            
+
             Log::warning('用户除虫失败', [
                 'user_id' => $this->user_id,
                 'error' => $e->getMessage(),
@@ -100,7 +99,7 @@ class PesticideHandler extends BaseHandler
             // 设置错误响应
             $this->response->setCode(500);
             $this->response->setMsg('系统错误,请稍后再试');
-            
+
             Log::error('除虫操作异常', [
                 'user_id' => $this->user_id,
                 'error' => $e->getMessage(),

+ 9 - 9
app/Module/Farm/Casts/DisasterResistanceCast.php

@@ -14,31 +14,31 @@ use UCore\Model\CastsAttributes;
 class DisasterResistanceCast extends CastsAttributes
 {
     /**
-     * 干旱抵抗率
-     * 
+     * 干旱抵抗率(百分比格式,1=1%,100=100%)
+     *
      * @var float $drought
      */
     public float $drought = 0.0;
 
     /**
-     * 虫害抵抗率
-     * 
+     * 虫害抵抗率(百分比格式,1=1%,100=100%)
+     *
      * @var float $pest
      */
     public float $pest = 0.0;
 
     /**
-     * 杂草抵抗率
-     * 
+     * 杂草抵抗率(百分比格式,1=1%,100=100%)
+     *
      * @var float $weed
      */
     public float $weed = 0.0;
 
     /**
      * 获取指定灾害类型的抵抗率
-     * 
+     *
      * @param DISASTER_TYPE|int $type 灾害类型
-     * @return float 抵抗率
+     * @return float 抵抗率(百分比格式,1=1%,需要除以100转换为小数)
      */
     public function getResistance($type): float
     {
@@ -56,7 +56,7 @@ class DisasterResistanceCast extends CastsAttributes
 
     /**
      * 设置指定灾害类型的抵抗率
-     * 
+     *
      * @param DISASTER_TYPE|int $type 灾害类型
      * @param float $value 抵抗率
      * @return self

+ 9 - 8
app/Module/Farm/Docs/土地配置表设计.md

@@ -11,8 +11,8 @@ CREATE TABLE `farm_land_types` (
   `id` tinyint(3) unsigned NOT NULL AUTO_INCREMENT COMMENT '土地类型ID',
   `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '土地类型名称',
   `code` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '土地类型编码',
-  `output_bonus` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '产量加成',
-  `disaster_resistance` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '灾害抵抗',
+  `output_bonus` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '产量加成(百分比格式,1=1%)',
+  `disaster_resistance` decimal(5,2) NOT NULL DEFAULT '0.00' COMMENT '灾害抵抗(百分比格式,1=1%)',
   `unlock_house_level` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '解锁所需房屋等级',
   `is_special` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为特殊土地',
   `icon` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图标路径',
@@ -27,13 +27,14 @@ CREATE TABLE `farm_land_types` (
 ### 1.1 初始数据
 
 ```sql
+-- 注意:数值为百分比格式,1=1%,10=10%
 INSERT INTO `farm_land_types` (`id`, `name`, `code`, `output_bonus`, `disaster_resistance`, `unlock_house_level`, `is_special`) VALUES
-(1, '普通土地', 'NORMAL', 0.00, 0.00, 1, 0),
-(2, '红土地', 'RED', 0.10, 0.05, 1, 0),
-(3, '黑土地', 'BLACK', 0.25, 0.10, 1, 0),
-(4, '金色特殊土地', 'GOLD', 0.50, 0.15, 7, 1),
-(5, '蓝色特殊土地', 'BLUE', 0.40, 0.25, 7, 1),
-(6, '紫色特殊土地', 'PURPLE', 0.60, 0.10, 7, 1);
+(1, '普通土地', 'NORMAL', 0.00, 0.00, 1, 0),    -- 0%产量加成,0%灾害抵抗
+(2, '红土地', 'RED', 10.00, 5.00, 1, 0),        -- 10%产量加成,5%灾害抵抗
+(3, '黑土地', 'BLACK', 25.00, 10.00, 1, 0),     -- 25%产量加成,10%灾害抵抗
+(4, '金色特殊土地', 'GOLD', 50.00, 15.00, 7, 1), -- 50%产量加成,15%灾害抵抗
+(5, '蓝色特殊土地', 'BLUE', 40.00, 25.00, 7, 1), -- 40%产量加成,25%灾害抵抗
+(6, '紫色特殊土地', 'PURPLE', 60.00, 10.00, 7, 1); -- 60%产量加成,10%灾害抵抗
 ```
 
 ## 2. 土地升级配置表 (farm_land_upgrade_configs)

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

@@ -114,9 +114,21 @@ $crops = FarmCrop::whereIn('growth_stage', [GROWTH_STAGE::SPROUT, GROWTH_STAGE::
 ### 灾害概率
 - **基础概率**:90%(所有灾害类型)
 - **影响因素**:
-  - 种子抵抗属性
-  - 土地抵抗属性
-  - 神灵加持效果
+  - 种子抵抗属性(百分比格式,1=1%,需要除以100转换为小数)
+  - 土地抵抗属性(百分比格式,1=1%,需要除以100转换为小数)
+  - 神灵加持效果(直接设置概率为0)
+
+### 概率计算公式
+
+```
+最终概率 = 基础概率(0.9) - 种子抵抗/100 - 土地抵抗/100
+```
+
+**示例**:
+- 基础概率:90% (0.9)
+- 种子抵抗:20% (数据库存储20,转换为0.2)
+- 土地抵抗:10% (数据库存储10,转换为0.1)
+- 最终概率:0.9 - 0.2 - 0.1 = 0.6 (60%)
 
 ## 使用方式
 
@@ -158,9 +170,54 @@ $result = $disasterLogic->batchGenerateDisasters();
 - 统一的异常处理和日志记录
 - 详细的错误信息便于调试
 
+## 多灾害功能
+
+### 功能特性
+- **并发灾害**:一个作物可以同时遭受多种不同类型的灾害(干旱、虫害、杂草)
+- **独立判定**:每种灾害类型都有独立的概率计算和判定过程
+- **完整记录**:所有灾害信息存储在作物的 `disasters` JSON字段中
+
+### 实现原理
+```php
+// 对每种灾害类型都进行独立判定
+foreach ($disasterTypes as $disasterType => $baseProb) {
+    // 计算该类型灾害的最终概率
+    $finalProb = $baseProb - $seedResistance - $landResistance;
+
+    // 独立的随机数判定
+    if (mt_rand(1, 100) <= $finalProb * 100) {
+        $generatedDisasters[] = $disasterInfo;
+    }
+}
+```
+
+### 数据结构
+```json
+{
+  "disasters": [
+    {
+      "type": 1,
+      "generated_ts": "2025-05-24 11:46:51",
+      "status": "active"
+    },
+    {
+      "type": 3,
+      "generated_ts": "2025-05-24 11:46:51",
+      "status": "active"
+    }
+  ]
+}
+```
+
+### 测试验证
+- ✅ 成功实现一个作物同时生成多种灾害
+- ✅ 每种灾害类型独立计算概率
+- ✅ 数据结构和事件触发正常
+
 ## 后续优化建议
 
 1. **缓存优化**:考虑缓存神灵加持信息,减少重复查询
 2. **配置化**:将灾害概率等参数移到配置文件
 3. **队列处理**:对于大量作物的情况,考虑使用队列异步处理
 4. **监控告警**:添加性能监控和异常告警机制
+5. **灾害处理**:优化多灾害的处理和恢复机制

+ 169 - 77
app/Module/Farm/Logics/DisasterLogic.php

@@ -9,12 +9,11 @@ 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\Models\FarmLand;
 use App\Module\Farm\Services\DisasterService;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Log;
 
-use function Symfony\Component\Translation\t;
-
 /**
  * 灾害管理逻辑
  */
@@ -100,6 +99,9 @@ class DisasterLogic
                 throw new \Exception('作物不存在');
             }
 
+            /**
+             * @var FarmLand $land
+             */
             $land = $crop->land;
 
             if (!$land) {
@@ -122,8 +124,8 @@ class DisasterLogic
             // 获取种子的灾害抵抗属性
             $disasterResistance = $seed->disaster_resistance ?? [];
 
-            // 获取土地的灾害抵抗属性
-            $landDisasterResistance = $land->landType->disaster_resistance ?? 0;
+            // 获取土地的灾害抵抗属性(数据库存储百分比,需要除以100转换为小数)
+            $landDisasterResistance = ($land->landType->disaster_resistance ?? 0) / 100;
 
             // 检查用户是否有有效的神灵加持
             $activeBuffs = FarmGodBuff::where('user_id', $userId)
@@ -131,18 +133,21 @@ class DisasterLogic
                 ->pluck('buff_type')
                 ->toArray();
 
-            // 尝试生成灾害
-            $disasterInfo = $this->tryGenerateDisasterForCrop($crop, $disasterResistance, $landDisasterResistance, $activeBuffs);
+            // 尝试生成灾害(支持多种灾害)
+            $disasterInfos = $this->tryGenerateDisasterForCrop($crop, $disasterResistance, $landDisasterResistance, $activeBuffs);
+
+            if (!empty($disasterInfos)) {
+                $disasterDtos = $this->applyDisastersToCrop($crop, $disasterInfos);
 
-            if ($disasterInfo) {
                 Log::info('灾害生成成功', [
                     'user_id'       => $userId,
                     'crop_id'       => $crop->id,
                     'land_id'       => $land->id,
-                    'disaster_type' => $disasterInfo->type
+                    'disaster_count' => count($disasterInfos),
+                    'disaster_types' => array_column($disasterInfos, 'type')
                 ]);
 
-                return $disasterInfo;
+                return $disasterDtos[0] ?? null; // 返回第一个灾害信息保持兼容性
             }
 
             return null;
@@ -158,67 +163,123 @@ class DisasterLogic
     }
 
     /**
-     * 尝试为作物生成灾害
+     * 尝试为作物生成灾害(支持多种灾害同时发生)
      *
      * @param FarmCrop $crop
-     * @param array $disasterResistance
+     * @param mixed $disasterResistance
      * @param float $landDisasterResistance
      * @param array $activeBuffs
-     * @return DisasterInfoDto|null
+     * @return array 生成的灾害信息数组
      */
-    private function tryGenerateDisasterForCrop(FarmCrop $crop, array $disasterResistance, float $landDisasterResistance, array $activeBuffs): ?DisasterInfoDto
+    private function tryGenerateDisasterForCrop(FarmCrop $crop, $disasterResistance, float $landDisasterResistance, array $activeBuffs): array
     {
         // 灾害类型及其基础概率
         $disasterTypes = DisasterService::getRate();
+        $generatedDisasters = [];
+
+        // 对每种灾害类型都进行判定
+        foreach ($disasterTypes as $disasterType => $baseProb) {
+            // 计算最终概率,考虑种子抵抗、土地抵抗和神灵加持
+            $seedResistance = 0;
+            if ($disasterResistance) {
+                if (is_object($disasterResistance) && method_exists($disasterResistance, 'getResistance')) {
+                    // 如果是 DisasterResistanceCast 对象(数据库存储百分比,需要除以100转换为小数)
+                    $seedResistance = $disasterResistance->getResistance($disasterType) / 100;
+                } elseif (is_array($disasterResistance)) {
+                    // 如果是数组格式(数据库存储百分比,需要除以100转换为小数)
+                    $seedResistance = ($disasterResistance[DisasterService::getDisasterKey($disasterType)] ?? 0) / 100;
+                }
+            }
 
-        // 随机选择一种灾害类型
-        $randomDisasterType = array_rand($disasterTypes);
-        $baseProb           = $disasterTypes[$randomDisasterType];
+            $finalProb = $baseProb - $seedResistance - $landDisasterResistance;
 
-        // 计算最终概率,考虑种子抵抗、土地抵抗和神灵加持
-        $seedResistance = $disasterResistance[DisasterService::getDisasterKey($randomDisasterType)] ?? 0;
-        $finalProb      = $baseProb - $seedResistance - $landDisasterResistance;
+            // 如果有对应的神灵加持,则不生成该类型的灾害
+            $buffType = DISASTER_TYPE::getPreventBuffType($disasterType);
+            $hasGodBuff = $buffType && in_array($buffType, $activeBuffs);
+            if ($hasGodBuff) {
+                $finalProb = 0;
+            }
 
-        // 如果有对应的神灵加持,则不生成该类型的灾害
-        $buffType = DISASTER_TYPE::getPreventBuffType($randomDisasterType);
-        if ($buffType && in_array($buffType, $activeBuffs)) {
-            $finalProb = 0;
-        }
+            // 确保概率在有效范围内
+            $finalProb = max(0, min(1, $finalProb));
+
+            // 生成随机数
+            $randomNumber = mt_rand(1, 100);
+            $threshold = $finalProb * 100;
 
-        // 确保概率在有效范围内
-        $finalProb = max(0, min(1, $finalProb));
+            // 详细的debug日志
+            Log::debug('灾害生成详细信息', [
+                'crop_id' => $crop->id,
+                'user_id' => $crop->user_id,
+                'growth_stage' => $crop->growth_stage->valueInt(),
+                'disaster_type' => $disasterType,
+                'disaster_type_name' => DISASTER_TYPE::getName($disasterType),
+                'base_probability' => $baseProb,
+                'seed_resistance' => $seedResistance,
+                'land_resistance' => $landDisasterResistance,
+                'active_buffs' => $activeBuffs,
+                'god_buff_type' => $buffType,
+                'has_god_buff' => $hasGodBuff,
+                'final_probability' => $finalProb,
+                'threshold' => $threshold,
+                'random_number' => $randomNumber,
+                'will_generate' => $randomNumber <= $threshold,
+                'disaster_resistance_type' => is_object($disasterResistance) ? get_class($disasterResistance) : gettype($disasterResistance),
+                'disaster_resistance_raw' => is_object($disasterResistance) ? 'object' : $disasterResistance
+            ]);
 
-        // 随机决定是否生成灾害
-        if (mt_rand(1, 100) <= $finalProb * 100) {
-            return $this->applyDisasterToCrop($crop, $randomDisasterType);
+            // 随机决定是否生成该类型灾害
+            if ($randomNumber <= $threshold) {
+                $disasterInfo = [
+                    'type' => $disasterType,
+                    'generated_ts' => now()->toDateTimeString(),
+                    'status' => 'active'
+                ];
+
+                $generatedDisasters[] = $disasterInfo;
+
+                Log::debug('灾害生成成功', [
+                    'crop_id' => $crop->id,
+                    'disaster_type' => $disasterType,
+                    'disaster_type_name' => DISASTER_TYPE::getName($disasterType),
+                    'final_probability' => $finalProb
+                ]);
+            } else {
+                Log::info('灾害未生成', [
+                    'crop_id' => $crop->id,
+                    'disaster_type' => $disasterType,
+                    'disaster_type_name' => DISASTER_TYPE::getName($disasterType),
+                    'reason' => '随机数超过阈值',
+                    'random_number' => $randomNumber,
+                    'threshold' => $threshold,
+                    'final_probability' => $finalProb
+                ]);
+            }
         }
 
-        return null;
+        return $generatedDisasters;
     }
 
     /**
-     * 将灾害应用到作物上
+     * 将多个灾害应用到作物上
      *
      * @param FarmCrop $crop
-     * @param int $disasterType
-     * @return DisasterInfoDto
+     * @param array $disasterInfos
+     * @return array
      */
-    private function applyDisasterToCrop(FarmCrop $crop, int $disasterType): DisasterInfoDto
+    private function applyDisastersToCrop(FarmCrop $crop, array $disasterInfos): array
     {
-        // 生成灾害信息
-        $disasterInfo = [
-            'type'         => $disasterType,
-            'generated_ts' => now()->toDateTimeString(),
-            'status'       => 'active'
-        ];
+        if (empty($disasterInfos)) {
+            return [];
+        }
 
         // 更新作物灾害信息
-        $disasters       = $crop->disasters ?? [];
-        $disasters[]     = $disasterInfo;
+        $disasters = $crop->disasters ?? [];
+        $disasters = array_merge($disasters, $disasterInfos);
         $crop->disasters = $disasters;
 
         // 更新土地状态为灾害
-        $land         = $crop->land;
+        $land = $crop->land;
         $land->status = LAND_STATUS::DISASTER;
 
         // 保存更改
@@ -226,13 +287,17 @@ class DisasterLogic
         $land->save();
 
         // 触发灾害生成事件
-        event(new DisasterGeneratedEvent($crop->user_id, $crop, $disasterType, $disasterInfo));
+        $disasterDtos = [];
+        foreach ($disasterInfos as $disasterInfo) {
+            event(new DisasterGeneratedEvent($crop->user_id, $crop, $disasterInfo['type'], $disasterInfo));
+            $disasterDtos[] = DisasterInfoDto::fromArray($disasterInfo);
+        }
 
-        return DisasterInfoDto::fromArray($disasterInfo);
+        return $disasterDtos;
     }
 
     /**
-     * 批量检查并生成灾害
+     * 批量获取需要检查灾害的作物并处理
      *
      * @param int|null $checkIntervalMinutes 检查间隔(分钟)
      * @return array 生成结果统计
@@ -249,40 +314,45 @@ class DisasterLogic
         // 1. 必须在发芽期或生长期
         // 2. 当前阶段可以产生灾害 (can_disaster = 1)
         // 3. 满足时间检查条件(首次检查或超过检查间隔)
-        $crops = FarmCrop::whereIn('growth_stage', [ GROWTH_STAGE::SPROUT, GROWTH_STAGE::GROWTH ])
+        $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);
+                      ->orWhere('last_disaster_check_time', '<', $checkTime);
             })
-            ->with([
-                       'land.landType', 'seed', 'user.buffs' => function ($query) {
-                    $query->where('expire_time', '>', now());
-                }
-                   ])
+            ->with(['land.landType', 'seed', 'user.buffs' => function ($query) {
+                $query->where('expire_time', '>', now());
+            }])
             ->get();
 
-        $totalCount     = $crops->count();
+        $totalCount = $crops->count();
         $generatedCount = 0;
-        $skippedCount   = 0;
+        $skippedCount = 0;
 
         foreach ($crops as $crop) {
-            $res = $this->generateDisasters($crop);
-            if ($res) {
+            $result = $this->generateDisasters($crop);
+
+            if ($result === 'generated') {
                 $generatedCount++;
-            } else {
+            } elseif ($result === 'skipped') {
                 $skippedCount++;
             }
         }
 
         return [
-            'total'     => $totalCount,
+            'total' => $totalCount,
             'generated' => $generatedCount,
-            'skipped'   => $skippedCount
+            'skipped' => $skippedCount
         ];
     }
 
-    public function generateDisasters(FarmCrop $crop): bool
+    /**
+     * 为单个作物生成灾害
+     *
+     * @param FarmCrop $crop
+     * @return string 处理结果:'generated', 'skipped', 'failed'
+     */
+    public function generateDisasters(FarmCrop $crop): string
     {
         try {
             // 更新检查时间
@@ -291,37 +361,59 @@ class DisasterLogic
             // 跳过已经有灾害的土地
             if ($crop->land->status === LAND_STATUS::DISASTER) {
                 $crop->save(); // 仍需更新检查时间
-
-                return false;
+                return 'skipped';
             }
 
             // 获取相关数据
-            $disasterResistance     = $crop->seed->disaster_resistance ?? [];
-            $landDisasterResistance = $crop->land->landType->disaster_resistance ?? 0;
-            $activeBuffs            = $crop->user->buffs->pluck('buff_type')->toArray();
+            $disasterResistance = $crop->seed->disaster_resistance ?? null;
+            $landDisasterResistance = ($crop->land->landType->disaster_resistance ?? 0) / 100; // 数据库存储百分比,需要除以100
+            $activeBuffs = $crop->user->buffs->pluck('buff_type')->toArray();
 
-            // 尝试生成灾害
-            $disasterInfo = $this->tryGenerateDisasterForCrop($crop, $disasterResistance, $landDisasterResistance, $activeBuffs);
+            // 尝试生成灾害(支持多种灾害)
+            $disasterInfos = $this->tryGenerateDisasterForCrop($crop, $disasterResistance, $landDisasterResistance, $activeBuffs);
+
+            if (!empty($disasterInfos)) {
+                // 应用灾害到作物
+                $this->applyDisastersToCrop($crop, $disasterInfos);
 
-            if ($disasterInfo) {
                 // 生成灾害后,设置当前阶段不能再产生灾害
                 $crop->can_disaster = false;
+                $crop->save();
 
-            }
-
-            // 保存更改
-            $crop->save();
+                Log::info('单个作物灾害生成成功', [
+                    'crop_id' => $crop->id,
+                    'user_id' => $crop->user_id,
+                    'disaster_count' => count($disasterInfos),
+                    'disaster_types' => array_column($disasterInfos, 'type')
+                ]);
 
-            return true;
+                return 'generated';
+            } else {
+                // 没有生成灾害,但需要保存检查时间
+                $crop->save();
+                return 'checked';
+            }
 
         } catch (\Exception $e) {
-            Log::error('批量生成灾害失败', [
+            Log::error('单个作物灾害生成失败', [
                 'crop_id' => $crop->id,
-                'error'   => $e->getMessage()
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
             ]);
+
+            return 'failed';
         }
+    }
 
-        return false;
+    /**
+     * 批量检查并生成灾害(兼容性方法)
+     *
+     * @param int|null $checkIntervalMinutes 检查间隔(分钟)
+     * @return array 生成结果统计
+     */
+    public function batchGenerateDisasters(?int $checkIntervalMinutes = null): array
+    {
+        return $this->generateDisasterBatchs($checkIntervalMinutes);
     }
 
 }

+ 4 - 0
app/Module/Farm/Models/FarmLand.php

@@ -18,6 +18,10 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
  * @property  \Carbon\Carbon  $created_at  创建时间
  * @property  \Carbon\Carbon  $updated_at  更新时间
  * field end
+ * @property-read FarmUser $farmUser  关联的用户农场
+ * @property-read FarmLandType $landType  关联的土地类型
+ *
+ * @package App\Module\Farm\Models
  */
 class FarmLand extends Model
 {

+ 1 - 1
app/Module/Game/AdminControllers/FarmUserSummaryController.php

@@ -459,7 +459,7 @@ class FarmUserSummaryController extends AdminController
         {$table->render()}
         <div class="row mt-2">
             <div class="col-md-12">
-                <a href="javascript:void(0);" class="btn btn-sm btn-primary" onclick="window.open('/admin/game-items-users?user_id={$userId}', '_blank')">查看物品详情</a>
+                <a href="javascript:void(0);" class="btn btn-sm btn-primary" onclick="window.open('/admin/game-items-user-items?user_id={$userId}', '_blank')">查看物品详情</a>
             </div>
         </div>
         HTML;

+ 22 - 0
app/Module/GameItems/Casts/NumericAttributesCast.php

@@ -40,23 +40,45 @@ class NumericAttributesCast extends CastsAttributes
 
     /**
      * 增加宠物体力
+     *
      * @var int $pet_power
      */
     public int $pet_power = 0;
 
     /**
      * 使用后随即奖励物品组
+     *
      * @var int $reward_group_id
      */
     public int $reward_group_id = 0;
 
     /**
      * 增加宠物经验
+     *
      * @var int $pet_exp
      */
     public int $pet_exp = 0;
 
 
+    /**
+     * 除虫概率
+     *
+     * @var int $pesticide_rate
+     */
+    public int $fram_pesticide_rate = 0;
+
 
+    /**
+     * 解决干旱概率
+     *
+     * @var int $drought_rate
+     */
+    public int $fram_drought_rate = 0;
+
+    /**
+     * 解决草宰概率
+     * @var int $grass_rate
+     */
+    public int $fram_grass_rate = 0;
 
 }

+ 3 - 0
app/Module/GameItems/Services/ItemService.php

@@ -159,6 +159,9 @@ class ItemService
      */
     public static function getItemNumericAttribute(int $itemId, string $attributeName, int $defaultValue = 0): int
     {
+        /**
+         * @var Item $item
+         */
         $item = Item::find($itemId);
 
         if (!$item) {