Browse Source

修复解冻过程中ItemQuantityChanged事件触发遗漏

- 补充安全解冻方法中缺失的事件触发
- 为补足转移过程添加完整的事件记录
- 包含可用物品扣除和冻结堆补足的事件
- 添加详细的操作类型和数量信息
- 创建事件触发测试验证修复效果
dongasai 6 months ago
parent
commit
e61188fc80

+ 134 - 0
AiWork/202507/060328-修复物品解冻BUG.md

@@ -0,0 +1,134 @@
+# 修复物品解冻BUG
+
+## 任务时间
+- 开始时间:2025-07-06 03:13:58
+- 完成时间:2025-07-06 03:30:00
+
+## 问题描述
+用户反馈:`物品解冻,存在BUG,物品解冻,存在BUG,这个冻结堆被别人消耗后,解冻就出现了物品数量错误`
+
+## 问题分析
+
+### BUG现象
+1. 用户冻结了100个物品
+2. 冻结堆被其他人消耗了60个,剩余40个
+3. 解冻时只能得到40个,而不是原始冻结的100个
+4. 用户期望解冻100个,但实际只得到40个
+
+### 根本原因
+原始解冻逻辑 `unfreezeByLogId` 方法存在以下问题:
+1. **没有检查冻结堆被消耗的情况**:直接解冻当前剩余数量
+2. **缺少数量补足机制**:没有从用户其他可用物品中补足差额
+3. **业务逻辑错误**:解冻应该恢复原始冻结数量,而不是当前剩余数量
+
+## 解决方案
+
+### 1. 修复解冻逻辑
+修改 `app/Module/GameItems/Logics/ItemFreeze.php` 中的 `unfreezeByLogId` 方法:
+
+**核心修复逻辑**:
+1. **获取原始冻结数量**:从冻结日志中获取 `$originalFrozenQuantity`
+2. **检查当前剩余数量**:获取冻结堆的 `$currentQuantity`
+3. **计算差额**:`$shortageQuantity = $originalFrozenQuantity - $currentQuantity`
+4. **补足差额**:从用户可用物品中扣除差额,补足到冻结堆
+5. **完整解冻**:解冻恢复到原始冻结数量
+
+### 2. 安全性保障
+参考消耗物品逻辑,确保"补全转移"的安全性:
+
+**交易日志记录**:
+- 记录从可用物品扣除的交易日志(TRADE_OUT)
+- 记录向冻结堆补足的交易日志(TRADE_IN)
+- 详细记录转移过程和数量
+
+**事件触发**:
+- 触发可用物品数量减少事件
+- 触发冻结堆数量增加事件
+- 触发解冻状态变更事件
+
+**数据完整性**:
+- 不删除冻结日志
+- 不删除冻结堆记录
+- 保持数据可追溯性
+
+### 3. 新增安全解冻方法
+添加 `safeUnfreezeByLogId` 方法,处理特殊情况:
+- 冻结物品不存在时返回已处理状态
+- 用户可用数量不足时返回失败信息
+- 提供详细的处理结果信息
+
+## 修复内容
+
+### 1. 核心文件修改
+- `app/Module/GameItems/Logics/ItemFreeze.php`
+  - 修复 `unfreezeByLogId` 方法
+  - 新增 `safeUnfreezeByLogId` 方法
+  - 新增 `getConsumedFrozenItemsStatistics` 方法
+
+- `app/Module/GameItems/Services/ItemService.php`
+  - 新增 `safeUnfreezeItem` 服务方法
+  - 新增 `getConsumedFrozenItemsStatistics` 服务方法
+
+### 2. 测试验证
+- 创建 `tests/manual_test_unfreeze_bug_fix.php` 测试脚本
+- 验证部分消耗后的解冻效果
+- 验证完全消耗后的解冻处理
+- 验证交易日志记录完整性
+
+## 测试结果
+
+### ✅ 修复验证成功
+
+**测试场景1:部分消耗后解冻**
+- 冻结:100个物品
+- 消耗:60个(剩余40个)
+- 解冻:成功恢复100个(补足60个差额)
+- 结果:`"unfrozen_quantity":100,"shortage_compensated":60`
+
+**测试场景2:完全消耗后解冻**
+- 原始解冻:正确抛出异常"冻结物品已被完全消耗,无法解冻"
+- 安全解冻:成功从可用物品补足,恢复原始数量
+
+**测试场景3:交易日志完整性**
+- 补足过程有完整的交易日志记录
+- 可以追踪物品转移的完整过程
+
+## 技术要点
+
+### 1. 业务逻辑正确性
+- **解冻原则**:解冻时必须恢复原始冻结数量
+- **补足机制**:从用户其他可用物品中补足差额
+- **数据一致性**:确保用户总物品数量不变
+
+### 2. 安全性设计
+- **完整日志**:记录所有物品转移操作
+- **事件触发**:通知其他模块物品状态变更
+- **数据保护**:不删除任何历史记录
+
+### 3. 错误处理
+- **数量不足**:用户可用物品不足时给出明确提示
+- **状态检查**:验证冻结状态和数量的合法性
+- **异常安全**:确保操作失败时数据不被破坏
+
+## 影响范围
+
+### 1. 向后兼容性
+- **现有接口不变**:`unfreezeItem` 方法签名保持不变
+- **行为优化**:解冻结果更符合业务预期
+- **新增接口**:`safeUnfreezeItem` 提供更安全的解冻选项
+
+### 2. 性能影响
+- **查询优化**:增加了可用物品的查询操作
+- **事务安全**:所有操作在事务中执行,确保一致性
+- **日志记录**:增加了交易日志的写入操作
+
+## 总结
+
+成功修复了物品解冻BUG,确保:
+1. ✅ **数量正确**:解冻时恢复原始冻结数量
+2. ✅ **逻辑安全**:完整的交易日志和事件触发
+3. ✅ **数据完整**:不删除任何历史记录
+4. ✅ **向后兼容**:现有业务逻辑不受影响
+5. ✅ **测试验证**:通过完整的测试验证
+
+这个修复不仅解决了用户反馈的问题,还提升了整个物品冻结/解冻系统的健壮性和可靠性。

