Browse Source

refactor(farm): 重构灾害去除功能,新增验证层架构

- 新增 DisasterRemovalValidation 类统一数据验证
- 新增 LandOwnershipValidator 和 DisasterRemovalItemValidator 类
- 修改 Handler 层,采用验证前置的处理模式
- 优化异常处理逻辑,提高系统稳定性
- 调整 CropService,支持验证和业务逻辑分离
- 更新测试用例,适应新的验证层架构
notfff 7 months ago
parent
commit
bdbd0f8687

+ 2 - 13
.roo/rules-code/rules.md

@@ -5,24 +5,13 @@
 - 使用目录(往目录里增加/修改/删除文件)先阅读目录的README.md
 
 # 开发流程
-1. 理解需求
-2. 阅读已有代码
-2. 提出开发方案,包含以下内容,逐条确认
-    - 在那个模块下进行开发
-    - 梳理基本基本思路
-    - 需要创建和修改的文件
-    - 没有明确需要测试,就不写测试
+1. 阅读已有代码,理解需求
+2. 提出开发方案,
 3. 用户确认方案
 3. 进行开发
 
 
 
-# 模块开发的规则
-- 模块在项目的app/Module目录下
-- 模块的规则可参考Test模块的README.md(读取最新内容)
-
-# Validation开发规则
-- 阅读文档 @/UCore/Validation/README.md 了解Validation开发规则,Validator开发规则
 
 # 枚举规范
 - 文件名/类名/case名采用全大写下划线命名

+ 32 - 6
app/Module/AppGame/Handler/Land/PesticideHandler.php

@@ -42,10 +42,21 @@ class PesticideHandler extends BaseHandler
             $userItemId = $data->getItemId();
             $userId = $this->user_id;
 
-            // 开启事务
+            // 先进行验证,避免不必要的事务开销
+            $validation = new \App\Module\Farm\Validations\DisasterRemovalValidation([
+                'user_id' => $userId,
+                'land_id' => $landId,
+                'item_id' => $userItemId,
+                'disaster_type' => DISASTER_TYPE::PEST->value
+            ]);
+
+            // 验证数据
+            $validation->validated();
+
+            // 验证通过后,开启事务
             DB::beginTransaction();
 
-            // 使用统一的灾害去除逻辑,包含概率判断和事务检测
+            // 执行业务逻辑(不再需要验证)
             $result = CropService::removeDisasterWithItem(
                 $userId,
                 $landId,
@@ -61,9 +72,22 @@ class PesticideHandler extends BaseHandler
             $this->response->setCode(0);
             $this->response->setMsg($result['message']);
 
+        } catch (\UCore\Exception\ValidateException $e) {
+            // 验证失败,此时可能还没有开启事务
+            $this->response->setCode(400);
+            $this->response->setMsg($e->getMessage());
+
+            Log::warning('用户除虫验证失败', [
+                'user_id' => $this->user_id,
+                'land_id' => $landId ?? null,
+                'item_id' => $userItemId ?? null,
+                'error' => $e->getMessage()
+            ]);
         } catch (LogicException $e) {
-            // 回滚事务
-            DB::rollBack();
+            // 业务逻辑异常,需要回滚事务
+            if (DB::transactionLevel() > 0) {
+                DB::rollBack();
+            }
 
             // 设置错误响应
             $this->response->setCode(400);
@@ -77,8 +101,10 @@ class PesticideHandler extends BaseHandler
                 'trace' => $e->getTraceAsString()
             ]);
         } catch (\Exception $e) {
-            // 回滚事务
-            DB::rollBack();
+            // 系统异常,需要回滚事务
+            if (DB::transactionLevel() > 0) {
+                DB::rollBack();
+            }
 
             // 设置错误响应
             $this->response->setCode(500);

+ 32 - 6
app/Module/AppGame/Handler/Land/WateringHandler.php

@@ -40,10 +40,21 @@ class WateringHandler extends BaseHandler
             $userItemId = $data->getItemId();
             $userId = $this->user_id;
 
-            // 开启事务
+            // 先进行验证,避免不必要的事务开销
+            $validation = new \App\Module\Farm\Validations\DisasterRemovalValidation([
+                'user_id' => $userId,
+                'land_id' => $landId,
+                'item_id' => $userItemId,
+                'disaster_type' => DISASTER_TYPE::DROUGHT->value
+            ]);
+
+            // 验证数据
+            $validation->validated();
+
+            // 验证通过后,开启事务
             DB::beginTransaction();
 
-            // 使用统一的灾害去除逻辑,包含概率判断和事务检测
+            // 执行业务逻辑(不再需要验证)
             $result = CropService::removeDisasterWithItem(
                 $userId,
                 $landId,
@@ -59,9 +70,22 @@ class WateringHandler extends BaseHandler
             $this->response->setCode(0);
             $this->response->setMsg($result['message']);
 
+        } catch (\UCore\Exception\ValidateException $e) {
+            // 验证失败,此时可能还没有开启事务
+            $this->response->setCode(400);
+            $this->response->setMsg($e->getMessage());
+
+            Log::warning('用户浇水验证失败', [
+                'user_id' => $this->user_id,
+                'land_id' => $landId ?? null,
+                'item_id' => $userItemId ?? null,
+                'error' => $e->getMessage()
+            ]);
         } catch (LogicException $e) {
-            // 回滚事务
-            DB::rollBack();
+            // 业务逻辑异常,需要回滚事务
+            if (DB::transactionLevel() > 0) {
+                DB::rollBack();
+            }
 
             // 设置错误响应
             $this->response->setCode(400);
@@ -75,8 +99,10 @@ class WateringHandler extends BaseHandler
                 'trace' => $e->getTraceAsString()
             ]);
         } catch (\Exception $e) {
-            // 回滚事务
-            DB::rollBack();
+            // 系统异常,需要回滚事务
+            if (DB::transactionLevel() > 0) {
+                DB::rollBack();
+            }
 
             // 设置错误响应
             $this->response->setCode(500);

+ 32 - 6
app/Module/AppGame/Handler/Land/WeedicideHandler.php

@@ -40,10 +40,21 @@ class WeedicideHandler extends BaseHandler
             $userItemId = $data->getItemId();
             $userId = $this->user_id;
 
-            // 开启事务
+            // 先进行验证,避免不必要的事务开销
+            $validation = new \App\Module\Farm\Validations\DisasterRemovalValidation([
+                'user_id' => $userId,
+                'land_id' => $landId,
+                'item_id' => $userItemId,
+                'disaster_type' => DISASTER_TYPE::WEED->value
+            ]);
+
+            // 验证数据
+            $validation->validated();
+
+            // 验证通过后,开启事务
             DB::beginTransaction();
 
-            // 使用统一的灾害去除逻辑,包含概率判断和事务检测
+            // 执行业务逻辑(不再需要验证)
             $result = CropService::removeDisasterWithItem(
                 $userId,
                 $landId,
@@ -59,9 +70,22 @@ class WeedicideHandler extends BaseHandler
             $this->response->setCode(0);
             $this->response->setMsg($result['message']);
 
+        } catch (\UCore\Exception\ValidateException $e) {
+            // 验证失败,此时可能还没有开启事务
+            $this->response->setCode(400);
+            $this->response->setMsg($e->getMessage());
+
+            Log::warning('用户除草验证失败', [
+                'user_id' => $this->user_id,
+                'land_id' => $landId ?? null,
+                'item_id' => $userItemId ?? null,
+                'error' => $e->getMessage()
+            ]);
         } catch (LogicException $e) {
-            // 回滚事务
-            DB::rollBack();
+            // 业务逻辑异常,需要回滚事务
+            if (DB::transactionLevel() > 0) {
+                DB::rollBack();
+            }
 
             // 设置错误响应
             $this->response->setCode(400);
@@ -75,8 +99,10 @@ class WeedicideHandler extends BaseHandler
                 'trace' => $e->getTraceAsString()
             ]);
         } catch (\Exception $e) {
-            // 回滚事务
-            DB::rollBack();
+            // 系统异常,需要回滚事务
+            if (DB::transactionLevel() > 0) {
+                DB::rollBack();
+            }
 
             // 设置错误响应
             $this->response->setCode(500);

+ 179 - 0
app/Module/AppGame/Tests/Land/DisasterRemovalBaseTest.php

@@ -0,0 +1,179 @@
+<?php
+
+namespace App\Module\AppGame\Tests\Land;
+
+use App\Module\AppGame\Tests\TestConfig;
+use Google\Protobuf\Internal\Message;
+use Tests\Unit\ProtoRequestTest;
+use Uraus\Kku\Request;
+use Uraus\Kku\Response;
+
+/**
+ * 灾害去除测试基类
+ *
+ * 提供灾害去除相关测试的通用方法和断言
+ */
+abstract class DisasterRemovalBaseTest extends ProtoRequestTest
+{
+    /**
+     * 测试用户ID
+     */
+    protected int $testUserId;
+
+    /**
+     * 设置测试环境
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->testUserId = TestConfig::getTestUserId();
+    }
+
+    /**
+     * 验证成功响应
+     */
+    protected function assertSuccessResponse(Response $response, ?string $expectedMessage = null): void
+    {
+        $this->assertInstanceOf(Response::class, $response);
+
+        // 检查响应码
+        $this->assertEquals(0, $response->getCode(), '响应码应该为0表示成功');
+
+        // 检查消息
+        if ($expectedMessage) {
+            $this->assertStringContainsString($expectedMessage, $response->getMsg());
+        }
+
+        // 输出响应内容用于调试
+        dump('成功响应: ' . $response->serializeToJsonString());
+    }
+
+    /**
+     * 验证失败响应
+     */
+    protected function assertFailureResponse(Response $response, ?string $expectedError = null): void
+    {
+        $this->assertInstanceOf(Response::class, $response);
+
+        // 检查响应码(非0表示失败)
+        $this->assertNotEquals(0, $response->getCode(), '响应码应该非0表示失败');
+
+        // 检查错误消息
+        if ($expectedError) {
+            $this->assertStringContainsString($expectedError, $response->getMsg());
+        }
+
+        // 输出响应内容用于调试
+        dump('失败响应: ' . $response->serializeToJsonString());
+    }
+
+    /**
+     * 验证验证失败响应(400错误)
+     */
+    protected function assertValidationFailureResponse(Response $response, ?string $expectedError = null): void
+    {
+        $this->assertInstanceOf(Response::class, $response);
+
+        // 检查响应码(400表示验证失败)
+        $this->assertEquals(400, $response->getCode(), '响应码应该为400表示验证失败');
+
+        // 检查错误消息
+        if ($expectedError) {
+            $this->assertStringContainsString($expectedError, $response->getMsg());
+        }
+
+        // 输出响应内容用于调试
+        dump('验证失败响应: ' . $response->serializeToJsonString());
+    }
+
+    /**
+     * 创建基础请求对象
+     */
+    protected function createBaseRequest(): Request
+    {
+        $request = new Request();
+        // 这里可以添加通用的请求头设置,如用户认证等
+        return $request;
+    }
+
+    /**
+     * 测试成功场景的通用方法
+     */
+    protected function runSuccessScenario(string $scenarioName, callable $requestCreator, ?string $expectedMessage = null): void
+    {
+        dump("开始测试成功场景: {$scenarioName}");
+
+        // 创建请求
+        $this->create_request_protobuf = $requestCreator;
+
+        // 发送请求并验证响应
+        $response = $this->protobufRequest();
+        $this->assertSuccessResponse($response, $expectedMessage);
+
+        dump("成功场景测试完成: {$scenarioName}");
+    }
+
+    /**
+     * 测试失败场景的通用方法
+     */
+    protected function runFailureScenario(string $scenarioName, callable $requestCreator, ?string $expectedError = null): void
+    {
+        dump("开始测试失败场景: {$scenarioName}");
+
+        // 创建请求
+        $this->create_request_protobuf = $requestCreator;
+
+        // 发送请求并验证响应
+        $response = $this->protobufRequest();
+        $this->assertFailureResponse($response, $expectedError);
+
+        dump("失败场景测试完成: {$scenarioName}");
+    }
+
+    /**
+     * 测试验证失败场景的通用方法
+     */
+    protected function runValidationFailureScenario(string $scenarioName, callable $requestCreator, ?string $expectedError = null): void
+    {
+        dump("开始测试验证失败场景: {$scenarioName}");
+
+        // 创建请求
+        $this->create_request_protobuf = $requestCreator;
+
+        // 发送请求并验证响应
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response, $expectedError);
+
+        dump("验证失败场景测试完成: {$scenarioName}");
+    }
+
+    /**
+     * 输出测试配置信息
+     */
+    protected function dumpTestConfig(array $config): void
+    {
+        dump('测试配置:', $config);
+    }
+
+    /**
+     * 输出测试开始信息
+     */
+    protected function dumpTestStart(string $testName): void
+    {
+        dump("=== 开始测试: {$testName} ===");
+    }
+
+    /**
+     * 输出测试结束信息
+     */
+    protected function dumpTestEnd(string $testName): void
+    {
+        dump("=== 测试完成: {$testName} ===");
+    }
+
+    /**
+     * 创建protobuf请求的抽象方法
+     * 子类必须实现此方法
+     */
+    abstract public function create_request_protobuf(): Message;
+}

+ 347 - 0
app/Module/AppGame/Tests/Land/DisasterRemovalTestSuite.php

