dongasai e597c0bb66 14 8 mesi fa
..
AdminControllers fd3f036dd9 13 8 mesi fa
Commands fd3f036dd9 13 8 mesi fa
Config b8f85a6cf7 12 8 mesi fa
Enums fd3f036dd9 13 8 mesi fa
Events fd3f036dd9 13 8 mesi fa
Exceptions fd3f036dd9 13 8 mesi fa
Jobs fd3f036dd9 13 8 mesi fa
Listeners fd3f036dd9 13 8 mesi fa
Models fd3f036dd9 13 8 mesi fa
Providers fd3f036dd9 13 8 mesi fa
Queues fd3f036dd9 13 8 mesi fa
Repositorys fd3f036dd9 13 8 mesi fa
Services b8f85a6cf7 12 8 mesi fa
Tests b8f85a6cf7 12 8 mesi fa
Validations b8f85a6cf7 12 8 mesi fa
Validators b8f85a6cf7 12 8 mesi fa
GUILD.md e597c0bb66 14 8 mesi fa
README.md e597c0bb66 14 8 mesi fa
WORK.md e597c0bb66 14 8 mesi fa

README.md

GameItems模块

游戏物品模块

1. 模块功能和目的

GameItems模块主要负责游戏内所有物品的管理,包括但不限于:

  • 物品基础属性管理(名称、描述、图标、类型等)
  • 物品获取与消耗逻辑
  • 物品库存管理
  • 物品交易系统
  • 特殊物品效果实现
  • 宝箱类物品管理与开启机制
  • 物品过期时间管理(全局过期和用户特定过期)

核心功能特点:

  1. 多物品宝箱系统:支持一个宝箱同时掉落多个物品,并可配置每个物品的数量范围和概率
  2. 灵活的过期机制:支持全局过期时间和用户特定过期时间,适应不同的游戏需求
  3. 完善的属性系统:通过键值对存储物品属性,支持复杂的物品效果实现

该模块为游戏内经济系统和玩家进度提供基础支持,是连接游戏多个子系统的核心模块。

2. 数据结构设计

游戏物品模块采用关系型数据库设计,通过多个相互关联的数据表实现物品管理、用户物品关联、属性存储和宝箱配置等功能。数据结构设计遵循以下原则:

  1. 模块化:各个表结构清晰,职责单一
  2. 扩展性:支持通过属性表扩展物品特性,无需修改数据库结构
  3. 性能优化:合理的索引设计和关联关系

2.1 数据表设计

items 表(统一属性物品)

字段名 类型 说明
id int 物品ID,主键
name varchar 物品名称
description text 物品描述
type tinyint 物品类型(1:消耗品, 2:装备, 3:材料, 4:任务物品, 5:宝箱...)
rarity tinyint 稀有度(1:普通, 2:稀有, 3:史诗, 4:传说...)
icon varchar 物品图标路径
max_stack int 最大堆叠数量
sell_price int 出售价格
display_attributes json 展示属性,以JSON格式存储键值对,用于界面展示和描述的属性
numeric_attributes json 数值属性,以JSON格式存储键值对,用于计算和游戏逻辑的属性
min_drop_count int 宝箱最小掉落物品数量(仅宝箱类型物品有效)
max_drop_count int 宝箱最大掉落物品数量(仅宝箱类型物品有效)
global_expire_at timestamp 物品全局过期时间(可为空)
created_at timestamp 创建时间
updated_at timestamp 更新时间

unique_items 表(单独属性物品)

字段名 类型 说明
id int 唯一物品ID,主键
item_id int 关联的基础物品ID,外键关联items表
name varchar 物品名称(可以与基础物品不同,如“锐利的钢刀”)
display_attributes json 展示属性,以JSON格式存储键值对,用于界面展示和描述的属性
numeric_attributes json 数值属性,以JSON格式存储键值对,用于计算和游戏逻辑的属性
expire_at timestamp 物品过期时间(可为空)
created_at timestamp 创建时间
updated_at timestamp 更新时间

user_items 表(用户物品关联)

字段名 类型 说明
id int 记录ID,主键
user_id int 用户ID,外键
item_id int 统一属性物品ID,外键关联items表(当该字段有值时,unique_item_id应为空)
unique_item_id int 单独属性物品ID,外键关联unique_items表(当该字段有值时,item_id应为空)
quantity int 数量(对于单独属性物品,该值始终为1)
expire_at timestamp 用户物品过期时间(可为空)
created_at timestamp 获取时间
updated_at timestamp 更新时间

chest_items 表(宝箱内容配置)