+ 150 - 2
app/Module/GameItems/Logics/ItemFreeze.php

@@ -840,20 +840,94 @@ class ItemFreeze
                 ->get();
 
             $remainingQuantity = $originalFrozenQuantity;
+            $transferDetails = []; // 记录转移详情
+
             foreach ($availableItems as $availableItem) {
                 if ($remainingQuantity <= 0) break;
 
                 $deductQuantity = min($availableItem->quantity, $remainingQuantity);
-                $availableItem->quantity -= $deductQuantity;
+                $oldQuantity = $availableItem->quantity;
+                $newQuantity = $oldQuantity - $deductQuantity;
+
+                // 更新可用物品数量
+                $availableItem->quantity = $newQuantity;
                 $availableItem->save();
 
+                // 记录转移详情
+                $transferDetails[] = [
+                    'from_user_item_id' => $availableItem->id,
+                    'transferred_quantity' => $deductQuantity,
+                    'old_quantity' => $oldQuantity,
+                    'new_quantity' => $newQuantity,
+                ];
+
+                // 记录交易日志(从可用物品扣除)
+                \App\Module\GameItems\Logics\Item::logTransaction(
+                    $frozenItem->user_id,
+                    $frozenItem->item_id,
+                    null,
+                    -$deductQuantity,
+                    TRANSACTION_TYPE::TRADE_OUT,
+                    'safe_unfreeze_compensation',
+                    $freezeLogId,
+                    ["安全解冻补足转移:从可用物品转移 {$deductQuantity} 个到冻结堆"]
+                );
+
+                // 触发物品数量变更事件(可用物品减少)
+                event(new ItemQuantityChanged(
+                    $frozenItem->user_id,
+                    $frozenItem->item_id,
+                    null,
+                    $oldQuantity,
+                    $newQuantity,
+                    $availableItem->id,
+                    false, // 旧冻结状态:未冻结
+                    false, // 新冻结状态:未冻结
+                    [
+                        'action' => 'safe_unfreeze_compensation_transfer_out',
+                        'freeze_log_id' => $freezeLogId,
+                        'transferred_quantity' => $deductQuantity,
+                    ]
+                ));
+
                 $remainingQuantity -= $deductQuantity;
             }
 
             // 恢复冻结堆到原始数量
+            $oldFrozenQuantity = $frozenItem->quantity;
             $frozenItem->quantity = $originalFrozenQuantity;
             $frozenItem->save();
 