@@ -0,0 +1,347 @@
+<?php
+
+namespace App\Module\AppGame\Tests\Land;
+
+use App\Module\AppGame\Tests\TestConfig;
+use Tests\TestCase;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 灾害去除测试套件
+ * 
+ * 集成所有灾害去除相关的测试,提供统一的测试入口和报告
+ */
+class DisasterRemovalTestSuite extends TestCase
+{
+    /**
+     * 测试结果统计
+     */
+    private array $testResults = [
+        'total' => 0,
+        'passed' => 0,
+        'failed' => 0,
+        'errors' => []
+    ];
+
+    /**
+     * 运行完整的灾害去除测试套件
+     */
+    public function testCompleteDisasterRemovalSuite()
+    {
+        $this->dumpSuiteStart();
+        
+        try {
+            // 1. 运行除虫测试
+            $this->runPesticideTests();
+            
+            // 2. 运行除草测试
+            $this->runWeedicideTests();
+            
+            // 3. 运行浇水测试
+            $this->runWateringTests();
+            
+            // 4. 运行集成测试
+            $this->runIntegrationTests();
+            
+        } catch (\Exception $e) {
+            $this->recordError('测试套件执行异常', $e);
+        }
+        
+        $this->dumpSuiteResults();
+        $this->dumpSuiteEnd();
+    }
+
+    /**
+     * 运行除虫测试
+     */
+    private function runPesticideTests(): void
+    {
+        dump("=== 开始除虫测试模块 ===");
+        
+        try {
+            $pesticideTest = new PesticideHandlerTest();
+            $pesticideTest->setUp();
+            
+            // 基础功能测试
+            $this->runSingleTest('除虫成功测试', function() use ($pesticideTest) {
+                $pesticideTest->testPesticideSuccess();
+            });
+            
+            $this->runSingleTest('无效物品除虫测试', function() use ($pesticideTest) {
+                $pesticideTest->testPesticideWithInvalidItem();
+            });
+            
+            $this->runSingleTest('其他用户土地除虫测试', function() use ($pesticideTest) {
+                $pesticideTest->testPesticideOnOtherUserLand();
+            });
+            
+            // 概率测试
+            $this->runSingleTest('除虫概率机制测试', function() use ($pesticideTest) {
+                $pesticideTest->testPesticideProbability();
+            });
+            
+        } catch (\Exception $e) {
+            $this->recordError('除虫测试模块', $e);
+        }
+        
+        dump("=== 除虫测试模块完成 ===");
+    }
+
+    /**
+     * 运行除草测试
+     */
+    private function runWeedicideTests(): void
+    {
+        dump("=== 开始除草测试模块 ===");
+        
+        try {
+            $weedicideTest = new WeedicideHandlerTest();
+            $weedicideTest->setUp();
+            
+            // 基础功能测试
+            $this->runSingleTest('除草成功测试', function() use ($weedicideTest) {
+                $weedicideTest->testWeedicideSuccess();
+            });
+            
+            $this->runSingleTest('无效物品除草测试', function() use ($weedicideTest) {
+                $weedicideTest->testWeedicideWithInvalidItem();
+            });
+            
+            $this->runSingleTest('错误物品类型除草测试', function() use ($weedicideTest) {
+                $weedicideTest->testWeedicideWithWrongItemType();
+            });
+            
+            // 概率测试
+            $this->runSingleTest('除草概率机制测试', function() use ($weedicideTest) {
+                $weedicideTest->testWeedicideProbability();
+            });
+            
+            // 并发测试
+            $this->runSingleTest('并发除草请求测试', function() use ($weedicideTest) {
+                $weedicideTest->testConcurrentWeedicideRequests();
+            });
+            
+        } catch (\Exception $e) {
+            $this->recordError('除草测试模块', $e);
+        }
+        
+        dump("=== 除草测试模块完成 ===");
+    }
+
+    /**
+     * 运行浇水测试
+     */
+    private function runWateringTests(): void
+    {
+        dump("=== 开始浇水测试模块 ===");
+        
+        try {
+            $wateringTest = new WateringHandlerTest();
+            $wateringTest->setUp();
+            
+            // 基础功能测试
+            $this->runSingleTest('浇水成功测试', function() use ($wateringTest) {
+                $wateringTest->testWateringSuccess();
+            });
+            
+            $this->runSingleTest('无效物品浇水测试', function() use ($wateringTest) {
+                $wateringTest->testWateringWithInvalidItem();
+            });
+            
+            $this->runSingleTest('错误物品类型浇水测试', function() use ($wateringTest) {
+                $wateringTest->testWateringWithWrongItemType();
+            });
+            
+            // 概率测试
+            $this->runSingleTest('浇水概率机制测试', function() use ($wateringTest) {
+                $wateringTest->testWateringProbability();
+            });
+            
+            // 重复操作测试
+            $this->runSingleTest('重复浇水测试', function() use ($wateringTest) {
+                $wateringTest->testRepeatedWateringOnSameLand();
+            });
+            
+            // 性能测试
+            $this->runSingleTest('浇水性能测试', function() use ($wateringTest) {
+                $wateringTest->testWateringPerformance();
+            });
+            
+        } catch (\Exception $e) {
+            $this->recordError('浇水测试模块', $e);
+        }
+        
+        dump("=== 浇水测试模块完成 ===");
+    }
+
+    /**
+     * 运行集成测试
+     */
+    private function runIntegrationTests(): void
+    {
+        dump("=== 开始集成测试模块 ===");
+        
+        try {
+            // 测试混合使用不同类型的灾害去除道具
+            $this->runSingleTest('混合灾害去除测试', function() {
+                $this->testMixedDisasterRemoval();
+            });
+            
+            // 测试配置验证
+            $this->runSingleTest('测试配置验证', function() {
+                $this->testConfigValidation();
+            });
+            
+        } catch (\Exception $e) {
+            $this->recordError('集成测试模块', $e);
+        }
+        
+        dump("=== 集成测试模块完成 ===");
+    }
+
+    /**
+     * 测试混合灾害去除
+     */
+    private function testMixedDisasterRemoval(): void
+    {
+        dump("开始混合灾害去除测试");
+        
+        // 依次测试不同类型的灾害去除
+        $scenarios = [
+            ['type' => 'pesticide', 'description' => '除虫'],
+            ['type' => 'weedicide', 'description' => '除草'],
+            ['type' => 'watering', 'description' => '浇水']
+        ];
+        
+        foreach ($scenarios as $scenario) {
+            dump("测试 {$scenario['description']} 功能");
+            
+            $config = TestConfig::getTestScenario('success_scenarios', $scenario['type'] . '_success');
+            if (!empty($config)) {
+                dump("配置: ", $config);
+                // 这里可以添加具体的测试逻辑
+                $this->assertTrue(true, "{$scenario['description']} 测试通过");
+            }
+        }
+        
+        dump("混合灾害去除测试完成");
+    }
+
+    /**
+     * 测试配置验证
+     */
+    private function testConfigValidation(): void
+    {
+        dump("开始测试配置验证");
+        
+        // 验证测试用户配置
+        $testUser = TestConfig::getTestUserId();
+        $this->assertGreaterThan(0, $testUser, '测试用户ID应该大于0');
+        
+        // 验证灾害去除配置
+        $pesticideConfig = TestConfig::getPesticideTestConfig();
+        $this->assertArrayHasKey('land_id', $pesticideConfig, '除虫配置应包含land_id');
+        $this->assertArrayHasKey('item_id', $pesticideConfig, '除虫配置应包含item_id');
+        
+        $weedicideConfig = TestConfig::getWeedicideTestConfig();
+        $this->assertArrayHasKey('land_id', $weedicideConfig, '除草配置应包含land_id');
+        $this->assertArrayHasKey('item_id', $weedicideConfig, '除草配置应包含item_id');
+        
+        $wateringConfig = TestConfig::getWateringTestConfig();
+        $this->assertArrayHasKey('land_id', $wateringConfig, '浇水配置应包含land_id');
+        $this->assertArrayHasKey('item_id', $wateringConfig, '浇水配置应包含item_id');
+        
+        // 验证测试物品配置
+        $pesticideItem = TestConfig::getTestItem('pesticide');
+        $this->assertArrayHasKey('item_id', $pesticideItem, '除虫剂配置应包含item_id');
+        
+        dump("测试配置验证完成");
+    }
+
+    /**
+     * 运行单个测试
+     */
+    private function runSingleTest(string $testName, callable $testFunction): void
+    {
+        $this->testResults['total']++;
+        
+        try {
+            dump("开始测试: {$testName}");
+            $testFunction();
+            $this->testResults['passed']++;
+            dump("测试通过: {$testName}");
+        } catch (\Exception $e) {
+            $this->testResults['failed']++;
+            $this->recordError($testName, $e);
+            dump("测试失败: {$testName} - " . $e->getMessage());
+        }
+    }
+
+    /**
+     * 记录错误
+     */
+    private function recordError(string $testName, \Exception $e): void
+    {
+        $this->testResults['errors'][] = [
+            'test' => $testName,
+            'error' => $e->getMessage(),
+            'trace' => $e->getTraceAsString()
+        ];
+        
+        Log::error("测试失败: {$testName}", [
+            'error' => $e->getMessage(),
+            'trace' => $e->getTraceAsString()
+        ]);
+    }
+
+    /**
+     * 输出测试套件开始信息
+     */
+    private function dumpSuiteStart(): void
+    {
+        dump("========================================");
+        dump("    灾害去除系统 E2E 测试套件");
+        dump("========================================");
+        dump("测试环境: " . env('UNITTEST_URL', 'http://localhost:8000'));
+        dump("测试用户: " . TestConfig::getTestUserId());
+        dump("开始时间: " . date('Y-m-d H:i:s'));
+        dump("========================================");
+    }
+
+    /**
+     * 输出测试结果
+     */
+    private function dumpSuiteResults(): void
+    {
+        dump("========================================");
+        dump("           测试结果统计");
+        dump("========================================");
+        dump("总测试数: " . $this->testResults['total']);
+        dump("通过数量: " . $this->testResults['passed']);
+        dump("失败数量: " . $this->testResults['failed']);
+        
+        if (!empty($this->testResults['errors'])) {
+            dump("失败详情:");
+            foreach ($this->testResults['errors'] as $error) {
+                dump("- {$error['test']}: {$error['error']}");
+            }
+        }
+        
+        $successRate = $this->testResults['total'] > 0 
+            ? round(($this->testResults['passed'] / $this->testResults['total']) * 100, 2) 
+            : 0;
+        dump("成功率: {$successRate}%");
+        dump("========================================");
+    }
+
+    /**
+     * 输出测试套件结束信息
+     */
+    private function dumpSuiteEnd(): void
+    {
+        dump("结束时间: " . date('Y-m-d H:i:s'));
+        dump("========================================");
+        dump("    灾害去除系统 E2E 测试套件完成");
+        dump("========================================");
+    }
+}

+ 211 - 0
app/Module/AppGame/Tests/Land/PesticideHandlerTest.php

@@ -0,0 +1,211 @@
+<?php
+
+namespace App\Module\AppGame\Tests\Land;
+
+use App\Module\AppGame\Tests\TestConfig;
+use Google\Protobuf\Internal\Message;
+use Uraus\Kku\Request;
+use Uraus\Kku\Request\RequestLandPesticide;
+
+/**
+ * 除虫Handler E2E测试
+ * 
+ * 测试除虫功能的各种场景,包括成功、失败和边界情况
+ */
+class PesticideHandlerTest extends DisasterRemovalBaseTest
+{
+    /**
+     * 当前测试配置
+     */
+    private array $currentTestConfig;
+
+    /**
+     * 测试除虫成功场景
+     */
+    public function testPesticideSuccess()
+    {
+        $this->dumpTestStart('除虫成功测试');
+        
+        $config = TestConfig::getPesticideTestConfig();
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertSuccessResponse($response, '除虫成功');
+        
+        $this->dumpTestEnd('除虫成功测试');
+    }
+
+    /**
+     * 测试使用无效物品除虫
+     */
+    public function testPesticideWithInvalidItem()
+    {
+        $this->dumpTestStart('无效物品除虫测试');
+        
+        $config = TestConfig::getPesticideTestConfig();
+        $config['item_id'] = TestConfig::getTestItem('invalid_item')['item_id'];
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response, '不是除虫物品');
+        
+        $this->dumpTestEnd('无效物品除虫测试');
+    }
+
+    /**
+     * 测试对其他用户土地除虫
+     */
+    public function testPesticideOnOtherUserLand()
+    {
+        $this->dumpTestStart('其他用户土地除虫测试');
+        
+        $config = TestConfig::getPesticideTestConfig();
+        $config['land_id'] = TestConfig::getTestLand('other_user_land')['land_id'];
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response, '土地不存在或不属于当前用户');
+        
+        $this->dumpTestEnd('其他用户土地除虫测试');
+    }
+
+    /**
+     * 测试对无虫害土地除虫
+     */
+    public function testPesticideOnNormalLand()
+    {
+        $this->dumpTestStart('无虫害土地除虫测试');
+        
+        $config = TestConfig::getPesticideTestConfig();
+        $config['land_id'] = TestConfig::getTestLand('normal_land')['land_id'];
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertFailureResponse($response, '灾害清理失败');
+        
+        $this->dumpTestEnd('无虫害土地除虫测试');
+    }
+
+    /**
+     * 测试参数验证 - 缺少必填字段
+     */
+    public function testPesticideWithMissingFields()
+    {
+        $this->dumpTestStart('缺少必填字段测试');
+        
+        $config = TestConfig::getPesticideTestConfig();
+        $config['land_id'] = 0; // 无效的土地ID
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response);
+        
+        $this->dumpTestEnd('缺少必填字段测试');
+    }
+
+    /**
+     * 测试概率机制 - 多次测试验证概率
+     */
+    public function testPesticideProbability()
+    {
+        $this->dumpTestStart('除虫概率机制测试');
+        
+        $config = TestConfig::getPesticideTestConfig();
+        $this->dumpTestConfig($config);
+        
+        $successCount = 0;
+        $totalAttempts = 10; // 测试10次
+        
+        dump("开始进行 {$totalAttempts} 次除虫测试,验证概率机制");
+        
+        for ($i = 1; $i <= $totalAttempts; $i++) {
+            dump("第 {$i} 次测试");
+            $this->currentTestConfig = $config;
+            
+            $response = $this->protobufRequest();
+            
+            if ($response->getCode() === 0) {
+                $successCount++;
+                dump("第 {$i} 次测试成功");
+            } else {
+                dump("第 {$i} 次测试失败: " . $response->getMsg());
+            }
+        }
+        
+        $successRate = ($successCount / $totalAttempts) * 100;
+        $expectedRate = $config['expected_success_rate'];
+        
+        dump("测试结果: {$successCount}/{$totalAttempts} 成功,实际成功率: {$successRate}%,预期成功率: {$expectedRate}%");
+        
+        // 允许一定的误差范围(±30%)
+        $this->assertGreaterThanOrEqual($expectedRate - 30, $successRate, '实际成功率不应低于预期太多');
+        $this->assertLessThanOrEqual($expectedRate + 30, $successRate, '实际成功率不应高于预期太多');
+        
+        $this->dumpTestEnd('除虫概率机制测试');
+    }
+
+    /**
+     * 创建除虫请求的protobuf消息
+     */
+    public function create_request_protobuf(): Message
+    {
+        $request = $this->createBaseRequest();
+        $pesticideRequest = new RequestLandPesticide();
+        
+        // 使用当前测试配置
+        $config = $this->currentTestConfig ?? TestConfig::getPesticideTestConfig();
+        
+        $pesticideRequest->setLandId($config['land_id']);
+        $pesticideRequest->setItemId($config['item_id']);
+        
+        $request->setLandPesticide($pesticideRequest);
+        
+        dump('创建除虫请求:', [
+            'land_id' => $config['land_id'],
+            'item_id' => $config['item_id']
+        ]);
+        
+        return $request;
+    }
+
+    /**
+     * 测试所有预定义场景
+     */
+    public function testAllPesticideScenarios()
+    {
+        $this->dumpTestStart('所有除虫场景测试');
+        
+        // 测试成功场景
+        $successScenarios = TestConfig::getTestScenario('success_scenarios', 'pesticide_success');
+        if (!empty($successScenarios)) {
+            dump('测试成功场景:', $successScenarios);
+            $this->currentTestConfig = [
+                'land_id' => $successScenarios['land_id'],
+                'item_id' => $successScenarios['item_id']
+            ];
+            
+            $response = $this->protobufRequest();
+            $this->assertSuccessResponse($response, '除虫成功');
+        }
+        
+        // 测试失败场景
+        $failureScenarios = TestConfig::getTestScenario('failure_scenarios', 'invalid_item');
+        if (!empty($failureScenarios)) {
+            dump('测试失败场景:', $failureScenarios);
+            $this->currentTestConfig = [
+                'land_id' => $failureScenarios['land_id'],
+                'item_id' => $failureScenarios['item_id']
+            ];
+            
+            $response = $this->protobufRequest();
+            $this->assertValidationFailureResponse($response, $failureScenarios['expected_error']);
+        }
+        
+        $this->dumpTestEnd('所有除虫场景测试');
+    }
+}

