Jelajahi Sumber

feat(game-items): 添加宝箱开启消耗配置功能

- 新增宝箱开启消耗配置控制器和相关模型
- 实现批量激活和禁用功能
- 添加复制消耗配置到其他宝箱的功能
-生成宝箱配置JSON数据并保存到文件
- 添加测试命令验证JSON生成和文件输出
-编写宝箱开启消耗配置系统设计文档
Your Name 8 bulan lalu
induk
melakukan
36a73eb634

+ 0 - 2
app/Module/GameItems/ASK.md

@@ -1,2 +0,0 @@
-# 问题
-1. 宝箱消耗

+ 23 - 0
app/Module/GameItems/AdminControllers/Actions/BatchActivate.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Module\GameItems\AdminControllers\Actions;
+
+use App\Module\GameItems\Models\ItemChestOpenCost;
+use Dcat\Admin\Grid\BatchAction;
+use Illuminate\Http\Request;
+
+class BatchActivate extends BatchAction
+{
+    protected $title = '批量激活';
+
+    public function handle(Request $request)
+    {
+        $keys = $this->getKey();
+
+        $count = ItemChestOpenCost::whereIn('id', $keys)->update(['is_active' => 1]);
+
+        return $this->response()
+            ->success("成功激活 {$count} 条记录")
+            ->refresh();
+    }
+}

+ 23 - 0
app/Module/GameItems/AdminControllers/Actions/BatchDeactivate.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Module\GameItems\AdminControllers\Actions;
+
+use App\Module\GameItems\Models\ItemChestOpenCost;
+use Dcat\Admin\Grid\BatchAction;
+use Illuminate\Http\Request;
+
+class BatchDeactivate extends BatchAction
+{
+    protected $title = '批量禁用';
+
+    public function handle(Request $request)
+    {
+        $keys = $this->getKey();
+
+        $count = ItemChestOpenCost::whereIn('id', $keys)->update(['is_active' => 0]);
+
+        return $this->response()
+            ->success("成功禁用 {$count} 条记录")
+            ->refresh();
+    }
+}

+ 63 - 0
app/Module/GameItems/AdminControllers/Actions/CopyToAnotherChest.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Module\GameItems\AdminControllers\Actions;
+
+use App\Module\GameItems\Models\Item;
+use App\Module\GameItems\Models\ItemChestOpenCost;
+use Dcat\Admin\Grid\Tools\AbstractTool;
+use Dcat\Admin\Widgets\Modal;
+use Dcat\Admin\Widgets\Form;
+
+class CopyToAnotherChest extends AbstractTool
+{
+    protected $title = '复制消耗配置到其他宝箱';
+
+    public function render()
+    {
+        $form = new Form();
+
+        // 源宝箱选择
+        $form->select('source_chest_id', '源宝箱')
+            ->options(Item::where('type', 3)->pluck('name', 'id'))
+            ->required();
+
+        // 目标宝箱选择
+        $form->select('target_chest_id', '目标宝箱')
+            ->options(Item::where('type', 3)->pluck('name', 'id'))
+            ->required();
+
+        $form->action(admin_url('game-items/chest-open-costs/copy'));
+
+        return Modal::make()
+            ->lg()
+            ->title($this->title)
+            ->body($form)
+            ->button("<button class='btn btn-primary'>{$this->title}</button>");
+    }
+
+    public function handle($request)
+    {
+        $sourceChestId = $request->get('source_chest_id');
+        $targetChestId = $request->get('target_chest_id');
+
+        if ($sourceChestId == $targetChestId) {
+            return response()->json(['status' => false, 'message' => '源宝箱和目标宝箱不能相同']);
+        }
+
+        $sourceCosts = ItemChestOpenCost::where('chest_id', $sourceChestId)->get();
+
+        if ($sourceCosts->isEmpty()) {
+            return response()->json(['status' => false, 'message' => '源宝箱没有消耗配置']);
+        }
+
+        $count = 0;
+        foreach ($sourceCosts as $cost) {
+            $newCost = $cost->replicate();
+            $newCost->chest_id = $targetChestId;
+            $newCost->save();
+            $count++;
+        }
+
+        return response()->json(['status' => true, 'message' => "成功复制 {$count} 条消耗配置"]);
+    }
+}

+ 151 - 0
app/Module/GameItems/AdminControllers/ItemChestOpenCostController.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace App\Module\GameItems\AdminControllers;
+
+use App\Module\GameItems\Enums\CHEST_COST_TYPE;
+use App\Module\GameItems\Models\Item;
+use App\Module\GameItems\Models\ItemChestOpenCost;
+use Dcat\Admin\Form;
+use Dcat\Admin\Grid;
+use Dcat\Admin\Show;
+use Dcat\Admin\Http\Controllers\AdminController;
+use Dcat\Admin\Layout\Content;
+use Spatie\RouteAttributes\Attributes\Resource;
+
+/**
+ * 宝箱开启消耗配置控制器
+ */
+#[Resource('game-items-chest-costs', names: 'dcat.admin.game-items-chest-costs')]
+class ItemChestOpenCostController extends AdminController
+{
+    protected $title ='宝箱开启消耗配置';
+
+
+
+
+
+    /**
+     * 列表页
+     *
+     * @return Grid
+     */
+    protected function grid()
+    {
+        $grid = new Grid(new ItemChestOpenCost());
+
+        $grid->column('id', 'ID')->sortable();
+        $grid->column('chest.name', '宝箱名称')->link(function () {
+            return admin_url('game-items/items/' . $this->chest_id);
+        });
+        $grid->column('cost_type', '消耗类型')->display(function ($value) {
+            return CHEST_COST_TYPE::getAll()[$value] ?? '未知';
+        });
+        $grid->column('cost_id', '消耗ID')->display(function ($value) {
+            if ($this->cost_type == CHEST_COST_TYPE::ITEM->value) {
+                $item = Item::find($value);
+                return $item ? "{$item->name} (ID: {$value})" : $value;
+            }
+            return $value;
+        });
+        $grid->column('cost_quantity', '消耗数量');
+        $grid->column('is_active', '是否激活')->switch();
+        $grid->column('created_at', '创建时间');
+        $grid->column('updated_at', '更新时间');
+
+        // 筛选器
+        $grid->filter(function (Grid\Filter $filter) {
+            $filter->equal('chest_id', '宝箱ID');
+            $filter->where('chest_name', function ($query) {
+                $query->whereHas('chest', function ($query) {
+                    $query->where('name', 'like', "%{$this->input}%");
+                });
+            }, '宝箱名称');
+            $filter->equal('cost_type', '消耗类型')->select(CHEST_COST_TYPE::getAll());
+            $filter->equal('cost_id', '消耗ID');
+            $filter->equal('is_active', '是否激活')->select([0 => '否', 1 => '是']);
+        });
+
+        // 批量操作
+        $grid->batchActions(function (Grid\Tools\BatchActions $batch) {
+            $batch->add(new \App\Module\GameItems\AdminControllers\Actions\BatchActivate());
+            $batch->add(new \App\Module\GameItems\AdminControllers\Actions\BatchDeactivate());
+        });
+
+        // 工具栏
+        $grid->tools(function (Grid\Tools $tools) {
+            $tools->append(new \App\Module\GameItems\AdminControllers\Actions\CopyToAnotherChest());
+        });
+
+        return $grid;
+    }
+
+    /**
+     * 详情页
+     *
+     * @param mixed $id
+     * @return Show
+     */
+    protected function detail($id)
+    {
+        $show = new Show(ItemChestOpenCost::findOrFail($id));
+
+        $show->field('id', 'ID');
+        $show->field('chest.name', '宝箱名称');
+        $show->field('chest_id', '宝箱ID');
+        $show->field('cost_type', '消耗类型')->as(function ($value) {
+            return CHEST_COST_TYPE::getAll()[$value] ?? '未知';
+        });
+        $show->field('cost_id', '消耗ID');
+        $show->field('cost_quantity', '消耗数量');
+        $show->field('is_active', '是否激活')->as(function ($value) {
+            return $value ? '是' : '否';
+        });
+        $show->field('created_at', '创建时间');
+        $show->field('updated_at', '更新时间');
+
+        return $show;
+    }
+
+    /**
+     * 表单
+     *
+     * @return Form
+     */
+    protected function form()
+    {
+        $form = new Form(new ItemChestOpenCost());
+
+        // 只显示宝箱类型的物品
+        $chests = Item::where('type', 3)->pluck('name', 'id');
+
+        $form->select('chest_id', '宝箱')->options($chests)->required();
+        $form->select('cost_type', '消耗类型')->options(CHEST_COST_TYPE::getAll())->required();
+
+        // 根据消耗类型显示不同的选择器
+        $form->select('cost_id', '消耗ID')->options(function ($id) {
+            $costType = request()->get('cost_type');
+
+            if ($costType == CHEST_COST_TYPE::ITEM->value) {
+                return Item::pluck('name', 'id');
+            }
+
+            return [];
+        })->required();
+
+        $form->number('cost_quantity', '消耗数量')->min(1)->default(1)->required();
+        $form->switch('is_active', '是否激活')->default(1);
+
+        // 保存前回调
+        $form->saving(function (Form $form) {
+            // 验证消耗ID
+            if ($form->cost_type == CHEST_COST_TYPE::ITEM->value) {
+                $item = Item::find($form->cost_id);
+                if (!$item) {
+                    return $form->response()->error('无效的物品ID');
+                }
+            }
+        });
+
+        return $form;
+    }
+}