字段名 类型 说明
id int 记录ID,主键
chest_id int 宝箱物品ID,外键关联items表
item_id int 可能获得的物品ID
is_unique tinyint 是否生成单独属性物品(0:否, 1:是)
min_quantity int 最小数量
max_quantity int 最大数量
weight int 权重,决定获取概率
allow_duplicate tinyint 是否允许在同一宝箱中重复掉落(0:不允许, 1:允许)
created_at timestamp 创建时间
updated_at timestamp 更新时间

2.2 数据库索引设计

为了确保系统的高性能运行,我们对关键字段进行了索引设计:

表名 索引字段 索引类型 说明
items id 主键 物品ID主键索引
items type 普通索引 加速按物品类型查询
items global_expire_at 普通索引 加速过期物品查询
unique_items id 主键 唯一物品ID主键索引
unique_items item_id 普通索引 加速根据基础物品查询唯一物品
unique_items expire_at 普通索引 加速过期物品查询
user_items user_id, item_id 复合索引 加速用户统一属性物品查询
user_items user_id, unique_item_id 复合索引 加速用户单独属性物品查询
user_items expire_at 普通索引 加速过期物品查询
chest_items chest_id 普通索引 加速宝箱内容查询
chest_items item_id 普通索引 加速物品在宝箱中的查询
chest_items is_unique 普通索引 加速查询生成唯一物品的宝箱配置

2.3 可视图的方式

以下是数据模型之间的关联关系图:

graph TD
    User["User"] --- UserItems{"user_items<br>M:N"}
    UserItems --- Item["Item<br>统一属性物品"]
    UserItems --- UniqueItem["unique_items<br>单独属性物品"]
    UniqueItem -->|"N:1"| Item

    subgraph "宝箱系统"
        ChestItem["Item<br>(type=5)宝箱"] -->|"1:N"| ChestConfig["chest_items<br>宝箱配置"]
        ChestConfig -->|"N:1"| ItemDrop["Item<br>掉落物品"]
        ChestConfig -->|"is_unique=1"| UniqueItem
    end

    classDef entity fill:#f9f,stroke:#333,stroke-width:2px;
    classDef junction fill:#bbf,stroke:#33f,stroke-width:2px;

    class User,Item,UniqueItem,ChestItem,ItemDrop entity;
    class UserItems,ChestConfig junction;

图表说明:

  • User 与 Item/UniqueItem:用户可以拥有统一属性物品和单独属性物品,通过 user_items 表关联
  • UniqueItem 与 Item:多对一关系,每个单独属性物品都关联到一个基础物品模板
  • 宝箱物品与掉落物品:宝箱(Item type=5)与 chest_items 表是一对多关系
  • chest_items 与物品:可以配置生成统一属性物品或单独属性物品(通过is_unique字段控制)

2.4 单独属性物品实现

为了支持“每个用户拥有的同类物品可以有不同属性”的需求(如不同的刀有不同的属性),我们采用了两种物品类型的设计:

实现方式说明

该设计允许我们实现以下功能:

  1. 物品类型分离

    • items表存储统一属性物品,属性以JSON格式存储
    • unique_items表存储单独属性物品,属性以JSON格式存储
    • 每个单独属性物品都关联到一个基础物品模板
  2. 用户物品关联的灵活性

    • 用户可以同时拥有统一属性物品和单独属性物品
    • user_items表中的item_idunique_item_id字段互斥,一个记录只能关联其中一种物品
    • 对于单独属性物品,quantity始终为1
  3. 宝箱生成机制

    • 宝箱可以配置生成统一属性物品或单独属性物品
    • 通过chest_items表中的is_unique字段控制生成类型

使用示例