+ 251 - 0
app/Module/AppGame/Tests/Land/README.md

@@ -0,0 +1,251 @@
+# 灾害去除系统 E2E 测试
+
+本目录包含了灾害去除系统的完整端到端测试套件,用于验证除虫、除草、浇水等功能的正确性。
+
+## 测试结构
+
+```
+Land/
+├── DisasterRemovalBaseTest.php      # 测试基类,提供通用方法
+├── PesticideHandlerTest.php         # 除虫Handler测试
+├── WeedicideHandlerTest.php         # 除草Handler测试  
+├── WateringHandlerTest.php          # 浇水Handler测试
+├── DisasterRemovalTestSuite.php     # 综合测试套件
+└── README.md                        # 本文档
+```
+
+## 配置文件
+
+- `TestConfig.php` - 集中管理所有测试配置和环境变量
+
+## 快速开始
+
+### 1. 环境准备
+
+设置测试环境变量:
+
+```bash
+# 在 .env 文件中添加
+UNITTEST_URL=http://localhost:8000
+```
+
+### 2. 运行测试
+
+#### 使用测试运行器(推荐)
+
+```bash
+# 运行所有测试
+php app/Module/AppGame/Tests/run_disaster_removal_tests.php --all
+
+# 只运行除虫测试
+php app/Module/AppGame/Tests/run_disaster_removal_tests.php --pesticide
+
+# 只运行除草测试
+php app/Module/AppGame/Tests/run_disaster_removal_tests.php --weedicide
+
+# 只运行浇水测试
+php app/Module/AppGame/Tests/run_disaster_removal_tests.php --watering
+
+# 运行完整测试套件
+php app/Module/AppGame/Tests/run_disaster_removal_tests.php --suite
+
+# 查看测试配置
+php app/Module/AppGame/Tests/run_disaster_removal_tests.php --config
+
+# 查看帮助
+php app/Module/AppGame/Tests/run_disaster_removal_tests.php --help
+```
+
+#### 使用 Laravel 测试命令
+
+```bash
+# 运行单个测试类
+php artisan test app/Module/AppGame/Tests/Land/PesticideHandlerTest.php
+
+# 运行特定测试方法
+php artisan test app/Module/AppGame/Tests/Land/PesticideHandlerTest.php --filter testPesticideSuccess
+
+# 运行所有灾害去除测试
+php artisan test app/Module/AppGame/Tests/Land/
+```
+
+## 测试配置
+
+### 测试用户
+
+```php
+'user_id' => 1,
+'password' => 'test123456',
+'username' => 'test_user'
+```
+
+### 测试物品
+
+- **除虫剂** (ID: 101) - 80%成功率
+- **除草剂** (ID: 102) - 75%成功率  
+- **浇水道具** (ID: 103) - 90%成功率
+- **无效物品** (ID: 999) - 用于测试失败情况
+
+### 测试土地
+
+- **虫害土地** (ID: 1) - 有虫害的土地
+- **杂草土地** (ID: 2) - 有杂草的土地
+- **干旱土地** (ID: 3) - 有干旱的土地
+- **正常土地** (ID: 4) - 无灾害的土地
+- **其他用户土地** (ID: 5) - 用于测试权限
+
+## 测试场景
+
+### 成功场景
+
+1. **除虫成功** - 使用有效除虫剂对有虫害的土地进行除虫
+2. **除草成功** - 使用有效除草剂对有杂草的土地进行除草
+3. **浇水成功** - 使用有效浇水道具对有干旱的土地进行浇水
+
+### 失败场景
+
+1. **无效物品** - 使用不具备对应属性的物品
+2. **权限验证** - 尝试操作其他用户的土地
+3. **无对应灾害** - 对没有对应灾害的土地进行操作
+4. **参数验证** - 传入无效的参数
+
+### 边界情况
+
+1. **概率测试** - 验证成功率机制
+2. **重复操作** - 对同一土地重复操作
+3. **并发请求** - 同时发送多个请求
+4. **性能测试** - 测试响应时间
+
+## 测试类详解
+
+### DisasterRemovalBaseTest
+
+测试基类,提供:
+- 通用的断言方法
+- 响应验证逻辑
+- 测试场景运行框架
+- 调试输出方法
+
+### PesticideHandlerTest
+
+除虫功能测试,包含:
+- `testPesticideSuccess()` - 成功除虫
+- `testPesticideWithInvalidItem()` - 无效物品测试
+- `testPesticideOnOtherUserLand()` - 权限测试
+- `testPesticideProbability()` - 概率机制测试
+
+### WeedicideHandlerTest
+
+除草功能测试,包含:
+- `testWeedicideSuccess()` - 成功除草
+- `testWeedicideWithWrongItemType()` - 错误物品类型测试
+- `testConcurrentWeedicideRequests()` - 并发请求测试
+
+### WateringHandlerTest
+
+浇水功能测试,包含:
+- `testWateringSuccess()` - 成功浇水
+- `testRepeatedWateringOnSameLand()` - 重复操作测试
+- `testWateringPerformance()` - 性能测试
+
+### DisasterRemovalTestSuite
+
+综合测试套件,提供:
+- 完整的测试流程编排
+- 测试结果统计
+- 错误收集和报告
+- 集成测试场景
+
+## 自定义测试配置
+
+可以通过修改 `TestConfig.php` 来自定义测试配置:
+
+```php
+// 修改测试用户
+public const TEST_USER = [
+    'user_id' => 2,  // 改为其他用户ID
+    'password' => 'your_password',
+    'username' => 'your_username'
+];
+
+// 修改测试物品
+public const TEST_ITEMS = [
+    'pesticide' => [
+        'item_id' => 201,  // 改为实际的除虫剂ID
+        'name' => '超级除虫剂',
+        'numeric_attributes' => [
+            'fram_pesticide_rate' => 95  // 改为95%成功率
+        ]
+    ]
+];
+```
+
+## 调试和故障排除
+
+### 查看详细输出
+
+测试会输出详细的调试信息,包括:
+- 请求参数
+- 响应内容
+- 测试配置
+- 执行时间
+
+### 常见问题
+
+1. **连接失败**
+   - 检查 `UNITTEST_URL` 环境变量
+   - 确保测试服务器正在运行
+
+2. **验证失败**
+   - 检查测试数据是否存在
+   - 确认用户权限设置
+
+3. **概率测试不稳定**
+   - 概率测试允许一定误差范围
+   - 可以调整测试次数或误差范围
+
+### 日志查看
+
+测试过程中的错误会记录到 Laravel 日志中:
+
+```bash
+tail -f storage/logs/laravel.log
+```
+
+## 扩展测试
+
+### 添加新的测试场景
+
+1. 在 `TestConfig.php` 中添加新的配置
+2. 在对应的测试类中添加新的测试方法
+3. 在 `DisasterRemovalTestSuite.php` 中集成新测试
+
+### 添加新的灾害类型
+
+1. 更新 `TestConfig.php` 中的配置
+2. 创建新的测试类继承 `DisasterRemovalBaseTest`
+3. 实现对应的测试方法
+
+## 持续集成
+
+这些测试可以集成到 CI/CD 流程中:
+
+```yaml
+# GitHub Actions 示例
+- name: Run Disaster Removal Tests
+  run: |
+    php app/Module/AppGame/Tests/run_disaster_removal_tests.php --all
+```
+
+## 性能基准
+
+- 单个请求响应时间:< 5秒
+- 并发请求处理:支持同时处理多个请求
+- 概率准确性:误差范围 ±30%
+
+## 注意事项
+
+1. 测试会实际消耗物品,请使用测试环境
+2. 某些测试可能会修改数据库状态
+3. 概率测试结果可能因随机性而有所不同
+4. 确保测试环境与生产环境隔离

+ 308 - 0
app/Module/AppGame/Tests/Land/WateringHandlerTest.php

