瀏覽代碼

强制发放奖励使用枚举类型,提升类型安全性

- 修改RewardService所有发放奖励方法,强制使用REWARD_SOURCE_TYPE枚举
- 添加来源类型验证,拒绝无效的枚举值
- 保留兼容性方法(标记为废弃),支持字符串参数的旧代码
- 添加createSourceTypeEnum辅助方法,支持字符串到枚举的转换
- 创建TestRewardSourceTypeEnum测试命令,验证枚举强制功能
- 确保类型安全:发放奖励必须传入枚举而不是字符串
- 提升代码规范性和可维护性
notfff 6 月之前
父節點
當前提交
1ce50d0a2c

+ 225 - 0
AiWork/2025年06月/17日1905-改进奖励来源追溯功能使用枚举管理source_type.md

@@ -0,0 +1,225 @@
+# 改进奖励来源追溯功能,使用枚举管理source_type
+
+## 任务概述
+改进奖励日志系统的追溯性问题,通过扩展枚举和创建解析服务,让管理员能够清楚地知道每个奖励的具体来源。
+
+## 问题分析
+
+### 原始问题
+用户反馈:目前奖励组的奖励日志存在问题,无法知晓这个奖励是从哪里来的。
+
+### 具体问题
+1. **来源信息不够详细**:虽然有 `source_type` 和 `source_id`,但后台显示时只显示枚举名称
+2. **缺乏业务上下文**:无法直接看出奖励是从哪个具体的任务、活动、农场操作等触发的
+3. **追溯困难**:管理员需要手动根据 `source_id` 去对应的业务表中查找具体信息
+
+## 解决方案
+
+### 1. 扩展 REWARD_SOURCE_TYPE 枚举
+
+#### 新增的来源类型
+```php
+// 原有类型
+case TASK = 'task';
+case ACTIVITY = 'activity';
+case SIGN_IN = 'sign_in';
+case ACHIEVEMENT = 'achievement';
+case LEVEL = 'level';
+case CHEST = 'chest';
+case SYSTEM = 'system';
+
+// 新增类型
+case TEST = 'test';
+case FARM_INIT = 'farm_init';
+case FARM_HARVEST = 'farm_harvest';
+case FARM_PLANT = 'farm_plant';
+case USER_REGISTER_TEST = 'user_register_test';
+case PROMOTION_REWARD = 'promotion_reward';
+case SHOP_PURCHASE = 'shop_purchase';
+case DAILY_LOGIN = 'daily_login';
+case INVITE_FRIEND = 'invite_friend';
+```
+
+#### 新增的枚举方法
+- `getTypeInfo($type)`: 获取详细的类型信息,包括名称、描述、分类、管理链接
+- `getByCategory($category)`: 根据分类获取奖励来源类型
+- `getCategories()`: 获取所有分类
+
+### 2. 创建 RewardSourceResolver 服务类
+
+#### 核心功能
+- **动态解析来源信息**:根据 `source_type` 和 `source_id` 解析具体的业务信息
+- **统一的返回格式**:提供标准化的信息结构
+- **支持多种来源类型**:任务、活动、农场、推广等
+
+#### 返回信息结构
+```php
+[
+    'type' => '来源类型名称',
+    'name' => '具体名称',
+    'description' => '详细描述',
+    'link' => '管理链接',
+    'status' => '状态',
+    'category' => '分类',
+    'extra' => ['额外信息']
+]
+```
+
+#### 支持的解析类型
+- **任务来源** (`resolveTaskSource`)
+- **活动来源** (`resolveActivitySource`)
+- **农场来源** (`resolveFarmSource`)
+- **推广来源** (`resolvePromotionSource`)
+- **签到来源** (`resolveSignInSource`)
+- **成就来源** (`resolveAchievementSource`)
+- **等级来源** (`resolveLevelSource`)
+- **宝箱来源** (`resolveChestSource`)
+- **商店来源** (`resolveShopSource`)
+- **每日登录来源** (`resolveDailyLoginSource`)
+- **邀请好友来源** (`resolveInviteSource`)
+- **系统来源** (`resolveSystemSource`)
+
+### 3. 改进后台显示界面
+
+#### 列表页面改进
+- **新增"来源详情"列**:显示解析后的具体来源信息
+- **支持链接跳转**:可点击链接直接跳转到相关管理页面
+- **保持原有列**:保留"来源类型"和"来源ID"列以便技术人员查看
+
+#### 详情页面改进
+- **丰富的来源信息展示**:
+  - 标题:类型 + 具体名称
+  - 描述:详细的业务描述
+  - 查看详情按钮:链接到相关管理页面
+  - 额外信息:显示所有相关的技术信息
+
+## 实现细节
+
+### 文件结构
+```
+app/Module/Game/
+├── Enums/
+│   └── REWARD_SOURCE_TYPE.php          # 扩展的枚举类
+├── Services/
+│   └── RewardSourceResolver.php        # 新增的解析服务
+└── AdminControllers/
+    └── GameRewardLogController.php     # 改进的后台控制器
+```
+
+### 关键代码示例
+
+#### 枚举扩展
+```php
+public static function getTypeInfo(string $type): array
+{
+    $descriptions = [
+        self::FARM_INIT->value => [
+            'name' => '农场初始化',
+            'description' => '农场初始化时获得的奖励',
+            'category' => 'farm',
+            'admin_link' => '/admin/farm'
+        ],
+        // ... 其他类型
+    ];
+    
+    return $descriptions[$type] ?? ['name' => '未知', ...];
+}
+```
+
+#### 解析服务
+```php
+public static function resolve(string $sourceType, int $sourceId): array
+{
+    $typeInfo = REWARD_SOURCE_TYPE::getTypeInfo($sourceType);
+    
+    switch ($sourceType) {
+        case REWARD_SOURCE_TYPE::FARM_INIT->value:
+            return self::resolveFarmSource($sourceType, $sourceId, $typeInfo);
+        // ... 其他类型
+    }
+}
+```
+
+#### 后台显示
+```php
+$grid->column('source_detail', '来源详情')->display(function () {
+    $sourceInfo = RewardSourceResolver::resolve($this->source_type, $this->source_id);
+    $text = $sourceInfo['name'];
+    if ($sourceInfo['link']) {
+        return "<a href='{$sourceInfo['link']}' target='_blank' class='text-primary'>{$text}</a>";
+    }
+    return $text;
+});
+```
+
+## 测试验证
+
+### 测试数据
+- 奖励日志ID: 4457
+- 用户ID: 39002
+- 来源类型: farm_init
+- 来源ID: 11
+
+### 测试结果
+
+#### 列表页面
+- ✅ 显示"来源详情"列
+- ✅ 显示"用户农场初始化 (用户ID: 11)"
+- ✅ 支持点击链接跳转到 `/admin/farm/users/11`
+
+#### 详情页面
+- ✅ 显示完整的来源信息卡片
+- ✅ 标题:农场初始化: 用户农场初始化 (用户ID: 11)
+- ✅ 描述:农场初始化时获得的奖励
+- ✅ 查看详情按钮:链接到农场管理页面
+- ✅ 额外信息:显示操作类型、目标用户ID等技术信息
+
+## 技术优势
+
+### 1. 可扩展性
+- **枚举驱动**:新增来源类型只需在枚举中添加
+- **解析器模式**:每种类型有独立的解析方法
+- **统一接口**:所有解析器返回相同的数据结构
+
+### 2. 维护性
+- **集中管理**:所有来源类型信息集中在枚举中
+- **类型安全**:使用枚举避免魔法字符串
+- **错误处理**:完善的异常处理和日志记录
+
+### 3. 用户体验
+- **直观显示**:管理员可以直接看到奖励的具体来源
+- **快速跳转**:支持一键跳转到相关管理页面
+- **详细信息**:提供完整的上下文信息
+
+## 后续扩展建议
+
+### 1. 增强解析能力
+- 集成实际的业务模型查询
+- 支持更复杂的业务逻辑解析
+- 添加缓存机制提升性能
+
+### 2. 改进用户界面
+- 添加来源类型的图标显示
+- 支持按来源类型筛选
+- 添加来源统计图表
+
+### 3. 数据完整性
+- 添加来源数据验证
+- 支持历史数据修复
+- 提供数据一致性检查工具
+
+## 提交信息
+```
+改进奖励来源追溯功能,使用枚举管理source_type
+
+- 扩展REWARD_SOURCE_TYPE枚举,增加农场、推广、商店等来源类型
+- 为枚举添加详细的类型信息和分类功能
+- 创建RewardSourceResolver服务类,解析不同来源类型的具体信息
+- 改进后台奖励日志显示界面,增加来源详情列
+- 列表页面显示简化的来源信息,支持链接跳转
+- 详情页面显示完整的来源信息,包括描述、链接和额外信息
+- 提升奖励日志的可追溯性和管理便利性
+```
+
+## 总结
+此次改进成功解决了奖励来源追溯性不足的问题。通过扩展枚举、创建解析服务和改进后台界面,管理员现在可以清楚地知道每个奖励的具体来源,大大提升了系统的可管理性和用户体验。整个方案具有良好的可扩展性和维护性,为后续功能扩展奠定了坚实基础。