// 为用户创建一把具有单独属性的刀
public function createUniqueWeapon($userId, $baseItemId)
{
    // 开始事务
    DB::beginTransaction();

    try {
        // 1. 获取基础物品信息
        $baseItem = Item::findOrFail($baseItemId);

        // 2. 生成随机属性
        // 继承基础物品的展示属性
        $baseDisplayAttributes = json_decode($baseItem->display_attributes, true) ?? [];
        $displayAttributes = $baseDisplayAttributes;

        // 添加自定义展示属性
        $customDisplayAttributes = [
            'appearance' => '闪亮的刀身上刻有精美的纹理',
            'material' => '特殊合金打造',
            'description' => '这把刀经过精心锻造,锐利无比'
        ];

        // 合并展示属性,自定义属性会覆盖基础属性
        $displayAttributes = array_merge($displayAttributes, $customDisplayAttributes);

        // 继承基础物品的数值属性
        $baseNumericAttributes = json_decode($baseItem->numeric_attributes, true) ?? [];
        $numericAttributes = $baseNumericAttributes;

        // 生成随机数值属性
        $randomNumericAttributes = [
            'attack' => mt_rand(10, 20),
            'durability' => mt_rand(80, 100),
            'sharpness' => mt_rand(1, 5),
            'critical_chance' => mt_rand(5, 15) / 100 // 暴击几率
        ];

        // 合并数值属性,随机属性会覆盖基础属性
        $numericAttributes = array_merge($numericAttributes, $randomNumericAttributes);

        // 3. 创建单独属性物品
        $uniqueItem = new UniqueItem();
        $uniqueItem->item_id = $baseItemId;
        $uniqueItem->name = "锐利的" . $baseItem->name; // 可以自定义名称
        $uniqueItem->display_attributes = json_encode($displayAttributes);
        $uniqueItem->numeric_attributes = json_encode($numericAttributes);
        $uniqueItem->save();

        // 4. 关联到用户
        $userItem = new UserItem();
        $userItem->user_id = $userId;
        $userItem->unique_item_id = $uniqueItem->id; // 注意这里使用unique_item_id而非item_id
        $userItem->quantity = 1; // 单独属性物品数量始终为1
        $userItem->save();

        DB::commit();
        return [
            'user_item' => $userItem,
            'unique_item' => $uniqueItem
        ];

    } catch (\Exception $e) {
        DB::rollBack();
        throw $e;
    }
}

// 获取用户的所有单独属性武器
public function getUserUniqueWeapons($userId, $baseItemId = null)
{
    // 构建查询
    $query = UserItem::where('user_id', $userId)
        ->whereNotNull('unique_item_id'); // 只查询单独属性物品

    // 如果指定了基础物品ID,则只查询该类型的武器
    if ($baseItemId) {
        $query->whereHas('uniqueItem', function($q) use ($baseItemId) {
            $q->where('item_id', $baseItemId);
        });
    }

    // 获取用户物品记录,并预加载单独属性物品信息
    $userItems = $query->with('uniqueItem.baseItem')->get();

    $result = [];

    foreach ($userItems as $userItem) {
        $uniqueItem = $userItem->uniqueItem;
        $baseItem = $uniqueItem->baseItem;

        $weaponData = [
            'id' => $userItem->id,
            'unique_item_id' => $uniqueItem->id,
            'base_item_id' => $baseItem->id,
            'name' => $uniqueItem->name,
            'base_name' => $baseItem->name,
            'display_attributes' => json_decode($uniqueItem->display_attributes, true),
            'numeric_attributes' => json_decode($uniqueItem->numeric_attributes, true),
            'obtained_at' => $userItem->created_at->format('Y-m-d H:i:s')
        ];

        $result[] = $weaponData;
    }

    return $result;
}

// 为用户添加统一属性物品
public function addItemToUser($userId, $itemId, $quantity = 1)
{
    // 查找用户是否已有该物品
    $userItem = UserItem::where('user_id', $userId)
        ->where('item_id', $itemId)
        ->whereNull('unique_item_id') // 确保是统一属性物品
        ->first();

    if ($userItem) {
        // 如果已有,增加数量
        $userItem->quantity += $quantity;
        $userItem->save();
    } else {
        // 如果没有,创建新记录
        $userItem = new UserItem();
        $userItem->user_id = $userId;
        $userItem->item_id = $itemId;
        $userItem->quantity = $quantity;
        $userItem->save();
    }

    return $userItem;
}

2.5 数据库事务

在涉及多表操作的关键业务逻辑中,使用数据库事务确保数据一致性:

  1. 宝箱开启操作:包含宝箱扣除和物品添加的原子性操作
  2. 物品交易:确保物品转移的原子性
  3. 批量物品操作:如批量删除过期物品
  4. 单独属性物品创建:包含创建 unique_items 记录和关联到用户的原子性操作
  5. 宝箱生成单独属性物品:当宝箱配置为生成单独属性物品时,需要保证创建和关联的原子性

3. 与其他模块的交互

3.1 与用户模块交互

  • 用户模块通过GameItems提供的服务接口获取用户拥有的物品
  • 用户等级提升时,可通过GameItems模块发放奖励物品

3.2 与商店模块交互

  • 商店模块调用GameItems的接口进行物品购买和出售
  • 商店模块从GameItems获取物品基础信息用于展示

3.3 与任务模块交互

  • 任务完成时,通过GameItems模块发放奖励物品
  • 任务进度可能需要检查用户是否拥有特定物品或数量

3.4 与战斗模块交互

  • 战斗中使用消耗品时,调用GameItems模块的物品使用接口
  • 战斗结束后,通过GameItems模块发放掉落物品

