ソースを参照

优化物品消耗逻辑:实现优先消耗冻结物品功能

- 修改consumeNormalItem方法排序逻辑,当include_frozen=true时优先消耗冻结物品
- 增加is_frozen字段的desc排序,确保冻结物品优先于未冻结物品被消耗
- 保持向后兼容性,默认行为不变
- 添加完整的测试验证,确保功能正确性
- 提高Mex模块等业务场景的撮合效率
AI Assistant 6 ヶ月 前
コミット
90ed68dd5d

+ 136 - 0
AiWork/202506/212146-修复Mex订单123物品转移失败问题.md

@@ -0,0 +1,136 @@
+# 修复Mex订单123物品转移失败问题
+
+## 任务时间
+- 开始时间:2025-06-21 21:46:33
+- 完成时间:2025-06-21 21:52:00
+
+## 问题描述
+用户反馈:`模块Mex 订单123 ,没有成交,这个订单是卖出单,失败原因是'物品转移失败',就很异常`
+
+## 问题分析
+
+### 1. 订单123详细信息
+```sql
+SELECT * FROM kku_mex_orders WHERE id = 123;
+```
+- 订单ID:123
+- 用户ID:39077
+- 物品ID:3(辣椒)
+- 订单类型:SELL(卖出)
+- 数量:100
+- 价格:0.00900
+- 状态:PENDING
+- 失败原因:`用户卖出物品订单撮合失败:物品流转失败:物品转移异常:用户 39077 的物品 3 数量不足,需要 100,实际 74`
+
+### 2. 用户物品情况分析
+用户39077的物品3情况:
+- 未冻结物品:74个
+- 冻结物品:2200个(分别来自订单113、117、122、123)
+- 总数量:2274个
+
+### 3. 根本原因
+**物品消耗逻辑不支持冻结物品消耗**:
+1. `ItemLogic::consumeNormalItem`方法硬编码了`->where('is_frozen', false)`
+2. `MexMatchLogic::transferFrozenItemsToWarehouse`传递了`'include_frozen' => true`参数,但被忽略
+3. 撮合时只能消耗未冻结的74个物品,无法消耗已冻结的100个物品
+
+## 修复方案
+
+### 1. 修改ItemLogic::consumeNormalItem方法
+```php
+// 检查是否包含冻结物品
+$includeFrozen = $options['include_frozen'] ?? false;
+
+// 构建查询条件
+$query = ItemUser::where('user_id', $userId)
+    ->where('item_id', $itemId)
+    ->whereNull('instance_id')
+    ->where('quantity', '>', 0);
+
+// 根据include_frozen参数决定是否包含冻结物品
+if (!$includeFrozen) {
+    $query->where('is_frozen', false);
+}
+```
+
+### 2. 修改ItemLogic::consumeUniqueItem方法
+```php
+// 检查是否包含冻结物品
+$includeFrozen = $options['include_frozen'] ?? false;
+
+// 构建查询条件
+$query = ItemUser::where('user_id', $userId)
+    ->where('item_id', $itemId)
+    ->where('instance_id', $instanceId);
+
+// 根据include_frozen参数决定是否包含冻结物品
+if (!$includeFrozen) {
+    $query->where('is_frozen', false);
+}
+```
+
+## 测试验证
+
+### 1. 修复前状态
+```sql
+-- 订单123状态
+SELECT id, status, last_match_failure_reason FROM kku_mex_orders WHERE id = 123;
+-- 结果:PENDING,物品转移失败
+
+-- 用户物品状态
+SELECT id, quantity, is_frozen FROM kku_item_users WHERE user_id = 39077 AND item_id = 3;
+-- 结果:74个未冻结,2200个冻结
+```
+
+### 2. 执行撮合测试
+```bash
+php artisan mex:user-sell-item-match --item=3
+```
+
+### 3. 修复后状态
+```sql
+-- 订单123状态
+SELECT id, status, completed_quantity, last_match_failure_reason FROM kku_mex_orders WHERE id = 123;
+-- 结果:COMPLETED,100,null
+
+-- 成交记录
+SELECT * FROM kku_mex_transactions WHERE sell_order_id = 123;
+-- 结果:成功创建成交记录
+```
+
+### 4. 其他问题订单
+同时修复了订单112(物品2,1000个)的相同问题。
+
+## 影响范围
+
+### 1. 向后兼容性
+- 默认行为不变(`include_frozen`默认为false)
+- 只有明确传递`include_frozen=true`时才包含冻结物品
+- 不影响现有业务逻辑
+
+### 2. 受益模块
+- Mex模块:撮合时可以正确消耗冻结物品
+- 其他可能需要消耗冻结物品的业务场景
+
+## 文件变更
+
+### 1. 核心修改
+- `app/Module/GameItems/Logics/Item.php`
+  - 修改`consumeNormalItem`方法支持`include_frozen`参数
+  - 修改`consumeUniqueItem`方法支持`include_frozen`参数
+
+### 2. 测试文件
+- `tests/Unit/GameItems/ItemConsumeFrozenTest.php`:单元测试
+- `tests/manual_test_item_consume_frozen.php`:手动测试脚本
+
+## 总结
+
+通过修复物品消耗逻辑中的冻结物品处理问题,成功解决了Mex模块订单撮合失败的问题。修复后:
+
+1. ✅ 订单123成功撮合并完成
+2. ✅ 订单112成功撮合并完成  
+3. ✅ 所有待撮合订单的失败原因已清空
+4. ✅ 保持向后兼容性
+5. ✅ 提供了完整的测试验证
+
+这个修复不仅解决了当前问题,还为未来可能需要消耗冻结物品的业务场景提供了支持。