@@ -0,0 +1,308 @@
+<?php
+
+namespace App\Module\AppGame\Tests\Land;
+
+use App\Module\AppGame\Tests\TestConfig;
+use Google\Protobuf\Internal\Message;
+use Uraus\Kku\Request;
+use Uraus\Kku\Request\RequestLandWatering;
+
+/**
+ * 浇水Handler E2E测试
+ * 
+ * 测试浇水功能的各种场景,包括成功、失败和边界情况
+ */
+class WateringHandlerTest extends DisasterRemovalBaseTest
+{
+    /**
+     * 当前测试配置
+     */
+    private array $currentTestConfig;
+
+    /**
+     * 测试浇水成功场景
+     */
+    public function testWateringSuccess()
+    {
+        $this->dumpTestStart('浇水成功测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertSuccessResponse($response, '浇水成功');
+        
+        $this->dumpTestEnd('浇水成功测试');
+    }
+
+    /**
+     * 测试使用无效物品浇水
+     */
+    public function testWateringWithInvalidItem()
+    {
+        $this->dumpTestStart('无效物品浇水测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        $config['item_id'] = TestConfig::getTestItem('invalid_item')['item_id'];
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response, '不是浇水物品');
+        
+        $this->dumpTestEnd('无效物品浇水测试');
+    }
+
+    /**
+     * 测试使用除虫剂进行浇水(物品类型不匹配)
+     */
+    public function testWateringWithWrongItemType()
+    {
+        $this->dumpTestStart('错误物品类型浇水测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        $config['item_id'] = TestConfig::getTestItem('pesticide')['item_id']; // 使用除虫剂
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response, '不是浇水物品');
+        
+        $this->dumpTestEnd('错误物品类型浇水测试');
+    }
+
+    /**
+     * 测试对其他用户土地浇水
+     */
+    public function testWateringOnOtherUserLand()
+    {
+        $this->dumpTestStart('其他用户土地浇水测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        $config['land_id'] = TestConfig::getTestLand('other_user_land')['land_id'];
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response, '土地不存在或不属于当前用户');
+        
+        $this->dumpTestEnd('其他用户土地浇水测试');
+    }
+
+    /**
+     * 测试对无干旱土地浇水
+     */
+    public function testWateringOnNormalLand()
+    {
+        $this->dumpTestStart('无干旱土地浇水测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        $config['land_id'] = TestConfig::getTestLand('normal_land')['land_id'];
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertFailureResponse($response, '灾害清理失败');
+        
+        $this->dumpTestEnd('无干旱土地浇水测试');
+    }
+
+    /**
+     * 测试参数验证 - 无效的物品ID
+     */
+    public function testWateringWithInvalidItemId()
+    {
+        $this->dumpTestStart('无效物品ID测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        $config['item_id'] = 0; // 无效的物品ID
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response);
+        
+        $this->dumpTestEnd('无效物品ID测试');
+    }
+
+    /**
+     * 测试概率机制 - 多次测试验证概率
+     */
+    public function testWateringProbability()
+    {
+        $this->dumpTestStart('浇水概率机制测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        $this->dumpTestConfig($config);
+        
+        $successCount = 0;
+        $totalAttempts = 10; // 测试10次
+        
+        dump("开始进行 {$totalAttempts} 次浇水测试,验证概率机制");
+        
+        for ($i = 1; $i <= $totalAttempts; $i++) {
+            dump("第 {$i} 次测试");
+            $this->currentTestConfig = $config;
+            
+            $response = $this->protobufRequest();
+            
+            if ($response->getCode() === 0) {
+                $successCount++;
+                dump("第 {$i} 次测试成功");
+            } else {
+                dump("第 {$i} 次测试失败: " . $response->getMsg());
+            }
+        }
+        
+        $successRate = ($successCount / $totalAttempts) * 100;
+        $expectedRate = $config['expected_success_rate'];
+        
+        dump("测试结果: {$successCount}/{$totalAttempts} 成功,实际成功率: {$successRate}%,预期成功率: {$expectedRate}%");
+        
+        // 浇水通常有较高的成功率,允许一定的误差范围(±20%)
+        $this->assertGreaterThanOrEqual($expectedRate - 20, $successRate, '实际成功率不应低于预期太多');
+        $this->assertLessThanOrEqual($expectedRate + 20, $successRate, '实际成功率不应高于预期太多');
+        
+        $this->dumpTestEnd('浇水概率机制测试');
+    }
+
+    /**
+     * 测试边界情况 - 100%成功率物品
+     */
+    public function testWateringWithHundredPercentSuccessRate()
+    {
+        $this->dumpTestStart('100%成功率浇水测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        // 假设有一个100%成功率的浇水道具用于测试
+        $config['item_id'] = 1030; // 假设的100%成功率浇水道具ID
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        // 100%成功率应该总是成功
+        $this->assertSuccessResponse($response, '浇水成功');
+        
+        $this->dumpTestEnd('100%成功率浇水测试');
+    }
+
+    /**
+     * 测试重复浇水同一块土地
+     */
+    public function testRepeatedWateringOnSameLand()
+    {
+        $this->dumpTestStart('重复浇水测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        $this->dumpTestConfig($config);
+        
+        // 第一次浇水
+        dump("第一次浇水");
+        $this->currentTestConfig = $config;
+        $firstResponse = $this->protobufRequest();
+        dump("第一次浇水响应: " . $firstResponse->serializeToJsonString());
+        
+        // 第二次浇水(如果第一次成功,第二次应该失败)
+        dump("第二次浇水");
+        $this->currentTestConfig = $config;
+        $secondResponse = $this->protobufRequest();
+        dump("第二次浇水响应: " . $secondResponse->serializeToJsonString());
+        
+        // 如果第一次成功,第二次应该失败(因为已经没有干旱了)
+        if ($firstResponse->getCode() === 0) {
+            $this->assertFailureResponse($secondResponse, '灾害清理失败');
+            dump("符合预期:第一次成功后,第二次失败");
+        } else {
+            dump("第一次失败,第二次结果不确定");
+        }
+        
+        $this->dumpTestEnd('重复浇水测试');
+    }
+
+    /**
+     * 创建浇水请求的protobuf消息
+     */
+    public function create_request_protobuf(): Message
+    {
+        $request = $this->createBaseRequest();
+        $wateringRequest = new RequestLandWatering();
+        
+        // 使用当前测试配置
+        $config = $this->currentTestConfig ?? TestConfig::getWateringTestConfig();
+        
+        $wateringRequest->setLandId($config['land_id']);
+        $wateringRequest->setUserItemId($config['item_id']);
+        
+        $request->setLandWatering($wateringRequest);
+        
+        dump('创建浇水请求:', [
+            'land_id' => $config['land_id'],
+            'user_item_id' => $config['item_id']
+        ]);
+        
+        return $request;
+    }
+
+    /**
+     * 测试所有预定义场景
+     */
+    public function testAllWateringScenarios()
+    {
+        $this->dumpTestStart('所有浇水场景测试');
+        
+        // 测试成功场景
+        $successScenarios = TestConfig::getTestScenario('success_scenarios', 'watering_success');
+        if (!empty($successScenarios)) {
+            dump('测试成功场景:', $successScenarios);
+            $this->currentTestConfig = [
+                'land_id' => $successScenarios['land_id'],
+                'item_id' => $successScenarios['item_id']
+            ];
+            
+            $response = $this->protobufRequest();
+            $this->assertSuccessResponse($response, '浇水成功');
+        }
+        
+        $this->dumpTestEnd('所有浇水场景测试');
+    }
+
+    /**
+     * 测试性能 - 大量浇水请求
+     */
+    public function testWateringPerformance()
+    {
+        $this->dumpTestStart('浇水性能测试');
+        
+        $config = TestConfig::getWateringTestConfig();
+        $this->dumpTestConfig($config);
+        
+        $startTime = microtime(true);
+        $requestCount = 5; // 测试5个请求
+        
+        dump("开始性能测试,发送 {$requestCount} 个浇水请求");
+        
+        for ($i = 1; $i <= $requestCount; $i++) {
+            $requestStartTime = microtime(true);
+            $this->currentTestConfig = $config;
+            
+            $response = $this->protobufRequest();
+            
+            $requestEndTime = microtime(true);
+            $requestDuration = ($requestEndTime - $requestStartTime) * 1000; // 转换为毫秒
+            
+            dump("第 {$i} 个请求耗时: {$requestDuration}ms,响应码: " . $response->getCode());
+        }
+        
+        $endTime = microtime(true);
+        $totalDuration = ($endTime - $startTime) * 1000; // 转换为毫秒
+        $averageDuration = $totalDuration / $requestCount;
+        
+        dump("性能测试结果: 总耗时 {$totalDuration}ms,平均耗时 {$averageDuration}ms");
+        
+        // 验证平均响应时间不超过5秒
+        $this->assertLessThan(5000, $averageDuration, '平均响应时间不应超过5秒');
+        
+        $this->dumpTestEnd('浇水性能测试');
+    }
+}

+ 271 - 0
app/Module/AppGame/Tests/Land/WeedicideHandlerTest.php

@@ -0,0 +1,271 @@
+<?php
+
+namespace App\Module\AppGame\Tests\Land;
+
+use App\Module\AppGame\Tests\TestConfig;
+use Google\Protobuf\Internal\Message;
+use Uraus\Kku\Request;
+use Uraus\Kku\Request\RequestLandWeedicide;
+
+/**
+ * 除草Handler E2E测试
+ * 
+ * 测试除草功能的各种场景,包括成功、失败和边界情况
+ */
+class WeedicideHandlerTest extends DisasterRemovalBaseTest
+{
+    /**
+     * 当前测试配置
+     */
+    private array $currentTestConfig;
+
+    /**
+     * 测试除草成功场景
+     */
+    public function testWeedicideSuccess()
+    {
+        $this->dumpTestStart('除草成功测试');
+        
+        $config = TestConfig::getWeedicideTestConfig();
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertSuccessResponse($response, '除草成功');
+        
+        $this->dumpTestEnd('除草成功测试');
+    }
+
+    /**
+     * 测试使用无效物品除草
+     */
+    public function testWeedicideWithInvalidItem()
+    {
+        $this->dumpTestStart('无效物品除草测试');
+        
+        $config = TestConfig::getWeedicideTestConfig();
+        $config['item_id'] = TestConfig::getTestItem('invalid_item')['item_id'];
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response, '不是除草物品');
+        
+        $this->dumpTestEnd('无效物品除草测试');
+    }
+
+    /**
+     * 测试使用除虫剂进行除草(物品类型不匹配)
+     */
+    public function testWeedicideWithWrongItemType()
+    {
+        $this->dumpTestStart('错误物品类型除草测试');
+        
+        $config = TestConfig::getWeedicideTestConfig();
+        $config['item_id'] = TestConfig::getTestItem('pesticide')['item_id']; // 使用除虫剂
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response, '不是除草物品');
+        
+        $this->dumpTestEnd('错误物品类型除草测试');
+    }
+
+    /**
+     * 测试对其他用户土地除草
+     */
+    public function testWeedicideOnOtherUserLand()
+    {
+        $this->dumpTestStart('其他用户土地除草测试');
+        
+        $config = TestConfig::getWeedicideTestConfig();
+        $config['land_id'] = TestConfig::getTestLand('other_user_land')['land_id'];
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response, '土地不存在或不属于当前用户');
+        
+        $this->dumpTestEnd('其他用户土地除草测试');
+    }
+
+    /**
+     * 测试对无杂草土地除草
+     */
+    public function testWeedicideOnNormalLand()
+    {
+        $this->dumpTestStart('无杂草土地除草测试');
+        
+        $config = TestConfig::getWeedicideTestConfig();
+        $config['land_id'] = TestConfig::getTestLand('normal_land')['land_id'];
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertFailureResponse($response, '灾害清理失败');
+        
+        $this->dumpTestEnd('无杂草土地除草测试');
+    }
+
+    /**
+     * 测试参数验证 - 无效的土地ID
+     */
+    public function testWeedicideWithInvalidLandId()
+    {
+        $this->dumpTestStart('无效土地ID测试');
+        
+        $config = TestConfig::getWeedicideTestConfig();
+        $config['land_id'] = -1; // 无效的土地ID
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        $this->assertValidationFailureResponse($response);
+        
+        $this->dumpTestEnd('无效土地ID测试');
+    }
+
+    /**
+     * 测试概率机制 - 多次测试验证概率
+     */
+    public function testWeedicideProbability()
+    {
+        $this->dumpTestStart('除草概率机制测试');
+        
+        $config = TestConfig::getWeedicideTestConfig();
+        $this->dumpTestConfig($config);
+        
+        $successCount = 0;
+        $totalAttempts = 10; // 测试10次
+        
+        dump("开始进行 {$totalAttempts} 次除草测试,验证概率机制");
+        
+        for ($i = 1; $i <= $totalAttempts; $i++) {
+            dump("第 {$i} 次测试");
+            $this->currentTestConfig = $config;
+            
+            $response = $this->protobufRequest();
+            
+            if ($response->getCode() === 0) {
+                $successCount++;
+                dump("第 {$i} 次测试成功");
+            } else {
+                dump("第 {$i} 次测试失败: " . $response->getMsg());
+            }
+        }
+        
+        $successRate = ($successCount / $totalAttempts) * 100;
+        $expectedRate = $config['expected_success_rate'];
+        
+        dump("测试结果: {$successCount}/{$totalAttempts} 成功,实际成功率: {$successRate}%,预期成功率: {$expectedRate}%");
+        
+        // 允许一定的误差范围(±30%)
+        $this->assertGreaterThanOrEqual($expectedRate - 30, $successRate, '实际成功率不应低于预期太多');
+        $this->assertLessThanOrEqual($expectedRate + 30, $successRate, '实际成功率不应高于预期太多');
+        
+        $this->dumpTestEnd('除草概率机制测试');
+    }
+
+    /**
+     * 测试边界情况 - 0%成功率物品
+     */
+    public function testWeedicideWithZeroSuccessRate()
+    {
+        $this->dumpTestStart('0%成功率除草测试');
+        
+        $config = TestConfig::getWeedicideTestConfig();
+        // 假设有一个0%成功率的除草剂用于测试
+        $config['item_id'] = 1020; // 假设的0%成功率除草剂ID
+        $this->dumpTestConfig($config);
+        $this->currentTestConfig = $config;
+
+        $response = $this->protobufRequest();
+        // 0%成功率应该总是失败,但仍会消耗物品
+        $this->assertFailureResponse($response, '除草失败,请再次尝试');
+        
+        $this->dumpTestEnd('0%成功率除草测试');
+    }
+
+    /**
+     * 创建除草请求的protobuf消息
+     */
+    public function create_request_protobuf(): Message
+    {
+        $request = $this->createBaseRequest();
+        $weedicideRequest = new RequestLandWeedicide();
+        
+        // 使用当前测试配置
+        $config = $this->currentTestConfig ?? TestConfig::getWeedicideTestConfig();
+        
+        $weedicideRequest->setLandId($config['land_id']);
+        $weedicideRequest->setUserItemId($config['item_id']);
+        
+        $request->setLandWeedicide($weedicideRequest);
+        
+        dump('创建除草请求:', [
+            'land_id' => $config['land_id'],
+            'user_item_id' => $config['item_id']
+        ]);
+        
+        return $request;
+    }
+
+    /**
+     * 测试所有预定义场景
+     */
+    public function testAllWeedicideScenarios()
+    {
+        $this->dumpTestStart('所有除草场景测试');
+        
+        // 测试成功场景
+        $successScenarios = TestConfig::getTestScenario('success_scenarios', 'weedicide_success');
+        if (!empty($successScenarios)) {
+            dump('测试成功场景:', $successScenarios);
+            $this->currentTestConfig = [
+                'land_id' => $successScenarios['land_id'],
+                'item_id' => $successScenarios['item_id']
+            ];
+            
+            $response = $this->protobufRequest();
+            $this->assertSuccessResponse($response, '除草成功');
+        }
+        
+        $this->dumpTestEnd('所有除草场景测试');
+    }
+
+    /**
+     * 测试并发除草请求
+     */
+    public function testConcurrentWeedicideRequests()
+    {
+        $this->dumpTestStart('并发除草请求测试');
+        
+        $config = TestConfig::getWeedicideTestConfig();
+        $this->dumpTestConfig($config);
+        
+        // 模拟快速连续的请求
+        $responses = [];
+        for ($i = 1; $i <= 3; $i++) {
+            dump("发送第 {$i} 个并发请求");
+            $this->currentTestConfig = $config;
+            $responses[] = $this->protobufRequest();
+        }
+        
+        // 验证响应
+        $successCount = 0;
+        foreach ($responses as $index => $response) {
+            dump("第 " . ($index + 1) . " 个响应: " . $response->serializeToJsonString());
+            if ($response->getCode() === 0) {
+                $successCount++;
+            }
+        }
+        
+        dump("并发请求结果: {$successCount}/3 成功");
+        
+        // 至少应该有一个成功(假设有足够的物品)
+        $this->assertGreaterThanOrEqual(1, $successCount, '并发请求中至少应该有一个成功');
+        
+        $this->dumpTestEnd('并发除草请求测试');
+    }
+}

+ 6 - 0
app/Module/AppGame/Tests/README.md

@@ -0,0 +1,6 @@
+# 单元测试
+
+给所有的 Handler编写测试,E2E的 ,可以参考 app/Module/AppGame/Tests/Public/PublicTokenTest.php
+在这个目录创建测试用环境变量,比如: 请求用户 ID,密码,除草测试的土地ID,除草剂ID等 ,方便编排测试
+继续编写测试,为所有的 Handler编写测试
+环境变量,环境变量,独立与代码的

+ 257 - 0
app/Module/AppGame/Tests/TestConfig.php

@@ -0,0 +1,257 @@
+<?php
+
+namespace App\Module\AppGame\Tests;
+
+/**
+ * 测试配置类
+ * 
+ * 集中管理所有测试用的环境变量和配置,方便编排测试
+ */
+class TestConfig
+{
+    /**
+     * 测试用户配置
+     */
+    public const TEST_USER = [
+        'user_id' => 1,
+        'password' => 'test123456',
+        'username' => 'test_user'
+    ];
+
+    /**
+     * 灾害去除测试配置
+     */
+    public const DISASTER_REMOVAL_TEST = [
+        // 除虫测试配置
+        'pesticide' => [
+            'land_id' => 1,           // 测试土地ID(需要有虫害的土地)
+            'item_id' => 101,         // 除虫剂物品ID
+            'disaster_type' => 2,     // 虫害类型
+            'expected_success_rate' => 80  // 预期成功率
+        ],
+        
+        // 除草测试配置
+        'weedicide' => [
+            'land_id' => 2,           // 测试土地ID(需要有杂草的土地)
+            'item_id' => 102,         // 除草剂物品ID
+            'disaster_type' => 3,     // 杂草类型
+            'expected_success_rate' => 75  // 预期成功率
+        ],
+        
+        // 浇水测试配置
+        'watering' => [
+            'land_id' => 3,           // 测试土地ID(需要有干旱的土地)
+            'item_id' => 103,         // 浇水道具物品ID
+            'disaster_type' => 1,     // 干旱类型
+            'expected_success_rate' => 90  // 预期成功率
+        ]
+    ];
+
+    /**
+     * 测试物品配置
+     */
+    public const TEST_ITEMS = [
+        // 除虫剂
+        'pesticide' => [
+            'item_id' => 101,
+            'name' => '高效除虫剂',
+            'numeric_attributes' => [
+                'fram_pesticide_rate' => 80  // 80%成功率
+            ]
+        ],
+        
+        // 除草剂
+        'weedicide' => [
+            'item_id' => 102,
+            'name' => '强力除草剂',
+            'numeric_attributes' => [
+                'fram_weedicide_rate' => 75  // 75%成功率
+            ]
+        ],
+        
+        // 浇水道具
+        'watering_tool' => [
+            'item_id' => 103,
+            'name' => '自动洒水器',
+            'numeric_attributes' => [
+                'fram_drought_rate' => 90  // 90%成功率
+            ]
+        ],
+        
+        // 无效物品(用于测试失败情况)
+        'invalid_item' => [
+            'item_id' => 999,
+            'name' => '无效物品',
+            'numeric_attributes' => []
+        ]
+    ];
+
+    /**
+     * 测试土地配置
+     */
+    public const TEST_LANDS = [
+        // 有虫害的土地
+        'pest_land' => [
+            'land_id' => 1,
+            'user_id' => 1,
+            'disaster_type' => 2,  // 虫害
+            'crop_id' => 1,
+            'growth_stage' => 2
+        ],
+        
+        // 有杂草的土地
+        'weed_land' => [
+            'land_id' => 2,
+            'user_id' => 1,
+            'disaster_type' => 3,  // 杂草
+            'crop_id' => 2,
+            'growth_stage' => 3
+        ],
+        
+        // 有干旱的土地
+        'drought_land' => [
+            'land_id' => 3,
+            'user_id' => 1,
+            'disaster_type' => 1,  // 干旱
+            'crop_id' => 3,
+            'growth_stage' => 1
+        ],
+        
+        // 无灾害的土地(用于测试失败情况)
+        'normal_land' => [
+            'land_id' => 4,
+            'user_id' => 1,
+            'disaster_type' => 0,  // 无灾害
+            'crop_id' => 4,
+            'growth_stage' => 2
+        ],
+        
+        // 其他用户的土地(用于测试权限)
+        'other_user_land' => [
+            'land_id' => 5,
+            'user_id' => 2,  // 不同用户
+            'disaster_type' => 2,
+            'crop_id' => 5,
+            'growth_stage' => 1
+        ]
+    ];
+
+    /**
+     * 测试场景配置
+     */
+    public const TEST_SCENARIOS = [
+        // 成功场景
+        'success_scenarios' => [
+            'pesticide_success' => [
+                'description' => '除虫成功测试',
+                'user_id' => 1,
+                'land_id' => 1,
+                'item_id' => 101,
+                'expected_result' => 'success'
+            ],
+            'weedicide_success' => [
+                'description' => '除草成功测试',
+                'user_id' => 1,
+                'land_id' => 2,
+                'item_id' => 102,
+                'expected_result' => 'success'
+            ],
+            'watering_success' => [
+                'description' => '浇水成功测试',
+                'user_id' => 1,
+                'land_id' => 3,
+                'item_id' => 103,
+                'expected_result' => 'success'
+            ]
+        ],
+        
+        // 失败场景
+        'failure_scenarios' => [
+            'invalid_item' => [
+                'description' => '使用无效物品测试',
+                'user_id' => 1,
+                'land_id' => 1,
+                'item_id' => 999,
+                'expected_error' => '不是除虫物品'
+            ],
+            'no_permission' => [
+                'description' => '无权限访问土地测试',
+                'user_id' => 1,
+                'land_id' => 5,  // 其他用户的土地
+                'item_id' => 101,
+                'expected_error' => '土地不存在或不属于当前用户'
+            ],
+            'no_disaster' => [
+                'description' => '土地无对应灾害测试',
+                'user_id' => 1,
+                'land_id' => 4,  // 无灾害的土地
+                'item_id' => 101,
+                'expected_error' => '灾害清理失败'
+            ]
+        ]
+    ];
+
+    /**
+     * 获取测试用户ID
+     */
+    public static function getTestUserId(): int
+    {
+        return self::TEST_USER['user_id'];
+    }
+
+    /**
+     * 获取测试用户密码
+     */
+    public static function getTestUserPassword(): string
+    {
+        return self::TEST_USER['password'];
+    }
+
+    /**
+     * 获取除虫测试配置
+     */
+    public static function getPesticideTestConfig(): array
+    {
+        return self::DISASTER_REMOVAL_TEST['pesticide'];
+    }
+
+    /**
+     * 获取除草测试配置
+     */
+    public static function getWeedicideTestConfig(): array
+    {
+        return self::DISASTER_REMOVAL_TEST['weedicide'];
+    }
+
+    /**
+     * 获取浇水测试配置
+     */
+    public static function getWateringTestConfig(): array
+    {
+        return self::DISASTER_REMOVAL_TEST['watering'];
+    }
+
+    /**
+     * 获取测试物品配置
+     */
+    public static function getTestItem(string $type): array
+    {
+        return self::TEST_ITEMS[$type] ?? [];
+    }
+
+    /**
+     * 获取测试土地配置
+     */
+    public static function getTestLand(string $type): array
+    {
+        return self::TEST_LANDS[$type] ?? [];
+    }
+
+    /**
+     * 获取测试场景配置
+     */
+    public static function getTestScenario(string $category, string $scenario): array
+    {
+        return self::TEST_SCENARIOS[$category][$scenario] ?? [];
+    }
+}

+ 382 - 0
app/Module/AppGame/Tests/prepare_test_data.php

@@ -0,0 +1,382 @@
+<?php
+
+/**
+ * 测试数据准备脚本
+ * 
+ * 为灾害去除测试准备必要的测试数据,包括用户、土地、物品等
+ * 
+ * 使用方法:
+ * php app/Module/AppGame/Tests/prepare_test_data.php
+ */
+
+require_once __DIR__ . '/../../../vendor/autoload.php';
+
+use App\Module\AppGame\Tests\TestConfig;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class TestDataPreparer
+{
+    /**
+     * 准备所有测试数据
+     */
+    public function prepare(): void
+    {
+        $this->displayHeader();
+        
+        try {
+            // 1. 准备测试用户
+            $this->prepareTestUser();
+            
+            // 2. 准备测试物品
+            $this->prepareTestItems();
+            
+            // 3. 准备测试土地
+            $this->prepareTestLands();
+            
+            // 4. 准备用户物品关联
+            $this->prepareUserItems();
+            
+            // 5. 验证数据
+            $this->verifyTestData();
+            
+            echo "✅ 测试数据准备完成!\n\n";
+            
+        } catch (\Exception $e) {
+            echo "❌ 测试数据准备失败: " . $e->getMessage() . "\n";
+            echo "错误详情: " . $e->getTraceAsString() . "\n";
+        }
+    }
+
+    /**
+     * 显示头部信息
+     */
+    private function displayHeader(): void
+    {
+        echo "\n";
+        echo "========================================\n";
+        echo "    灾害去除测试数据准备工具\n";
+        echo "========================================\n";
+        echo "时间: " . date('Y-m-d H:i:s') . "\n";
+        echo "========================================\n\n";
+    }
+
+    /**
+     * 准备测试用户
+     */
+    private function prepareTestUser(): void
+    {
+        echo "准备测试用户...\n";
+        
+        $userId = TestConfig::getTestUserId();
+        $userConfig = TestConfig::TEST_USER;
+        
+        // 检查用户是否存在
+        $userExists = DB::table('users')->where('id', $userId)->exists();
+        
+        if (!$userExists) {
+            echo "创建测试用户 (ID: {$userId})...\n";
+            
+            DB::table('users')->insert([
+                'id' => $userId,
+                'username' => $userConfig['username'],
+                'password' => bcrypt($userConfig['password']),
+                'email' => 'test@example.com',
+                'created_at' => now(),
+                'updated_at' => now()
+            ]);
+            
+            echo "✅ 测试用户创建成功\n";
+        } else {
+            echo "✅ 测试用户已存在\n";
+        }
+        
+        echo "\n";
+    }
+
+    /**
+     * 准备测试物品
+     */
+    private function prepareTestItems(): void
+    {
+        echo "准备测试物品...\n";
+        
+        $items = [
+            TestConfig::getTestItem('pesticide'),
+            TestConfig::getTestItem('weedicide'),
+            TestConfig::getTestItem('watering_tool'),
+            TestConfig::getTestItem('invalid_item')
+        ];
+        
+        foreach ($items as $item) {
+            if (empty($item)) continue;
+            
+            $itemId = $item['item_id'];
+            $itemExists = DB::table('game_items')->where('id', $itemId)->exists();
+            
+            if (!$itemExists) {
+                echo "创建物品: {$item['name']} (ID: {$itemId})...\n";
+                
+                DB::table('game_items')->insert([
+                    'id' => $itemId,
+                    'name' => $item['name'],
+                    'description' => "测试用{$item['name']}",
+                    'type' => 'tool',
+                    'numeric_attributes' => json_encode($item['numeric_attributes'] ?? []),
+                    'created_at' => now(),
+                    'updated_at' => now()
+                ]);
+                
+                echo "✅ 物品创建成功\n";
+            } else {
+                echo "✅ 物品已存在: {$item['name']}\n";
+            }
+        }
+        
+        echo "\n";
+    }
+
+    /**
+     * 准备测试土地
+     */
+    private function prepareTestLands(): void
+    {
+        echo "准备测试土地...\n";
+        
+        $lands = [
+            TestConfig::getTestLand('pest_land'),
+            TestConfig::getTestLand('weed_land'),
+            TestConfig::getTestLand('drought_land'),
+            TestConfig::getTestLand('normal_land'),
+            TestConfig::getTestLand('other_user_land')
+        ];
+        
+        foreach ($lands as $land) {
+            if (empty($land)) continue;
+            
+            $landId = $land['land_id'];
+            $landExists = DB::table('kku_farm_lands')->where('id', $landId)->exists();
+            
+            if (!$landExists) {
+                echo "创建土地 (ID: {$landId}, 用户: {$land['user_id']})...\n";
+                
+                DB::table('kku_farm_lands')->insert([
+                    'id' => $landId,
+                    'user_id' => $land['user_id'],
+                    'status' => 2, // 已种植
+                    'created_at' => now(),
+                    'updated_at' => now()
+                ]);
+                
+                // 如果有作物,创建作物记录
+                if (isset($land['crop_id'])) {
+                    $this->createCropForLand($land);
+                }
+                
+                echo "✅ 土地创建成功\n";
+            } else {
+                echo "✅ 土地已存在 (ID: {$landId})\n";
+            }
+        }
+        
+        echo "\n";
+    }
+
+    /**
+     * 为土地创建作物
+     */
+    private function createCropForLand(array $land): void
+    {
+        $cropExists = DB::table('kku_farm_crops')
+            ->where('land_id', $land['land_id'])
+            ->exists();
+            
+        if (!$cropExists) {
+            echo "  创建作物 (土地: {$land['land_id']}, 灾害类型: {$land['disaster_type']})...\n";
+            
+            DB::table('kku_farm_crops')->insert([
+                'land_id' => $land['land_id'],
+                'user_id' => $land['user_id'],
+                'seed_id' => $land['crop_id'],
+                'growth_stage' => $land['growth_stage'],
+                'disaster_type' => $land['disaster_type'],
+                'planted_at' => now()->subHours(2), // 2小时前种植
+                'created_at' => now(),
+                'updated_at' => now()
+            ]);
+            
+            echo "  ✅ 作物创建成功\n";
+        }
+    }
+
+    /**
+     * 准备用户物品关联
+     */
+    private function prepareUserItems(): void
+    {
+        echo "准备用户物品关联...\n";
+        
+        $userId = TestConfig::getTestUserId();
+        $items = [
+            TestConfig::getTestItem('pesticide'),
+            TestConfig::getTestItem('weedicide'),
+            TestConfig::getTestItem('watering_tool')
+        ];
+        
+        foreach ($items as $item) {
+            if (empty($item)) continue;
+            
+            $itemId = $item['item_id'];
+            $userItemExists = DB::table('kku_user_items')
+                ->where('user_id', $userId)
+                ->where('item_id', $itemId)
+                ->exists();
+                
+            if (!$userItemExists) {
+                echo "给用户添加物品: {$item['name']}...\n";
+                
+                DB::table('kku_user_items')->insert([
+                    'user_id' => $userId,
+                    'item_id' => $itemId,
+                    'quantity' => 100, // 给足够的数量用于测试
+                    'created_at' => now(),
+                    'updated_at' => now()
+                ]);
+                
+                echo "✅ 用户物品添加成功\n";
+            } else {
+                echo "✅ 用户已拥有物品: {$item['name']}\n";
+            }
+        }
+        
+        echo "\n";
+    }
+
+    /**
+     * 验证测试数据
+     */
+    private function verifyTestData(): void
+    {
+        echo "验证测试数据...\n";
+        
+        $errors = [];
+        
+        // 验证测试用户
+        $userId = TestConfig::getTestUserId();
+        if (!DB::table('users')->where('id', $userId)->exists()) {
+            $errors[] = "测试用户不存在 (ID: {$userId})";
+        }
+        
+        // 验证测试物品
+        $items = [
+            TestConfig::getTestItem('pesticide'),
+            TestConfig::getTestItem('weedicide'),
+            TestConfig::getTestItem('watering_tool')
+        ];
+        
+        foreach ($items as $item) {
+            if (empty($item)) continue;
+            
+            if (!DB::table('game_items')->where('id', $item['item_id'])->exists()) {
+                $errors[] = "测试物品不存在: {$item['name']} (ID: {$item['item_id']})";
+            }
+            
+            if (!DB::table('kku_user_items')
+                ->where('user_id', $userId)
+                ->where('item_id', $item['item_id'])
+                ->exists()) {
+                $errors[] = "用户未拥有物品: {$item['name']}";
+            }
+        }
+        
+        // 验证测试土地
+        $lands = [
+            TestConfig::getTestLand('pest_land'),
+            TestConfig::getTestLand('weed_land'),
+            TestConfig::getTestLand('drought_land')
+        ];
+        
+        foreach ($lands as $land) {
+            if (empty($land)) continue;
+            
+            if (!DB::table('kku_farm_lands')->where('id', $land['land_id'])->exists()) {
+                $errors[] = "测试土地不存在 (ID: {$land['land_id']})";
+            }
+            
+            if (!DB::table('kku_farm_crops')
+                ->where('land_id', $land['land_id'])
+                ->where('disaster_type', $land['disaster_type'])
+                ->exists()) {
+                $errors[] = "土地缺少对应灾害 (土地: {$land['land_id']}, 灾害: {$land['disaster_type']})";
+            }
+        }
+        
+        if (empty($errors)) {
+            echo "✅ 所有测试数据验证通过\n";
+        } else {
+            echo "❌ 发现以下问题:\n";
+            foreach ($errors as $error) {
+                echo "  - {$error}\n";
+            }
+        }
+        
+        echo "\n";
+    }
+
+    /**
+     * 清理测试数据
+     */
+    public function cleanup(): void
+    {
+        echo "清理测试数据...\n";
+        
+        try {
+            $userId = TestConfig::getTestUserId();
+            
+            // 清理用户物品
+            DB::table('kku_user_items')->where('user_id', $userId)->delete();
+            
+            // 清理作物
+            DB::table('kku_farm_crops')->where('user_id', $userId)->delete();
+            
+            // 清理土地
+            DB::table('kku_farm_lands')->where('user_id', $userId)->delete();
+            
+            // 清理测试物品
+            $itemIds = [
+                TestConfig::getTestItem('pesticide')['item_id'],
+                TestConfig::getTestItem('weedicide')['item_id'],
+                TestConfig::getTestItem('watering_tool')['item_id'],
+                TestConfig::getTestItem('invalid_item')['item_id']
+            ];
+            DB::table('game_items')->whereIn('id', $itemIds)->delete();
+            
+            // 清理测试用户
+            DB::table('users')->where('id', $userId)->delete();
+            
+            echo "✅ 测试数据清理完成\n";
+            
+        } catch (\Exception $e) {
+            echo "❌ 测试数据清理失败: " . $e->getMessage() . "\n";
+        }
+    }
+}
+
+// 检查命令行参数
+$action = $argv[1] ?? 'prepare';
+
+$preparer = new TestDataPreparer();
+
+switch ($action) {
+    case 'prepare':
+        $preparer->prepare();
+        break;
+    case 'cleanup':
+        $preparer->cleanup();
+        break;
+    default:
+        echo "使用方法:\n";
+        echo "  php prepare_test_data.php prepare  # 准备测试数据\n";
+        echo "  php prepare_test_data.php cleanup  # 清理测试数据\n";
+        break;
+}

+ 297 - 0
app/Module/AppGame/Tests/run_disaster_removal_tests.php

@@ -0,0 +1,297 @@
+<?php
+
+/**
+ * 灾害去除测试运行脚本
+ * 
+ * 提供便捷的命令行接口来运行灾害去除相关的测试
+ * 
+ * 使用方法:
+ * php app/Module/AppGame/Tests/run_disaster_removal_tests.php [选项]
+ * 
+ * 选项:
+ * --all          运行所有测试
+ * --pesticide    只运行除虫测试
+ * --weedicide    只运行除草测试
+ * --watering     只运行浇水测试
+ * --suite        运行完整测试套件
+ * --config       显示测试配置
+ * --help         显示帮助信息
+ */
+
+require_once __DIR__ . '/../../../../vendor/autoload.php';
+
+use App\Module\AppGame\Tests\TestConfig;
+
+class DisasterRemovalTestRunner
+{
+    private array $options;
+    private string $baseCommand;
+
+    public function __construct(array $argv)
+    {
+        $this->options = $this->parseArguments($argv);
+        $this->baseCommand = 'php artisan test';
+    }
+
+    /**
+     * 运行测试
+     */
+    public function run(): void
+    {
+        $this->displayHeader();
+
+        if (isset($this->options['help'])) {
+            $this->displayHelp();
+            return;
+        }
+
+        if (isset($this->options['config'])) {
+            $this->displayConfig();
+            return;
+        }
+
+        $this->checkEnvironment();
+
+        if (isset($this->options['all'])) {
+            $this->runAllTests();
+        } elseif (isset($this->options['pesticide'])) {
+            $this->runPesticideTests();
+        } elseif (isset($this->options['weedicide'])) {
+            $this->runWeedicideTests();
+        } elseif (isset($this->options['watering'])) {
+            $this->runWateringTests();
+        } elseif (isset($this->options['suite'])) {
+            $this->runTestSuite();
+        } else {
+            $this->displayHelp();
+        }
+    }
+
+    /**
+     * 解析命令行参数
+     */
+    private function parseArguments(array $argv): array
+    {
+        $options = [];
+        for ($i = 1; $i < count($argv); $i++) {
+            $arg = $argv[$i];
+            if (strpos($arg, '--') === 0) {
+                $options[substr($arg, 2)] = true;
+            }
+        }
+        return $options;
+    }
+
+    /**
+     * 显示头部信息
+     */
+    private function displayHeader(): void
+    {
+        echo "\n";
+        echo "========================================\n";
+        echo "    灾害去除系统测试运行器\n";
+        echo "========================================\n";
+        echo "时间: " . date('Y-m-d H:i:s') . "\n";
+        echo "环境: " . (env('UNITTEST_URL') ?: '未配置') . "\n";
+        echo "========================================\n\n";
+    }
+
+    /**
+     * 显示帮助信息
+     */
+    private function displayHelp(): void
+    {
+        echo "使用方法:\n";
+        echo "php run_disaster_removal_tests.php [选项]\n\n";
+        echo "选项:\n";
+        echo "  --all          运行所有测试\n";
+        echo "  --pesticide    只运行除虫测试\n";
+        echo "  --weedicide    只运行除草测试\n";
+        echo "  --watering     只运行浇水测试\n";
+        echo "  --suite        运行完整测试套件\n";
+        echo "  --config       显示测试配置\n";
+        echo "  --help         显示帮助信息\n\n";
+        echo "示例:\n";
+        echo "  php run_disaster_removal_tests.php --all\n";
+        echo "  php run_disaster_removal_tests.php --pesticide\n";
+        echo "  php run_disaster_removal_tests.php --suite\n\n";
+    }
+
+    /**
+     * 显示测试配置
+     */
+    private function displayConfig(): void
+    {
+        echo "测试配置信息:\n";
+        echo "========================================\n";
+        
+        echo "测试用户:\n";
+        echo "  用户ID: " . TestConfig::getTestUserId() . "\n";
+        echo "  密码: " . TestConfig::getTestUserPassword() . "\n\n";
+        
+        echo "除虫测试配置:\n";
+        $pesticideConfig = TestConfig::getPesticideTestConfig();
+        foreach ($pesticideConfig as $key => $value) {
+            echo "  {$key}: {$value}\n";
+        }
+        echo "\n";
+        
+        echo "除草测试配置:\n";
+        $weedicideConfig = TestConfig::getWeedicideTestConfig();
+        foreach ($weedicideConfig as $key => $value) {
+            echo "  {$key}: {$value}\n";
+        }
+        echo "\n";
+        
+        echo "浇水测试配置:\n";
+        $wateringConfig = TestConfig::getWateringTestConfig();
+        foreach ($wateringConfig as $key => $value) {
+            echo "  {$key}: {$value}\n";
+        }
+        echo "\n";
+        
+        echo "测试物品:\n";
+        $items = ['pesticide', 'weedicide', 'watering_tool', 'invalid_item'];
+        foreach ($items as $itemType) {
+            $item = TestConfig::getTestItem($itemType);
+            if (!empty($item)) {
+                echo "  {$itemType}: ID={$item['item_id']}, 名称={$item['name']}\n";
+            }
+        }
+        echo "\n";
+    }
+
+    /**
+     * 检查环境
+     */
+    private function checkEnvironment(): void
+    {
+        echo "检查测试环境...\n";
+        
+        // 检查环境变量
+        $testUrl = env('UNITTEST_URL');
+        if (!$testUrl) {
+            echo "警告: 未设置 UNITTEST_URL 环境变量,将使用默认值 http://localhost:8000\n";
+        } else {
+            echo "测试URL: {$testUrl}\n";
+        }
+        
+        // 检查网络连接
+        $this->checkConnection($testUrl ?: 'http://localhost:8000');
+        
+        echo "环境检查完成\n\n";
+    }
+
+    /**
+     * 检查网络连接
+     */
+    private function checkConnection(string $url): void
+    {
+        echo "检查服务器连接: {$url}...\n";
+        
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_NOBODY, true);
+        
+        $result = curl_exec($ch);
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+        
+        if ($result !== false && $httpCode > 0) {
+            echo "服务器连接正常 (HTTP {$httpCode})\n";
+        } else {
+            echo "警告: 无法连接到服务器,测试可能会失败\n";
+        }
+    }
+
+    /**
+     * 运行所有测试
+     */
+    private function runAllTests(): void
+    {
+        echo "运行所有灾害去除测试...\n\n";
+        
+        $this->runPesticideTests();
+        $this->runWeedicideTests();
+        $this->runWateringTests();
+        
+        echo "\n所有测试完成!\n";
+    }
+
+    /**
+     * 运行除虫测试
+     */
+    private function runPesticideTests(): void
+    {
+        echo "运行除虫测试...\n";
+        $this->executeTest('app/Module/AppGame/Tests/Land/PesticideHandlerTest.php');
+    }
+
+    /**
+     * 运行除草测试
+     */
+    private function runWeedicideTests(): void
+    {
+        echo "运行除草测试...\n";
+        $this->executeTest('app/Module/AppGame/Tests/Land/WeedicideHandlerTest.php');
+    }
+
+    /**
+     * 运行浇水测试
+     */
+    private function runWateringTests(): void
+    {
+        echo "运行浇水测试...\n";
+        $this->executeTest('app/Module/AppGame/Tests/Land/WateringHandlerTest.php');
+    }
+
+    /**
+     * 运行测试套件
+     */
+    private function runTestSuite(): void
+    {
+        echo "运行完整测试套件...\n";
+        $this->executeTest('app/Module/AppGame/Tests/Land/DisasterRemovalTestSuite.php');
+    }
+
+    /**
+     * 执行测试
+     */
+    private function executeTest(string $testFile): void
+    {
+        $command = "{$this->baseCommand} {$testFile}";
+        echo "执行命令: {$command}\n";
+        echo "----------------------------------------\n";
+        
+        $startTime = microtime(true);
+        
+        // 执行测试命令
+        $output = [];
+        $returnCode = 0;
+        exec($command . ' 2>&1', $output, $returnCode);
+        
+        $endTime = microtime(true);
+        $duration = round($endTime - $startTime, 2);
+        
+        // 显示输出
+        foreach ($output as $line) {
+            echo $line . "\n";
+        }
+        
+        echo "----------------------------------------\n";
+        echo "测试耗时: {$duration}秒\n";
+        echo "返回码: {$returnCode}\n";
+        
+        if ($returnCode === 0) {
+            echo "✅ 测试通过\n\n";
+        } else {
+            echo "❌ 测试失败\n\n";
+        }
+    }
+}
+
+// 运行测试
+$runner = new DisasterRemovalTestRunner($argv);
+$runner->run();

+ 214 - 0
app/Module/AppGame/Tests/run_tests.sh

@@ -0,0 +1,214 @@
+#!/bin/bash
+
+# 灾害去除系统测试运行脚本
+# 
+# 使用方法:
+# ./run_tests.sh [选项]
+# 
+# 选项:
+# all          运行所有测试
+# pesticide    只运行除虫测试
+# weedicide    只运行除草测试
+# watering     只运行浇水测试
+# suite        运行完整测试套件
+# unit         运行单元测试
+# help         显示帮助信息
+
+# 颜色定义
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# 显示头部信息
+show_header() {
+    echo -e "${BLUE}========================================${NC}"
+    echo -e "${BLUE}    灾害去除系统测试运行器${NC}"
+    echo -e "${BLUE}========================================${NC}"
+    echo -e "时间: $(date '+%Y-%m-%d %H:%M:%S')"
+    echo -e "环境: ${UNITTEST_URL:-http://localhost:8000}"
+    echo -e "${BLUE}========================================${NC}"
+    echo
+}
+
+# 显示帮助信息
+show_help() {
+    echo "使用方法:"
+    echo "./run_tests.sh [选项]"
+    echo
+    echo "选项:"
+    echo "  all          运行所有测试"
+    echo "  pesticide    只运行除虫测试"
+    echo "  weedicide    只运行除草测试"
+    echo "  watering     只运行浇水测试"
+    echo "  suite        运行完整测试套件"
+    echo "  unit         运行单元测试"
+    echo "  help         显示帮助信息"
+    echo
+    echo "示例:"
+    echo "  ./run_tests.sh all"
+    echo "  ./run_tests.sh pesticide"
+    echo "  ./run_tests.sh unit"
+    echo
+}
+
+# 检查环境
+check_environment() {
+    echo -e "${YELLOW}检查测试环境...${NC}"
+    
+    # 检查PHP
+    if ! command -v php &> /dev/null; then
+        echo -e "${RED}错误: 未找到PHP${NC}"
+        exit 1
+    fi
+    
+    # 检查Laravel
+    if [ ! -f "artisan" ]; then
+        echo -e "${RED}错误: 未找到Laravel项目${NC}"
+        exit 1
+    fi
+    
+    # 检查测试URL
+    if [ -z "$UNITTEST_URL" ]; then
+        echo -e "${YELLOW}警告: 未设置 UNITTEST_URL 环境变量${NC}"
+        export UNITTEST_URL="http://localhost:8000"
+    fi
+    
+    echo -e "${GREEN}环境检查完成${NC}"
+    echo
+}
+
+# 运行测试
+run_test() {
+    local test_file=$1
+    local test_name=$2
+    
+    echo -e "${BLUE}运行 ${test_name}...${NC}"
+    echo "----------------------------------------"
+    
+    start_time=$(date +%s)
+    
+    # 运行测试
+    php artisan test "$test_file" --verbose
+    exit_code=$?
+    
+    end_time=$(date +%s)
+    duration=$((end_time - start_time))
+    
+    echo "----------------------------------------"
+    echo "测试耗时: ${duration}秒"
+    
+    if [ $exit_code -eq 0 ]; then
+        echo -e "${GREEN}✅ ${test_name} 测试通过${NC}"
+    else
+        echo -e "${RED}❌ ${test_name} 测试失败${NC}"
+    fi
+    echo
+    
+    return $exit_code
+}
+
+# 运行所有E2E测试
+run_all_e2e_tests() {
+    echo -e "${BLUE}运行所有E2E测试...${NC}"
+    echo
+    
+    local failed_tests=0
+    
+    # 运行除虫测试
+    run_test "app/Module/AppGame/Tests/Land/PesticideHandlerTest.php" "除虫测试"
+    if [ $? -ne 0 ]; then
+        ((failed_tests++))
+    fi
+    
+    # 运行除草测试
+    run_test "app/Module/AppGame/Tests/Land/WeedicideHandlerTest.php" "除草测试"
+    if [ $? -ne 0 ]; then
+        ((failed_tests++))
+    fi
+    
+    # 运行浇水测试
+    run_test "app/Module/AppGame/Tests/Land/WateringHandlerTest.php" "浇水测试"
+    if [ $? -ne 0 ]; then
+        ((failed_tests++))
+    fi
+    
+    echo -e "${BLUE}========================================${NC}"
+    if [ $failed_tests -eq 0 ]; then
+        echo -e "${GREEN}🎉 所有E2E测试通过!${NC}"
+    else
+        echo -e "${RED}❌ ${failed_tests} 个测试失败${NC}"
+    fi
+    echo -e "${BLUE}========================================${NC}"
+    
+    return $failed_tests
+}
+
+# 运行单元测试
+run_unit_tests() {
+    echo -e "${BLUE}运行单元测试...${NC}"
+    echo
+    
+    local failed_tests=0
+    
+    # 运行验证测试
+    run_test "tests/Unit/Farm/DisasterRemovalValidationTest.php" "验证测试"
+    if [ $? -ne 0 ]; then
+        ((failed_tests++))
+    fi
+    
+    # 运行逻辑测试
+    run_test "tests/Unit/Farm/DisasterRemovalLogicTest.php" "逻辑测试"
+    if [ $? -ne 0 ]; then
+        ((failed_tests++))
+    fi
+    
+    echo -e "${BLUE}========================================${NC}"
+    if [ $failed_tests -eq 0 ]; then
+        echo -e "${GREEN}🎉 所有单元测试通过!${NC}"
+    else
+        echo -e "${RED}❌ ${failed_tests} 个测试失败${NC}"
+    fi
+    echo -e "${BLUE}========================================${NC}"
+    
+    return $failed_tests
+}
+
+# 主函数
+main() {
+    show_header
+    
+    case "${1:-help}" in
+        "all")
+            check_environment
+            run_all_e2e_tests
+            ;;
+        "pesticide")
+            check_environment
+            run_test "app/Module/AppGame/Tests/Land/PesticideHandlerTest.php" "除虫测试"
+            ;;
+        "weedicide")
+            check_environment
+            run_test "app/Module/AppGame/Tests/Land/WeedicideHandlerTest.php" "除草测试"
+            ;;
+        "watering")
+            check_environment
+            run_test "app/Module/AppGame/Tests/Land/WateringHandlerTest.php" "浇水测试"
+            ;;
+        "suite")
+            check_environment
+            run_test "app/Module/AppGame/Tests/Land/DisasterRemovalTestSuite.php" "测试套件"
+            ;;
+        "unit")
+            check_environment
+            run_unit_tests
+            ;;
+        "help"|*)
+            show_help
+            ;;
+    esac
+}
+
+# 运行主函数
+main "$@"

