冻结实现.md 11 KB

GameItem模块 - 物品冻结功能实现思路

1. 问题背景

在实现匹配交易的过程中,当用户发起卖出物品订单时,需要冻结该物品,防止用户在订单处理期间使用、交易或消耗这些物品。目前GameItem模块缺少物品冻结机制,需要设计并实现完整的物品冻结功能。

2. 现状分析

2.1 当前物品状态管理

  • ItemUser表:记录用户物品关联,包含数量、过期时间等基本信息
  • ItemInstance表:单独属性物品实例,包含绑定状态(is_bound)、交易状态(tradable)
  • 交易日志:通过ItemTransactionLog记录所有物品流转
  • 状态枚举:已有TRANSACTION_TYPEITEM_BIND_TYPE等状态管理

2.2 缺失的功能

  • 缺少物品冻结状态字段
  • 缺少冻结/解冻操作逻辑
  • 缺少冻结状态验证机制
  • 缺少冻结记录追踪

3. 设计方案

3.1 数据库设计

3.1.1 ItemUser表扩展

item_users表中新增冻结状态字段:

ALTER TABLE `kku_item_users`
ADD COLUMN `is_frozen` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否冻结(0:未冻结, 1:已冻结)',
ADD COLUMN `freeze_log_id` int DEFAULT NULL COMMENT '冻结日志ID,关联kku_item_freeze_logs表',
ADD INDEX `idx_frozen_status` (`user_id`, `is_frozen`),
ADD INDEX `idx_freeze_log` (`freeze_log_id`);

冻结拆堆机制说明

  • 冻结时采用拆"堆"模式:将原有堆叠拆分为冻结部分和可用部分两个独立的ItemUser记录
  • 例如:1000个物品堆叠,冻结200个时,拆分为:200个(is_frozen=1)+ 800个(is_frozen=0)
  • 冻结的堆叠不能使用、消耗或交易

拆堆示例

原始状态:
ItemUser: user_id=1, item_id=100, quantity=1000, is_frozen=0

冻结200个后:
ItemUser: user_id=1, item_id=100, quantity=200, is_frozen=1, freeze_log_id=123
ItemUser: user_id=1, item_id=100, quantity=800, is_frozen=0

解冻后(可选择合并或保持独立):
选择1 - 合并:ItemUser: user_id=1, item_id=100, quantity=1000, is_frozen=0
选择2 - 独立:两条记录都保持is_frozen=0

说明:ItemInstance表不需要扩展冻结字段,因为ItemInstance是物品实例表,记录的是物品的基本信息和属性,而不涉及物品归属关系。冻结功能是针对用户拥有的物品进行的操作,应该在ItemUser表中实现。

3.1.2 冻结记录表

创建专门的冻结记录表用于追踪:

CREATE TABLE `kku_item_freeze_logs` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '记录ID,主键',
  `user_id` int NOT NULL COMMENT '用户ID',
  `item_id` int NOT NULL COMMENT '物品ID',
  `instance_id` int DEFAULT NULL COMMENT '物品实例ID(单独属性物品)',
  `quantity` int NOT NULL COMMENT '冻结数量',
  `action_type` tinyint NOT NULL COMMENT '操作类型(1:冻结, 2:解冻)',
  `reason` varchar(255) NOT NULL COMMENT '操作原因',
  `source_id` int DEFAULT NULL COMMENT '操作方记录ID',
  `source_type` varchar(50) DEFAULT NULL COMMENT '操作类型(如:order, admin, system等)',
  `operator_id` int DEFAULT NULL COMMENT '操作员ID(系统操作为NULL)',
  `created_at` timestamp NULL DEFAULT NULL COMMENT '操作时间',
  PRIMARY KEY (`id`),
  KEY `idx_user_item` (`user_id`, `item_id`),
  KEY `idx_source` (`source_type`, `source_id`),
  KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品冻结记录表';

3.2 枚举定义

3.2.1 冻结操作类型

// app/Module/GameItems/Enums/FREEZE_ACTION_TYPE.php
class FREEZE_ACTION_TYPE
{
    const FREEZE = 1;    // 冻结
    const UNFREEZE = 2;  // 解冻
    