4. 使用示例

4.1 获取用户物品并展示

// 控制器中
public function showUserItems(Request $request)
{
    $user = Auth::user();
    $items = app(GameItemService::class)->getUserItems($user->id);

    return view('inventory', ['items' => $items]);
}

4.2 使用物品示例

// 控制器中
public function useItem(Request $request)
{
    $user = Auth::user();
    $itemId = $request->input('item_id');
    $quantity = $request->input('quantity', 1);

    $result = app(GameItemService::class)->useItem($user->id, $itemId, $quantity);

    if ($result['success']) {
        return response()->json([
            'code' => 0,
            'message' => '物品使用成功',
            'data' => $result['data']
        ]);
    }

    return response()->json([
        'code' => 1,
        'message' => $result['message']
    ]);
}

4.3 添加物品到用户背包

// 在任务完成奖励中
public function completeQuest($userId, $questId)
{
    // 处理任务完成逻辑...

    // 发放物品奖励
    $rewards = [
        ['item_id' => 1, 'quantity' => 5],  // 5个生命药水
        ['item_id' => 10, 'quantity' => 1, 'expire_days' => 7],  // 1把稀有武器,7天后过期
    ];

    foreach ($rewards as $reward) {
        $expireAt = null;
        if (isset($reward['expire_days'])) {
            $expireAt = now()->addDays($reward['expire_days']);
        }

        app(GameItemService::class)->addItemToUser(
            $userId,
            $reward['item_id'],
            $reward['quantity'],
            $expireAt
        );
    }

    // 返回奖励信息...
}

4.4 检查物品是否过期

// 在使用物品前检查
public function checkItemValidity($userId, $itemId)
{
    $userItem = UserItem::where('user_id', $userId)
        ->where('item_id', $itemId)
        ->first();

    if (!$userItem) {
        return ['valid' => false, 'message' => '物品不存在'];
    }

    // 检查用户物品过期时间
    if ($userItem->expire_at && $userItem->expire_at < now()) {
        return ['valid' => false, 'message' => '物品已过期'];
    }

    // 检查全局过期时间
    $item = Item::find($itemId);
    if ($item->global_expire_at && $item->global_expire_at < now()) {
        return ['valid' => false, 'message' => '物品已全局过期'];
    }

    return ['valid' => true];
}

4.5 打开宝箱示例

// 宝箱服务中
public function openChest($userId, $chestId, $quantity = 1)
{
    // 验证用户是否拥有足够的宝箱
    $userChest = UserItem::where('user_id', $userId)
        ->where('item_id', $chestId)
        ->first();

    if (!$userChest || $userChest->quantity < $quantity) {
        return [
            'success' => false,
            'message' => '宝箱数量不足'
        ];
    }

    // 检查宝箱是否过期
    $validityCheck = $this->checkItemValidity($userId, $chestId);
    if (!$validityCheck['valid']) {
        return [
            'success' => false,
            'message' => $validityCheck['message']
        ];
    }

    // 获取宝箱配置
    $chestConfig = ChestItem::where('chest_id', $chestId)->get();
    if ($chestConfig->isEmpty()) {
        return [
            'success' => false,
            'message' => '宝箱配置错误'
        ];
    }

    // 开始事务
    DB::beginTransaction();

    try {
        // 扣除宝箱
        $userChest->quantity -= $quantity;
        if ($userChest->quantity <= 0) {
            $userChest->delete();
        } else {
            $userChest->save();
        }

        $rewards = [];

        // 处理每个宝箱
        for ($i = 0; $i < $quantity; $i++) {
            // 获取当前宝箱的掉落物品数量范围
            $chest = Item::find($chestId);
            $dropItemsCount = mt_rand(
                $chest->getAttribute('min_drop_count', 1),
                $chest->getAttribute('max_drop_count', 3)
            );

            // 为每个宝箱随机选择多个物品
            $selectedItems = [];
            $availableConfigs = $chestConfig->toArray();

            // 选择多个物品
            for ($j = 0; $j < $dropItemsCount; $j++) {
                if (empty($availableConfigs)) {
                    break;
                }

                // 随机选择物品
                $totalWeight = array_sum(array_column($availableConfigs, 'weight'));
                $randomWeight = mt_rand(1, $totalWeight);
                $currentWeight = 0;
                $selectedIndex = 0;

                foreach ($availableConfigs as $index => $config) {
                    $currentWeight += $config['weight'];

                    if ($randomWeight <= $currentWeight) {
                        $selectedIndex = $index;
                        break;
                    }
                }

                // 获取选中的配置
                $selectedConfig = $availableConfigs[$selectedIndex];

                // 随机决定数量
                $itemQuantity = mt_rand($selectedConfig['min_quantity'], $selectedConfig['max_quantity']);

                // 获取物品信息
                $item = Item::find($selectedConfig['item_id']);

                // 添加到用户背包
                $this->addItemToUser($userId, $selectedConfig['item_id'], $itemQuantity);

                // 记录奖励
                $rewards[] = [
                    'item_id' => $item->id,
                    'name' => $item->name,
                    'quantity' => $itemQuantity,
                    'icon' => $item->icon
                ];

                // 如果该物品不允许重复掉落,则从可选列表中移除
                if (!$selectedConfig['allow_duplicate'] ?? false) {
                    array_splice($availableConfigs, $selectedIndex, 1);
                }
            }
        }

        DB::commit();

        return [
            'success' => true,
            'chests' => [
                [
                    'chest_id' => $chestId,
                    'chest_name' => $chest->name,
                    'rewards' => $rewards
                ]
            ],
            'remaining_chests' => $userChest->quantity ?? 0
        ];

    } catch (\Exception $e) {
        DB::rollBack();

        return [
            'success' => false,
            'message' => '宝箱打开失败: ' . $e->getMessage()
        ];
    }
}