+ 144 - 0
app/Console/Commands/TestRewardSourceTypeEnum.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Module\Game\Enums\REWARD_SOURCE_TYPE;
+use App\Module\Game\Services\RewardService;
+use Illuminate\Console\Command;
+
+/**
+ * 测试奖励来源类型枚举强制使用
+ */
+class TestRewardSourceTypeEnum extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'test:reward-source-type-enum';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '测试奖励来源类型枚举强制使用功能';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle()
+    {
+        $this->info('开始测试奖励来源类型枚举强制使用功能...');
+
+        // 测试1:使用有效的枚举
+        $this->info("\n=== 测试1:使用有效的枚举 ===");
+        try {
+            $result = RewardService::grantReward(
+                39002, // 用户ID
+                1, // 奖励组ID
+                REWARD_SOURCE_TYPE::TEST, // 使用枚举
+                999 // 来源ID
+            );
+            
+            if ($result->success) {
+                $this->info("✅ 使用枚举发放奖励成功");
+                $this->info("奖励组: {$result->groupName}");
+                $this->info("来源类型: {$result->sourceType}");
+                $this->info("奖励项数量: " . count($result->items));
+            } else {
+                $this->error("❌ 使用枚举发放奖励失败: {$result->errorMessage}");
+            }
+        } catch (\Exception $e) {
+            $this->error("❌ 测试1异常: " . $e->getMessage());
+        }
+
+        // 测试2:测试枚举验证
+        $this->info("\n=== 测试2:测试枚举验证 ===");
+        $validTypes = [
+            REWARD_SOURCE_TYPE::TASK,
+            REWARD_SOURCE_TYPE::ACTIVITY,
+            REWARD_SOURCE_TYPE::FARM_INIT,
+            REWARD_SOURCE_TYPE::USER_REGISTER_TEST,
+            REWARD_SOURCE_TYPE::TEST
+        ];
+
+        foreach ($validTypes as $type) {
+            $isValid = REWARD_SOURCE_TYPE::isValid($type->value);
+            $this->info("枚举 {$type->value}: " . ($isValid ? "✅ 有效" : "❌ 无效"));
+        }
+
+        // 测试3:测试字符串到枚举的转换
+        $this->info("\n=== 测试3:测试字符串到枚举的转换 ===");
+        $testStrings = ['test', 'farm_init', 'invalid_type', 'task'];
+        
+        foreach ($testStrings as $str) {
+            $enum = RewardService::createSourceTypeEnum($str);
+            if ($enum) {
+                $this->info("字符串 '{$str}' -> 枚举: {$enum->value} ✅");
+            } else {
+                $this->info("字符串 '{$str}' -> 无效 ❌");
+            }
+        }
+
+        // 测试4:测试兼容性方法
+        $this->info("\n=== 测试4:测试兼容性方法 ===");
+        try {
+            $result = RewardService::grantRewardLegacy(
+                39002, // 用户ID
+                1, // 奖励组ID
+                'test', // 使用字符串(兼容性方法)
+                998 // 来源ID
+            );
+            
+            if ($result->success) {
+                $this->info("✅ 兼容性方法发放奖励成功");
+            } else {
+                $this->error("❌ 兼容性方法发放奖励失败: {$result->errorMessage}");
+            }
+        } catch (\Exception $e) {
+            $this->error("❌ 测试4异常: " . $e->getMessage());
+        }
+
+        // 测试5:测试无效字符串
+        $this->info("\n=== 测试5:测试无效字符串 ===");
+        try {
+            $result = RewardService::grantRewardLegacy(
+                39002, // 用户ID
+                1, // 奖励组ID
+                'invalid_source_type', // 无效的字符串
+                997 // 来源ID
+            );
+            
+            if (!$result->success) {
+                $this->info("✅ 正确拒绝了无效的来源类型: {$result->errorMessage}");
+            } else {
+                $this->error("❌ 应该拒绝无效的来源类型,但却成功了");
+            }
+        } catch (\Exception $e) {
+            $this->error("❌ 测试5异常: " . $e->getMessage());
+        }
+
+        // 测试6:测试枚举信息获取
+        $this->info("\n=== 测试6:测试枚举信息获取 ===");
+        $testType = REWARD_SOURCE_TYPE::FARM_INIT;
+        $typeInfo = REWARD_SOURCE_TYPE::getTypeInfo($testType->value);
+        
+        $this->info("枚举类型: {$testType->value}");
+        $this->info("名称: {$typeInfo['name']}");
+        $this->info("描述: {$typeInfo['description']}");
+        $this->info("分类: {$typeInfo['category']}");
+        $this->info("管理链接: " . ($typeInfo['admin_link'] ?? '无'));
+
+        // 测试7:测试分类功能
+        $this->info("\n=== 测试7:测试分类功能 ===");
+        $categories = REWARD_SOURCE_TYPE::getCategories();
+        foreach ($categories as $key => $name) {
+            $types = REWARD_SOURCE_TYPE::getByCategory($key);
+            $this->info("分类 '{$name}' ({$key}): " . count($types) . " 个类型");
+        }
+
+        $this->info("\n🎉 所有测试完成!");
+    }
+}

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