+ 40 - 4
app/Module/GameItems/Commands/GenerateChestJsonCommand.php

@@ -7,6 +7,7 @@ use App\Module\Game\DCache\ItemJsonConfig;
 use App\Module\GameItems\Enums\ITEM_TYPE;
 use Illuminate\Console\Command;
 use App\Module\GameItems\Models\Item;
+use Illuminate\Support\Facades\File;
 use Illuminate\Support\Facades\Log;
 
 /**
@@ -37,9 +38,12 @@ class GenerateChestJsonCommand extends Command
      * 执行命令
      */
     /**
-     * 生成物品JSON数据
+     * 生成宝箱JSON数据
+     *
+     * @param bool $saveToFile 是否保存到文件
+     * @return array|bool 生成的数据或失败标志
      */
-    public static function generateJson()
+    public static function generateJson(bool $saveToFile = true)
     {
         try {
             // 查询Item表中的 宝箱 数据
@@ -52,14 +56,17 @@ class GenerateChestJsonCommand extends Command
                 ->get()
                 ->toArray();
 
-//            dd($items);
-
             // 准备完整数据,包含生成时间
             $data = [
                 'generated_at' => now()->toDateTimeString(),
                 'chest'        => $items
             ];
 
+            // 如果需要保存到文件
+            if ($saveToFile) {
+                self::saveJsonToFile($data);
+            }
+
             return $data;
         } catch (\Exception $e) {
             Log::error('Generate chest.json failed: ' . $e->getMessage());
@@ -68,10 +75,39 @@ class GenerateChestJsonCommand extends Command
         }
     }
 
+    /**
+     * 将JSON数据保存到文件
+     *
+     * @param array $data 要保存的数据
+     * @return bool 是否保存成功
+     */
+    protected static function saveJsonToFile(array $data): bool
+    {
+        try {
+            // 确保目录存在
+            $directory = 'public/json';
+            if (!File::exists($directory)) {
+                File::makeDirectory($directory, 0755, true);
+            }
+
+            // 将数据保存为JSON文件
+            $jsonContent = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            $filePath = $directory . '/chest.json';
+            File::put($filePath, $jsonContent);
+
+            Log::info('Chest JSON file saved to: ' . $filePath);
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Save chest.json to file failed: ' . $e->getMessage());
+            return false;
+        }
+    }
+
     public function handle()
     {
         if (ChestJsonConfig::getData([], true)) {
             $this->info('Successfully generated chest.json with timestamp');
+            $this->info('JSON file saved to public/json/chest.json');
         } else {
             $this->error('Failed to generate chest.json');
         }

+ 39 - 3
app/Module/GameItems/Commands/GenerateItemsJsonCommand.php

@@ -8,6 +8,7 @@ use Illuminate\Console\Command;
 use App\Module\GameItems\Models\Item;
 use Illuminate\Support\Facades\File;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
 
 /**
  * 生成物品配置表JSON数据命令
@@ -38,8 +39,11 @@ class GenerateItemsJsonCommand extends Command
      */
     /**
      * 生成物品JSON数据
+     *
+     * @param bool $saveToFile 是否保存到文件
+     * @return array|bool 生成的数据或失败标志
      */
-    public static function generateJson()
+    public static function generateJson(bool $saveToFile = true)
     {
         try {
             // 查询Item表中的数据
@@ -63,14 +67,17 @@ class GenerateItemsJsonCommand extends Command
                 })
                 ->toArray();
 
-
-
             // 准备完整数据,包含生成时间
             $data = [
                 'generated_at' => now()->toDateTimeString(),
                 'items'        => $items
             ];
 
+            // 如果需要保存到文件
+            if ($saveToFile) {
+                self::saveJsonToFile($data);
+            }
+
             return $data;
         } catch (\Exception $e) {
             Log::error('Generate items.json failed: ' . $e->getMessage());
@@ -79,10 +86,39 @@ class GenerateItemsJsonCommand extends Command
         }
     }
 
