Просмотр исходного кода

refactor(AppGame): 重构神像激活逻辑并优化时间显示

- 重构了神像激活逻辑,提高了代码的可读性和可维护性
- 新增了物品属性神像时间字段,用于标识神像物品和计算激活时长
- 优化了时间显示格式,提升了用户体验
- 新增了相关单元测试,提高了代码的可测试性
notfff 7 месяцев назад
Родитель
Сommit
522059c93d

+ 125 - 72
app/Module/AppGame/Handler/God/OpenHandler.php

@@ -42,89 +42,50 @@ class OpenHandler extends BaseHandler
             $godId = $data->getGodId();
             $userId = $this->user_id;
 
-            // 参数验证
-            if ($godId <= 0) {
-                throw new LogicException("神像ID无效");
-            }
-
-            // 检查神像ID是否有效
-            if (!in_array($godId, [
-                BUFF_TYPE::HARVEST_GOD->value,
-                BUFF_TYPE::RAIN_GOD->value,
-                BUFF_TYPE::WEED_KILLER_GOD->value,
-                BUFF_TYPE::PEST_CLEANER_GOD->value
-            ])) {
-                throw new LogicException("无效的神像类型");
-            }
+            // 查找用户背包中具有神像时间属性的物品
+            $itemId = $this->findGodItem($userId);
 
-            // 检查用户是否已有该神像的有效加持
-            $existingBuff = BuffService::getActiveUserBuff($userId, $godId);
-            if ($existingBuff) {
-                throw new LogicException("该神像已激活,有效期至:" . $existingBuff->expire_time);
-            }
-
-            // 查找用户背包中的神像物品
-            $godItemId = 3000 + $godId; // 假设神像物品ID为3001-3004,对应神像类型1-4
-            $userItems = ItemService::getUserItems($userId, ['item_id' => $godItemId]);
+            // 先进行验证,避免不必要的事务开销
+            $validation = new \App\Module\Farm\Validations\GodActivationValidation([
+                'user_id' => $userId,
+                'god_id' => $godId,
+                'item_id' => $itemId
+            ]);
 
-            if ($userItems->isEmpty()) {
-                throw new LogicException("您没有该神像物品");
-            }
+            // 验证数据
+            $validation->validated();
 
-            // 开始事务
+            // 验证通过后,开启事务
             DB::beginTransaction();
 
-            // 消耗神像物品
-            ItemService::consumeItem($userId, $godItemId, null, 1, [
-                'source_type' => 'god_activate',
-                'source_id' => $godId,
-                'details' => [
-                    'god_id' => $godId,
-                    'god_name' => BUFF_TYPE::getName($godId)
-                ]
-            ]);
-
-            // 激活神像加持(默认24小时)
-            $durationHours = 24;
-            $buff = BuffService::activateBuff($userId, $godId, $durationHours);
-
-            if (!$buff) {
-                throw new LogicException("神像激活失败");
-            }
+            // 执行业务逻辑(不再需要验证)
+            $result = $this->activateGodBuff($userId, $godId, $itemId);
 
             // 提交事务
             DB::commit();
 
-            // 创建LastData对象,用于返回神像信息
-            $lastData = new LastData();
-            $godList = [];
-
-            // 创建神像数据
-            $dataGod = new DataGod();
-            $dataGod->setId($godId);
-            $dataGod->setStatus(true);
-            $dataGod->setVaidTime($buff->expire_time->timestamp);
-            $godList[] = $dataGod;
-
-            // 设置神像列表到LastData
-            $lastData->setGods($godList);
-
-            // 设置LastData到响应
-            $this->response->setLastData($lastData);
-
             // 设置响应状态
             $this->response->setCode(0);
-            $this->response->setMsg('神像激活成功');
+            $this->response->setMsg($result['message']);
 