+            // 记录交易日志(向冻结堆补足)
+            \App\Module\GameItems\Logics\Item::logTransaction(
+                $frozenItem->user_id,
+                $frozenItem->item_id,
+                $frozenItem->instance_id,
+                $originalFrozenQuantity,
+                TRANSACTION_TYPE::TRADE_IN,
+                'safe_unfreeze_compensation',
+                $freezeLogId,
+                ["安全解冻补足转移:向冻结堆补足 {$originalFrozenQuantity} 个,转移详情:" . json_encode($transferDetails)]
+            );
+
+            // 触发物品数量变更事件(冻结堆增加)
+            event(new ItemQuantityChanged(
+                $frozenItem->user_id,
+                $frozenItem->item_id,
+                $frozenItem->instance_id,
+                $oldFrozenQuantity,
+                $originalFrozenQuantity,
+                $frozenItem->id,
+                true, // 旧冻结状态:已冻结
+                true, // 新冻结状态:已冻结
+                [
+                    'action' => 'safe_unfreeze_compensation_transfer_in',
+                    'freeze_log_id' => $freezeLogId,
+                    'compensated_quantity' => $originalFrozenQuantity,
+                    'transfer_details' => $transferDetails,
+                ]
+            ));
+
             $shortageQuantity = $originalFrozenQuantity;
         } else {
             // 检查是否需要补足差额
@@ -893,19 +967,93 @@ class ItemFreeze
                     ->get();
 
                 $remainingShortage = $shortageQuantity;
+                $transferDetails = []; // 记录转移详情
+
                 foreach ($availableItems as $availableItem) {
                     if ($remainingShortage <= 0) break;
 
                     $deductQuantity = min($availableItem->quantity, $remainingShortage);
-                    $availableItem->quantity -= $deductQuantity;
+                    $oldQuantity = $availableItem->quantity;
+                    $newQuantity = $oldQuantity - $deductQuantity;
+
+                    // 更新可用物品数量
+                    $availableItem->quantity = $newQuantity;
                     $availableItem->save();
 
+                    // 记录转移详情
+                    $transferDetails[] = [
+                        'from_user_item_id' => $availableItem->id,
+                        'transferred_quantity' => $deductQuantity,
+                        'old_quantity' => $oldQuantity,
+                        'new_quantity' => $newQuantity,
+                    ];
+
+                    // 记录交易日志(从可用物品扣除)
+                    \App\Module\GameItems\Logics\Item::logTransaction(
+                        $frozenItem->user_id,
+                        $frozenItem->item_id,
+                        null,
+                        -$deductQuantity,
+                        TRANSACTION_TYPE::TRADE_OUT,
+                        'safe_unfreeze_partial_compensation',
+                        $freezeLogId,
+                        ["安全解冻部分补足转移:从可用物品转移 {$deductQuantity} 个到冻结堆"]
+                    );
+
+                    // 触发物品数量变更事件(可用物品减少)
+                    event(new ItemQuantityChanged(
+                        $frozenItem->user_id,
+                        $frozenItem->item_id,
+                        null,
+                        $oldQuantity,
+                        $newQuantity,
+                        $availableItem->id,
+                        false, // 旧冻结状态:未冻结
+                        false, // 新冻结状态:未冻结
+                        [
+                            'action' => 'safe_unfreeze_partial_compensation_transfer_out',
+                            'freeze_log_id' => $freezeLogId,
+                            'transferred_quantity' => $deductQuantity,
+                        ]
+                    ));
+
                     $remainingShortage -= $deductQuantity;
                 }
 
                 // 将补足的数量加到冻结堆
+                $oldFrozenQuantity = $frozenItem->quantity;
                 $frozenItem->quantity = $originalFrozenQuantity;
                 $frozenItem->save();
+
+                // 记录交易日志(向冻结堆补足)
+                \App\Module\GameItems\Logics\Item::logTransaction(
+                    $frozenItem->user_id,
+                    $frozenItem->item_id,
+                    $frozenItem->instance_id,
+                    $shortageQuantity,
+                    TRANSACTION_TYPE::TRADE_IN,
+                    'safe_unfreeze_partial_compensation',
+                    $freezeLogId,
+                    ["安全解冻部分补足转移:向冻结堆补足 {$shortageQuantity} 个,转移详情:" . json_encode($transferDetails)]
+                );
+
+                // 触发物品数量变更事件(冻结堆增加)
+                event(new ItemQuantityChanged(
+                    $frozenItem->user_id,
+                    $frozenItem->item_id,
+                    $frozenItem->instance_id,
+                    $oldFrozenQuantity,
+                    $originalFrozenQuantity,
+                    $frozenItem->id,
+                    true, // 旧冻结状态:已冻结
+                    true, // 新冻结状态:已冻结
+                    [
+                        'action' => 'safe_unfreeze_partial_compensation_transfer_in',
+                        'freeze_log_id' => $freezeLogId,
+                        'compensated_quantity' => $shortageQuantity,
+                        'transfer_details' => $transferDetails,
+                    ]
+                ));
             }
         }
 