+    /**
+     * 将JSON数据保存到文件
+     *
+     * @param array $data 要保存的数据
+     * @return bool 是否保存成功
+     */
+    protected static function saveJsonToFile(array $data): bool
+    {
+        try {
+            // 确保目录存在
+            $directory = 'public/json';
+            if (!File::exists($directory)) {
+                File::makeDirectory($directory, 0755, true);
+            }
+
+            // 将数据保存为JSON文件
+            $jsonContent = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            $filePath = $directory . '/items.json';
+            File::put($filePath, $jsonContent);
+
+            Log::info('Items JSON file saved to: ' . $filePath);
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Save items.json to file failed: ' . $e->getMessage());
+            return false;
+        }
+    }
+
     public function handle()
     {
         if (ItemJsonConfig::getData([], true)) {
             $this->info('Successfully generated items.json with timestamp');
+            $this->info('JSON file saved to public/json/items.json');
         } else {
             $this->error('Failed to generate items.json');
         }

+ 106 - 0
app/Module/GameItems/Commands/TestJsonGenerationCommand.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace App\Module\GameItems\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\File;
+
+/**
+ * 测试JSON生成和文件输出功能
+ */
+class TestJsonGenerationCommand extends Command
+{
+    /**
+     * 命令名称和签名
+     *
+     * @var string
+     */
+    protected $signature = 'gameitems:test-json-generation';
+
+    /**
+     * 命令描述
+     *
+     * @var string
+     */
+    protected $description = 'Test JSON generation and file output functionality';
+
+    /**
+     * 执行命令
+     */
+    public function handle()
+    {
+        $this->info('开始测试物品配置表JSON生成和文件输出功能...');
+
+        // 测试物品配置表生成
+        $this->info('1. 生成物品配置表...');
+        $itemsData = GenerateItemsJsonCommand::generateJson(true);
+
+        if ($itemsData) {
+            $this->info('   物品配置表生成成功!');
+            $this->info('   物品数量: ' . count($itemsData['items']));
+            $this->info('   生成时间: ' . $itemsData['generated_at']);
+        } else {
+            $this->error('   物品配置表生成失败!');
+            return 1;
+        }
+
+        // 测试宝箱配置表生成
+        $this->info('2. 生成宝箱配置表...');
+        $chestData = GenerateChestJsonCommand::generateJson(true);
+
+        if ($chestData) {
+            $this->info('   宝箱配置表生成成功!');
+            $this->info('   宝箱数量: ' . count($chestData['chest']));
+            $this->info('   生成时间: ' . $chestData['generated_at']);
+        } else {
+            $this->error('   宝箱配置表生成失败!');
+            return 1;
+        }
+
+        // 验证文件是否存在
+        $this->info('3. 验证配置文件是否存在...');
+        $itemsJsonPath = 'public/json/items.json';
+        $chestJsonPath = 'public/json/chest.json';
+
+        if (File::exists($itemsJsonPath)) {
+            $this->info('   物品配置文件存在: ' . $itemsJsonPath);
+            $fileSize = File::size($itemsJsonPath);
+            $this->info('   文件大小: ' . $this->formatBytes($fileSize));
+        } else {
+            $this->error('   物品配置文件不存在: ' . $itemsJsonPath);
+            return 1;
+        }
+
+        if (File::exists($chestJsonPath)) {
+            $this->info('   宝箱配置文件存在: ' . $chestJsonPath);
+            $fileSize = File::size($chestJsonPath);
+            $this->info('   文件大小: ' . $this->formatBytes($fileSize));
+        } else {
+            $this->error('   宝箱配置文件不存在: ' . $chestJsonPath);
+            return 1;
+        }
+
+        $this->info('测试完成,所有功能正常!');
+        return 0;
+    }
+
+    /**
+     * 格式化字节数为可读格式
+     *
+     * @param int $bytes 字节数
+     * @param int $precision 精度
+     * @return string 格式化后的字符串
+     */
+    protected function formatBytes($bytes, $precision = 2)
+    {
+        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
+
+        $bytes = max($bytes, 0);
+        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
+        $pow = min($pow, count($units) - 1);
+
+        $bytes /= pow(1024, $pow);
+
+        return round($bytes, $precision) . ' ' . $units[$pow];
+    }
+}

+ 87 - 0
app/Module/GameItems/DEV.md

@@ -0,0 +1,87 @@
+# 宝箱开启消耗配置系统开发计划
+
+## 1. 创建基本目录结构 ✅
+
+- [x] 确认 AdminControllers 目录
+- [x] 确认 Commands 目录
+- [x] 确认 Enums 目录
+- [x] 确认 Events 目录
+- [x] 确认 Logics 目录
+- [x] 确认 Models 目录
+- [x] 确认 Providers 目录
+- [x] 确认 Repositorys 目录
+- [x] 确认 Services 目录
+- [x] 确认 Dtos 目录
+
+## 2. 实现枚举类 ✅
+
+- [x] CHEST_COST_TYPE - 宝箱开启消耗类型枚举(物品、货币、其他资源)
+
+## 3. 实现模型类 ✅
+
+- [x] ItemChestOpenCost - 宝箱开启消耗配置模型
+
+## 4. 实现DTO类 ✅
+
+- [x] ItemChestOpenCostDto - 宝箱开启消耗配置DTO
+
+## 5. 实现数据仓库 ✅
+
+- [x] ItemChestOpenCostRepository - 宝箱开启消耗配置仓库
+
+## 6. 实现逻辑层和服务层 ⏳
+
+### 6.1 逻辑层(内部)
+
+- [x] ChestOpenCostLogic - 宝箱开启消耗逻辑
+
+### 6.2 服务层(对外,静态方法)
+
+- [x] ChestService - 更新宝箱服务,支持消耗配置
+
+## 7. 更新现有宝箱开启流程 ✅
+
+- [x] 修改宝箱开启逻辑,支持额外消耗验证
+- [x] 实现消耗资源验证和扣除功能
+- [x] 更新宝箱开启日志,记录消耗信息
+
+## 8. 实现后台控制器 ✅
+
+- [x] ItemChestOpenCostController - 宝箱开启消耗配置管理控制器
+- [x] 创建批量操作类 - 用于批量激活/禁用消耗配置
+
+## 9. 更新服务提供者 ✅
+
+- [x] 更新GameItemsServiceProvider - 注册新的服务和控制器
+
+## 10. 实现单元测试 ⏳
+
+- [ ] ChestOpenCostLogic测试
+- [ ] ChestService测试(宝箱开启消耗部分)
+
+## 开发进度
+
+- ✅ 已完成
+- 🔄 进行中
+- ⏳ 待开始
+
+### 当前进度
+
+- 已完成宝箱开启消耗配置系统设计文档
+- 已执行数据库表创建SQL
+- 已完成基本目录结构确认
+- 已完成模型类、枚举类、DTO类和数据仓库实现
+- 已完成宝箱开启逻辑更新,支持额外消耗验证
+- 已完成后台管理界面实现
+- 已完成服务提供者更新
+- 待实现单元测试
+
+### 下一步计划
+
+1. 编写单元测试
+   - 测试宝箱开启消耗验证功能
+   - 测试宝箱开启消耗扣除功能
+
+2. 集成测试
+   - 测试宝箱开启流程
+   - 测试后台管理功能

+ 175 - 0
app/Module/GameItems/Docs/宝箱开启消耗配置系统.md

@@ -0,0 +1,175 @@
+# 宝箱开启消耗配置系统设计文档
+
+## 1. 概述
+
+为了增强游戏的经济系统和提供更多的游戏策略选择,我们计划为宝箱开启功能添加额外消耗配置系统。该系统将允许配置宝箱开启时除了消耗宝箱本身外,还可能需要消耗其他物品或资源。
+
+## 2. 设计目标
+
+- 提供灵活的宝箱开启消耗配置机制
+- 支持多种消耗类型(物品、货币等)
+- 便于运营人员进行配置和调整
+- 与现有宝箱系统无缝集成
+- 支持不同宝箱设置不同的消耗要求
+
+## 3. 数据结构设计
+
+### 3.1 宝箱开启消耗配置表
+
+**表名**:`item_chest_open_costs`
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| id | int | 记录ID,主键 |
+| chest_id | int | 宝箱ID,外键关联item_items表 |
+| cost_type | tinyint | 消耗类型(1:物品, 2:货币, 3:其他资源) |
+| cost_id | int | 消耗的物品/货币/资源ID |
+| cost_quantity | int | 消耗数量 |
+| is_active | tinyint | 是否激活(0:否, 1:是) |
+| created_at | timestamp | 创建时间 |
+| updated_at | timestamp | 更新时间 |
+
+### 3.2 字段说明
+
+- **chest_id**:关联到物品表中的宝箱物品ID
+- **cost_type**:定义消耗的类型,支持多种消耗类型
+  - 1: 物品 - 消耗背包中的物品
+  - 2: 货币 - 消耗游戏内货币(金币、钻石等)
+  - 3: 其他资源 - 消耗其他游戏资源(体力、精力等)
+- **cost_id**:根据cost_type的不同,关联到不同的资源ID
+- **cost_quantity**:每次开启宝箱需要消耗的数量
+- **is_active**:控制该消耗配置是否生效,便于临时调整
+
+## 4. 业务流程
+
+### 4.1 宝箱开启流程
+
+1. 用户请求开启宝箱
+2. 系统查询宝箱开启消耗配置
+3. 验证用户是否拥有足够的消耗资源
+4. 验证用户是否拥有足够数量的宝箱
+5. 消耗配置的资源
+6. 消耗宝箱
+7. 获取宝箱内容配置和用户保底计数
+8. 计算实际掉落物品
+9. 添加获得的物品到用户背包
+10. 更新保底计数
+11. 记录宝箱开启日志
+12. 返回开启结果
+
+### 4.2 消耗验证流程
+
+1. 根据宝箱ID查询所有激活的消耗配置
+2. 对每种消耗类型进行验证:
+   - 物品类型:检查用户背包中是否有足够数量的物品
+   - 货币类型:检查用户账户中是否有足够数量的货币
+   - 其他资源:检查用户是否有足够数量的相应资源
+3. 如果任何一种资源不足,返回相应的错误信息
+4. 如果所有资源都足够,进行消耗操作
+
+## 5. 后台管理
+
+### 5.1 宝箱开启消耗配置管理
+
+在后台管理系统中添加宝箱开启消耗配置管理页面,提供以下功能:
+
+- 查看所有宝箱的消耗配置
+- 按宝箱ID筛选消耗配置
+- 添加新的消耗配置
+- 编辑现有消耗配置
+- 启用/禁用消耗配置
+- 批量导入/导出消耗配置
+
+### 5.2 宝箱详情页增强
+
+在宝箱详情页中增加消耗配置相关信息的展示,包括:
+
+- 当前宝箱的所有消耗配置
+- 消耗资源的名称和图标
+- 消耗数量
+- 配置状态(激活/未激活)
+
+## 6. 客户端交互
+
+### 6.1 宝箱开启界面
+
+在客户端宝箱开启界面中,需要显示以下信息:
+
+- 宝箱名称和图标
+- 开启所需的消耗资源列表
+- 用户当前拥有的资源数量
+- 资源是否足够的状态提示
+- 开启按钮(资源不足时禁用或显示提示)
+
+### 6.2 开启结果展示
+
+开启宝箱后,客户端需要展示以下信息:
+
+- 消耗的资源列表和数量
+- 获得的物品列表和数量
+- 是否触发保底机制
+- 特殊效果动画(如稀有物品获取时)
+
+## 7. 数据统计与分析
+
+### 7.1 消耗统计
+
+系统需要记录和统计以下数据:
+
+- 各类宝箱的开启次数
+- 各类资源的消耗总量
+- 用户开启宝箱的频率和时间分布
+- 资源消耗与获得物品的价值比
+
+### 7.2 分析报表
+
+基于统计数据,系统应提供以下分析报表:
+
+- 宝箱开启消耗资源排行
+- 宝箱开启频率趋势图
+- 资源消耗与物品产出价值对比
+- 用户宝箱开启行为分析
+
+## 8. 扩展性考虑
+
+### 8.1 多重消耗组合
+
+未来可能需要支持多重消耗组合,例如:
+- 允许用户选择使用物品A或物品B来开启宝箱
+- 设置基础消耗和可选消耗,提供不同的开箱概率
+
+### 8.2 时间限制
+
+可以为消耗配置添加时间限制,例如:
+- 在特定活动期间使用特殊消耗
+- 在不同时间段使用不同的消耗配置
+
+### 8.3 用户等级关联
+
+可以将消耗配置与用户等级关联,例如:
+- 高等级用户享有更低的消耗
+- 不同等级的用户有不同的消耗选项
+
+## 9. 实施计划
+
+1. 数据库设计与创建
+2. 后台管理界面开发
+3. 服务层逻辑实现
+4. API接口开发
+5. 客户端界面适配
+6. 测试与调优
+7. 数据统计与分析功能开发
+8. 上线与监控
+
+## 10. 风险与应对措施
+
+| 风险 | 可能性 | 影响 | 应对措施 |
+|------|--------|------|----------|
+| 消耗配置不合理导致用户反感 | 中 | 高 | 进行充分的游戏平衡测试,收集用户反馈 |
+| 系统复杂度增加导致性能问题 | 低 | 中 | 优化查询,使用缓存,进行性能测试 |
+| 与现有宝箱系统集成困难 | 中 | 高 | 采用渐进式开发,确保向后兼容 |
+| 数据迁移和兼容性问题 | 中 | 中 | 制定详细的数据迁移计划,保留回滚机制 |
+
+## 11. 总结
+
+宝箱开启消耗配置系统将为游戏提供更丰富的经济系统和玩法策略,通过合理设计和实施,可以增强游戏的深度和用户粘性。该系统设计具有良好的灵活性和扩展性,能够满足未来游戏运营的各种需求。

+ 202 - 0
app/Module/GameItems/Docs/物品配置表.md

@@ -0,0 +1,202 @@
+# 物品配置表
+
+> 本文档详细说明物品配置表的设计、生成和使用方法
+
+## 1. 概述
+
+物品配置表是游戏中所有物品基础信息的集中配置,主要用于:
+
+- 为客户端提供物品基础数据(名称、描述、售价等)
+- 为服务端提供物品属性参考
+- 支持游戏内物品系统的正常运行
+
+物品配置表通过JSON格式文件提供给客户端,由服务端根据数据库中的物品数据动态生成。
+
+## 2. 数据来源
+
+### 2.1 核心数据表
+
+物品配置表的数据主要来源于以下数据库表:
+
+| 表名 | 说明 | 主要字段 |
+|------|------|---------|
+| item_items | 物品基础信息表 | id, name, description, type, sell_price, display_attributes |
+| item_categories | 物品分类表 | id, name, code, parent_id |
+| item_groups | 物品组表 | id, name, code |
+| item_group_items | 物品组内容表 | group_id, item_id, weight |
+
+### 2.2 item_items 表结构
+
+物品基础信息表(item_items)是物品配置的核心表,包含以下主要字段:
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| id | int | 物品ID,主键 |
+| name | varchar | 物品名称 |
+| description | text | 物品描述 |
+| category_id | int | 物品分类ID,外键关联item_categories表 |
+| type | tinyint | 物品类型(1:可使用, 2:可装备, 3:可合成, 4:可交任务, 5:可开启...) |
+| is_unique | tinyint | 是否是单独属性物品(0:否,默认, 1:是) |
+| max_stack | int | 最大堆叠数量 |
+| sell_price | int | 出售价格 |
+| tradable | tinyint | 是否可交易(0:不可交易, 1:可交易,默认) |
+| dismantlable | tinyint | 是否可分解(0:不可分解, 1:可分解,默认) |
+| default_expire_seconds | int | 玩家获取物品后的默认有效秒数(0表示永久有效) |
+| display_attributes | json | 展示属性,以JSON格式存储键值对,用于界面展示和描述的属性 |
+| numeric_attributes | json | 数值属性,以JSON格式存储键值对,用于计算和游戏逻辑的属性 |
+| global_expire_at | timestamp | 物品全局过期时间(可为空) |
+
+## 3. JSON配置生成机制
+
+### 3.1 生成流程
+
+物品配置表JSON的生成由`GenerateItemsJsonCommand`命令类负责,主要流程如下:
+
+1. 从数据库查询物品基础信息
+2. 提取需要的字段并格式化
+3. 生成包含时间戳的JSON数据
+4. 通过缓存系统存储生成的数据
+5. 将JSON数据保存到`public/json/items.json`文件
+
+### 3.2 生成命令
+
+可以通过以下命令手动触发物品配置表的生成:
+
+```bash
+php artisan gameitems:generate-json
+```
+
+也可以通过后台管理界面的"同步物品JSON"功能触发生成。
+
+### 3.3 JSON数据结构
+
+生成的物品配置表JSON结构如下:
+
+```json
+{
+  "generated_at": "2023-05-01 12:00:00",
+  "items": [
+    {
+      "id": 1,
+      "name": "木剑",
+      "description": "一把普通的木剑",
+      "sell_price": 10,
+      "display_attributes": {
+        "攻击力": "5",
+        "耐久度": "100"
+      }
+    },
+    // 更多物品...
+  ]
+}
+```
+
+### 3.4 缓存机制
+
+物品配置表使用`ItemJsonConfig`类进行缓存管理,主要特点:
+
+- 缓存时间:3600秒(1小时)
+- 防重复生成时间:600秒(10分钟)
+- 支持通过事件触发更新
+
+## 4. 配置表使用方法
+
+### 4.1 客户端获取
+
+客户端可以通过以下两种方式获取物品配置表数据:
+
+1. **API接口获取**:通过API接口动态获取最新的物品配置数据
+2. **静态文件获取**:直接访问`/json/items.json`文件获取物品配置数据
+
+建议在游戏启动时获取并缓存物品配置数据。
+
+### 4.2 服务端使用
+
+服务端可以通过以下方式获取物品配置数据:
+
+```php
+use App\Module\Game\DCache\ItemJsonConfig;
+
+// 获取物品配置数据
+$itemConfig = ItemJsonConfig::getData();
+
+// 强制刷新配置数据
+$itemConfig = ItemJsonConfig::getData([], true);
+```
+
+### 4.3 与其他配置表的关系
+
+物品配置表与其他配置表(如宝箱配置表)相互关联:
+
+- 宝箱配置表引用物品配置表中的物品ID
+- 物品组配置引用物品配置表中的物品
+
+## 5. 配置表维护
+
+### 5.1 更新触发
+
+以下操作会触发物品配置表的更新:
+
+1. 添加新物品
+2. 修改物品基础信息
+3. 删除物品
+4. 手动触发更新
+
+### 5.2 注意事项
+
+1. 物品ID一旦分配不应更改,客户端依赖物品ID进行识别
+2. 物品属性修改后需要及时更新配置表
+3. 大量物品数据变更时,应考虑性能影响
+
+### 5.3 配置表监控
+
+可以通过以下方式监控配置表状态:
+
+1. 检查生成时间戳是否最新
+2. 验证物品数量是否符合预期
+3. 定期检查配置表完整性
+
+## 6. JSON文件输出与使用
+
+### 6.1 JSON文件输出机制
+
+物品配置表生成时,除了将数据存入缓存外,还会将JSON数据保存到文件系统中:
+
+1. **物品配置表**:`public/json/items.json`
+2. **宝箱配置表**:`public/json/chest.json`
+
+这些文件在每次生成配置表时自动更新,确保客户端可以直接获取最新的配置数据。
+
+### 6.2 JSON文件的优势
+
+直接输出到文件系统有以下优势:
+
+1. **减轻服务器负担**:客户端可以直接访问静态文件,无需每次都调用API
+2. **加快访问速度**:静态文件可以被 CDN 缓存,提供更快的访问速度
+3. **降低依赖性**:即使缓存系统或数据库出现问题,客户端仍然可以获取配置数据
+4. **方便调试**:开发人员可以直接查看文件内容,方便调试
+
+### 6.3 文件访问方式
+
+客户端可以通过以下 URL 访问配置文件:
+
+```
+http://[domain]/json/items.json  // 物品配置表
+http://[domain]/json/chest.json  // 宝箱配置表
+```
+
+### 6.4 文件更新机制
+
+配置文件在以下情况下会自动更新:
+
+1. 执行生成命令时:`php artisan gameitems:generate-json`
+2. 通过后台管理界面触发生成时
+3. 缓存过期自动更新时
+
+## 7. 最佳实践
+
+1. 物品属性设计应保持一致性,避免不同物品使用不同的属性命名
+2. 使用物品分类系统组织物品,便于管理和查询
+3. 定期清理过期物品,避免配置表过大
+4. 重要更新前备份配置数据
+5. 使用物品组功能管理相关物品集合

+ 113 - 0
app/Module/GameItems/Dtos/ItemChestOpenCostDto.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace App\Module\GameItems\Dtos;
+
+use App\Module\GameItems\Enums\CHEST_COST_TYPE;
+use App\Module\GameItems\Models\ItemChestOpenCost;
+use UCore\Dto\BaseDto;
+
+/**
+ * 宝箱开启消耗配置DTO
+ */
+class ItemChestOpenCostDto extends BaseDto
+{
+    /**
+     * 记录ID
+     *
+     * @var int|null
+     */
+    public ?int $id = null;
+
+    /**
+     * 宝箱ID
+     *
+     * @var int
+     */
+    public int $chestId;
+
+    /**
+     * 消耗类型
+     *
+     * @var int
+     */
+    public int $costType;
+
+    /**
+     * 消耗的物品/货币/资源ID
+     *
+     * @var int
+     */
+    public int $costId;
+
+    /**
+     * 消耗数量
+     *
+     * @var int
+     */
+    public int $costQuantity;
+
+    /**
+     * 是否激活
+     *
+     * @var bool
+     */
+    public bool $isActive;
+
+    /**
+     * 消耗类型文本描述
+     *
+     * @var string|null
+     */
+    public ?string $costTypeText = null;
+
+    /**
+     * 消耗物品名称(如果消耗类型为物品)
+     *
+     * @var string|null
+     */
+    public ?string $costItemName = null;
+
+    /**
+     * 从模型创建DTO
+     *
+     * @param ItemChestOpenCost $model
+     * @param bool $withRelations 是否加载关联数据
+     * @return self
+     */
+    public static function fromModel($model, bool $withRelations = false): self
+    {
+        $dto = new self();
+        $dto->id = $model->id;
+        $dto->chestId = $model->chest_id;
+        $dto->costType = $model->cost_type;
+        $dto->costId = $model->cost_id;
+        $dto->costQuantity = $model->cost_quantity;
+        $dto->isActive = (bool)$model->is_active;
+        $dto->costTypeText = $model->cost_type_text;
+
+        if ($withRelations && $model->cost_type == CHEST_COST_TYPE::ITEM->value && $model->relationLoaded('costItem')) {
+            $dto->costItemName = $model->costItem->name ?? null;
+        }
+
+        return $dto;
+    }
+
+    /**
+     * 转换为数组
+     *
+     * @return array
+     */
+    public function toArray(): array
+    {
+        return [
+            'id' => $this->id,
+            'chest_id' => $this->chestId,
+            'cost_type' => $this->costType,
+            'cost_id' => $this->costId,
+            'cost_quantity' => $this->costQuantity,
+            'is_active' => $this->isActive,
+            'cost_type_text' => $this->costTypeText,
+            'cost_item_name' => $this->costItemName,
+        ];
+    }
+}

+ 43 - 0
app/Module/GameItems/Enums/CHEST_COST_TYPE.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Module\GameItems\Enums;
+
+use UCore\Enum\EnumCore;
+use UCore\Enum\EnumToInt;
+
+/**
+ * 宝箱开启消耗类型枚举
+ */
+enum CHEST_COST_TYPE: int
+{
+    use EnumCore, EnumToInt;
+
+    /**
+     * 物品消耗
+     */
+    case ITEM = 1;
+
+    /**
+     * 货币消耗
+     */
+    case CURRENCY = 2;
+
+    /**
+     * 其他资源消耗(如体力、精力等)
+     */
+    case OTHER_RESOURCE = 3;
+
+    /**
+     * 获取所有枚举值及其描述
+     *
+     * @return array
+     */
+    public static function getAll(): array
+    {
+        return [
+            self::ITEM->value => '物品',
+            self::CURRENCY->value => '货币',
+            self::OTHER_RESOURCE->value => '其他资源',
+        ];
+    }
+}

+ 275 - 0
app/Module/GameItems/Logics/ChestOpenCostLogic.php

@@ -0,0 +1,275 @@
+<?php
+
+namespace App\Module\GameItems\Logics;
+
+use App\Module\GameItems\Dtos\ItemChestOpenCostDto;
+use App\Module\GameItems\Enums\CHEST_COST_TYPE;
+use App\Module\GameItems\Models\ItemChestOpenCost;
+use App\Module\GameItems\Repositorys\ItemChestOpenCostRepository;
+use App\Module\User\Services\UserCurrencyService;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 宝箱开启消耗逻辑类
+ */
+class ChestOpenCostLogic
+{
+    /**
+     * @var ItemChestOpenCostRepository
+     */
+    protected $costRepository;
+
+    /**
+     * @var ItemLogic
+     */
+    protected $itemLogic;
+
+    /**
+     * 构造函数
+     *
+     * @param ItemChestOpenCostRepository $costRepository
+     * @param ItemLogic $itemLogic
+     */
+    public function __construct(
+        ItemChestOpenCostRepository $costRepository,
+        ItemLogic $itemLogic
+    ) {
+        $this->costRepository = $costRepository;
+        $this->itemLogic = $itemLogic;
+    }
+
+    /**
+     * 获取宝箱的所有激活消耗配置
+     *
+     * @param int $chestId 宝箱ID
+     * @return ItemChestOpenCostDto[]
+     */
+    public function getActiveChestCosts(int $chestId): array
+    {
+        $costs = $this->costRepository->getActiveByChestId($chestId);
+        $result = [];
+
+        foreach ($costs as $cost) {
+            $result[] = ItemChestOpenCostDto::fromModel($cost, true);
+        }
+
+        return $result;
+    }
+
+    /**
+     * 验证用户是否有足够的资源支付宝箱开启消耗
+     *
+     * @param int $userId 用户ID
+     * @param int $chestId 宝箱ID
+     * @param int $quantity 开启数量
+     * @return array [bool $isValid, string $message, array $costs]
+     */
+    public function validateChestOpenCosts(int $userId, int $chestId, int $quantity = 1): array
+    {
+        $costs = $this->costRepository->getActiveByChestId($chestId);
+
+        if ($costs->isEmpty()) {
+            return [true, '无需额外消耗', []];
+        }
+
+        $costDetails = [];
+
+        foreach ($costs as $cost) {
+            $totalQuantity = $cost->cost_quantity * $quantity;
+            $costDetails[] = [
+                'type' => $cost->cost_type,
+                'id' => $cost->cost_id,
+                'quantity' => $totalQuantity,
+                'model' => $cost
+            ];
+
+            // 验证用户是否有足够的资源
+            switch ($cost->cost_type) {
+                case CHEST_COST_TYPE::ITEM->value:
+                    // 验证物品数量
+                    $userItem = $this->itemLogic->getUserItem($userId, $cost->cost_id);
+                    if (!$userItem || $userItem->quantity < $totalQuantity) {
+                        return [
+                            false,
+                            "物品不足,需要 {$totalQuantity} 个 " . ($cost->costItem->name ?? "ID:{$cost->cost_id}"),
+                            $costDetails
+                        ];
+                    }
+                    break;
+
+                case CHEST_COST_TYPE::CURRENCY->value:
+                    // 验证货币数量
+                    $userCurrency = UserCurrencyService::getUserCurrency($userId, $cost->cost_id);
+                    if ($userCurrency < $totalQuantity) {
+                        return [
+                            false,
+                            "货币不足,需要 {$totalQuantity} 个 ID:{$cost->cost_id}",
+                            $costDetails
+                        ];
+                    }
+                    break;
+
+                case CHEST_COST_TYPE::OTHER_RESOURCE->value:
+                    // 验证其他资源,根据实际情况实现
+                    // 这里仅作为示例,实际实现可能需要调用其他模块的服务
+                    $hasEnoughResource = true; // 假设有足够资源
+                    if (!$hasEnoughResource) {
+                        return [
+                            false,
+                            "资源不足,需要 {$totalQuantity} 个 ID:{$cost->cost_id}",
+                            $costDetails
+                        ];
+                    }
+                    break;
+            }
+        }
+
+        return [true, '验证通过', $costDetails];
+    }
+
+    /**
+     * 扣除宝箱开启所需的消耗资源
+     *
+     * @param int $userId 用户ID
+     * @param array $costDetails 消耗详情数组
+     * @param string $reason 消耗原因
+     * @return bool 是否成功扣除
+     */
+    public function deductChestOpenCosts(int $userId, array $costDetails, string $reason = '开启宝箱'): bool
+    {
+        try {
+            DB::beginTransaction();
+
+            foreach ($costDetails as $detail) {
+                switch ($detail['type']) {
+                    case CHEST_COST_TYPE::ITEM->value:
+                        // 扣除物品
+                        $result = $this->itemLogic->consumeUserItem(
+                            $userId,
+                            $detail['id'],
+                            $detail['quantity'],
+                            $reason
+                        );
+
+                        if (!$result) {
+                            throw new \Exception("扣除物品失败: ID {$detail['id']}");
+                        }
+                        break;
+
+                    case CHEST_COST_TYPE::CURRENCY->value:
+                        // 扣除货币
+                        $result = UserCurrencyService::deductCurrency(
+                            $userId,
+                            $detail['id'],
+                            $detail['quantity'],
+                            $reason
+                        );
+
+                        if (!$result) {
+                            throw new \Exception("扣除货币失败: ID {$detail['id']}");
+                        }
+                        break;
+
+                    case CHEST_COST_TYPE::OTHER_RESOURCE->value:
+                        // 扣除其他资源,根据实际情况实现
+                        // 这里仅作为示例,实际实现可能需要调用其他模块的服务
+                        $deductSuccess = true; // 假设扣除成功
+
+                        if (!$deductSuccess) {
+                            throw new \Exception("扣除资源失败: ID {$detail['id']}");
+                        }
+                        break;
+                }
+            }
+
+            DB::commit();
+            return true;
+        } catch (\Exception $e) {
+            DB::rollBack();
+            Log::error('扣除宝箱开启消耗失败: ' . $e->getMessage(), [
+                'user_id' => $userId,
+                'cost_details' => $costDetails,
+                'exception' => $e
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 创建宝箱开启消耗配置
+     *
+     * @param ItemChestOpenCostDto $dto
+     * @return ItemChestOpenCost|null
+     */
+    public function createChestOpenCost(ItemChestOpenCostDto $dto): ?ItemChestOpenCost
+    {
+        try {
+            return $this->costRepository->create([
+                'chest_id' => $dto->chestId,
+                'cost_type' => $dto->costType,
+                'cost_id' => $dto->costId,
+                'cost_quantity' => $dto->costQuantity,
+                'is_active' => $dto->isActive,
+            ]);
+        } catch (\Exception $e) {
+            Log::error('创建宝箱开启消耗配置失败: ' . $e->getMessage(), [
+                'dto' => $dto->toArray(),
+                'exception' => $e
+            ]);
+            return null;
+        }
+    }
+
+    /**
+     * 更新宝箱开启消耗配置
+     *
+     * @param int $id
+     * @param ItemChestOpenCostDto $dto
+     * @return bool
+     */
+    public function updateChestOpenCost(int $id, ItemChestOpenCostDto $dto): bool
+    {
+        try {
+            $cost = $this->costRepository->find($id);
+
+            if (!$cost) {
+                return false;
+            }
+
+            return $this->costRepository->update($id, [
+                'chest_id' => $dto->chestId,
+                'cost_type' => $dto->costType,
+                'cost_id' => $dto->costId,
+                'cost_quantity' => $dto->costQuantity,
+                'is_active' => $dto->isActive,
+            ]);
+        } catch (\Exception $e) {
+            Log::error('更新宝箱开启消耗配置失败: ' . $e->getMessage(), [
+                'id' => $id,
+                'dto' => $dto->toArray(),
+                'exception' => $e
+            ]);
+            return false;
+        }
+    }
+
+    /**
+     * 删除宝箱开启消耗配置
+     *
+     * @param int $id
+     * @return bool
+     */
+    public function deleteChestOpenCost(int $id): bool
+    {
+        try {
+            return $this->costRepository->delete($id);
+        } catch (\Exception $e) {
+            Log::error('删除宝箱开启消耗配置失败: ' . $e->getMessage(), [
+                'id' => $id,
+                'exception' => $e
+            ]);
+            return false;
+        }
+    }
+}

+ 88 - 0
app/Module/GameItems/Models/ItemChestOpenCost.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace App\Module\GameItems\Models;
+
+use App\Module\GameItems\Enums\CHEST_COST_TYPE;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use UCore\ModelCore;
+
+/**
+ * 宝箱开启消耗配置
+ *
+ * field start
+ * @property   int  $id  记录ID,主键
+ * @property   int  $chest_id  宝箱ID,外键关联item_items表
+ * @property   int  $cost_type  消耗类型(1:物品, 2:货币, 3:其他资源)
+ * @property   int  $cost_id  消耗的物品/货币/资源ID
+ * @property   int  $cost_quantity  消耗数量
+ * @property   int  $is_active  是否激活(0:否, 1:是)
+ * @property   \Carbon\Carbon  $created_at  创建时间
+ * @property   \Carbon\Carbon  $updated_at  更新时间
+ * field end
+ */
+class ItemChestOpenCost extends ModelCore
+{
+    /**
+     * 与模型关联的表名
+     *
+     * @var string
+     */
+    protected $table = 'item_chest_open_costs';
+
+    // attrlist start
+    protected $fillable = [
+        'id',
+        'chest_id',
+        'cost_type',
+        'cost_id',
+        'cost_quantity',
+        'is_active',
+    ];
+    // attrlist end
+
+    /**
+     * 应该被转换为原生类型的属性
+     *
+     * @var array
+     */
+    protected $casts = [
+        'cost_type' => 'integer',
+        'cost_id' => 'integer',
+        'cost_quantity' => 'integer',
+        'is_active' => 'boolean',
+    ];
+
+    /**
+     * 获取关联的宝箱物品
+     *
+     * @return BelongsTo
+     */
+    public function chest(): BelongsTo
+    {
+        return $this->belongsTo(Item::class, 'chest_id');
+    }
+
+    /**
+     * 获取关联的消耗物品(如果消耗类型为物品)
+     *
+     * @return BelongsTo|null
+     */
+    public function costItem(): ?BelongsTo
+    {
+        if ($this->cost_type === CHEST_COST_TYPE::ITEM->value) {
+            return $this->belongsTo(Item::class, 'cost_id');
+        }
+
+        return null;
+    }
+
+    /**
+     * 获取消耗类型的文本描述
+     *
+     * @return string
+     */
+    public function getCostTypeTextAttribute(): string
+    {
+        return CHEST_COST_TYPE::getAll()[$this->cost_type] ?? '未知';
+    }
+}

+ 27 - 0
app/Module/GameItems/Providers/GameItemsServiceProvider.php

@@ -45,6 +45,9 @@ class GameItemsServiceProvider extends ServiceProvider
             ]);
         }
 
+        // 注册后台路由
+        $this->registerAdminRoutes();
+
         // 注册事件监听器
         $this->registerEventListeners();
     }
@@ -73,4 +76,28 @@ class GameItemsServiceProvider extends ServiceProvider
         //     ]);
         // });
     }