@@ -131,7 +131,7 @@ class GameRewardLogController extends AdminController
 
                 $html .= "</div>";
                 return $html;
-            });
+            })->unescape();
 
             $show->field('created_at', '创建时间');
 

+ 116 - 9
app/Module/Game/Services/RewardService.php

@@ -4,6 +4,7 @@ namespace App\Module\Game\Services;
 
 use App\Module\Game\Dtos\RewardGroupDto;
 use App\Module\Game\Dtos\RewardResultDto;
+use App\Module\Game\Enums\REWARD_SOURCE_TYPE;
 use App\Module\Game\Logics\RewardLogic;
 
 /**
@@ -47,14 +48,20 @@ class RewardService
      *
      * @param int $userId 用户ID
      * @param int|string $groupIdOrCode 奖励组ID或编码
-     * @param string $sourceType 来源类型(任务、活动、签到等)
+     * @param REWARD_SOURCE_TYPE $sourceType 来源类型枚举
      * @param int $sourceId 来源ID
+     * @param int $multiplier 倍率
      * @return RewardResultDto 奖励结果
      */
-    public static function grantReward(int $userId, $groupIdOrCode, string $sourceType, int $sourceId ,int $multiplier = 1): RewardResultDto
+    public static function grantReward(int $userId, $groupIdOrCode, REWARD_SOURCE_TYPE $sourceType, int $sourceId, int $multiplier = 1): RewardResultDto
     {
+        // 验证来源类型是否有效
+        if (!REWARD_SOURCE_TYPE::isValid($sourceType->value)) {
+            return RewardResultDto::fail("无效的奖励来源类型: {$sourceType->value}");
+        }
+
         $logic = new RewardLogic();
-        return $logic->grantReward($userId, $groupIdOrCode, $sourceType, $sourceId,$multiplier);
+        return $logic->grantReward($userId, $groupIdOrCode, $sourceType->value, $sourceId, $multiplier);
     }
 
     /**
@@ -62,17 +69,23 @@ class RewardService
      *
      * @param array $userIds 用户ID数组
      * @param int|string $groupIdOrCode 奖励组ID或编码
-     * @param string $sourceType 来源类型
+     * @param REWARD_SOURCE_TYPE $sourceType 来源类型枚举
      * @param int $sourceId 来源ID
      * @return array 奖励结果数组,键为用户ID,值为RewardResultDto
      */