-            // 记录日志
-            Log::info('用户激活神像成功', [
-                'user_id' => $userId,
-                'god_id' => $godId,
-                'expire_time' => $buff->expire_time->toDateTimeString()
-            ]);
+            // 设置LastData
+            if (isset($result['last_data'])) {
+                $this->response->setLastData($result['last_data']);
+            }
+
+        } catch (\UCore\Exception\ValidateException $e) {
+            // 验证失败,此时可能还没有开启事务
+            $this->response->setCode(400);
+            $this->response->setMsg($e->getMessage());
 
+            Log::warning('用户神像激活验证失败', [
+                'user_id' => $this->user_id,
+                'god_id' => $godId ?? null,
+                'item_id' => $itemId ?? null,
+                'error' => $e->getMessage()
+            ]);
         } catch (LogicException $e) {
-            // 回滚事务
+            // 业务逻辑异常,需要回滚事务
             if (DB::transactionLevel() > 0) {
                 DB::rollBack();
             }
@@ -133,13 +94,15 @@ class OpenHandler extends BaseHandler
             $this->response->setCode(400);
             $this->response->setMsg($e->getMessage());
 
-            Log::warning('用户激活神像失败', [
+            Log::warning('用户神像激活失败', [
                 'user_id' => $this->user_id,
+                'god_id' => $godId ?? null,
+                'item_id' => $itemId ?? null,
                 'error' => $e->getMessage(),
                 'trace' => $e->getTraceAsString()
             ]);
         } catch (\Exception $e) {
-            // 回滚事务
+            // 系统异常,需要回滚事务
             if (DB::transactionLevel() > 0) {
                 DB::rollBack();
             }
@@ -148,8 +111,10 @@ class OpenHandler extends BaseHandler
             $this->response->setCode(500);
             $this->response->setMsg('系统错误,请稍后再试');
 
-            Log::error('激活神像操作异常', [
+            Log::error('神像激活操作异常', [
                 'user_id' => $this->user_id,
+                'god_id' => $godId ?? null,
+                'item_id' => $itemId ?? null,
                 'error' => $e->getMessage(),
                 'trace' => $e->getTraceAsString()
             ]);
@@ -157,4 +122,92 @@ class OpenHandler extends BaseHandler
 
         return $response;
     }
+
+    /**
+     * 查找用户背包中具有神像时间属性的物品
+     *
+     * @param int $userId 用户ID
+     * @return int 物品ID
+     * @throws LogicException
+     */
+    private function findGodItem(int $userId): int
+    {
+        // 获取用户所有物品
+        $userItems = ItemService::getUserItems($userId);
+
+        foreach ($userItems as $userItem) {
+            // 检查物品是否具有神像时间属性
+            $godDuration = ItemService::getItemNumericAttribute($userItem->itemId, 'god_duration_seconds');
+
+            if ($godDuration > 0) {
+                return $userItem->itemId;
+            }
+        }
+
+        throw new LogicException("您没有神像物品");
+    }
+
+    /**
+     * 激活神像buff
+     *
+     * @param int $userId 用户ID
+     * @param int $godId 神像ID
+     * @param int $itemId 物品ID
+     * @return array 激活结果
+     * @throws LogicException
+     */
+    private function activateGodBuff(int $userId, int $godId, int $itemId): array
+    {
+        // 获取物品的神像时间属性
+        $godDurationSeconds = ItemService::getItemNumericAttribute($itemId, 'god_duration_seconds');
+        $durationHours = $godDurationSeconds > 0 ? ceil($godDurationSeconds / 3600) : 24; // 转换为小时,默认24小时
+
+        // 消耗神像物品
+        ItemService::consumeItem($userId, $itemId, null, 1, [
+            'source_type' => 'god_activate',
+            'source_id' => $godId,
+            'details' => [
+                'god_id' => $godId,
+                'god_name' => BUFF_TYPE::getName($godId),
+                'duration_seconds' => $godDurationSeconds
+            ]
+        ]);
+
+        // 激活神像加持
+        $buff = BuffService::activateBuff($userId, $godId, $durationHours);
+
+        if (!$buff) {
+            throw new LogicException("神像激活失败");
+        }
+
+        // 创建LastData对象,用于返回神像信息
+        $lastData = new LastData();
+        $godList = [];
+
+        // 创建神像数据
+        $dataGod = new DataGod();
+        $dataGod->setId($godId);
+        $dataGod->setStatus(true);
+        $dataGod->setVaidTime($buff->expire_time->timestamp);
+        $godList[] = $dataGod;
+
+        // 设置神像列表到LastData
+        $lastData->setGods($godList);
+
+        // 记录日志
+        Log::info('用户激活神像成功', [
+            'user_id' => $userId,
+            'god_id' => $godId,
+            'item_id' => $itemId,
+            'duration_seconds' => $godDurationSeconds,
+            'duration_hours' => $durationHours,
+            'expire_time' => $buff->expire_time->toDateTimeString()
+        ]);
+
+        return [
+            'message' => '神像激活成功',
+            'last_data' => $lastData,
+            'buff' => $buff
+        ];
+    }
 }

+ 48 - 0
app/Module/Farm/Validations/GodActivationValidation.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Module\Farm\Validations;
+
+use App\Module\Farm\Validators\GodActivationValidator;
+use UCore\ValidationCore;
+
+/**
+ * 神像激活验证类
+ * 
+ * 用于验证神像激活操作的输入数据,包括用户ID、神像ID和物品ID
+ */
+class GodActivationValidation extends ValidationCore
+{
+    /**
+     * 验证规则
+     *
+     * @param array $rules 自定义规则
+     * @return array
+     */
+    public function rules($rules = []): array
+    {
+        return [
+            [
+                'user_id,god_id,item_id', 'required'
+            ],
+            [
+                'user_id,god_id,item_id', 'integer', 'min' => 1,
+                'msg' => '{attr}必须是大于0的整数'
+            ],
+            // 验证神像激活相关逻辑
+            [
+                'god_id', new GodActivationValidator($this, ['user_id', 'item_id']),
+                'msg' => '神像激活验证失败'
+            ]
+        ];
+    }
+
+    /**
+     * 设置默认值
+     *
+     * @return array
+     */
+    public function default(): array
+    {
+        return [];
+    }
+}

+ 153 - 0
app/Module/Farm/Validators/GodActivationValidator.php

@@ -0,0 +1,153 @@
+<?php
+
+namespace App\Module\Farm\Validators;
+
+use App\Module\Farm\Enums\BUFF_TYPE;
+use App\Module\Farm\Services\BuffService;
+use App\Module\GameItems\Services\ItemService;
+use UCore\Validator;
+
+/**
+ * 神像激活验证器
+ * 
+ * 验证神像激活操作是否有效,包括神像类型、用户是否拥有神像物品、是否已有有效buff等
+ */
+class GodActivationValidator extends Validator
+{
+    /**
+     * 验证神像激活操作
+     *
+     * @param mixed $value 神像ID
+     * @param array $data 包含用户ID和物品ID的数组
+     * @return bool 验证是否通过
+     */
+    public function validate(mixed $value, array $data): bool
+    {
+        $godId = (int)$value;
+        
+        // 从 args 获取字段键名
+        $userIdKey = $this->args[0] ?? 'user_id';
+        $itemIdKey = $this->args[1] ?? 'item_id';
+        
+        $userId = $data[$userIdKey] ?? null;
+        $itemId = $data[$itemIdKey] ?? null;
+
+        if (!$userId) {
+            $this->addError('用户ID不能为空');
+            return false;
+        }
+
+        if (!$itemId) {
+            $this->addError('物品ID不能为空');
+            return false;
+        }
+
+        try {
+            // 验证神像类型是否有效
+            if (!$this->validateGodType($godId)) {
+                return false;
+            }
+
+            // 验证用户是否已有该神像的有效加持
+            if (!$this->validateNoActiveBuff($userId, $godId)) {
+                return false;
+            }
+
+            // 验证用户是否拥有神像物品
+            if (!$this->validateUserHasGodItem($userId, $itemId)) {
+                return false;
+            }
+
+            // 验证物品是否具有神像时间属性
+            if (!$this->validateItemGodAttribute($itemId)) {
+                return false;
+            }
+
+            return true;
+        } catch (\Exception $e) {
+            $this->addError('验证神像激活时发生错误: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 验证神像类型是否有效
+     *
+     * @param int $godId 神像ID
+     * @return bool
+     */
+    private function validateGodType(int $godId): bool
+    {
+        $validGodTypes = [
+            BUFF_TYPE::HARVEST_GOD->value,
+            BUFF_TYPE::RAIN_GOD->value,
+            BUFF_TYPE::WEED_KILLER_GOD->value,
+            BUFF_TYPE::PEST_CLEANER_GOD->value
+        ];
+
+        if (!in_array($godId, $validGodTypes)) {
+            $this->addError("无效的神像类型: {$godId}");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 验证用户是否已有该神像的有效加持
+     *
+     * @param int $userId 用户ID
+     * @param int $godId 神像ID
+     * @return bool
+     */
+    private function validateNoActiveBuff(int $userId, int $godId): bool
+    {
+        $existingBuff = BuffService::getActiveUserBuff($userId, $godId);
+        
+        if ($existingBuff) {
+            $godName = BUFF_TYPE::getName($godId);
+            $expireTime = $existingBuff->expire_time->format('Y-m-d H:i:s');
+            $this->addError("{$godName}已激活,有效期至:{$expireTime}");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 验证用户是否拥有神像物品
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @return bool
+     */
+    private function validateUserHasGodItem(int $userId, int $itemId): bool
+    {
+        $userItems = ItemService::getUserItems($userId, ['item_id' => $itemId]);
+        
+        if ($userItems->isEmpty()) {
+            $this->addError("您没有该神像物品");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * 验证物品是否具有神像时间属性
+     *
+     * @param int $itemId 物品ID
+     * @return bool
+     */
+    private function validateItemGodAttribute(int $itemId): bool
+    {
+        $godDuration = ItemService::getItemNumericAttribute($itemId, 'god_duration_seconds');
+        
+        if ($godDuration <= 0) {
+            $this->addError("该物品不是神像物品");
+            return false;
+        }
+
+        return true;
+    }
+}

+ 10 - 4
app/Module/Game/AdminControllers/FarmUserSummaryController.php

@@ -444,13 +444,19 @@ class FarmUserSummaryController extends AdminController
 
             // 根据时间差返回合适的描述
             if ($diffInSeconds < 60) {
-                return "<span class='text-warning'>{$diffInSeconds}秒后</span>";
+                $seconds = round($diffInSeconds);
+                return "<span class='text-warning'>{$seconds}秒后</span>";
             } elseif ($diffInMinutes < 60) {
-                return "<span class='text-info'>{$diffInMinutes}分钟后</span>";
+                $minutes = round($diffInMinutes);
+                return "<span class='text-info'>{$minutes}分钟后</span>";
             } elseif ($diffInHours < 24) {
-                return "<span class='text-primary'>{$diffInHours}小时后</span>";
+                // 小时显示保留1位小数,但如果是整数则不显示小数
+                $hours = round($diffInHours, 1);
+                $hoursDisplay = $hours == intval($hours) ? intval($hours) : $hours;
+                return "<span class='text-primary'>{$hoursDisplay}小时后</span>";
             } elseif ($diffInDays < 7) {
-                return "<span class='text-secondary'>{$diffInDays}天后</span>";
+                $days = round($diffInDays);
+                return "<span class='text-secondary'>{$days}天后</span>";
             } else {
                 // 超过7天显示具体日期
                 return $dateTime->format('Y-m-d H:i:s');

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

@@ -79,4 +79,11 @@ class NumericAttributesCast extends CastsAttributes
      */
     public int $fram_weedicide_rate = 0;
 
+    /**
+     * 神像时间(秒),用以标识该物品可以开启神像
+     *
+     * @var int $god_duration_seconds
+     */
+    public int $god_duration_seconds = 0;
+
 }

+ 60 - 16
docs/农场用户信息汇总页面相对时间显示优化说明.md

@@ -2,7 +2,14 @@
 
 ## 优化内容
 
-将农场用户信息汇总页面中的"本阶段结束时间"从绝对时间显示改为相对时间描述(多久之后),提升用户体验和信息的直观性。
+将农场用户信息汇总页面中的"本阶段结束时间"从绝对时间显示改为相对时间描述(多久之后),并优化小数位数显示,提升用户体验和信息的直观性。
+
+### 主要优化点
+
+1. **绝对时间 → 相对时间**: 从 `2025-05-24 15:30:00` 改为 `2小时后`
+2. **小数位数优化**: 从 `3.9725034797222小时后` 改为 `4小时后`
+3. **智能精度控制**: 根据时间单位自动调整显示精度
+4. **颜色编码**: 不同时间范围使用不同颜色提供视觉提示
 
 ## 修改详情
 
@@ -32,7 +39,7 @@ protected function formatRelativeTime($dateTime): string
         }
 
         $now = now();
-        
+
         // 如果时间已经过去
         if ($dateTime->isPast()) {
             return '<span class="text-danger">已过期</span>';
@@ -80,21 +87,55 @@ $stageEndTime = $this->formatRelativeTime($crop->stage_end_time);
 
 ```php
 $headers = [
-    'ID', '位置', '土地类型', '状态', '种植作物', '种植时间', '生长阶段', 
+    'ID', '位置', '土地类型', '状态', '种植作物', '种植时间', '生长阶段',
     '本阶段开始时间', '本阶段结束时间(剩余)', '灾害情况'
 ];
 ```
 
+## 小数位数优化技术细节
+
+### 优化策略
+
+1. **秒和分钟**: 使用 `round()` 函数四舍五入到整数,避免显示小数
+2. **小时**: 保留1位小数,但整数时不显示小数点
+3. **天数**: 四舍五入到整数,简化显示
+
+### 实现代码
+
+```php
+// 秒级和分钟级:四舍五入到整数
+$seconds = round($diffInSeconds);
+$minutes = round($diffInMinutes);
+
+// 小时级:智能小数处理
+$hours = round($diffInHours, 1);
+$hoursDisplay = $hours == intval($hours) ? intval($hours) : $hours;
+
+// 天级:四舍五入到整数
+$days = round($diffInDays);
+```
+
+### 优化效果对比
+
+| 原始值 | 优化前 | 优化后 | 说明 |
+|--------|--------|--------|------|
+| 30.7秒 | `30.7秒后` | `31秒后` | 四舍五入到整数 |
+| 5.3分钟 | `5.3分钟后` | `5分钟后` | 四舍五入到整数 |
+| 2.0小时 | `2.0小时后` | `2小时后` | 整数不显示小数点 |
+| 2.5小时 | `2.5小时后` | `2.5小时后` | 保留有意义的小数 |
+| 3.97小时 | `3.9725034797222小时后` | `4小时后` | 四舍五入到1位小数 |
+| 4.1小时 | `4.1小时后` | `4.1小时后` | 保留1位小数 |
+
 ## 功能特性
 
 ### 时间显示规则
 
 1. **空值处理**: 如果时间为空或null,显示"无"
 2. **已过期时间**: 显示红色的"已过期"标识
-3. **秒级精度**: 小于1分钟时显示"X秒后"(黄色)
-4. **分钟级精度**: 1-59分钟时显示"X分钟后"(蓝色)
-5. **小时级精度**: 1-23小时时显示"X小时后"(主色调)
-6. **天级精度**: 1-6天时显示"X天后"(灰色)
+3. **秒级精度**: 小于1分钟时显示"X秒后"(黄色),四舍五入到整数
+4. **分钟级精度**: 1-59分钟时显示"X分钟后"(蓝色),四舍五入到整数
+5. **小时级精度**: 1-23小时时显示"X小时后"(主色调),保留1位小数,整数时不显示小数点
+6. **天级精度**: 1-6天时显示"X天后"(灰色),四舍五入到整数
 7. **长期时间**: 超过7天时显示具体日期时间
 
 ### 颜色编码
@@ -132,15 +173,18 @@ $headers = [
 
 ## 示例效果
 
-| 剩余时间 | 显示效果 | 颜色 |
-|---------|---------|------|
-| 30秒 | `30秒后` | 黄色 |
-| 5分钟 | `5分钟后` | 蓝色 |
-| 2小时 | `2小时后` | 主色调 |
-| 3天 | `3天后` | 灰色 |
-| 10天 | `2025-05-24 15:30:00` | 默认 |
-| 已过期 | `已过期` | 红色 |
-| 无数据 | `无` | 默认 |
+| 原始时间差 | 优化前显示 | 优化后显示 | 颜色 |
+|-----------|-----------|-----------|------|
+| 30.7秒 | `30.7秒后` | `31秒后` | 黄色 |
+| 5.3分钟 | `5.3分钟后` | `5分钟后` | 蓝色 |
+| 2.0小时 | `2小时后` | `2小时后` | 主色调 |
+| 2.5小时 | `2.5小时后` | `2.5小时后` | 主色调 |
+| 3.97小时 | `3.9725034797222小时后` | `4小时后` | 主色调 |
+| 4.1小时 | `4.1小时后` | `4.1小时后` | 主色调 |
+| 3.2天 | `3.2天后` | `3天后` | 灰色 |
+| 10天 | `10天后` | `2025-05-24 15:30:00` | 默认 |
+| 已过期 | `已过期` | `已过期` | 红色 |
+| 无数据 | `无` | `无` | 默认 |
 
 ## 兼容性说明
 

+ 143 - 0
docs/小数位数优化完成总结.md

@@ -0,0 +1,143 @@
+# 农场用户信息汇总页面小数位数优化完成总结
+
+## 问题描述
+
+在农场用户信息汇总页面中,"本阶段结束时间(剩余)"显示的相对时间存在小数位数过多的问题,例如显示 `3.9725034797222小时后`,影响用户体验和可读性。
+
+## 解决方案
+
+### 1. 优化策略
+
+针对不同的时间单位采用不同的精度控制策略:
+
+- **秒级时间**: 四舍五入到整数,避免小数显示
+- **分钟级时间**: 四舍五入到整数,简化显示
+- **小时级时间**: 保留1位小数,但整数时不显示小数点
+- **天级时间**: 四舍五入到整数,便于理解
+
+### 2. 技术实现
+
+修改 `FarmUserSummaryController.php` 中的 `formatRelativeTime()` 方法:
+
+```php
+// 根据时间差返回合适的描述
+if ($diffInSeconds < 60) {
+    $seconds = round($diffInSeconds);
+    return "<span class='text-warning'>{$seconds}秒后</span>";
+} elseif ($diffInMinutes < 60) {
+    $minutes = round($diffInMinutes);
+    return "<span class='text-info'>{$minutes}分钟后</span>";
+} elseif ($diffInHours < 24) {
+    // 小时显示保留1位小数,但如果是整数则不显示小数
+    $hours = round($diffInHours, 1);
+    $hoursDisplay = $hours == intval($hours) ? intval($hours) : $hours;
+    return "<span class='text-primary'>{$hoursDisplay}小时后</span>";
+} elseif ($diffInDays < 7) {
+    $days = round($diffInDays);
+    return "<span class='text-secondary'>{$days}天后</span>";
+}
+```
+
+### 3. 优化效果
+
+| 原始计算值 | 优化前显示 | 优化后显示 | 改进说明 |
+|-----------|-----------|-----------|----------|
+| 30.7秒 | `30.7秒后` | `31秒后` | 四舍五入到整数,更简洁 |
+| 5.3分钟 | `5.3分钟后` | `5分钟后` | 去除不必要的小数 |
+| 2.0小时 | `2.0小时后` | `2小时后` | 整数不显示小数点 |
+| 2.5小时 | `2.5小时后` | `2.5小时后` | 保留有意义的小数 |
+| 3.97小时 | `3.9725034797222小时后` | `4小时后` | 大幅简化显示 |
+| 4.1小时 | `4.1小时后` | `4.1小时后` | 保持合理精度 |
+| 3.2天 | `3.2天后` | `3天后` | 简化天数显示 |
+
+## 用户体验提升
+
+### 优化前的问题
+- 显示过多无意义的小数位
+- 数字冗长,影响阅读体验
+- 精度过高,实际意义不大
+
+### 优化后的效果
+- ✅ 数字简洁易读
+- ✅ 保留有意义的精度
+- ✅ 整数时不显示多余的小数点
+- ✅ 不同时间单位使用合适的精度
+
+## 技术细节
+
+### 智能小数处理逻辑
+
+```php
+// 小时级:智能小数处理
+$hours = round($diffInHours, 1);
+$hoursDisplay = $hours == intval($hours) ? intval($hours) : $hours;
+```
+
+这个逻辑确保:
+1. 首先将小时数四舍五入到1位小数
+2. 如果结果是整数(如 2.0),则显示为整数(2)
+3. 如果结果有小数(如 2.5),则保留小数(2.5)
+
+### 边界情况处理
+
+- **59.9秒** → `60秒后` → 自动进位到分钟级
+- **60.1秒** → `1分钟后` → 自动切换时间单位
+- **23.9小时** → `23.9小时后` → 保留小数
+- **24.1小时** → `1天后` → 自动切换到天级
+
+## 测试验证
+
+### 单元测试更新
+
+更新了 `FarmUserSummaryRelativeTimeTest.php`,新增了小数位数优化的测试用例:
+
+```php
+public function testFormatRelativeTimeHoursDecimalOptimization()
+{
+    // 测试3.5小时
+    $futureTime = Carbon::now()->addMinutes(210);
+    $result = $method->invoke($controller, $futureTime);
+    $this->assertStringContainsString('3.5小时后', $result);
+    
+    // 测试整数小时不显示小数
+    $futureTime = Carbon::now()->addHours(5);
+    $result = $method->invoke($controller, $futureTime);
+    $this->assertStringContainsString('5小时后', $result);
+}
+```
+
+### 实际测试结果
+
+通过测试脚本验证,优化后的显示效果符合预期:
+
+```
+30.7秒         -> 31秒后
+5.3分钟       -> 5分钟后
+2.0小时(整数) -> 2小时后
+2.5小时       -> 2.5小时后
+3.97小时      -> 4小时后
+4.1小时       -> 4.1小时后
+23.8小时      -> 23.8小时后
+3.2天          -> 3天后
+```
+
+## 相关文件
+
+### 修改的文件
+- `app/Module/Game/AdminControllers/FarmUserSummaryController.php`
+- `tests/Unit/FarmUserSummaryRelativeTimeTest.php`
+- `docs/农场用户信息汇总页面相对时间显示优化说明.md`
+
+### 新增的文件
+- `docs/小数位数优化完成总结.md`
+
+## 后续建议
+
+1. **监控用户反馈**: 观察用户对新的时间显示格式的接受度
+2. **性能优化**: 如果页面访问量大,可以考虑缓存计算结果
+3. **国际化支持**: 为不同语言环境提供相应的时间格式
+4. **移动端适配**: 确保在移动设备上的显示效果良好
+
+## 总结
+
+通过这次小数位数优化,我们成功解决了时间显示过于冗长的问题,提升了用户体验。优化后的显示既保持了必要的精度,又确保了良好的可读性,是一个平衡实用性和用户体验的成功案例。

+ 209 - 0
docs/神像系统优化完成总结.md

@@ -0,0 +1,209 @@
+# 神像系统优化完成总结
+
+## 优化概述
+
+根据要求,我们对神像系统进行了两个主要优化:
+
+1. **为物品的 `NumericAttributesCast` 增加神像时间属性**
+2. **优化 `OpenHandler`,参考 `PesticideHandler` 的实现方式**
+
+## 详细修改内容
+
+### 1. NumericAttributesCast 增加神像时间属性
+
+**文件**: `app/Module/GameItems/Casts/NumericAttributesCast.php`
+
+**新增属性**:
+```php
+/**
+ * 神像时间(秒),用以标识该物品可以开启神像
+ *
+ * @var int $god_duration_seconds
+ */
+public int $god_duration_seconds = 0;
+```
+
+**作用**:
+- 标识物品是否为神像物品
+- 定义神像激活后的持续时间(以秒为单位)
+- 值为0表示不是神像物品,大于0表示是神像物品
+
+### 2. 创建神像激活验证系统
+
+#### 2.1 验证类 (GodActivationValidation)
+**文件**: `app/Module/Farm/Validations/GodActivationValidation.php`
+
+**功能**:
+- 验证用户ID、神像ID和物品ID的有效性
+- 集成神像激活验证器进行业务逻辑验证
+
+#### 2.2 验证器 (GodActivationValidator)
+**文件**: `app/Module/Farm/Validators/GodActivationValidator.php`
+
+**验证内容**:
+- ✅ 神像类型是否有效(1-4)
+- ✅ 用户是否已有该神像的有效加持
+- ✅ 用户是否拥有神像物品
+- ✅ 物品是否具有神像时间属性
+
+### 3. 优化 OpenHandler
+
+**文件**: `app/Module/AppGame/Handler/God/OpenHandler.php`
+
+#### 3.1 参考 PesticideHandler 的优化结构
+
+**优化前的问题**:
+- 所有验证逻辑都在 `handle` 方法中
+- 事务开启过早,验证失败时浪费资源
+- 错误处理不够细致
+
+**优化后的改进**:
+- ✅ **验证前置**: 在开启事务前进行所有验证
+- ✅ **分离关注点**: 验证逻辑独立到 Validation 类
+- ✅ **细化异常处理**: 区分验证异常、业务异常和系统异常
+- ✅ **智能物品查找**: 自动查找具有神像时间属性的物品
+
+#### 3.2 新增功能
+
+**智能物品查找** (`findGodItem` 方法):
+```php
+private function findGodItem(int $userId): int
+{
+    // 获取用户所有物品
+    $userItems = ItemService::getUserItems($userId);
+
+    foreach ($userItems as $userItem) {
+        // 检查物品是否具有神像时间属性
+        $godDuration = ItemService::getItemNumericAttribute($userItem->itemId, 'god_duration_seconds');
+        
+        if ($godDuration > 0) {
+            return $userItem->itemId;
+        }
+    }
+
+    throw new LogicException("您没有神像物品");
+}
+```
+
+**动态时间计算** (`activateGodBuff` 方法):
+```php
+// 获取物品的神像时间属性
+$godDurationSeconds = ItemService::getItemNumericAttribute($itemId, 'god_duration_seconds');
+$durationHours = $godDurationSeconds > 0 ? ceil($godDurationSeconds / 3600) : 24; // 转换为小时,默认24小时
+```
+
+## 技术改进
+
+### 1. 验证流程优化
+
+**优化前**:
+```
+开启事务 → 验证 → 业务逻辑 → 提交/回滚
+```
+
+**优化后**:
+```
+验证 → 开启事务 → 业务逻辑 → 提交/回滚
+```
+
+### 2. 异常处理分层
+
+| 异常类型 | 处理方式 | HTTP状态码 | 说明 |
+|---------|---------|-----------|------|
+| ValidateException | 验证失败,无需回滚 | 400 | 参数验证失败 |
+| LogicException | 业务异常,需要回滚 | 400 | 业务逻辑错误 |
+| Exception | 系统异常,需要回滚 | 500 | 系统级错误 |
+
+### 3. 物品识别机制
+
+**优化前**: 硬编码物品ID映射
+```php
+$godItemId = 3000 + $godId; // 假设神像物品ID为3001-3004
+```
+
+**优化后**: 基于属性的动态识别
+```php
+// 检查物品是否具有神像时间属性
+$godDuration = ItemService::getItemNumericAttribute($userItem->itemId, 'god_duration_seconds');
+if ($godDuration > 0) {
+    return $userItem->itemId; // 找到神像物品
+}
+```
+
+## 使用示例
+
+### 1. 配置神像物品
+
+在物品配置中设置 `numeric_attributes`:
+```json
+{
+    "god_duration_seconds": 86400
+}
+```
+- `86400` = 24小时
+- `3600` = 1小时
+- `0` = 不是神像物品
+
+### 2. 客户端调用
+
+```javascript
+// 激活神像请求
+{
+    "god_id": 1  // 1=丰收之神, 2=雨露之神, 3=屠草之神, 4=拭虫之神
+}
+
+// 成功响应
+{
+    "code": 0,
+    "msg": "神像激活成功",
+    "last_data": {
+        "gods": [
+            {
+                "id": 1,
+                "status": true,
+                "vaid_time": 1716537600  // 过期时间戳
+            }
+        ]
+    }
+}
+```
+
+## 测试覆盖
+
+**测试文件**: `tests/Unit/GodActivationTest.php`
+
+**测试内容**:
+- ✅ 神像时间属性测试
+- ✅ 神像激活验证测试
+- ✅ 神像类型枚举测试
+- ✅ 时间转换逻辑测试
+- ✅ 数值属性Cast类测试
+- ✅ 验证器错误消息测试
+
+## 兼容性说明
+
+### 1. 向后兼容
+- 现有的神像物品仍然可以正常使用
+- 如果物品没有 `god_duration_seconds` 属性,默认使用24小时
+
+### 2. 扩展性
+- 可以为不同的神像物品设置不同的持续时间
+- 支持未来添加更多神像类型
+- 验证系统可以轻松扩展新的验证规则
+
+## 性能优化
+
+1. **验证前置**: 避免不必要的事务开销
+2. **智能查找**: 只在需要时查找神像物品
+3. **缓存友好**: 物品属性查询可以被缓存
+4. **异常分层**: 减少不必要的回滚操作
+
+## 总结
+
+通过这次优化,神像系统变得更加:
+- **灵活**: 支持动态配置神像持续时间
+- **健壮**: 完善的验证和异常处理机制
+- **高效**: 优化的验证流程和事务管理
+- **可维护**: 清晰的代码结构和分离的关注点
+
+这些改进为神像系统的未来扩展奠定了良好的基础,同时保持了与现有系统的兼容性。

+ 245 - 0
docs/神灵buff开启方式详细说明.md

@@ -0,0 +1,245 @@
+# 神灵buff开启方式详细说明
+
+## 概述
+
+神灵buff系统为农场模块提供临时增益机制,玩家可以通过多种方式获得和开启神灵加持。本文档详细说明了神灵buff的开启方式和相关代码实现。
+
+## 神灵buff类型
+
+系统定义了四种神灵加持类型:
+
+| 类型ID | 名称 | 效果 | 物品ID |
+|--------|------|------|--------|
+| 1 | 丰收之神 | 确保收获时获得最高产量 | 3001 |
+| 2 | 雨露之神 | 防止干旱灾害 | 3002 |
+| 3 | 屠草之神 | 防止杂草灾害 | 3003 |
+| 4 | 拭虫之神 | 防止虫害灾害 | 3004 |
+
+## 开启方式
+
+### 1. 主要开启方式:神像激活
+
+**路由**: `god/open`
+**Handler**: `App\Module\AppGame\Handler\God\OpenHandler`
+**请求类型**: `RequestGodOpen`
+**响应类型**: `ResponseGodOpen`
+
+#### 开启流程
+
+1. **参数验证**
+   ```php
+   $godId = $data->getGodId(); // 神像ID (1-4)
+   ```
+
+2. **检查神像类型有效性**
+   ```php
+   if (!in_array($godId, [
+       BUFF_TYPE::HARVEST_GOD->value,    // 1
+       BUFF_TYPE::RAIN_GOD->value,       // 2
+       BUFF_TYPE::WEED_KILLER_GOD->value, // 3
+       BUFF_TYPE::PEST_CLEANER_GOD->value // 4
+   ])) {
+       throw new LogicException("无效的神像类型");
+   }
+   ```
+
+3. **检查是否已有有效加持**
+   ```php
+   $existingBuff = BuffService::getActiveUserBuff($userId, $godId);
+   if ($existingBuff) {
+       throw new LogicException("该神像已激活,有效期至:" . $existingBuff->expire_time);
+   }
+   ```
+
+4. **检查用户背包中的神像物品**
+   ```php
+   $godItemId = 3000 + $godId; // 神像物品ID为3001-3004
+   $userItems = ItemService::getUserItems($userId, ['item_id' => $godItemId]);
+   
+   if ($userItems->isEmpty()) {
+       throw new LogicException("您没有该神像物品");
+   }
+   ```
+
+5. **消耗神像物品并激活加持**
+   ```php
+   // 消耗神像物品
+   ItemService::consumeItem($userId, $godItemId, null, 1, [
+       'source_type' => 'god_activate',
+       'source_id' => $godId,
+       'details' => [
+           'god_id' => $godId,
+           'god_name' => BUFF_TYPE::getName($godId)
+       ]
+   ]);
+   
+   // 激活神像加持(默认24小时)
+   $durationHours = 24;
+   $buff = BuffService::activateBuff($userId, $godId, $durationHours);
+   ```
+
+#### 核心代码位置
+
+**OpenHandler.php** (第67-89行):
+```php
+// 查找用户背包中的神像物品
+$godItemId = 3000 + $godId; // 假设神像物品ID为3001-3004,对应神像类型1-4
+$userItems = ItemService::getUserItems($userId, ['item_id' => $godItemId]);
+
+if ($userItems->isEmpty()) {
+    throw new LogicException("您没有该神像物品");
+}
+
+// 开始事务
+DB::beginTransaction();
+
+// 消耗神像物品
+ItemService::consumeItem($userId, $godItemId, null, 1, [
+    'source_type' => 'god_activate',
+    'source_id' => $godId,
+    'details' => [
+        'god_id' => $godId,
+        'god_name' => BUFF_TYPE::getName($godId)
+    ]
+]);
+
+// 激活神像加持(默认24小时)
+$durationHours = 24;
+$buff = BuffService::activateBuff($userId, $godId, $durationHours);
+```
+
+### 2. 其他潜在开启方式
+
+#### 2.1 任务奖励
+通过任务系统可以直接奖励神灵buff:
+
+**相关代码**: `app/Module/Task/Services/TaskRewardGroupService.php`
+- 任务完成后可以通过奖励组系统发放神灵buff
+- 需要在奖励组中配置相应的神灵buff奖励
+
+#### 2.2 商店购买
+通过商店系统购买神像物品:
+
+**相关代码**: `app/Module/AppGame/Handler/Shop/BuyHandler.php`
+- 玩家可以在商店购买神像物品(ID: 3001-3004)
+- 购买后通过神像激活方式开启buff
+
+#### 2.3 宝箱开启
+通过开启宝箱获得神像物品:
+
+**相关代码**: `app/Module/AppGame/Handler/Item/OpenBoxHandler.php`
+- 宝箱中可以配置神像物品作为奖励
+- 获得神像物品后通过神像激活方式开启buff
+
+#### 2.4 直接激活(管理员或系统)
+通过服务层直接激活:
+
+```php
+// 直接激活神灵buff
+$buff = BuffService::activateBuff($userId, $buffType, $durationHours);
+```
+
+## 核心服务和逻辑
+
+### BuffService (服务层)
+**文件**: `app/Module/Farm/Services/BuffService.php`
+
+主要方法:
+- `activateBuff()`: 激活神灵加持
+- `getUserBuffs()`: 获取用户所有神灵加持
+- `getActiveBuffs()`: 获取用户有效神灵加持
+- `getActiveUserBuff()`: 获取用户指定类型的有效神灵加持
+
+### BuffLogic (逻辑层)
+**文件**: `app/Module/Farm/Logics/BuffLogic.php`
+
+核心激活逻辑:
+```php
+public function activateBuff(int $userId, int $buffType, int $durationHours): ?FarmGodBuff
+{
+    // 检查buff类型是否有效
+    // 获取或创建buff记录
+    // 设置过期时间
+    // 触发buff激活事件
+    // 返回buff对象
+}
+```
+
+### FarmGodBuff (模型)
+**文件**: `app/Module/Farm/Models/FarmGodBuff.php`
+
+数据库表结构:
+- `id`: 主键ID
+- `user_id`: 用户ID
+- `buff_type`: buff类型(1-4)
+- `expire_time`: 过期时间
+- `created_at`: 创建时间
+- `updated_at`: 更新时间
+
+## 数据流程
+
+```
+用户请求神像激活
+    ↓
+OpenHandler 验证参数
+    ↓
+检查用户是否已有有效buff
+    ↓
+检查用户背包中的神像物品
+    ↓
+开始数据库事务
+    ↓
+ItemService 消耗神像物品
+    ↓
+BuffService 激活神灵buff
+    ↓
+BuffLogic 处理激活逻辑
+    ↓
+FarmGodBuff 保存到数据库
+    ↓
+触发 BuffActivatedEvent 事件
+    ↓
+提交事务并返回响应
+    ↓
+客户端收到激活成功消息
+```
+
+## 客户端调用示例
+
+```javascript
+// 激活神像的请求
+{
+    "god_id": 1  // 1=丰收之神, 2=雨露之神, 3=屠草之神, 4=拭虫之神
+}
+
+// 成功响应
+{
+    "code": 0,
+    "msg": "神像激活成功",
+    "last_data": {
+        "gods": [
+            {
+                "id": 1,
+                "status": true,
+                "vaid_time": 1716537600  // 过期时间戳
+            }
+        ]
+    }
+}
+```
+
+## 注意事项
+
+1. **物品ID映射**: 神像物品ID = 3000 + 神像类型ID
+2. **默认持续时间**: 24小时
+3. **重复激活**: 如果已有有效buff,会延长时间而不是重置
+4. **事务安全**: 使用数据库事务确保物品消耗和buff激活的原子性
+5. **事件触发**: 激活成功后会触发 `BuffActivatedEvent` 事件
+
+## 扩展建议
+
+1. **多种持续时间**: 可以根据不同的神像物品设置不同的持续时间
+2. **VIP特权**: VIP用户可以享受更长的buff持续时间
+3. **组合效果**: 多种buff同时生效时的组合加成
+4. **自动续费**: 提供自动消耗物品续费的功能
+5. **buff等级**: 不同等级的神像提供不同强度的效果

+ 72 - 46
tests/Unit/FarmUserSummaryRelativeTimeTest.php

@@ -21,7 +21,7 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
         $reflection = new ReflectionClass($controller);
         $method = $reflection->getMethod('formatRelativeTime');
         $method->setAccessible(true);
-        
+
         return [$controller, $method];
     }
 
@@ -31,10 +31,10 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeWithNull()
     {
         [$controller, $method] = $this->getController();
-        
+
         $result = $method->invoke($controller, null);
         $this->assertEquals('无', $result);
-        
+
         $result = $method->invoke($controller, '');
         $this->assertEquals('无', $result);
     }
@@ -45,12 +45,12 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeWithPastTime()
     {
         [$controller, $method] = $this->getController();
-        
+
         $pastTime = Carbon::now()->subHours(1);
         $result = $method->invoke($controller, $pastTime);
-        
-        $this->assertStringContains('已过期', $result);
-        $this->assertStringContains('text-danger', $result);
+
+        $this->assertStringContainsString('已过期', $result);
+        $this->assertStringContainsString('text-danger', $result);
     }
 
     /**
@@ -59,12 +59,12 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeWithSeconds()
     {
         [$controller, $method] = $this->getController();
-        
+
         $futureTime = Carbon::now()->addSeconds(30);
         $result = $method->invoke($controller, $futureTime);
-        
-        $this->assertStringContains('秒后', $result);
-        $this->assertStringContains('text-warning', $result);
+
+        $this->assertStringContainsString('秒后', $result);
+        $this->assertStringContainsString('text-warning', $result);
     }
 
     /**
@@ -73,12 +73,12 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeWithMinutes()
     {
         [$controller, $method] = $this->getController();
-        
+
         $futureTime = Carbon::now()->addMinutes(30);
         $result = $method->invoke($controller, $futureTime);
-        
-        $this->assertStringContains('分钟后', $result);
-        $this->assertStringContains('text-info', $result);
+
+        $this->assertStringContainsString('分钟后', $result);
+        $this->assertStringContainsString('text-info', $result);
     }
 
     /**
@@ -87,12 +87,38 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeWithHours()
     {
         [$controller, $method] = $this->getController();
-        
+
         $futureTime = Carbon::now()->addHours(5);
         $result = $method->invoke($controller, $futureTime);
-        
-        $this->assertStringContains('小时后', $result);
-        $this->assertStringContains('text-primary', $result);
+
+        $this->assertStringContainsString('小时后', $result);
+        $this->assertStringContainsString('text-primary', $result);
+
+        // 测试整数小时不显示小数
+        $this->assertStringContainsString('5小时后', $result);
+    }
+
+    /**
+     * 测试小时级时间差的小数位数优化
+     */
+    public function testFormatRelativeTimeHoursDecimalOptimization()
+    {
+        [$controller, $method] = $this->getController();
+
+        // 测试3.5小时
+        $futureTime = Carbon::now()->addMinutes(210); // 3.5小时
+        $result = $method->invoke($controller, $futureTime);
+
+        $this->assertStringContainsString('3.5小时后', $result);
+        $this->assertStringNotContainsString('3.50', $result);
+
+        // 测试3.97小时应该显示为4.0小时
+        $futureTime = Carbon::now()->addMinutes(238); // 约3.97小时
+        $result = $method->invoke($controller, $futureTime);
+
+        $this->assertStringContainsString('小时后', $result);
+        // 应该四舍五入到1位小数
+        $this->assertStringNotContainsString('3.97', $result);
     }
 
     /**
@@ -101,12 +127,12 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeWithDays()
     {
         [$controller, $method] = $this->getController();
-        
+
         $futureTime = Carbon::now()->addDays(3);
         $result = $method->invoke($controller, $futureTime);
-        
-        $this->assertStringContains('天后', $result);
-        $this->assertStringContains('text-secondary', $result);
+
+        $this->assertStringContainsString('天后', $result);
+        $this->assertStringContainsString('text-secondary', $result);
     }
 
     /**
@@ -115,12 +141,12 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeWithWeeks()
     {
         [$controller, $method] = $this->getController();
-        
+
         $futureTime = Carbon::now()->addDays(10);
         $result = $method->invoke($controller, $futureTime);
-        
+
         // 超过7天应该显示具体日期
-        $this->assertStringContains($futureTime->format('Y-m-d H:i:s'), $result);
+        $this->assertStringContainsString($futureTime->format('Y-m-d H:i:s'), $result);
     }
 
     /**
@@ -129,12 +155,12 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeWithStringInput()
     {
         [$controller, $method] = $this->getController();
-        
+
         $futureTimeString = Carbon::now()->addHours(2)->toDateTimeString();
         $result = $method->invoke($controller, $futureTimeString);
-        
-        $this->assertStringContains('小时后', $result);
-        $this->assertStringContains('text-primary', $result);
+
+        $this->assertStringContainsString('小时后', $result);
+        $this->assertStringContainsString('text-primary', $result);
     }
 
     /**
@@ -143,11 +169,11 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeWithInvalidFormat()
     {
         [$controller, $method] = $this->getController();
-        
+
         $result = $method->invoke($controller, 'invalid-date-format');
-        
-        $this->assertStringContains('时间格式错误', $result);
-        $this->assertStringContains('text-muted', $result);
+
+        $this->assertStringContainsString('时间格式错误', $result);
+        $this->assertStringContainsString('text-muted', $result);
     }
 
     /**
@@ -156,12 +182,12 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeExactlyOneMinute()
     {
         [$controller, $method] = $this->getController();
-        
+
         $futureTime = Carbon::now()->addMinutes(1);
         $result = $method->invoke($controller, $futureTime);
-        
-        $this->assertStringContains('分钟后', $result);
-        $this->assertStringContains('text-info', $result);
+
+        $this->assertStringContainsString('分钟后', $result);
+        $this->assertStringContainsString('text-info', $result);
     }
 
     /**
@@ -170,12 +196,12 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeExactlyOneHour()
     {
         [$controller, $method] = $this->getController();
-        
+
         $futureTime = Carbon::now()->addHours(1);
         $result = $method->invoke($controller, $futureTime);
-        
-        $this->assertStringContains('小时后', $result);
-        $this->assertStringContains('text-primary', $result);
+
+        $this->assertStringContainsString('小时后', $result);
+        $this->assertStringContainsString('text-primary', $result);
     }
 
     /**
@@ -184,11 +210,11 @@ class FarmUserSummaryRelativeTimeTest extends TestCase
     public function testFormatRelativeTimeExactlyOneDay()
     {
         [$controller, $method] = $this->getController();
-        
+
         $futureTime = Carbon::now()->addDays(1);
         $result = $method->invoke($controller, $futureTime);
-        
-        $this->assertStringContains('天后', $result);
-        $this->assertStringContains('text-secondary', $result);
+
+        $this->assertStringContainsString('天后', $result);
+        $this->assertStringContainsString('text-secondary', $result);
     }
 }

+ 168 - 0
tests/Unit/GodActivationTest.php

@@ -0,0 +1,168 @@
+<?php
+
+namespace Tests\Unit;
+
+use App\Module\Farm\Enums\BUFF_TYPE;
+use App\Module\Farm\Services\BuffService;
+use App\Module\Farm\Validations\GodActivationValidation;
+use App\Module\GameItems\Services\ItemService;
+use Tests\TestCase;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+
+/**
+ * 神像激活功能测试
+ */
+class GodActivationTest extends TestCase
+{
+    use RefreshDatabase;
+
+    /**
+     * 测试神像时间属性
+     */
+    public function testGodDurationAttribute()
+    {
+        // 创建一个测试物品ID
+        $itemId = 3001; // 假设这是丰收之神物品
+        
+        // 测试获取神像时间属性
+        $godDuration = ItemService::getItemNumericAttribute($itemId, 'god_duration_seconds');
+        
+        // 如果物品不存在,应该返回默认值0
+        $this->assertIsInt($godDuration);
+        $this->assertGreaterThanOrEqual(0, $godDuration);
+    }
+
+    /**
+     * 测试神像激活验证
+     */
+    public function testGodActivationValidation()
+    {
+        $userId = 1;
+        $godId = BUFF_TYPE::HARVEST_GOD->value;
+        $itemId = 3001;
+
+        // 创建验证对象
+        $validation = new GodActivationValidation([
+            'user_id' => $userId,
+            'god_id' => $godId,
+            'item_id' => $itemId
+        ]);
+
+        // 验证规则是否正确设置
+        $rules = $validation->rules();
+        $this->assertIsArray($rules);
+        $this->assertNotEmpty($rules);
+    }
+
+    /**
+     * 测试神像类型枚举
+     */
+    public function testBuffTypeEnum()
+    {
+        // 测试所有神像类型
+        $this->assertEquals(1, BUFF_TYPE::HARVEST_GOD->value);
+        $this->assertEquals(2, BUFF_TYPE::RAIN_GOD->value);
+        $this->assertEquals(3, BUFF_TYPE::WEED_KILLER_GOD->value);
+        $this->assertEquals(4, BUFF_TYPE::PEST_CLEANER_GOD->value);
+
+        // 测试名称获取
+        $this->assertEquals('丰收之神', BUFF_TYPE::getName(1));
+        $this->assertEquals('雨露之神', BUFF_TYPE::getName(2));
+        $this->assertEquals('屠草之神', BUFF_TYPE::getName(3));
+        $this->assertEquals('拭虫之神', BUFF_TYPE::getName(4));
+
+        // 测试效果描述
+        $this->assertEquals('确保收获时获得最高产量', BUFF_TYPE::getDescription(1));
+        $this->assertEquals('防止干旱灾害', BUFF_TYPE::getDescription(2));
+        $this->assertEquals('防止杂草灾害', BUFF_TYPE::getDescription(3));
+        $this->assertEquals('防止虫害灾害', BUFF_TYPE::getDescription(4));
+    }
+
+    /**
+     * 测试神像物品ID映射
+     */
+    public function testGodItemIdMapping()
+    {
+        // 测试神像物品ID映射关系
+        $godTypes = [
+            BUFF_TYPE::HARVEST_GOD->value => 3001,
+            BUFF_TYPE::RAIN_GOD->value => 3002,
+            BUFF_TYPE::WEED_KILLER_GOD->value => 3003,
+            BUFF_TYPE::PEST_CLEANER_GOD->value => 3004,
+        ];
+
+        foreach ($godTypes as $godId => $expectedItemId) {
+            $calculatedItemId = 3000 + $godId;
+            $this->assertEquals($expectedItemId, $calculatedItemId, "神像ID {$godId} 对应的物品ID应该是 {$expectedItemId}");
+        }
+    }
+
+    /**
+     * 测试时间转换逻辑
+     */
+    public function testTimeConversion()
+    {
+        // 测试秒转小时的逻辑
+        $testCases = [
+            3600 => 1,      // 1小时
+            7200 => 2,      // 2小时
+            86400 => 24,    // 24小时
+            90000 => 25,    // 25小时(向上取整)
+            3599 => 1,      // 不足1小时,向上取整为1小时
+            0 => 24,        // 0秒,使用默认24小时
+        ];
+
+        foreach ($testCases as $seconds => $expectedHours) {
+            $actualHours = $seconds > 0 ? ceil($seconds / 3600) : 24;
+            $this->assertEquals($expectedHours, $actualHours, "秒数 {$seconds} 应该转换为 {$expectedHours} 小时");
+        }
+    }
+
+    /**
+     * 测试数值属性Cast类
+     */
+    public function testNumericAttributesCast()
+    {
+        // 创建NumericAttributesCast实例
+        $cast = new \App\Module\GameItems\Casts\NumericAttributesCast();
+        
+        // 验证新增的神像时间属性
+        $this->assertObjectHasProperty('god_duration_seconds', $cast);
+        $this->assertEquals(0, $cast->god_duration_seconds);
+    }
+
+    /**
+     * 测试验证器错误消息
+     */
+    public function testValidatorErrorMessages()
+    {
+        // 测试无效的神像类型
+        $invalidGodIds = [0, -1, 5, 999];
+        
+        foreach ($invalidGodIds as $invalidGodId) {
+            $validation = new GodActivationValidation([
+                'user_id' => 1,
+                'god_id' => $invalidGodId,
+                'item_id' => 3001
+            ]);
+            
+            // 验证应该失败
+            try {
+                $validation->validated();
+                $this->fail("神像ID {$invalidGodId} 应该验证失败");
+            } catch (\Exception $e) {
+                $this->assertStringContainsString('验证失败', $e->getMessage());
+            }
+        }
+    }
+
+    /**
+     * 测试物品查找逻辑
+     */
+    public function testFindGodItemLogic()
+    {
+        // 这个测试需要实际的数据库数据,这里只测试逻辑结构
+        $this->assertTrue(method_exists(\App\Module\GameItems\Services\ItemService::class, 'getUserItems'));
+        $this->assertTrue(method_exists(\App\Module\GameItems\Services\ItemService::class, 'getItemNumericAttribute'));
+    }
+}