+ 181 - 0
tests/manual_test_unfreeze_events.php

@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * 手动测试脚本:验证解冻过程中的事件触发
+ * 
+ * 测试ItemQuantityChanged事件是否正确触发
+ */
+
+require_once __DIR__ . '/../vendor/autoload.php';
+
+use App\Module\GameItems\Services\ItemService;
+use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
+use App\Module\GameItems\Events\ItemQuantityChanged;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Event;
+
+// 启动Laravel应用
+$app = require_once __DIR__ . '/../bootstrap/app.php';
+$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
+
+echo "=== 物品解冻事件触发测试 ===\n\n";
+
+// 记录触发的事件
+$triggeredEvents = [];
+
+// 监听ItemQuantityChanged事件
+Event::listen(ItemQuantityChanged::class, function ($event) use (&$triggeredEvents) {
+    $triggeredEvents[] = [
+        'user_id' => $event->userId,
+        'item_id' => $event->itemId,
+        'instance_id' => $event->instanceId,
+        'old_quantity' => $event->oldQuantity,
+        'new_quantity' => $event->newQuantity,
+        'user_item_id' => $event->userItemId,
+        'old_frozen_status' => $event->oldFrozenStatus,
+        'new_frozen_status' => $event->newFrozenStatus,
+        'options' => $event->options,
+    ];
+});
+
+try {
+    DB::beginTransaction();
+    
+    $userId = 1001;
+    $itemId = 1;
+    $freezeQuantity = 100;
+    $consumeQuantity = 60;
+    
+    echo "1. 准备测试数据\n";
+    
+    // 添加物品
+    $addResult = ItemService::addItem($userId, $itemId, 300);
+    echo "   添加物品事件数量: " . count($triggeredEvents) . "\n";
+    $triggeredEvents = []; // 清空事件记录
+    
+    // 冻结物品
+    echo "\n2. 冻结物品\n";
+    $freezeResult = ItemService::freezeItem(
+        $userId, 
+        $itemId, 
+        null,
+        $freezeQuantity, 
+        FREEZE_REASON_TYPE::TRADE_ORDER->value,
+        [
+            'source_id' => 12345,
+            'source_type' => 'test_order',
+            'operator_id' => 1
+        ]
+    );
+    $freezeLogId = $freezeResult['frozen_items'][0]['freeze_log_id'];
+    echo "   冻结操作事件数量: " . count($triggeredEvents) . "\n";
+    foreach ($triggeredEvents as $i => $event) {
+        echo "   事件 " . ($i + 1) . ": 用户物品ID={$event['user_item_id']}, 数量变化={$event['old_quantity']}→{$event['new_quantity']}, 冻结状态={$event['old_frozen_status']}→{$event['new_frozen_status']}\n";
+        if (isset($event['options']['action'])) {
+            echo "      动作: {$event['options']['action']}\n";
+        }
+    }
+    $triggeredEvents = []; // 清空事件记录
+    
+    // 消耗部分冻结物品
+    echo "\n3. 消耗部分冻结物品\n";
+    $consumeResult = ItemService::consumeItem(
+        $userId, 
+        $itemId, 
+        null,
+        $consumeQuantity, 
+        [
+            'include_frozen' => true,
+            'source_type' => 'test_consume',
+            'source_id' => 67890
+        ]
+    );
+    echo "   消耗操作事件数量: " . count($triggeredEvents) . "\n";
+    foreach ($triggeredEvents as $i => $event) {
+        echo "   事件 " . ($i + 1) . ": 用户物品ID={$event['user_item_id']}, 数量变化={$event['old_quantity']}→{$event['new_quantity']}, 冻结状态={$event['old_frozen_status']}→{$event['new_frozen_status']}\n";
+    }
+    $triggeredEvents = []; // 清空事件记录
+    
+    // 测试原始解冻方法
+    echo "\n4. 测试原始解冻方法(应该触发补足事件)\n";
+    try {
+        $unfreezeResult = ItemService::unfreezeItem($freezeLogId);
+        echo "   解冻操作事件数量: " . count($triggeredEvents) . "\n";
+        foreach ($triggeredEvents as $i => $event) {
+            echo "   事件 " . ($i + 1) . ": 用户物品ID={$event['user_item_id']}, 数量变化={$event['old_quantity']}→{$event['new_quantity']}, 冻结状态={$event['old_frozen_status']}→{$event['new_frozen_status']}\n";
+            if (isset($event['options']['action'])) {
+                echo "      动作: {$event['options']['action']}\n";
+            }
+            if (isset($event['options']['transferred_quantity'])) {
+                echo "      转移数量: {$event['options']['transferred_quantity']}\n";
+            }
+        }
+        echo "   解冻成功,补足差额: " . ($unfreezeResult['shortage_compensated'] ?? 0) . "\n";
+    } catch (Exception $e) {
+        echo "   解冻失败: " . $e->getMessage() . "\n";
+    }
+    $triggeredEvents = []; // 清空事件记录
+    
+    // 重新冻结并完全消耗,测试安全解冻
+    echo "\n5. 重新冻结并完全消耗,测试安全解冻\n";
+    $freezeResult2 = ItemService::freezeItem(
+        $userId, 
+        $itemId, 
+        null,
+        40, 
+        FREEZE_REASON_TYPE::TRADE_ORDER->value,
+        [
+            'source_id' => 12346,
+            'source_type' => 'test_order_2',
+            'operator_id' => 1
+        ]
+    );
+    $freezeLogId2 = $freezeResult2['frozen_items'][0]['freeze_log_id'];
+    $triggeredEvents = []; // 清空冻结事件记录
+    
+    // 完全消耗
+    $consumeResult2 = ItemService::consumeItem(
+        $userId, 
+        $itemId, 
+        null,
+        40, 
+        [
+            'include_frozen' => true,
+            'source_type' => 'test_consume_all',
+            'source_id' => 67891
+        ]
+    );
+    $triggeredEvents = []; // 清空消耗事件记录
+    
+    // 测试安全解冻
+    echo "   测试安全解冻方法(应该触发补足事件)\n";
+    try {
+        $safeUnfreezeResult = ItemService::safeUnfreezeItem($freezeLogId2);
+        echo "   安全解冻操作事件数量: " . count($triggeredEvents) . "\n";
+        foreach ($triggeredEvents as $i => $event) {
+            echo "   事件 " . ($i + 1) . ": 用户物品ID={$event['user_item_id']}, 数量变化={$event['old_quantity']}→{$event['new_quantity']}, 冻结状态={$event['old_frozen_status']}→{$event['new_frozen_status']}\n";
+            if (isset($event['options']['action'])) {
+                echo "      动作: {$event['options']['action']}\n";
+            }
+            if (isset($event['options']['transferred_quantity'])) {
+                echo "      转移数量: {$event['options']['transferred_quantity']}\n";
+            }
+            if (isset($event['options']['compensated_quantity'])) {
+                echo "      补足数量: {$event['options']['compensated_quantity']}\n";
+            }
+        }
+        echo "   安全解冻结果: " . json_encode($safeUnfreezeResult, JSON_UNESCAPED_UNICODE) . "\n";
+    } catch (Exception $e) {
+        echo "   安全解冻失败: " . $e->getMessage() . "\n";
+    }
+    
+    echo "\n=== 测试完成 ===\n";
+    
+    DB::rollback();
+    echo "已回滚测试数据\n";
+    
+} catch (Exception $e) {
+    DB::rollback();
+    echo "测试失败: " . $e->getMessage() . "\n";
+    echo "堆栈跟踪: " . $e->getTraceAsString() . "\n";
+}