    public static function all(): array
    {
        return [
            self::FREEZE => '冻结',
            self::UNFREEZE => '解冻',
        ];
    }
}

3.2.2 冻结原因类型

// app/Module/GameItems/Enums/FREEZE_REASON_TYPE.php
class FREEZE_REASON_TYPE
{
    const TRADE_ORDER = 'trade_order';      // 交易订单
    const ADMIN_FREEZE = 'admin_freeze';    // 管理员冻结
    const SYSTEM_FREEZE = 'system_freeze';  // 系统冻结
    const AUCTION = 'auction';              // 拍卖
    const MAIL_ATTACHMENT = 'mail_attachment'; // 邮件附件
    
    public static function all(): array
    {
        return [
            self::TRADE_ORDER => '交易订单',
            self::ADMIN_FREEZE => '管理员冻结',
            self::SYSTEM_FREEZE => '系统冻结',
            self::AUCTION => '拍卖',
            self::MAIL_ATTACHMENT => '邮件附件',
        ];
    }
}

3.3 模型扩展

3.3.1 ItemUser模型扩展

// 在ItemUser模型中添加字段和方法
protected $fillable = [
    // ... 现有字段
    'is_frozen',
    'freeze_log_id',
];

protected $casts = [
    // ... 现有字段
    'is_frozen' => 'boolean',
];

/**
 * 检查是否为冻结状态
 */
public function isFrozen(): bool
{
    return $this->is_frozen;
}

/**
 * 检查是否可用(未冻结)
 */
public function isAvailable(): bool
{
    return !$this->is_frozen;
}

/**
 * 获取关联的冻结日志
 */
public function freezeLog(): BelongsTo
{
    return $this->belongsTo(ItemFreezeLog::class, 'freeze_log_id');
}

/**
 * 获取用户指定物品的可用数量(排除冻结的堆叠)
 */
public static function getAvailableQuantity(int $userId, int $itemId, ?int $instanceId = null): int
{
    $query = static::where('user_id', $userId)
        ->where('item_id', $itemId)
        ->where('is_frozen', false);

    if ($instanceId) {
        $query->where('instance_id', $instanceId);
    } else {
        $query->whereNull('instance_id');
    }

    return $query->sum('quantity');
}

说明:ItemInstance模型不需要扩展冻结相关字段和方法,因为ItemInstance表记录的是物品实例的基本信息,冻结状态应该在ItemUser表中管理。

3.4 逻辑层实现

3.4.1 物品冻结逻辑类

// app/Module/GameItems/Logics/ItemFreeze.php
class ItemFreeze
{
    /**
     * 冻结统一属性物品(拆堆模式)
     *
     * 实现逻辑:
     * 1. 查找用户可用的物品堆叠(is_frozen=false)
     * 2. 从可用堆叠中扣除冻结数量
     * 3. 创建新的冻结堆叠记录(is_frozen=true)
     * 4. 记录冻结日志
     */
    public static function freezeNormalItem(
        int $userId,
        int $itemId,
        int $quantity,
        string $reason,
        ?int $sourceId = null,
        ?string $sourceType = null
    ): Res;

    /**
     * 冻结单独属性物品(拆堆模式)
     * 注:单独属性物品数量始终为1,冻结时直接标记is_frozen=true
     */
    public static function freezeUniqueItem(
        int $userId,
        int $itemId,
        int $instanceId,
        string $reason,
        ?int $sourceId = null,
        ?string $sourceType = null
    ): Res;

    /**
     * 解冻统一属性物品(合堆模式)
     *
     * 实现逻辑:
     * 1. 通过freeze_log_id找到冻结的堆叠记录
     * 2. 将冻结堆叠标记为可用(is_frozen=false)或删除
     * 3. 尝试与现有可用堆叠合并
     * 4. 记录解冻日志
     */
    public static function unfreezeByLogId(int $freezeLogId): Res;