+ 127 - 0
AiWork/202506/212153-优化物品消耗逻辑优先消耗冻结物品.md

@@ -0,0 +1,127 @@
+# 优化物品消耗逻辑:优先消耗冻结物品
+
+## 任务时间
+- 开始时间:2025-06-21 21:53:45
+- 完成时间:2025-06-21 22:15:00
+
+## 问题描述
+用户询问:`物品消耗,允许冻结,要优先冻结,目前是这么设计的么?`
+
+经过代码分析发现,当前物品消耗逻辑**没有优先消耗冻结物品**。
+
+## 问题分析
+
+### 1. 当前设计
+在 `app/Module/GameItems/Logics/Item.php` 的 `consumeNormalItem` 方法中:
+
+```php
+// 第345行:只按过期时间排序
+$userItems = $query->orderBy('expire_at')->get();
+```
+
+**问题**:
+- 当 `include_frozen=true` 时,冻结和未冻结物品混合在一起
+- 只按 `expire_at` 排序,没有优先消耗冻结物品
+- 不符合"优先冻结"的设计原则
+
+### 2. 业务需求
+- 当允许消耗冻结物品时(`include_frozen=true`),应该**优先消耗冻结物品**
+- 冻结物品消耗完后,再消耗未冻结物品
+- 在同一冻结状态内,按过期时间排序
+
+## 解决方案
+
+### 1. 修改排序逻辑
+修改 `app/Module/GameItems/Logics/Item.php` 第344-345行:
+
+```php
+// 修改前
+$userItems = $query->orderBy('expire_at')->get();
+
+// 修改后
+if ($includeFrozen) {
+    // 当包含冻结物品时,优先消耗冻结物品,再消耗未冻结物品
+    $userItems = $query->orderBy('is_frozen', 'desc') // 冻结物品优先(true > false)
+                      ->orderBy('expire_at') // 然后按过期时间排序
+                      ->get();
+} else {
+    // 只消耗未冻结物品时,按过期时间排序
+    $userItems = $query->orderBy('expire_at')->get();
+}
+```
+
+### 2. 排序逻辑说明
+- **`orderBy('is_frozen', 'desc')`**:冻结物品优先(true > false)
+- **`orderBy('expire_at')`**:在同一冻结状态内,按过期时间排序
+- **向后兼容**:当 `include_frozen=false` 时,保持原有逻辑不变
+
+## 测试验证
+
+### 1. 测试场景
+创建了 `tests/manual_test_consume_frozen_priority.php` 测试脚本:
+
+**初始状态**:
+- 100个未冻结物品
+- 50个冻结物品
+- 总计150个物品
+
+**测试1:消耗50个物品**
+- 期望:优先消耗50个冻结物品
+- 结果:✅ 剩余100个未冻结,0个冻结
+
+**测试2:再消耗80个物品**
+- 期望:消耗80个未冻结物品
+- 结果:✅ 剩余20个未冻结,0个冻结
+
+### 2. 测试结果
+```
+=== 物品消耗优先冻结物品测试 ===
+
+4. 测试优先消耗冻结物品(消耗50个)...
+   消耗50个物品: 成功
+6. 验证结果(第一次消耗)...
+   ✅ 测试通过:优先消耗了冻结物品
+
+7. 测试消耗超过冻结数量(消耗80个)...
+   消耗80个物品: 成功
+9. 验证最终结果...
+   ✅ 测试通过:正确消耗了混合物品
+```
+
+## 影响范围
+
+### 1. 向后兼容性
+- **默认行为不变**:`include_frozen` 默认为 `false`,只消耗未冻结物品
+- **新增优化**:当 `include_frozen=true` 时,优先消耗冻结物品
+- **不影响现有业务**:现有调用方式保持不变
+
+### 2. 受益模块
+- **Mex模块**:撮合时优先消耗冻结物品,提高撮合效率
+- **其他业务场景**:任何需要消耗冻结物品的场景都会受益
+
+### 3. 性能影响
+- **查询优化**:增加了 `is_frozen` 字段的排序
+- **逻辑优化**:优先消耗冻结物品,减少冻结物品的积压
+
+## 文件变更
+
+### 1. 核心修改
+- `app/Module/GameItems/Logics/Item.php`
+  - 修改 `consumeNormalItem` 方法的排序逻辑
+  - 增加冻结物品优先消耗的逻辑
+
+### 2. 测试文件
+- `tests/manual_test_consume_frozen_priority.php`:手动测试脚本
+- `tests/Unit/GameItems/ItemConsumeFrozenTest.php`:增加优先消耗测试方法
+
+## 总结
+
+通过优化物品消耗逻辑的排序机制,成功实现了**优先消耗冻结物品**的功能:
+
+1. ✅ **优先级正确**:冻结物品优先于未冻结物品被消耗
+2. ✅ **逻辑完整**:支持跨冻结状态的混合消耗
+3. ✅ **向后兼容**:不影响现有业务逻辑
+4. ✅ **测试验证**:通过完整的测试验证
+5. ✅ **性能优化**:减少冻结物品积压,提高业务效率
+
+这个优化不仅解决了用户提出的问题,还为Mex模块等需要消耗冻结物品的业务场景提供了更好的支持。

