安全解冻逻辑设计.md 6.8 KB

安全解冻逻辑设计文档

概述

安全解冻是物品冻结系统中的核心功能,用于处理冻结物品被部分或完全消耗后的解冻操作。与普通解冻不同,安全解冻需要处理复杂的补足逻辑。

业务场景

典型场景

  1. 用户冻结100个物品(用于交易订单)
  2. 冻结堆被部分消耗60个(其他操作消耗了冻结物品)
  3. 用户要求解冻(取消交易订单)
  4. 系统需要解冻100个物品(恢复原始冻结数量)

问题分析

  • 冻结堆当前只有40个,但用户期望解冻100个
  • 需要从其他地方补足60个差额
  • 关键原则:解冻是状态变更,不是物品消耗

核心设计原则

1. 解冻 ≠ 消耗

  • 解冻:改变物品的冻结状态,从"冻结"变为"可用"
  • 消耗:减少物品的总数量
  • 补足来源:只能从其他冻结堆中解冻,不能从可用物品中扣除

2. 数量守恒

  • 解冻前后,用户的物品总数量不变
  • 只是冻结状态发生变化

3. 日志类型

  • 解冻操作:只产生解冻日志(ItemFreezeLog)
  • 不产生:物品交易日志(ItemTransactionLog)

安全解冻逻辑流程

输入参数

  • freezeLogId: 要解冻的冻结日志ID

处理流程

第一步:验证和获取基础信息

1. 检查事务状态
2. 查找冻结日志记录
3. 验证冻结日志的有效性
4. 查找对应的冻结物品堆

第二步:计算补足需求

$originalFrozenQuantity = $freezeLog->quantity;  // 原始冻结数量
$currentQuantity = $frozenItem->quantity;        // 当前剩余数量
$shortageQuantity = $originalFrozenQuantity - $currentQuantity; // 需要补足的数量

第三步:分支处理

分支A:无需补足(shortageQuantity == 0)
if ($shortageQuantity == 0) {
    // 冻结堆完整,直接解冻
    return 正常解冻流程();
}
分支B:需要补足(shortageQuantity > 0)
if ($shortageQuantity > 0) {
    // 包含两种情况:
    // 1. 部分消耗:currentQuantity > 0,需要补足部分差额
    // 2. 完全消耗:currentQuantity <= 0,需要补足全部数量
    // 两种情况的处理逻辑相同:都是从其他冻结堆中解冻来补足
    return 补足解冻流程();
}

补足解冻详细流程

1. 查找补足来源

// 查找用户其他冻结物品(排除当前冻结堆)
$otherFrozenItems = ItemUser::where('user_id', $userId)
    ->where('item_id', $itemId)
    ->where('instance_id', $instanceId)
    ->where('is_frozen', true)
    ->where('frozen_log_id', '!=', $freezeLogId)  // 排除当前冻结堆
    ->where('quantity', '>', 0)
    ->orderBy('expire_at')  // 优先使用即将过期的
    ->lockForUpdate()       // 锁定防并发
    ->get();

2. 验证补足能力

$totalOtherFrozenQuantity = $otherFrozenItems->sum('quantity');
if ($totalOtherFrozenQuantity < $shortageQuantity) {
    throw new Exception("其他冻结数量不足以补足");
}

3. 执行补足操作

foreach ($otherFrozenItems as $otherFrozenItem) {
    $unfreezeQuantity = min($otherFrozenItem->quantity, $remainingShortage);
    
    // 从其他冻结堆中减少数量
    $otherFrozenItem->quantity -= $unfreezeQuantity;
    $otherFrozenItem->save();
    
    // 记录解冻日志
    ItemFreezeLog::createLog(
        $userId, $itemId, $instanceId, $unfreezeQuantity,
        FREEZE_ACTION_TYPE::UNFREEZE,
        "补足解冻:从冻结堆{$otherFrozenItem->frozen_log_id}解冻{$unfreezeQuantity}个用于补足解冻日志{$freezeLogId}"
    );
    
    // 触发事件
    event(new ItemQuantityChanged(...));
    
    $remainingShortage -= $unfreezeQuantity;
    if ($remainingShortage <= 0) break;
}

4. 恢复目标冻结堆

// 将目标冻结堆恢复到原始数量
$frozenItem->quantity = $originalFrozenQuantity;
$frozenItem->save();

5. 执行最终解冻

// 创建解冻日志
$unfreezeLog = ItemFreezeLog::createLog(
    $userId, $itemId, $instanceId, $originalFrozenQuantity,
    FREEZE_ACTION_TYPE::UNFREEZE,
    "安全解冻操作,原冻结日志ID: {$freezeLogId},补足差额: {$shortageQuantity}"
);

// 解冻物品
$frozenItem->is_frozen = false;
$frozenItem->frozen_log_id = null;
$frozenItem->save();

// 触发解冻事件
event(new ItemQuantityChanged(...));

异常处理

1. 冻结日志不存在

throw new Exception("冻结日志 {$freezeLogId} 不存在");

2. 冻结物品不存在

throw new Exception("未找到冻结日志 {$freezeLogId} 对应的冻结物品");

3. 其他冻结数量不足

throw new Exception("需要补足 {$shortageQuantity},但用户其他冻结数量只有 {$otherFrozenQuantity}");

4. 锁定后数量变化

throw new Exception("锁定后用户其他冻结数量只有 {$actualOtherFrozenQuantity}");

返回结果

成功返回

return [
    'success' => true,
    'user_id' => $userId,
    'item_id' => $itemId,
    'instance_id' => $instanceId,
    'unfrozen_quantity' => $originalFrozenQuantity,      // 实际解冻数量
    'shortage_compensated' => $shortageQuantity,         // 补足的差额
    'user_item_id' => $frozenItem->id,
    'unfreeze_log_id' => $unfreezeLog->id,
    'compensation_details' => $unfreezeDetails,          // 补足详情
];

事件触发

1. 补足过程事件

  • 事件类型:ItemQuantityChanged
  • 触发时机:每次从其他冻结堆减少数量时
  • 事件数据:包含补足操作的详细信息

2. 解冻完成事件

  • 事件类型:ItemQuantityChanged
  • 触发时机:最终解冻操作完成时
  • 事件数据:包含解冻操作的完整信息

并发安全

锁定机制

  • 使用 lockForUpdate() 锁定相关记录
  • 锁定后重新验证数量,防止并发修改
  • 确保补足操作的原子性

事务保护

  • 整个操作在数据库事务中执行
  • 任何步骤失败都会回滚所有变更
  • 保证数据一致性

与普通解冻的区别

特性 普通解冻 安全解冻
处理场景 冻结堆完整 冻结堆被消耗
补足机制 无需补足 从其他冻结堆补足
异常处理 严格抛异常 智能处理各种情况
返回信息 基础信息 详细补足信息
性能开销 较低 较高(需要查找和处理其他冻结堆)

使用建议

何时使用安全解冻

  1. 用户主动取消操作:如取消交易订单
  2. 系统自动回滚:如订单超时自动取消
  3. 异常恢复场景:如系统故障后的数据恢复

何时使用普通解冻

  1. 确定冻结堆完整:如刚冻结后立即解冻
  2. 性能敏感场景:如高频操作
  3. 简单业务逻辑:如临时冻结后快速解冻