    /**
     * 检查用户可用物品数量(排除冻结堆叠)
     */
    public static function getAvailableQuantity(
        int $userId,
        int $itemId,
        ?int $instanceId = null
    ): int;

    /**
     * 验证用户是否有足够的可用物品
     */
    public static function checkAvailableQuantity(
        int $userId,
        int $itemId,
        int $requiredQuantity,
        ?int $instanceId = null
    ): bool;
}

3.5 服务层扩展

3.5.1 ItemService扩展

// 在ItemService中添加冻结相关方法
public static function freezeItem(
    int $userId,
    int $itemId,
    ?int $instanceId,
    int $quantity,
    string $reason,
    array $options = []
): array;

public static function unfreezeItem(
    int $userId, 
    int $itemId,
    ?int $instanceId,
    int $quantity,
    string $reason,
    array $options = []
): array;

public static function getAvailableQuantity(
    int $userId,
    int $itemId
): int;

public static function getFrozenItems(
    int $userId,
    array $filters = []
): SupportCollection;

4. 集成方案

4.1 与交易系统集成

  • 创建卖出订单时:自动冻结对应数量的物品
  • 订单完成时:解冻物品并执行消耗操作
  • 订单取消时:解冻物品恢复可用状态
  • 订单失败时:解冻物品并记录失败原因

4.2 与现有验证集成

  • 消耗物品前:只查询is_frozen=false的堆叠,确保不消耗冻结物品
  • 交易物品前:验证物品堆叠未被冻结(is_frozen=false)
  • 使用物品前:确认物品堆叠处于可用状态(is_frozen=false)
  • 物品查询:默认只显示可用物品,冻结物品需要特殊查询

4.3 解冻机制

  • 主动解冻:由冻结发起方(如交易系统、管理员等)主动调用解冻方法
  • 日志追踪:通过freeze_log_id关联冻结记录,确保解冻操作的准确性

5. 实现优先级

5.1 第一阶段(核心功能)

  1. 数据库表结构修改
  2. 模型字段和基础方法扩展
  3. 冻结/解冻核心逻辑实现
  4. 基础验证机制

5.2 第二阶段(集成功能)

  1. 与交易系统集成
  2. 服务层方法完善
  3. 管理后台界面
  4. 冻结记录查询

5.3 第三阶段(优化功能)

  1. 批量操作优化
  2. 性能监控和优化
  3. 异常情况处理
  4. 冻结记录清理机制

6. 注意事项

6.1 数据一致性

  • 冻结操作必须在事务中执行,确保拆堆操作的原子性
  • 确保冻结数量不超过实际可用数量(is_frozen=false的堆叠总和)
  • 解冻时验证冻结记录的有效性,防止重复解冻
  • 拆堆和合堆操作需要保证数量的准确性

6.2 性能考虑

  • 冻结状态查询需要适当的索引支持(user_id, is_frozen)
  • 拆堆操作会增加ItemUser记录数量,需要考虑存储空间
  • 可用数量查询需要聚合计算,建议添加缓存机制
  • 大量物品冻结时考虑批量操作优化

6.3 业务规则

  • 冻结的堆叠(is_frozen=true)不能被消耗、交易或使用
  • 解冻操作由冻结发起方负责处理,通过freeze_log_id定位
  • 拆堆时需要保证原堆叠数量 = 冻结堆叠数量 + 剩余可用堆叠数量
  • 解冻时可以选择与现有可用堆叠合并或保持独立
  • 不同冻结原因(source_type)可能有不同的处理逻辑
  • 交易日志不记录冻结相关信息,冻结信息由冻结记录表单独管理

文档创建时间:2025年06月12日 版本:v1.3 状态:设计阶段 修正说明:

  • v1.1:移除ItemInstance表的冻结扩展,因为ItemInstance是物品实例表,不涉及物品归属关系
  • v1.2:简化设计,移除自动解冻逻辑,解冻由发起方处理;冻结记录表使用source_id/source_type;简化ItemUser表字段
  • v1.3:采用冻结即拆"堆"的模式,使用is_frozen字段标识冻结状态;交易日志不记录冻结信息