+ 75 - 18
app/Module/Farm/Docs/灾害去除系统优化说明.md

@@ -38,7 +38,33 @@ public int $fram_drought_rate = 0;
 public int $fram_weedicide_rate = 0;
 ```
 
-### 2. 创建统一的灾害去除逻辑
+### 2. 创建验证层架构
+
+#### 新增 DisasterRemovalValidation 类
+位置:`app/Module/Farm/Validations/DisasterRemovalValidation.php`
+
+**核心功能**:
+- 统一的数据验证规则定义
+- 集成自定义验证器
+- 符合框架的验证架构设计
+
+#### 新增 LandOwnershipValidator 类
+位置:`app/Module/Farm/Validators/LandOwnershipValidator.php`
+
+**核心功能**:
+- 验证土地是否属于指定用户
+- 使用addError方法进行错误处理
+- 支持参数化配置
+
+#### 新增 DisasterRemovalItemValidator 类
+位置:`app/Module/Farm/Validators/DisasterRemovalItemValidator.php`
+
+**核心功能**:
+- 验证用户是否拥有指定物品
+- 验证物品是否具有对应的灾害去除属性
+- 支持多种灾害类型的验证
+
+### 3. 创建统一的灾害去除逻辑
 
 #### 新增 DisasterRemovalLogic 类
 位置:`app/Module/Farm/Logics/DisasterRemovalLogic.php`
@@ -54,6 +80,7 @@ public int $fram_weedicide_rate = 0;
 - **失败处理**:即使失败也会消耗物品,符合游戏逻辑
 - **类型安全**:使用枚举替代魔法数字
 - **事务要求**:要求调用者开启事务,符合架构设计
+- **专注业务逻辑**:不再包含验证逻辑,假设所有验证已通过
 
 ### 3. 服务层扩展
 
@@ -63,28 +90,39 @@ public int $fram_weedicide_rate = 0;
  * 使用物品去除灾害(带概率判断)
  */
 public static function removeDisasterWithItem(
-    int $userId, 
-    int $landId, 
-    int $itemId, 
-    int $disasterType, 
+    int $userId,
+    int $landId,
+    int $itemId,
+    int $disasterType,
     string $sourceType
 ): array
 ```
 