-    public static function batchGrantReward(array $userIds, $groupIdOrCode, string $sourceType, int $sourceId): array
+    public static function batchGrantReward(array $userIds, $groupIdOrCode, REWARD_SOURCE_TYPE $sourceType, int $sourceId): array
     {
+        // 验证来源类型是否有效
+        if (!REWARD_SOURCE_TYPE::isValid($sourceType->value)) {
+            $failResult = RewardResultDto::fail("无效的奖励来源类型: {$sourceType->value}");
+            return array_fill_keys($userIds, $failResult);
+        }
+
         $results = [];
         $logic = new RewardLogic();
 
         foreach ($userIds as $userId) {
-            $results[$userId] = $logic->grantReward($userId, $groupIdOrCode, $sourceType, $sourceId);
+            $results[$userId] = $logic->grantReward($userId, $groupIdOrCode, $sourceType->value, $sourceId);
         }
 
         return $results;
@@ -94,15 +107,20 @@ class RewardService
      *
      * @param int $userId 用户ID
      * @param int|string $groupIdOrCode 奖励组ID或编码
-     * @param string $sourceType 来源类型(任务、活动、签到等)
+     * @param REWARD_SOURCE_TYPE $sourceType 来源类型枚举
      * @param int $sourceId 来源ID
      * @param bool $enablePity 是否启用保底机制
      * @return RewardResultDto 奖励结果
      */
-    public static function grantRewardWithPity(int $userId, $groupIdOrCode, string $sourceType, int $sourceId, bool $enablePity = true): RewardResultDto
+    public static function grantRewardWithPity(int $userId, $groupIdOrCode, REWARD_SOURCE_TYPE $sourceType, int $sourceId, bool $enablePity = true): RewardResultDto
     {
+        // 验证来源类型是否有效
+        if (!REWARD_SOURCE_TYPE::isValid($sourceType->value)) {
+            return RewardResultDto::fail("无效的奖励来源类型: {$sourceType->value}");
+        }
+
         $logic = new RewardLogic();
-        return $logic->grantRewardWithPity($userId, $groupIdOrCode, $sourceType, $sourceId, $enablePity);
+        return $logic->grantRewardWithPity($userId, $groupIdOrCode, $sourceType->value, $sourceId, $enablePity);
     }
 
     /**
@@ -170,4 +188,93 @@ class RewardService
         PityService::resetAllPityCounts($userId, $rewardGroup->id);
         return true;
     }
+
+    // ==================== 兼容性方法(已废弃,建议使用枚举版本) ====================
+
+    /**
+     * 发放奖励(兼容性方法,已废弃)
+     *
+     * @deprecated 请使用 grantReward(int $userId, $groupIdOrCode, REWARD_SOURCE_TYPE $sourceType, int $sourceId, int $multiplier = 1) 方法
+     * @param int $userId 用户ID
+     * @param int|string $groupIdOrCode 奖励组ID或编码
+     * @param string $sourceType 来源类型字符串
+     * @param int $sourceId 来源ID
+     * @param int $multiplier 倍率
+     * @return RewardResultDto 奖励结果
+     */
+    public static function grantRewardLegacy(int $userId, $groupIdOrCode, string $sourceType, int $sourceId, int $multiplier = 1): RewardResultDto
+    {
+        // 验证来源类型是否有效
+        if (!REWARD_SOURCE_TYPE::isValid($sourceType)) {
+            return RewardResultDto::fail("无效的奖励来源类型: {$sourceType}");
+        }
+
+        $logic = new RewardLogic();
+        return $logic->grantReward($userId, $groupIdOrCode, $sourceType, $sourceId, $multiplier);
+    }
+
+    /**
+     * 批量发放奖励(兼容性方法,已废弃)
+     *
+     * @deprecated 请使用 batchGrantReward(array $userIds, $groupIdOrCode, REWARD_SOURCE_TYPE $sourceType, int $sourceId) 方法
+     * @param array $userIds 用户ID数组
+     * @param int|string $groupIdOrCode 奖励组ID或编码
+     * @param string $sourceType 来源类型字符串
+     * @param int $sourceId 来源ID
+     * @return array 奖励结果数组,键为用户ID,值为RewardResultDto
+     */
+    public static function batchGrantRewardLegacy(array $userIds, $groupIdOrCode, string $sourceType, int $sourceId): array
+    {
+        // 验证来源类型是否有效
+        if (!REWARD_SOURCE_TYPE::isValid($sourceType)) {
+            $failResult = RewardResultDto::fail("无效的奖励来源类型: {$sourceType}");
+            return array_fill_keys($userIds, $failResult);
+        }
+
+        $results = [];
+        $logic = new RewardLogic();
+
+        foreach ($userIds as $userId) {
+            $results[$userId] = $logic->grantReward($userId, $groupIdOrCode, $sourceType, $sourceId);
+        }
+
+        return $results;
+    }
+
+    /**
+     * 发放奖励(支持保底机制,兼容性方法,已废弃)
+     *
+     * @deprecated 请使用 grantRewardWithPity(int $userId, $groupIdOrCode, REWARD_SOURCE_TYPE $sourceType, int $sourceId, bool $enablePity = true) 方法
+     * @param int $userId 用户ID
+     * @param int|string $groupIdOrCode 奖励组ID或编码
+     * @param string $sourceType 来源类型字符串
+     * @param int $sourceId 来源ID
+     * @param bool $enablePity 是否启用保底机制
+     * @return RewardResultDto 奖励结果
+     */
+    public static function grantRewardWithPityLegacy(int $userId, $groupIdOrCode, string $sourceType, int $sourceId, bool $enablePity = true): RewardResultDto
+    {
+        // 验证来源类型是否有效
+        if (!REWARD_SOURCE_TYPE::isValid($sourceType)) {
+            return RewardResultDto::fail("无效的奖励来源类型: {$sourceType}");
+        }
+
+        $logic = new RewardLogic();
+        return $logic->grantRewardWithPity($userId, $groupIdOrCode, $sourceType, $sourceId, $enablePity);
+    }
+
+    /**
+     * 根据字符串创建枚举实例的辅助方法
+     *
+     * @param string $sourceType 来源类型字符串
+     * @return REWARD_SOURCE_TYPE|null 枚举实例,无效时返回null
+     */
+    public static function createSourceTypeEnum(string $sourceType): ?REWARD_SOURCE_TYPE
+    {
+        if (!REWARD_SOURCE_TYPE::isValid($sourceType)) {
+            return null;
+        }
+
+        return REWARD_SOURCE_TYPE::from($sourceType);
+    }
 }