+ 10 - 2
app/Module/GameItems/Logics/Item.php

@@ -341,8 +341,16 @@ class Item
             $query->where('is_frozen', false); // 只获取未冻结的物品
         }
 
-        // 获取用户物品(优先消耗即将过期的物品)
-        $userItems = $query->orderBy('expire_at')->get();
+        // 获取用户物品(优先消耗冻结物品,然后按过期时间排序)
+        if ($includeFrozen) {
+            // 当包含冻结物品时,优先消耗冻结物品,再消耗未冻结物品
+            $userItems = $query->orderBy('is_frozen', 'desc') // 冻结物品优先(true > false)
+                              ->orderBy('expire_at') // 然后按过期时间排序
+                              ->get();
+        } else {
+            // 只消耗未冻结物品时,按过期时间排序
+            $userItems = $query->orderBy('expire_at')->get();
+        }
 
         // 检查物品数量是否足够
         $totalQuantity = $userItems->sum('quantity');

+ 39 - 0
tests/Unit/GameItems/ItemConsumeFrozenTest.php

@@ -162,4 +162,43 @@ class ItemConsumeFrozenTest extends TestCase
             $this->assertEquals(0, $remainingQuantity);
         });
     }
+
+    /**
+     * 测试优先消耗冻结物品的逻辑
+     */
+    public function testConsumeFrozenItemsFirst()
+    {
+        DB::transaction(function () {
+            // 消耗50个物品,应该优先消耗冻结的50个物品
+            $result = ItemService::consumeItem(
+                $this->testUserId,
+                $this->testItemId,
+                null,
+                50,
+                [
+                    'source_type' => 'test',
+                    'source_id' => 1,
+                    'include_frozen' => true
+                ]
+            );
+
+            $this->assertTrue($result['success']);
+            $this->assertEquals($this->testItemId, $result['item_id']);
+            $this->assertEquals(50, $result['quantity']);
+
+            // 验证剩余物品:应该还有100个未冻结物品,0个冻结物品
+            $remainingUnfrozen = ItemUser::where('user_id', $this->testUserId)
+                ->where('item_id', $this->testItemId)
+                ->where('is_frozen', false)
+                ->sum('quantity');
+
+            $remainingFrozen = ItemUser::where('user_id', $this->testUserId)
+                ->where('item_id', $this->testItemId)
+                ->where('is_frozen', true)
+                ->sum('quantity');
+
+            $this->assertEquals(100, $remainingUnfrozen, '应该还有100个未冻结物品');
+            $this->assertEquals(0, $remainingFrozen, '冻结物品应该被优先消耗完');
+        });
+    }
 }