-### 4. Handler层重构
+### 5. Handler层重构
 
-#### 统一的处理模式
+#### 验证前置的处理模式
 所有灾害去除Handler(PesticideHandler、WeedicideHandler、WateringHandler)都采用相同的处理模式:
 
 ```php
-// 开启事务
+// 先进行验证,避免不必要的事务开销
+$validation = new \App\Module\Farm\Validations\DisasterRemovalValidation([
+    'user_id' => $userId,
+    'land_id' => $landId,
+    'item_id' => $userItemId,
+    'disaster_type' => DISASTER_TYPE::PEST->value
+]);
+
+// 验证数据
+$validation->validated();
+
+// 验证通过后,开启事务
 DB::beginTransaction();
 
-// 使用统一的灾害去除逻辑
+// 执行业务逻辑(不再需要验证)
 $result = CropService::removeDisasterWithItem(
-    $userId, 
-    $landId, 
-    $userItemId, 
+    $userId,
+    $landId,
+    $userItemId,
     DISASTER_TYPE::PEST->value,  // 使用枚举替代魔法数字
     'land_pesticide'
 );
@@ -93,6 +131,25 @@ $result = CropService::removeDisasterWithItem(
 DB::commit();
 ```
 
+#### 分层异常处理
+```php
+} catch (\UCore\Exception\ValidateException $e) {
+    // 验证失败,此时可能还没有开启事务
+    $this->response->setCode(400);
+    $this->response->setMsg($e->getMessage());
+} catch (LogicException $e) {
+    // 业务逻辑异常,需要回滚事务
+    if (DB::transactionLevel() > 0) {
+        DB::rollBack();
+    }
+} catch (\Exception $e) {
+    // 系统异常,需要回滚事务
+    if (DB::transactionLevel() > 0) {
+        DB::rollBack();
+    }
+}
+```
+
 #### 消除魔法数字
 - **修正前**:`CropService::clearDisaster($userId, $landId, 2)`
 - **修正后**:`DISASTER_TYPE::PEST->value`