+
+    /**
+     * 注册后台路由
+     *
+     * @return void
+     */
+    protected function registerAdminRoutes()
+    {
+        // 注册宝箱开启消耗配置控制器路由
+        \Dcat\Admin\Admin::routes();
+
+        $attributes = [
+            'prefix' => config('admin.route.prefix'),
+            'middleware' => config('admin.route.middleware'),
+        ];
+
+        app('router')->group($attributes, function ($router) {
+            // 宝箱开启消耗配置路由
+            $router->resource('game-items/chest-open-costs', \App\Module\GameItems\AdminControllers\ItemChestOpenCostController::class);
+
+            // 复制消耗配置到其他宝箱的路由
+            $router->post('game-items/chest-open-costs/copy', [\App\Module\GameItems\AdminControllers\ItemChestOpenCostController::class, 'copy']);
+        });
+    }
 }

+ 67 - 0
app/Module/GameItems/Repositorys/ItemChestOpenCostRepository.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace App\Module\GameItems\Repositorys;
+
+use App\Module\GameItems\Models\ItemChestOpenCost;
+use UCore\Repository\EloquentRepository;
+
+/**
+ * 宝箱开启消耗配置数据仓库
+ */
+class ItemChestOpenCostRepository extends EloquentRepository
+{
+    /**
+     * 关联的Eloquent模型类
+     *
+     * @var string
+     */
+    protected $eloquentClass = ItemChestOpenCost::class;
+
+    /**
+     * 获取指定宝箱的所有激活消耗配置
+     *
+     * @param int $chestId 宝箱ID
+     * @return \Illuminate\Database\Eloquent\Collection
+     */
+    public function getActiveByChestId(int $chestId)
+    {
+        return $this->eloquentClass::where('chest_id', $chestId)
+            ->where('is_active', true)
+            ->get();
+    }
+
+    /**
+     * 批量更新消耗配置的激活状态
+     *
+     * @param array $ids 消耗配置ID数组
+     * @param bool $isActive 是否激活
+     * @return int 更新的记录数
+     */
+    public function batchUpdateActiveStatus(array $ids, bool $isActive): int
+    {
+        return $this->eloquentClass::whereIn('id', $ids)
+            ->update(['is_active' => $isActive]);
+    }
+
+    /**
+     * 复制消耗配置到另一个宝箱
+     *
+     * @param int $sourceChestId 源宝箱ID
+     * @param int $targetChestId 目标宝箱ID
+     * @return int 复制的记录数
+     */
+    public function copyToAnotherChest(int $sourceChestId, int $targetChestId): int
+    {
+        $sourceCosts = $this->eloquentClass::where('chest_id', $sourceChestId)->get();
+        $count = 0;
+
+        foreach ($sourceCosts as $cost) {
+            $newCost = $cost->replicate();
+            $newCost->chest_id = $targetChestId;
+            $newCost->save();
+            $count++;
+        }
+
+        return $count;
+    }
+}