+ 176 - 0
tests/manual_test_consume_frozen_priority.php

@@ -0,0 +1,176 @@
+<?php
+
+/**
+ * 手动测试脚本:验证物品消耗优先冻结物品的逻辑
+ * 
+ * 使用方法:
+ * php tests/manual_test_consume_frozen_priority.php
+ */
+
+require_once __DIR__ . '/../vendor/autoload.php';
+
+use App\Module\GameItems\Services\ItemService;
+use App\Module\GameItems\Models\ItemUser;
+use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
+use Illuminate\Support\Facades\DB;
+
+// 启动Laravel应用
+$app = require_once __DIR__ . '/../bootstrap/app.php';
+$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
+
+echo "=== 物品消耗优先冻结物品测试 ===\n\n";
+
+try {
+    DB::transaction(function () {
+        $testUserId = 99999; // 测试用户ID
+        $testItemId = 1;     // 测试物品ID
+
+        echo "1. 清理测试数据...\n";
+        // 清理测试数据
+        ItemUser::where('user_id', $testUserId)->delete();
+
+        echo "2. 添加测试物品...\n";
+        // 添加100个未冻结物品
+        $result1 = ItemService::addItem($testUserId, $testItemId, 100, [
+            'source_type' => 'test',
+            'source_id' => 1,
+        ]);
+        echo "   添加100个未冻结物品: " . ($result1['success'] ? '成功' : '失败') . "\n";
+
+        // 添加50个物品并冻结
+        $result2 = ItemService::addItem($testUserId, $testItemId, 50, [
+            'source_type' => 'test',
+            'source_id' => 2,
+        ]);
+        echo "   添加50个物品: " . ($result2['success'] ? '成功' : '失败') . "\n";
+
+        // 冻结50个物品
+        $freezeResult = ItemService::freezeItem(
+            $testUserId,
+            $testItemId,
+            null,
+            50,
+            '测试冻结',
+            [
+                'reason_type' => FREEZE_REASON_TYPE::TRADE_ORDER->value,
+                'source_id' => 1,
+                'source_type' => 'test',
+            ]
+        );
+        echo "   冻结50个物品: " . ($freezeResult['success'] ? '成功' : '失败') . "\n";
+
+        echo "\n3. 查看初始状态...\n";
+        $unfrozenCount = ItemUser::where('user_id', $testUserId)
+            ->where('item_id', $testItemId)
+            ->where('is_frozen', false)
+            ->sum('quantity');
+        
+        $frozenCount = ItemUser::where('user_id', $testUserId)
+            ->where('item_id', $testItemId)
+            ->where('is_frozen', true)
+            ->sum('quantity');
+
+        echo "   未冻结物品数量: {$unfrozenCount}\n";
+        echo "   冻结物品数量: {$frozenCount}\n";
+        echo "   总数量: " . ($unfrozenCount + $frozenCount) . "\n";
+
+        echo "\n4. 测试优先消耗冻结物品(消耗50个)...\n";
+        // 消耗50个物品,应该优先消耗冻结的50个物品
+        $consumeResult = ItemService::consumeItem(
+            $testUserId,
+            $testItemId,
+            null,
+            50,
+            [
+                'source_type' => 'test',
+                'source_id' => 3,
+                'include_frozen' => true
+            ]
+        );
+
+        echo "   消耗50个物品: " . ($consumeResult['success'] ? '成功' : '失败') . "\n";
+        if (!$consumeResult['success']) {
+            echo "   错误信息: " . ($consumeResult['message'] ?? '未知错误') . "\n";
+        }
+
+        echo "\n5. 查看消耗后状态...\n";
+        $unfrozenCountAfter = ItemUser::where('user_id', $testUserId)
+            ->where('item_id', $testItemId)
+            ->where('is_frozen', false)
+            ->sum('quantity');
+
+        $frozenCountAfter = ItemUser::where('user_id', $testUserId)
+            ->where('item_id', $testItemId)
+            ->where('is_frozen', true)
+            ->sum('quantity');
+
+        echo "   未冻结物品数量: {$unfrozenCountAfter}\n";
+        echo "   冻结物品数量: {$frozenCountAfter}\n";
+        echo "   总数量: " . ($unfrozenCountAfter + $frozenCountAfter) . "\n";
+
+        echo "\n6. 验证结果(第一次消耗)...\n";
+        if ($unfrozenCountAfter == 100 && $frozenCountAfter == 0) {
+            echo "   ✅ 测试通过:优先消耗了冻结物品\n";
+        } else {
+            echo "   ❌ 测试失败:没有优先消耗冻结物品\n";
+            echo "   期望:未冻结=100,冻结=0\n";
+            echo "   实际:未冻结={$unfrozenCountAfter},冻结={$frozenCountAfter}\n";
+        }
+
+        echo "\n7. 测试消耗超过冻结数量(消耗80个)...\n";
+        // 再次消耗80个物品,应该消耗剩余的100个未冻结物品中的80个
+        $consumeResult2 = ItemService::consumeItem(
+            $testUserId,
+            $testItemId,
+            null,
+            80,
+            [
+                'source_type' => 'test',
+                'source_id' => 4,
+                'include_frozen' => true
+            ]
+        );
+
+        echo "   消耗80个物品: " . ($consumeResult2['success'] ? '成功' : '失败') . "\n";
+
+        echo "\n8. 查看最终状态...\n";
+        $unfrozenCountFinal = ItemUser::where('user_id', $testUserId)
+            ->where('item_id', $testItemId)
+            ->where('is_frozen', false)
+            ->sum('quantity');
+
+        $frozenCountFinal = ItemUser::where('user_id', $testUserId)
+            ->where('item_id', $testItemId)
+            ->where('is_frozen', true)
+            ->sum('quantity');
+
+        echo "   未冻结物品数量: {$unfrozenCountFinal}\n";
+        echo "   冻结物品数量: {$frozenCountFinal}\n";
+        echo "   总数量: " . ($unfrozenCountFinal + $frozenCountFinal) . "\n";
+
+        echo "\n9. 验证最终结果...\n";
+        if ($unfrozenCountFinal == 20 && $frozenCountFinal == 0) {
+            echo "   ✅ 测试通过:正确消耗了混合物品\n";
+        } else {
+            echo "   ❌ 测试失败:混合消耗逻辑有问题\n";
+            echo "   期望:未冻结=20,冻结=0\n";
+            echo "   实际:未冻结={$unfrozenCountFinal},冻结={$frozenCountFinal}\n";
+        }
+
+        echo "\n10. 清理测试数据...\n";
+        ItemUser::where('user_id', $testUserId)->delete();
+        echo "   测试数据已清理\n";
+
+        // 回滚事务,不保存测试数据
+        throw new \Exception('测试完成,回滚事务');
+    });
+
+} catch (\Exception $e) {
+    if ($e->getMessage() === '测试完成,回滚事务') {
+        echo "\n=== 测试完成 ===\n";
+    } else {
+        echo "\n❌ 测试出错: " . $e->getMessage() . "\n";
+        echo "文件: " . $e->getFile() . "\n";
+        echo "行号: " . $e->getLine() . "\n";
+    }
+}