@@ -106,7 +163,7 @@ private function rollSuccess(int $successRate): bool
     if ($successRate <= 0) {
         return false;  // 0%成功率必定失败
     }
-    
+
     if ($successRate >= 100) {
         return true;   // 100%成功率必定成功
     }
@@ -127,7 +184,7 @@ private function rollSuccess(int $successRate): bool
 ```php
 private const DISASTER_ITEM_ATTRIBUTES = [
     DISASTER_TYPE::DROUGHT->value => 'fram_drought_rate',    // 干旱 -> 浇水概率
-    DISASTER_TYPE::PEST->value => 'fram_pesticide_rate',     // 虫害 -> 除虫概率  
+    DISASTER_TYPE::PEST->value => 'fram_pesticide_rate',     // 虫害 -> 除虫概率
     DISASTER_TYPE::WEED->value => 'fram_weedicide_rate',     // 杂草 -> 除草概率
 ];
 ```
@@ -195,10 +252,10 @@ Helper::check_tr();
 ```php
 // 使用除虫剂
 $result = CropService::removeDisasterWithItem(
-    $userId, 
-    $landId, 
-    $pesticideItemId, 
-    DISASTER_TYPE::PEST->value, 
+    $userId,
+    $landId,
+    $pesticideItemId,
+    DISASTER_TYPE::PEST->value,
     'land_pesticide'
 );
 

+ 9 - 68
app/Module/Farm/Logics/DisasterRemovalLogic.php

@@ -3,10 +3,6 @@
 namespace App\Module\Farm\Logics;
 
 use App\Module\Farm\Enums\DISASTER_TYPE;
-use App\Module\Farm\Models\FarmCrop;
-use App\Module\Farm\Models\FarmLand;
-use App\Module\Farm\Enums\LAND_STATUS;
-use App\Module\Farm\Events\DisasterClearedEvent;
 use App\Module\GameItems\Services\ItemService;
 use Illuminate\Support\Facades\Log;
 use UCore\Db\Helper;
@@ -14,7 +10,7 @@ use UCore\Exception\LogicException;
 
 /**
  * 灾害去除逻辑类
- * 
+ *
  * 处理各种类型的灾害去除操作,包括概率计算、状态验证和物品消耗
  */
 class DisasterRemovalLogic
@@ -24,7 +20,7 @@ class DisasterRemovalLogic
      */
     private const DISASTER_ITEM_ATTRIBUTES = [
         DISASTER_TYPE::DROUGHT->value => 'fram_drought_rate',    // 干旱 -> 浇水概率
-        DISASTER_TYPE::PEST->value => 'fram_pesticide_rate',     // 虫害 -> 除虫概率  
+        DISASTER_TYPE::PEST->value => 'fram_pesticide_rate',     // 虫害 -> 除虫概率
         DISASTER_TYPE::WEED->value => 'fram_weedicide_rate',     // 杂草 -> 除草概率
     ];
 
@@ -40,6 +36,8 @@ class DisasterRemovalLogic
     /**
      * 尝试去除指定类型的灾害
      *
+     * 注意:此方法假设所有验证已经通过,专注于业务逻辑处理
+     *
      * @param int $userId 用户ID
      * @param int $landId 土地ID
      * @param int $itemId 物品ID
@@ -53,32 +51,21 @@ class DisasterRemovalLogic
         // 检查事务状态
         Helper::check_tr();
 
-        // 验证灾害类型
-        if (!isset(self::DISASTER_ITEM_ATTRIBUTES[$disasterType])) {
-            throw new LogicException("不支持的灾害类型: {$disasterType}");
-        }
-
-        // 验证土地
-        $land = $this->validateLand($userId, $landId);
-        
-        // 验证物品
-        $this->validateItem($userId, $itemId, $disasterType);
-        
         // 获取物品成功率
         $successRate = $this->getItemSuccessRate($itemId, $disasterType);
-        
+
         // 进行概率判断
         if (!$this->rollSuccess($successRate)) {
             // 失败时仍然消耗物品
             $this->consumeItem($userId, $itemId, $landId, $sourceType);
-            
+
             $actionName = self::DISASTER_ACTION_NAMES[$disasterType];
             throw new LogicException("{$actionName}失败,请再次尝试");
         }
 
         // 执行灾害清理
         $result = $this->clearDisasterFromCrop($userId, $landId, $disasterType);
-        
+
         if (!$result) {
             throw new LogicException("灾害清理失败,请检查土地状态或作物生长阶段");
         }
@@ -87,7 +74,7 @@ class DisasterRemovalLogic
         $this->consumeItem($userId, $itemId, $landId, $sourceType);
 
         $actionName = self::DISASTER_ACTION_NAMES[$disasterType];
-        
+
         Log::info("用户{$actionName}成功", [
             'user_id' => $userId,
             'land_id' => $landId,
@@ -104,53 +91,7 @@ class DisasterRemovalLogic
         ];
     }
 
-    /**
-     * 验证土地是否存在且属于用户
-     *
-     * @param int $userId 用户ID
-     * @param int $landId 土地ID
-     * @return FarmLand
-     * @throws LogicException
-     */
-    private function validateLand(int $userId, int $landId): FarmLand
-    {
-        $land = FarmLand::where('id', $landId)
-            ->where('user_id', $userId)
-            ->first();
-
-        if (!$land) {
-            throw new LogicException("土地不存在或不属于当前用户");
-        }
-
-        return $land;
-    }
-
-    /**
-     * 验证用户是否拥有指定物品且物品具有对应的灾害去除属性
-     *
-     * @param int $userId 用户ID
-     * @param int $itemId 物品ID
-     * @param int $disasterType 灾害类型
-     * @throws LogicException
-     */
-    private function validateItem(int $userId, int $itemId, int $disasterType): void
-    {
-        // 验证用户是否拥有该物品
-        $hasItem = ItemService::getUserItems($userId, ['item_id' => $itemId]);
-        if ($hasItem->isEmpty()) {
-            $actionName = self::DISASTER_ACTION_NAMES[$disasterType];
-            throw new LogicException("您没有该{$actionName}物品");
-        }
 
-        // 验证物品是否具有对应的灾害去除属性
-        $attributeName = self::DISASTER_ITEM_ATTRIBUTES[$disasterType];
-        $rate = ItemService::getItemNumericAttribute($itemId, $attributeName);
-        
-        if ($rate <= 0) {
-            $actionName = self::DISASTER_ACTION_NAMES[$disasterType];
-            throw new LogicException("不是{$actionName}物品");
-        }
-    }
 
     /**
      * 获取物品的成功率
@@ -176,7 +117,7 @@ class DisasterRemovalLogic
         if ($successRate <= 0) {
             return false;
         }
-        
+
         if ($successRate >= 100) {
             return true;
         }

+ 43 - 0
app/Module/Farm/Services/CropService.php

@@ -203,6 +203,49 @@ class CropService
         }
     }
 
+    /**
+     * 使用物品去除灾害(带验证和概率判断)
+     *
+     * @param int $userId 用户ID
+     * @param int $landId 土地ID
+     * @param int $itemId 物品ID
+     * @param int $disasterType 灾害类型
+     * @param string $sourceType 消耗来源类型
+     * @return array 操作结果
+     * @throws \Exception
+     */
+    public static function removeDisasterWithValidation(int $userId, int $landId, int $itemId, int $disasterType, string $sourceType): array
+    {
+        try {
+            // 使用Validation进行数据验证
+            $validation = new \App\Module\Farm\Validations\DisasterRemovalValidation([
+                'user_id' => $userId,
+                'land_id' => $landId,
+                'item_id' => $itemId,
+                'disaster_type' => $disasterType
+            ]);
+
+            // 验证数据
+            $validation->validated();
+
+            // 验证通过后,执行业务逻辑
+            $disasterRemovalLogic = new \App\Module\Farm\Logics\DisasterRemovalLogic();
+            return $disasterRemovalLogic->removeDisaster($userId, $landId, $itemId, $disasterType, $sourceType);
+        } catch (\Exception $e) {
+            Log::error('使用物品去除灾害失败', [
+                'user_id' => $userId,
+                'land_id' => $landId,
+                'item_id' => $itemId,
+                'disaster_type' => $disasterType,
+                'source_type' => $sourceType,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            throw $e;
+        }
+    }
+
     /**
      * 铲除作物
      *

+ 54 - 0
app/Module/Farm/Validations/DisasterRemovalValidation.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Module\Farm\Validations;
+
+use App\Module\Farm\Validators\LandOwnershipValidator;
+use App\Module\Farm\Validators\DisasterRemovalItemValidator;
+use UCore\ValidationCore;
+
+/**
+ * 灾害去除验证类
+ * 
+ * 用于验证灾害去除操作的输入数据,包括用户ID、土地ID、物品ID和灾害类型
+ */
+class DisasterRemovalValidation extends ValidationCore
+{
+    /**
+     * 验证规则
+     *
+     * @param array $rules 自定义规则
+     * @return array
+     */
+    public function rules($rules = []): array
+    {
+        return [
+            [
+                'user_id,land_id,item_id,disaster_type', 'required'
+            ],
+            [
+                'user_id,land_id,item_id,disaster_type', 'integer', 'min' => 1,
+                'msg' => '{attr}必须是大于0的整数'
+            ],
+            // 验证土地是否属于用户
+            [
+                'land_id', new LandOwnershipValidator($this, ['user_id']),
+                'msg' => '土地不存在或不属于当前用户'
+            ],
+            // 验证物品是否为对应的灾害去除道具
+            [
+                'item_id', new DisasterRemovalItemValidator($this, ['user_id', 'disaster_type']),
+                'msg' => '物品验证失败'
+            ]
+        ];
+    }
+
+    /**
+     * 设置默认值
+     *
+     * @return array
+     */
+    public function default(): array
+    {
+        return [];
+    }
+}

+ 127 - 0
app/Module/Farm/Validators/DisasterRemovalItemValidator.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace App\Module\Farm\Validators;
+
+use App\Module\Farm\Enums\DISASTER_TYPE;
+use App\Module\GameItems\Services\ItemService;
+use UCore\Validator;
+
+/**
+ * 灾害去除物品验证器
+ * 
+ * 验证物品是否为有效的灾害去除道具,包括用户是否拥有该物品以及物品是否具有对应的灾害去除属性
+ */
+class DisasterRemovalItemValidator extends Validator
+{
+    /**
+     * 灾害类型与物品属性的映射关系
+     */
+    private const DISASTER_ITEM_ATTRIBUTES = [
+        DISASTER_TYPE::DROUGHT->value => 'fram_drought_rate',    // 干旱 -> 浇水概率
+        DISASTER_TYPE::PEST->value => 'fram_pesticide_rate',     // 虫害 -> 除虫概率  
+        DISASTER_TYPE::WEED->value => 'fram_weedicide_rate',     // 杂草 -> 除草概率
+    ];
+
+    /**
+     * 灾害类型与操作名称的映射关系
+     */
+    private const DISASTER_ACTION_NAMES = [
+        DISASTER_TYPE::DROUGHT->value => '浇水',
+        DISASTER_TYPE::PEST->value => '除虫',
+        DISASTER_TYPE::WEED->value => '除草',
+    ];
+
+    /**
+     * 验证物品是否为有效的灾害去除道具
+     *
+     * @param mixed $value 物品ID
+     * @param array $data 包含用户ID和灾害类型的数组
+     * @return bool 验证是否通过
+     */
+    public function validate(mixed $value, array $data): bool
+    {
+        $itemId = (int)$value;
+        
+        // 从 args 获取字段键名
+        $userIdKey = $this->args[0] ?? 'user_id';
+        $disasterTypeKey = $this->args[1] ?? 'disaster_type';
+        
+        $userId = $data[$userIdKey] ?? null;
+        $disasterType = $data[$disasterTypeKey] ?? null;
+
+        if (!$userId) {
+            $this->addError('用户ID不能为空');
+            return false;
+        }
+
+        if (!$disasterType) {
+            $this->addError('灾害类型不能为空');
+            return false;
+        }
+
+        // 验证灾害类型是否支持
+        if (!isset(self::DISASTER_ITEM_ATTRIBUTES[$disasterType])) {
+            $this->addError("不支持的灾害类型: {$disasterType}");
+            return false;
+        }
+
+        try {
+            // 验证用户是否拥有该物品
+            if (!$this->validateUserHasItem($userId, $itemId, $disasterType)) {
+                return false;
+            }
+
+            // 验证物品是否具有对应的灾害去除属性
+            if (!$this->validateItemAttribute($itemId, $disasterType)) {
+                return false;
+            }
+
+            return true;
+        } catch (\Exception $e) {
+            $this->addError('验证物品时发生错误: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 验证用户是否拥有指定物品
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int $disasterType 灾害类型
+     * @return bool
+     */
+    private function validateUserHasItem(int $userId, int $itemId, int $disasterType): bool
+    {
+        $hasItem = ItemService::getUserItems($userId, ['item_id' => $itemId]);
+        
+        if ($hasItem->isEmpty()) {
+            $actionName = self::DISASTER_ACTION_NAMES[$disasterType];
+            $this->addError("您没有该{$actionName}物品");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 验证物品是否具有对应的灾害去除属性
+     *
+     * @param int $itemId 物品ID
+     * @param int $disasterType 灾害类型
+     * @return bool
+     */
+    private function validateItemAttribute(int $itemId, int $disasterType): bool
+    {
+        $attributeName = self::DISASTER_ITEM_ATTRIBUTES[$disasterType];
+        $rate = ItemService::getItemNumericAttribute($itemId, $attributeName);
+        
+        if ($rate <= 0) {
+            $actionName = self::DISASTER_ACTION_NAMES[$disasterType];
+            $this->addError("不是{$actionName}物品");
+            return false;
+        }
+
+        return true;
+    }
+}

+ 58 - 0
app/Module/Farm/Validators/LandOwnershipValidator.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Module\Farm\Validators;
+
+use App\Module\Farm\Models\FarmLand;
+use UCore\Validator;
+
+/**
+ * 土地归属验证器
+ * 
+ * 验证指定的土地是否属于指定的用户
+ */
+class LandOwnershipValidator extends Validator
+{
+    /**
+     * 验证土地是否属于用户
+     *
+     * @param mixed $value 土地ID
+     * @param array $data 包含用户ID的数组
+     * @return bool 验证是否通过
+     */
+    public function validate(mixed $value, array $data): bool
+    {
+        $landId = (int)$value;
+        
+        // 从 args 获取用户ID的键名,默认为 'user_id'
+        $userIdKey = $this->args[0] ?? 'user_id';
+        $userId = $data[$userIdKey] ?? null;
+
+        if (!$userId) {
+            $this->addError('用户ID不能为空');
+            return false;
+        }
+
+        try {
+            // 查询土地是否存在且属于指定用户
+            $land = FarmLand::where('id', $landId)
+                ->where('user_id', $userId)
+                ->first();
+
+            if (!$land) {
+                $this->addError("土地(ID: {$landId})不存在或不属于当前用户");
+                return false;
+            }
+
+            // 可选:将土地实例保存到验证对象中,供后续使用
+            $landFieldKey = $this->args[1] ?? null;
+            if ($landFieldKey) {
+                $this->validation->$landFieldKey = $land;
+            }
+
+            return true;
+        } catch (\Exception $e) {
+            $this->addError('验证土地归属时发生错误: ' . $e->getMessage());
+            return false;
+        }
+    }
+}

+ 31 - 56
tests/Unit/Farm/DisasterRemovalLogicTest.php

@@ -6,8 +6,6 @@ use Tests\TestCase;
 use App\Module\Farm\Logics\DisasterRemovalLogic;
 use App\Module\Farm\Enums\DISASTER_TYPE;
 use App\Module\GameItems\Services\ItemService;
-use App\Module\Farm\Models\FarmLand;
-use App\Module\Farm\Models\FarmCrop;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Illuminate\Support\Facades\DB;
 use UCore\Exception\LogicException;
@@ -60,7 +58,7 @@ class DisasterRemovalLogicTest extends TestCase
     public function testDisasterTypeMapping()
     {
         $reflection = new \ReflectionClass(DisasterRemovalLogic::class);
-        
+
         // 获取常量
         $attributesConstant = $reflection->getConstant('DISASTER_ITEM_ATTRIBUTES');
         $namesConstant = $reflection->getConstant('DISASTER_ACTION_NAMES');
@@ -107,10 +105,10 @@ class DisasterRemovalLogicTest extends TestCase
 
         // 不开启事务,应该抛出异常
         $this->disasterRemovalLogic->removeDisaster(
-            1, 
-            1, 
-            1, 
-            DISASTER_TYPE::PEST->value, 
+            1,
+            1,
+            1,
+            DISASTER_TYPE::PEST->value,
             'test'
         );
     }
@@ -132,8 +130,8 @@ class DisasterRemovalLogicTest extends TestCase
         });
 
         $result = $getItemSuccessRateMethod->invoke(
-            $this->disasterRemovalLogic, 
-            123, 
+            $this->disasterRemovalLogic,
+            123,
             DISASTER_TYPE::PEST->value
         );
 
@@ -141,60 +139,37 @@ class DisasterRemovalLogicTest extends TestCase
     }
 
     /**
-     * 测试验证物品方法
-     */
-    public function testValidateItem()
-    {
-        $reflection = new \ReflectionClass(DisasterRemovalLogic::class);
-        $validateItemMethod = $reflection->getMethod('validateItem');
-        $validateItemMethod->setAccessible(true);
-
-        // Mock ItemService - 用户没有物品的情况
-        $this->mock(ItemService::class, function ($mock) {
-            $mock->shouldReceive('getUserItems')
-                ->with(1, ['item_id' => 123])
-                ->andReturn(collect([])); // 空集合表示没有物品
-        });
-
-        $this->expectException(LogicException::class);
-        $this->expectExceptionMessage('您没有该除虫物品');
-
-        $validateItemMethod->invoke(
-            $this->disasterRemovalLogic,
-            1,  // userId
-            123, // itemId
-            DISASTER_TYPE::PEST->value
-        );
-    }
-
-    /**
-     * 测试验证物品 - 物品没有对应属性
+     * 测试事务检查要求
      */
-    public function testValidateItemWithoutAttribute()
+    public function testTransactionRequirement()
     {
-        $reflection = new \ReflectionClass(DisasterRemovalLogic::class);
-        $validateItemMethod = $reflection->getMethod('validateItem');
-        $validateItemMethod->setAccessible(true);
-
         // Mock ItemService
         $this->mock(ItemService::class, function ($mock) {
-            $mock->shouldReceive('getUserItems')
-                ->with(1, ['item_id' => 123])
-                ->andReturn(collect([new \stdClass()])); // 有物品
-            
             $mock->shouldReceive('getItemNumericAttribute')
-                ->with(123, 'fram_pesticide_rate')
-                ->andReturn(0); // 没有对应属性或属性值为0
+                ->with(123, 'fram_pesticide_rate', 0)
+                ->andReturn(80);
         });
 
-        $this->expectException(LogicException::class);
-        $this->expectExceptionMessage('不是除虫物品');
+        // 开启事务
+        DB::beginTransaction();
 
-        $validateItemMethod->invoke(
-            $this->disasterRemovalLogic,
-            1,  // userId
-            123, // itemId
-            DISASTER_TYPE::PEST->value
-        );
+        try {
+            // 这个测试主要验证逻辑层不再进行验证,专注于业务逻辑
+            $this->disasterRemovalLogic->removeDisaster(
+                1,  // userId
+                1,  // landId
+                123, // itemId
+                DISASTER_TYPE::PEST->value,
+                'test'
+            );
+
+            // 由于没有实际的作物数据,这里会失败,但我们主要测试事务检查通过
+            $this->fail('应该因为没有作物数据而失败');
+        } catch (\Exception $e) {
+            // 预期会失败,因为没有实际的作物数据
+            $this->assertStringContainsString('灾害清理失败', $e->getMessage());
+        } finally {
+            DB::rollBack();
+        }
     }
 }

+ 242 - 0
tests/Unit/Farm/DisasterRemovalValidationTest.php

@@ -0,0 +1,242 @@
+<?php
+
+namespace Tests\Unit\Farm;
+
+use Tests\TestCase;
+use App\Module\Farm\Validations\DisasterRemovalValidation;
+use App\Module\Farm\Validators\LandOwnershipValidator;
+use App\Module\Farm\Validators\DisasterRemovalItemValidator;
+use App\Module\Farm\Enums\DISASTER_TYPE;
+use App\Module\Farm\Models\FarmLand;
+use App\Module\GameItems\Services\ItemService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use UCore\Exception\ValidateException;
+
+/**
+ * 灾害去除验证测试
+ */
+class DisasterRemovalValidationTest extends TestCase
+{
+    use RefreshDatabase;
+
+    /**
+     * 测试验证规则
+     */
+    public function testValidationRules()
+    {
+        $validation = new DisasterRemovalValidation([]);
+        $rules = $validation->rules();
+
+        // 验证必填字段规则
+        $this->assertNotEmpty($rules);
+        
+        // 检查是否包含必填字段验证
+        $requiredRule = $rules[0];
+        $this->assertEquals('user_id,land_id,item_id,disaster_type', $requiredRule[0]);
+        $this->assertEquals('required', $requiredRule[1]);
+    }
+
+    /**
+     * 测试缺少必填字段
+     */
+    public function testMissingRequiredFields()
+    {
+        $this->expectException(ValidateException::class);
+
+        $validation = new DisasterRemovalValidation([
+            'user_id' => 1,
+            // 缺少其他必填字段
+        ]);
+
+        $validation->validated();
+    }
+
+    /**
+     * 测试字段类型验证
+     */
+    public function testFieldTypeValidation()
+    {
+        $this->expectException(ValidateException::class);
+
+        $validation = new DisasterRemovalValidation([
+            'user_id' => 'invalid', // 应该是整数
+            'land_id' => 1,
+            'item_id' => 1,
+            'disaster_type' => DISASTER_TYPE::PEST->value
+        ]);
+
+        $validation->validated();
+    }
+
+    /**
+     * 测试土地归属验证器
+     */
+    public function testLandOwnershipValidator()
+    {
+        // 创建测试用户和土地
+        $userId = 1;
+        $landId = 1;
+
+        // Mock FarmLand 模型
+        $this->mock(FarmLand::class, function ($mock) use ($userId, $landId) {
+            $mock->shouldReceive('where')
+                ->with('id', $landId)
+                ->andReturnSelf();
+            $mock->shouldReceive('where')
+                ->with('user_id', $userId)
+                ->andReturnSelf();
+            $mock->shouldReceive('first')
+                ->andReturn(new FarmLand()); // 返回土地实例表示验证通过
+        });
+
+        $validator = new LandOwnershipValidator(new DisasterRemovalValidation([]), ['user_id']);
+        
+        $result = $validator->validate($landId, ['user_id' => $userId]);
+        
+        $this->assertTrue($result);
+    }
+
+    /**
+     * 测试土地归属验证失败
+     */
+    public function testLandOwnershipValidatorFail()
+    {
+        $userId = 1;
+        $landId = 999; // 不存在的土地
+
+        // Mock FarmLand 模型
+        $this->mock(FarmLand::class, function ($mock) use ($userId, $landId) {
+            $mock->shouldReceive('where')
+                ->with('id', $landId)
+                ->andReturnSelf();
+            $mock->shouldReceive('where')
+                ->with('user_id', $userId)
+                ->andReturnSelf();
+            $mock->shouldReceive('first')
+                ->andReturn(null); // 返回null表示土地不存在
+        });
+
+        $validator = new LandOwnershipValidator(new DisasterRemovalValidation([]), ['user_id']);
+        
+        $result = $validator->validate($landId, ['user_id' => $userId]);
+        
+        $this->assertFalse($result);
+    }
+
+    /**
+     * 测试灾害去除物品验证器
+     */
+    public function testDisasterRemovalItemValidator()
+    {
+        $userId = 1;
+        $itemId = 123;
+        $disasterType = DISASTER_TYPE::PEST->value;
+
+        // Mock ItemService
+        $this->mock(ItemService::class, function ($mock) use ($userId, $itemId) {
+            $mock->shouldReceive('getUserItems')
+                ->with($userId, ['item_id' => $itemId])
+                ->andReturn(collect([new \stdClass()])); // 用户拥有该物品
+            
+            $mock->shouldReceive('getItemNumericAttribute')
+                ->with($itemId, 'fram_pesticide_rate')
+                ->andReturn(80); // 物品具有除虫属性
+        });
+
+        $validator = new DisasterRemovalItemValidator(
+            new DisasterRemovalValidation([]), 
+            ['user_id', 'disaster_type']
+        );
+        
+        $result = $validator->validate($itemId, [
+            'user_id' => $userId,
+            'disaster_type' => $disasterType
+        ]);
+        
+        $this->assertTrue($result);
+    }
+
+    /**
+     * 测试物品验证失败 - 用户没有物品
+     */
+    public function testDisasterRemovalItemValidatorFailNoItem()
+    {
+        $userId = 1;
+        $itemId = 123;
+        $disasterType = DISASTER_TYPE::PEST->value;
+
+        // Mock ItemService
+        $this->mock(ItemService::class, function ($mock) use ($userId, $itemId) {
+            $mock->shouldReceive('getUserItems')
+                ->with($userId, ['item_id' => $itemId])
+                ->andReturn(collect([])); // 用户没有该物品
+        });
+
+        $validator = new DisasterRemovalItemValidator(
+            new DisasterRemovalValidation([]), 
+            ['user_id', 'disaster_type']
+        );
+        
+        $result = $validator->validate($itemId, [
+            'user_id' => $userId,
+            'disaster_type' => $disasterType
+        ]);
+        
+        $this->assertFalse($result);
+    }
+
+    /**
+     * 测试物品验证失败 - 物品没有对应属性
+     */
+    public function testDisasterRemovalItemValidatorFailNoAttribute()
+    {
+        $userId = 1;
+        $itemId = 123;
+        $disasterType = DISASTER_TYPE::PEST->value;
+
+        // Mock ItemService
+        $this->mock(ItemService::class, function ($mock) use ($userId, $itemId) {
+            $mock->shouldReceive('getUserItems')
+                ->with($userId, ['item_id' => $itemId])
+                ->andReturn(collect([new \stdClass()])); // 用户拥有该物品
+            
+            $mock->shouldReceive('getItemNumericAttribute')
+                ->with($itemId, 'fram_pesticide_rate')
+                ->andReturn(0); // 物品没有除虫属性
+        });
+
+        $validator = new DisasterRemovalItemValidator(
+            new DisasterRemovalValidation([]), 
+            ['user_id', 'disaster_type']
+        );
+        
+        $result = $validator->validate($itemId, [
+            'user_id' => $userId,
+            'disaster_type' => $disasterType
+        ]);
+        
+        $this->assertFalse($result);
+    }
+
+    /**
+     * 测试不支持的灾害类型
+     */
+    public function testUnsupportedDisasterType()
+    {
+        $userId = 1;
+        $itemId = 123;
+        $disasterType = 999; // 不支持的灾害类型
+
+        $validator = new DisasterRemovalItemValidator(
+            new DisasterRemovalValidation([]), 
+            ['user_id', 'disaster_type']
+        );
+        
+        $result = $validator->validate($itemId, [
+            'user_id' => $userId,
+            'disaster_type' => $disasterType
+        ]);
+        
+        $this->assertFalse($result);
+    }
+}