+ 45 - 0
app/Module/GameItems/Services/ChestService.php

@@ -4,6 +4,7 @@ namespace App\Module\GameItems\Services;
 
 use App\Module\GameItems\Enums\ITEM_TYPE;
 use App\Module\GameItems\Logics\ChestContent as ChestContentLogic;
+use App\Module\GameItems\Logics\ChestOpenCostLogic;
 use App\Module\GameItems\Logics\Item as ItemLogic;
 use App\Module\GameItems\Logics\PityTime as PityTimeLogic;
 use App\Module\GameItems\Models\ItemChestContent;
@@ -43,6 +44,11 @@ class ChestService
      */
     protected $pityTimeLogic;
 
+    /**
+     * @var ChestOpenCostLogic
+     */
+    protected $chestOpenCostLogic;
+
     /**
      * 构造函数
      *
@@ -54,6 +60,10 @@ class ChestService
         $this->chestContentLogic = new ChestContentLogic();
         $this->itemLogic = new ItemLogic();
         $this->pityTimeLogic = new PityTimeLogic();
+        $this->chestOpenCostLogic = new ChestOpenCostLogic(
+            app(\App\Module\GameItems\Repositorys\ItemChestOpenCostRepository::class),
+            $this->itemLogic
+        );
     }
 
     /**
@@ -82,9 +92,28 @@ class ChestService
             throw new Exception("宝箱 {$chestId} 没有配置内容");
         }
 
+        // 验证宝箱开启消耗
+        list($isValid, $message, $costDetails) = $this->chestOpenCostLogic->validateChestOpenCosts($userId, $chestId, $quantity);
+        if (!$isValid) {
+            throw new Exception($message);
+        }
+
         // 开始事务
         DB::beginTransaction();
         try {
+            // 扣除宝箱开启消耗
+            if (!empty($costDetails)) {
+                $deductResult = $this->chestOpenCostLogic->deductChestOpenCosts(
+                    $userId,
+                    $costDetails,
+                    "开启宝箱 ID:{$chestId}"
+                );
+
+                if (!$deductResult) {
+                    throw new Exception("扣除宝箱开启消耗失败");
+                }
+            }
+
             // 消耗宝箱
             $consumeResult = $this->itemService->consumeItem(
                 $userId,
@@ -167,6 +196,7 @@ class ChestService
                 'results' => $allResults,
                 'pity_triggered' => $allPityTriggered,
                 'log_id' => $openLog->id,
+                'costs' => $costDetails,
             ];
         } catch (Exception $e) {
             DB::rollBack();
@@ -477,12 +507,27 @@ class ChestService
             }
         }
 
+        // 获取宝箱开启消耗配置
+        $costs = $this->chestOpenCostLogic->getActiveChestCosts($chestId);
+
         return [
             'chest_id' => $chestId,
             'chest_name' => $chest->name,
             'min_drop_count' => $chest->numeric_attributes['min_drop_count'] ?? 1,
             'max_drop_count' => $chest->numeric_attributes['max_drop_count'] ?? 1,
             'contents' => $contents,
+            'costs' => $costs,
         ];
     }
+
+    /**
+     * 获取宝箱开启消耗配置
+     *
+     * @param int $chestId 宝箱ID
+     * @return array 宝箱开启消耗配置
+     */
+    public function getChestOpenCosts(int $chestId): array
+    {
+        return $this->chestOpenCostLogic->getActiveChestCosts($chestId);
+    }
 }