Kaynağa Gözat

feat(farm): 添加土地状态变更事件触发

- 在 CropLogic 和 DisasterLogic 中添加土地状态变更事件触发
- 在 GameServiceProvider 中注册灾害清理事件监听器
- 优化 WateringHandler 类,添加类注释
notfff 7 ay önce
ebeveyn
işleme
c2b8facca2

+ 3 - 1
app/Module/AppGame/Handler/Land/WateringHandler.php

@@ -14,6 +14,8 @@ use UCore\Exception\LogicException;
 
 /**
  * 处理浇水操作请求
+ * Watering
+ * 
  */
 class WateringHandler extends BaseHandler
 {
@@ -119,4 +121,4 @@ class WateringHandler extends BaseHandler
 
         return $response;
     }
-}
+}

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

@@ -461,6 +461,7 @@ class CropLogic
             }
 
             // 如果没有其他活跃灾害,更新土地状态
+            $oldLandStatus = $land->status;
             if (!$hasActiveDisaster) {
                 $land->status = LAND_STATUS::PLANTING->value;
             }
@@ -469,6 +470,11 @@ class CropLogic
             $crop->save();
             $land->save();
 
+            // 如果土地状态发生了变化,触发土地状态变更事件
+            if ($oldLandStatus !== $land->status) {
+                event(new LandStatusChangedEvent($userId, $landId, $oldLandStatus, $land->status));
+            }
+
             // 触发灾害清理事件
             event(new DisasterClearedEvent($userId, $crop, $disasterType, $disasterInfo));
 

+ 7 - 1
app/Module/Farm/Logics/DisasterLogic.php

@@ -280,12 +280,18 @@ class DisasterLogic
 
         // 更新土地状态为灾害
         $land = $crop->land;
-        $land->status = LAND_STATUS::DISASTER;
+        $oldLandStatus = $land->status;
+        $land->status = LAND_STATUS::DISASTER->value;
 
         // 保存更改
         $crop->save();
         $land->save();
 