5. 物品过期时间管理

物品模块支持两种过期时间机制:

5.1 全局过期时间

全局过期时间定义在items表的global_expire_at字段中,表示该物品在所有用户中的绝对过期时间。当超过这个时间点后,所有用户的该物品均失效。

应用场景:

  • 限时活动物品
  • 赠送的体验物品
  • 版本更新后需要清除的物品

5.2 用户物品过期时间

用户物品过期时间定义在user_items表的expire_at字段中,表示特定用户的特定物品的过期时间。这允许每个用户的同一物品有不同的过期时间。

应用场景:

  • 限时使用的装备或道具
  • 会员特权物品
  • 活动奖励物品

5.3 过期处理机制

系统应定期运行任务检查并处理过期物品:

// 计划任务中
public function handleExpiredItems()
{
    // 处理全局过期物品
    $expiredGlobalItems = Item::where('global_expire_at', '<', now())
        ->whereNotNull('global_expire_at')
        ->get();

    foreach ($expiredGlobalItems as $item) {
        // 删除所有用户的该物品或标记为失效
        UserItem::where('item_id', $item->id)->delete();

        // 可选操作:发送通知给用户
    }

    // 处理用户物品过期
    $expiredUserItems = UserItem::where('expire_at', '<', now())
        ->whereNotNull('expire_at')
        ->get();

    foreach ($expiredUserItems as $userItem) {
        // 删除过期的用户物品
        $userItem->delete();

        // 可选操作:发送通知给用户
    }
}

6. 宝箱系统设计

6.1 宝箱类型

宝箱可以根据稀有度和内容分为不同类型:

  1. 普通宝箱:包含基础消耗品和少量资源
  2. 稀有宝箱:有机会获得稀有装备和较多资源
  3. 限定宝箱:只在特定活动期间可获得,包含限定物品
  4. 主题宝箱:围绕特定主题设计,如节日宝箱、职业宝箱等

6.2 宝箱掉落机制

6.2.1 多物品掉落

每个宝箱可以同时掉落多个物品:

  • 每个宝箱类型物品在items表中定义了min_drop_countmax_drop_count字段
  • 系统在打开宝箱时会随机决定实际掉落的物品数量
  • 可以通过chest_items表中的allow_duplicate字段控制物品是否可以在同一宝箱中重复掉落

6.2.2 概率机制

宝箱内容的概率由chest_items表中的weight字段决定:

  • 每个物品的概率 = 该物品权重 / 所有物品权重之和
  • 系统支持不同程度的保底机制,确保玩家在多次开启后能获得稀有物品

6.3 宝箱日志记录

每次宝箱开启都应记录详细日志,包括:

  • 用户信息
  • 宝箱类型
  • 开启时间
  • 获得的物品详情

这些日志可用于数据分析和问题排查。

7. 注意事项

  1. 物品使用逻辑应根据物品类型进行不同处理
  2. 需要考虑物品数量上限和背包容量限制
  3. 稀有物品获取应有日志记录
  4. 物品交易需考虑并发安全问题,建议使用数据库事务
  5. 敏感操作(如删除物品)需要有权限验证
  6. 在获取用户物品时,始终检查过期时间,不展示已过期物品
  7. 在添加物品到用户背包时,需要检查全局过期时间
  8. 宝箱开启操作应使用事务确保原子性
  9. 宝箱配置应由管理员仔细调整和测试,确保概率合理