+        // 如果土地状态发生了变化,触发土地状态变更事件
+        if ($oldLandStatus !== $land->status) {
+            event(new \App\Module\Farm\Events\LandStatusChangedEvent($crop->user_id, $land->id, $oldLandStatus, $land->status));
+        }
+
         // 触发灾害生成事件
         $disasterDtos = [];
         foreach ($disasterInfos as $disasterInfo) {

+ 78 - 0
app/Module/Game/Listeners/DisasterClearedListener.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace App\Module\Game\Listeners;
+
+use App\Module\Farm\Events\DisasterClearedEvent;
+use App\Module\Game\Logics\LandTemp;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 灾害清理事件监听器
+ *
+ * 监听灾害清理事件,并调用逻辑层处理临时数据
+ */
+class DisasterClearedListener
+{
+    /**
+     * 处理事件
+     *
+     * @param DisasterClearedEvent $event 灾害清理事件
+     * @return void
+     */
+    public function handle(DisasterClearedEvent $event): void
+    {
+        try {
+            Log::info('Game模块接收到灾害清理事件', [
+                'user_id' => $event->userId,
+                'crop_id' => $event->crop->id,
+                'disaster_type' => $event->disasterType,
+                'disaster_info' => $event->disasterInfo
+            ]);
+
+            // 获取土地信息
+            $land = $event->crop->land;
+            if (!$land) {
+                Log::error('处理灾害清理事件失败:土地不存在', [
+                    'user_id' => $event->userId,
+                    'crop_id' => $event->crop->id
+                ]);
+                return;
+            }
+
+            // 构建土地状态变更数据
+            $landStatusData = [
+                'land_id' => $land->id,
+                'status' => $land->status,
+                'crop' => $event->crop->toArray(),
+                'updated_at' => now()->toDateTimeString()
+            ];
+
+            // 构建临时数据键,按用户ID进行存储
+            $tempKey = LandTemp::TEMP_KEY_STATUS_PREFIX . $event->userId;
+
+            // 获取当前用户的土地状态变更临时数据
+            $userLandsStatusTemp = \UCore\Helper\Cache::get($tempKey, []);
+
+            // 将新的土地状态变更数据添加到临时数据中
+            $userLandsStatusTemp[$land->id] = \App\Module\Game\Dtos\LandStatusTempDto::fromArray($landStatusData);
+
+            // 将更新后的临时数据存回缓存
+            \UCore\Helper\Cache::put($tempKey, $userLandsStatusTemp, LandTemp::TEMP_TTL);
+
+            Log::info('灾害清理事件处理成功,土地状态变更数据已存储', [
+                'user_id' => $event->userId,
+                'land_id' => $land->id,
+                'crop_id' => $event->crop->id,
+                'disaster_type' => $event->disasterType
+            ]);
+        } catch (\Exception $e) {
+            Log::error('处理灾害清理事件失败', [
+                'user_id' => $event->userId,
+                'crop_id' => $event->crop->id,
+                'disaster_type' => $event->disasterType,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+        }
+    }
+}

+ 8 - 0
app/Module/Game/Providers/GameServiceProvider.php

@@ -12,6 +12,7 @@ use App\Module\Game\Events\RewardGrantedEvent;
 // 如果 Farm 模块中尚未定义这些事件类,请先在 Farm 模块中创建它们
 use App\Module\Farm\Events\CropGrowthStageChangedEvent;
 use App\Module\Farm\Events\CropPlantedEvent;
+use App\Module\Farm\Events\DisasterClearedEvent;
 use App\Module\Farm\Events\HouseDowngradedEvent;
 use App\Module\Farm\Events\HouseUpgradedEvent;
 
@@ -19,6 +20,7 @@ use App\Module\Farm\Events\LandUpgradedEvent;
 use App\Module\Fund\Events\FundChangedEvent;
 use App\Module\Game\Listeners\CropGrowthStageChangedListener;
 use App\Module\Game\Listeners\CropPlantedListener;
+use App\Module\Game\Listeners\DisasterClearedListener;
 use App\Module\Game\Listeners\FundChangedListener;
 use App\Module\Game\Listeners\HouseDowngradedListener;
 use App\Module\Game\Listeners\HouseUpgradedListener;
@@ -127,6 +129,12 @@ class GameServiceProvider extends ServiceProvider
             CropGrowthStageChangedListener::class
         );
 
+        // 注册灾害清理事件监听器
+        Event::listen(
+            DisasterClearedEvent::class,
+            DisasterClearedListener::class
+        );
+
         // 注册房屋事件监听器
         Event::listen(
             HouseUpgradedEvent::class,

+ 178 - 0
docs/浇水成功Response的LastData缺失DataLands修复说明.md

@@ -0,0 +1,178 @@
+# 浇水成功 Response 的 LastData 缺失 DataLands 修复说明
+
+## 问题描述
+
+用户浇水成功后,Response 的 LastData 中缺失 DataLands 信息,导致客户端无法及时更新土地状态。
+
+## 问题原因分析
+
+通过代码分析发现,系统使用了土地暂存系统(LandTemp)来处理土地状态变更,然后通过 `AppGameProtobufResponseListener` 监听器在响应中自动添加 LastData。但是浇水操作成功后,存在以下问题:
+
+1. **灾害清理时未触发土地状态变更事件**:在 `CropLogic::clearDisaster` 方法中,虽然更新了土地状态,但没有触发 `LandStatusChangedEvent` 事件。
+
+2. **缺少灾害清理事件监听器**:虽然触发了 `DisasterClearedEvent` 事件,但没有相应的监听器来处理土地暂存数据的更新。
+
+3. **灾害生成时也存在同样问题**:在 `DisasterLogic::applyDisastersToCrop` 方法中,更新土地状态时也没有触发相应的事件。
+
+## 修复方案
+
+### 1. 修复灾害清理时的土地状态变更事件触发
+
+**文件**: `app/Module/Farm/Logics/CropLogic.php`
+
+在 `clearDisaster` 方法中添加土地状态变更事件的触发:
+
+```php
+// 如果没有其他活跃灾害,更新土地状态
+$oldLandStatus = $land->status;
+if (!$hasActiveDisaster) {
+    $land->status = LAND_STATUS::PLANTING->value;
+}
+
+// 保存更改
+$crop->save();
+$land->save();
+
+// 如果土地状态发生了变化,触发土地状态变更事件
+if ($oldLandStatus !== $land->status) {
+    event(new LandStatusChangedEvent($userId, $landId, $oldLandStatus, $land->status));
+}
+```
+
+### 2. 创建灾害清理事件监听器
+
+**文件**: `app/Module/Game/Listeners/DisasterClearedListener.php`
+
+创建专门的监听器来处理 `DisasterClearedEvent` 事件,更新土地暂存数据:
+
+```php
+public function handle(DisasterClearedEvent $event): void
+{
+    // 获取土地信息并构建暂存数据
+    $land = $event->crop->land;
+    $landStatusData = [
+        'land_id' => $land->id,
+        'status' => $land->status,
+        'crop' => $event->crop->toArray(),
+        'updated_at' => now()->toDateTimeString()
+    ];
+
+    // 存储到暂存系统
+    $tempKey = LandTemp::TEMP_KEY_STATUS_PREFIX . $event->userId;
+    $userLandsStatusTemp = \UCore\Helper\Cache::get($tempKey, []);
+    $userLandsStatusTemp[$land->id] = \App\Module\Game\Dtos\LandStatusTempDto::fromArray($landStatusData);
+    \UCore\Helper\Cache::put($tempKey, $userLandsStatusTemp, LandTemp::TEMP_TTL);
+}
+```
+
+### 3. 注册灾害清理事件监听器
+
+**文件**: `app/Module/Game/Providers/GameServiceProvider.php`
+
+在 Game 模块的服务提供者中注册新的监听器:
+
+```php
+// 注册灾害清理事件监听器
+Event::listen(
+    DisasterClearedEvent::class,
+    DisasterClearedListener::class
+);
+```
+
+### 4. 修复灾害生成时的土地状态变更事件触发
+
+**文件**: `app/Module/Farm/Logics/DisasterLogic.php`
+
+在 `applyDisastersToCrop` 方法中添加土地状态变更事件的触发:
+
+```php
+// 更新土地状态为灾害
+$land = $crop->land;
+$oldLandStatus = $land->status;
+$land->status = LAND_STATUS::DISASTER->value;
+
+// 保存更改
+$crop->save();
+$land->save();
+
+// 如果土地状态发生了变化,触发土地状态变更事件
+if ($oldLandStatus !== $land->status) {
+    event(new \App\Module\Farm\Events\LandStatusChangedEvent($crop->user_id, $land->id, $oldLandStatus, $land->status));
+}
+```
+
+## 修复效果
+
+修复后,当用户进行浇水操作时:
+
+1. **成功浇水**:
+   - 灾害被清理,土地状态从 `DISASTER` 更新为 `PLANTING`
+   - 触发 `LandStatusChangedEvent` 和 `DisasterClearedEvent` 事件
+   - 监听器更新土地暂存数据
+   - `AppGameProtobufResponseListener` 自动将暂存数据添加到 Response 的 LastData 中
+   - 客户端收到完整的土地状态更新
+
+2. **失败浇水**:
+   - 物品被消耗,但灾害状态不变
+   - 不触发土地状态变更事件
+   - 返回错误信息
+
+## 数据流程
+
+```
+用户浇水操作
+    ↓
+WateringHandler 处理请求
+    ↓
+CropService::removeDisasterWithItem (概率判断)
+    ↓
+DisasterRemovalLogic::removeDisaster
+    ↓
+CropLogic::clearDisaster
+    ↓
+触发 LandStatusChangedEvent + DisasterClearedEvent
+    ↓
+LandStatusChangedListener + DisasterClearedListener 处理事件
+    ↓
+更新土地暂存数据 (LandTemp)
+    ↓
+AppGameProtobufResponseListener 监听响应事件
+    ↓
+从暂存数据构建 LastData.lands
+    ↓
+客户端收到包含 DataLands 的响应
+```
+
+## 测试验证
+
+创建了功能测试 `tests/Feature/WateringResponseTest.php` 来验证:
+
+1. 浇水成功后 Response 包含 LastData.lands
+2. 土地状态正确更新
+3. 灾害状态正确清理
+4. 浇水失败时的错误处理
+
+## 相关文件
+
+### 修改的文件
+- `app/Module/Farm/Logics/CropLogic.php`
+- `app/Module/Farm/Logics/DisasterLogic.php`
+- `app/Module/Game/Providers/GameServiceProvider.php`
+
+### 新增的文件
+- `app/Module/Game/Listeners/DisasterClearedListener.php`
+- `tests/Feature/WateringResponseTest.php`
+
+## 注意事项
+
+1. **事件触发顺序**:确保在保存数据库更改后再触发事件
+2. **状态比较**:只有在土地状态真正发生变化时才触发事件,避免不必要的处理
+3. **异常处理**:监听器中包含完整的异常处理,确保单个事件处理失败不影响整体流程
+4. **缓存一致性**:暂存数据的 TTL 设置合理,避免数据过期导致的不一致
+
+## 后续优化建议
+
+1. 考虑将土地状态变更事件的触发逻辑抽取为通用方法
+2. 添加更多的单元测试覆盖边界情况
+3. 监控事件处理的性能,确保不影响响应速度
+4. 考虑添加事件处理失败的重试机制

+ 148 - 0
tests/Feature/WateringResponseTest.php

@@ -0,0 +1,148 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Module\Farm\Models\FarmCrop;
+use App\Module\Farm\Models\FarmLand;
+use App\Module\Farm\Services\CropService;
+use App\Module\GameItems\Services\ItemService;
+use App\Module\User\Models\User;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Cache;
+use Tests\TestCase;
+
+/**
+ * 浇水响应测试
+ * 
+ * 测试浇水成功后Response的LastData中是否包含DataLands
+ */
+class WateringResponseTest extends TestCase
+{
+    use RefreshDatabase;
+
+    /**
+     * 测试浇水成功后LastData包含土地信息
+     */
+    public function testWateringSuccessIncludesLandData()
+    {
+        // 创建测试用户
+        $user = User::factory()->create();
+        
+        // 创建测试土地
+        $land = FarmLand::factory()->create([
+            'user_id' => $user->id,
+            'status' => 3, // DISASTER状态
+        ]);
+        
+        // 创建测试作物(带有干旱灾害)
+        $crop = FarmCrop::factory()->create([
+            'land_id' => $land->id,
+            'user_id' => $user->id,
+            'disasters' => [
+                [
+                    'type' => 1, // 干旱
+                    'status' => 'active',
+                    'start_time' => now()->timestamp,
+                    'penalty' => 0.05
+                ]
+            ]
+        ]);
+        
+        // 创建浇水道具
+        $wateringItem = \App\Module\GameItems\Models\Item::factory()->create([
+            'fram_drought_rate' => 100 // 100%成功率
+        ]);
+        
+        // 给用户添加浇水道具
+        ItemService::addItem($user->id, $wateringItem->id, 1);
+        
+        // 清空缓存,确保测试环境干净
+        Cache::flush();
+        
+        // 模拟浇水请求
+        $response = $this->actingAs($user)
+            ->postJson('/api/land/watering', [
+                'land_id' => $land->id,
+                'item_id' => $wateringItem->id
+            ]);
+        
+        // 验证响应成功
+        $response->assertStatus(200);
+        
+        // 验证响应中包含LastData
+        $responseData = $response->json();
+        $this->assertArrayHasKey('last_data', $responseData);
+        
+        // 验证LastData中包含lands信息
+        $lastData = $responseData['last_data'];
+        $this->assertArrayHasKey('lands', $lastData);
+        $this->assertNotEmpty($lastData['lands']);
+        
+        // 验证土地信息正确
+        $landData = collect($lastData['lands'])->firstWhere('id', $land->id);
+        $this->assertNotNull($landData);
+        
+        // 验证灾害状态已更新
+        $this->assertFalse($landData['need_watering'] ?? true);
+        
+        // 验证数据库中的灾害状态已更新
+        $crop->refresh();
+        $disasters = $crop->disasters ?? [];
+        $droughtDisaster = collect($disasters)->firstWhere('type', 1);
+        $this->assertEquals('cleared', $droughtDisaster['status'] ?? 'active');
+    }
+    
+    /**
+     * 测试浇水失败时的响应
+     */
+    public function testWateringFailureResponse()
+    {
+        // 创建测试用户
+        $user = User::factory()->create();
+        
+        // 创建测试土地
+        $land = FarmLand::factory()->create([
+            'user_id' => $user->id,
+            'status' => 3, // DISASTER状态
+        ]);
+        
+        // 创建测试作物(带有干旱灾害)
+        $crop = FarmCrop::factory()->create([
+            'land_id' => $land->id,
+            'user_id' => $user->id,
+            'disasters' => [
+                [
+                    'type' => 1, // 干旱
+                    'status' => 'active',
+                    'start_time' => now()->timestamp,
+                    'penalty' => 0.05
+                ]
+            ]
+        ]);
+        
+        // 创建浇水道具(低成功率)
+        $wateringItem = \App\Module\GameItems\Models\Item::factory()->create([
+            'fram_drought_rate' => 0 // 0%成功率,确保失败
+        ]);
+        
+        // 给用户添加浇水道具
+        ItemService::addItem($user->id, $wateringItem->id, 1);
+        
+        // 清空缓存,确保测试环境干净
+        Cache::flush();
+        
+        // 模拟浇水请求
+        $response = $this->actingAs($user)
+            ->postJson('/api/land/watering', [
+                'land_id' => $land->id,
+                'item_id' => $wateringItem->id
+            ]);
+        
+        // 验证响应失败
+        $response->assertStatus(400);
+        
+        // 验证错误消息
+        $responseData = $response->json();
+        $this->assertStringContains('浇水失败', $responseData['msg'] ?? '');
+    }
+}