فهرست منبع

实现GameItems模块物品冻结功能

- 新增FREEZE_ACTION_TYPE和FREEZE_REASON_TYPE枚举
- 创建ItemFreezeLog模型和数据库表
- 扩展ItemUser模型支持冻结状态
- 实现ItemFreeze逻辑类,支持拆堆冻结和独立解冻
- 扩展ItemService服务类,提供冻结相关API
- 修改现有消耗和添加逻辑,确保排除冻结物品
- 添加完整的测试用例和手动测试脚本
- 支持统一属性和单独属性物品的冻结/解冻
- 实现批量冻结、冻结统计等高级功能
- 完善数据库索引优化查询性能
notfff 7 ماه پیش
والد
کامیت
38e9445400

+ 10 - 0
app/Module/GameItems/Databases/GenerateSql/alter_item_users_add_freeze_fields.sql

@@ -0,0 +1,10 @@
+-- 为item_users表添加冻结相关字段
+-- 支持物品冻结状态管理和冻结日志关联
+
+ALTER TABLE `kku_item_users`
+ADD COLUMN `is_frozen` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否冻结(0:未冻结, 1:已冻结)',
+ADD COLUMN `frozen_log_id` int DEFAULT NULL COMMENT '冻结日志ID,关联kku_item_freeze_logs表',
+ADD INDEX `idx_frozen_status` (`user_id`, `is_frozen`),
+ADD INDEX `idx_frozen_log` (`frozen_log_id`),
+ADD INDEX `idx_user_item_frozen` (`user_id`, `item_id`, `is_frozen`),
+ADD INDEX `idx_expire_frozen` (`expire_at`, `is_frozen`);

+ 25 - 0
app/Module/GameItems/Databases/GenerateSql/item_freeze_logs.sql

@@ -0,0 +1,25 @@
+-- 物品冻结记录表
+-- 用于记录物品冻结和解冻操作的详细日志
+-- 支持追踪冻结原因、操作来源和操作员信息
+
+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 '操作时间',
+  `updated_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`),
+  KEY `idx_action_type` (`action_type`),
+  KEY `idx_user_action` (`user_id`, `action_type`),
+  KEY `idx_instance` (`instance_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品冻结记录表';

+ 190 - 0
app/Module/GameItems/Docs/冻结功能实现完成.md

@@ -0,0 +1,190 @@
+# GameItem模块 - 物品冻结功能实现完成
+
+## 实现概述
+
+物品冻结功能已成功实现,支持统一属性物品和单独属性物品的冻结/解冻操作,采用拆堆模式确保数据一致性。
+
+## 已实现的功能
+
+### 1. 核心数据结构
+
+#### 1.1 枚举类型
+- **FREEZE_ACTION_TYPE**: 冻结操作类型枚举(冻结/解冻)
+- **FREEZE_REASON_TYPE**: 冻结原因类型枚举(交易订单、管理员冻结、系统冻结等)
+
+#### 1.2 数据模型
+- **ItemFreezeLog**: 冻结记录模型,记录所有冻结/解冻操作
+- **ItemUser**: 扩展了冻结相关字段(is_frozen、frozen_log_id)
+
+#### 1.3 数据库表结构
+- **kku_item_freeze_logs**: 冻结记录表
+- **kku_item_users**: 添加了冻结状态字段和相关索引
+
+### 2. 核心业务逻辑
+
+#### 2.1 ItemFreeze逻辑类
+- `freezeNormalItem()`: 冻结统一属性物品(拆堆模式)
+- `freezeUniqueItem()`: 冻结单独属性物品
+- `unfreezeByLogId()`: 通过冻结日志ID解冻物品
+- `getAvailableQuantity()`: 获取可用数量(排除冻结)
+- `batchFreezeItems()`: 批量冻结操作
+- `getFrozenItems()`: 获取冻结物品列表
+- `getFreezeStatistics()`: 获取冻结统计信息
+
+#### 2.2 ItemService服务类扩展
+- `freezeItem()`: 冻结物品服务接口
+- `unfreezeItem()`: 解冻物品服务接口
+- `getAvailableQuantity()`: 获取可用数量服务接口
+- `getFrozenItems()`: 获取冻结物品服务接口
+- `batchFreezeItems()`: 批量冻结服务接口
+- `getFreezeStatistics()`: 获取冻结统计服务接口
+
+### 3. 集成改进
+
+#### 3.1 现有逻辑修改
+- **物品消耗逻辑**: 修改了`consumeNormalItem()`和`consumeUniqueItem()`方法,确保只消耗未冻结的物品
+- **物品添加逻辑**: 修改了`addNormalItem()`方法,确保查找可堆叠物品时排除冻结的物品
+
+#### 3.2 数据查询优化
+- 添加了多个索引以优化冻结状态查询性能
+- 实现了可用数量的快速查询方法
+
+## 核心特性
+
+### 1. 拆堆模式冻结
+- 冻结时将原堆叠拆分为冻结部分和可用部分
+- 例如:1000个物品冻结200个 → 200个(冻结)+ 800个(可用)
+- 确保冻结操作的精确性和可追溯性
+
+### 2. 独立解冻机制
+- 解冻后不自动合并堆叠,保持独立状态
+- 通过frozen_log_id精确定位需要解冻的物品
+- 支持部分解冻和批量解冻操作
+
+### 3. 完整的日志追踪
+- 记录所有冻结/解冻操作的详细信息
+- 支持按原因、来源类型、操作员等维度查询
+- 提供完整的操作审计轨迹
+
+### 4. 业务场景支持
+- **交易订单**: 卖出订单时自动冻结物品
+- **管理员操作**: 支持管理员手动冻结/解冻
+- **系统冻结**: 支持系统自动冻结(如异常检测)
+- **拍卖系统**: 支持拍卖物品冻结
+- **任务系统**: 支持任务物品冻结
+
+## 使用示例
+
+### 1. 冻结统一属性物品
+```php
+use App\Module\GameItems\Services\ItemService;
+use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
+
+// 冻结用户的物品
+$result = ItemService::freezeItem(
+    $userId,
+    $itemId,
+    null, // 统一属性物品
+    20,   // 冻结数量
+    '交易订单冻结',
+    [
+        'reason_type' => FREEZE_REASON_TYPE::TRADE_ORDER->value,
+        'source_id' => $orderId,
+        'source_type' => 'order'
+    ]
+);
+```
+
+### 2. 解冻物品
+```php
+// 通过冻结日志ID解冻
+$result = ItemService::unfreezeItem($freezeLogId);
+```
+
+### 3. 查询可用数量
+```php
+// 获取用户可用物品数量(排除冻结的)
+$availableQuantity = ItemService::getAvailableQuantity($userId, $itemId);
+```
+
+### 4. 批量冻结
+```php
+$items = [
+    ['item_id' => 1001, 'quantity' => 10],
+    ['item_id' => 1002, 'quantity' => 5],
+];
+
+$result = ItemService::batchFreezeItems(
+    $userId,
+    $items,
+    '系统批量冻结',
+    ['reason_type' => FREEZE_REASON_TYPE::SYSTEM_FREEZE->value]
+);
+```
+
+## 数据库变更
+
+### 1. 新增表
+```sql
+-- 冻结记录表
+CREATE TABLE `kku_item_freeze_logs` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `user_id` int NOT NULL,
+  `item_id` int NOT NULL,
+  `instance_id` int DEFAULT NULL,
+  `quantity` int NOT NULL,
+  `action_type` tinyint NOT NULL,
+  `reason` varchar(255) NOT NULL,
+  `source_id` int DEFAULT NULL,
+  `source_type` varchar(50) DEFAULT NULL,
+  `operator_id` int DEFAULT NULL,
+  `created_at` timestamp NULL DEFAULT NULL,
+  `updated_at` timestamp NULL DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  -- 索引省略...
+);
+```
+
+### 2. 修改表
+```sql
+-- 为item_users表添加冻结字段
+ALTER TABLE `kku_item_users`
+ADD COLUMN `is_frozen` tinyint(1) NOT NULL DEFAULT 0,
+ADD COLUMN `frozen_log_id` int DEFAULT NULL,
+-- 索引省略...
+```
+
+## 测试覆盖
+
+已创建完整的测试用例覆盖以下场景:
+- 统一属性物品冻结/解冻
+- 单独属性物品冻结/解冻
+- 消耗物品时排除冻结物品
+- 批量冻结操作
+- 冻结统计信息查询
+
+## 性能考虑
+
+1. **索引优化**: 添加了针对冻结状态查询的复合索引
+2. **查询优化**: 可用数量查询使用聚合函数,避免大量数据传输
+3. **事务保护**: 所有冻结/解冻操作都在事务中执行,确保数据一致性
+
+## 注意事项
+
+1. **事务要求**: 所有冻结/解冻操作必须在事务中执行
+2. **数量验证**: 冻结前会验证可用数量是否足够
+3. **日志完整性**: 每次冻结/解冻操作都会记录详细日志
+4. **解冻独立性**: 解冻后的物品保持独立,不会自动合并堆叠
+
+## 后续扩展
+
+1. **自动解冻**: 可以根据业务需求添加定时解冻机制
+2. **冻结过期**: 可以为冻结操作添加过期时间
+3. **冻结通知**: 可以添加冻结/解冻的事件通知机制
+4. **管理界面**: 可以在后台管理系统中添加冻结管理界面
+
+---
+
+**实现完成时间**: 2025年06月12日  
+**版本**: v1.0  
+**状态**: 已完成并测试通过

+ 78 - 0
app/Module/GameItems/Enums/FREEZE_ACTION_TYPE.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace App\Module\GameItems\Enums;
+
+use UCore\Enum\EnumCore;
+use UCore\Enum\EnumExpression;
+use UCore\Enum\EnumToInt;
+
+/**
+ * 物品冻结操作类型枚举
+ *
+ * 定义了物品冻结操作的类型,用于记录冻结和解冻操作。
+ * 该枚举用于ItemFreezeLog表中的action_type字段,
+ * 帮助追踪物品冻结状态的变更历史。
+ */
+enum FREEZE_ACTION_TYPE: int
+{
+    use EnumCore, EnumExpression, EnumToInt;
+
+    /**
+     * 冻结操作
+     */
+    case FREEZE = 1;
+
+    /**
+     * 解冻操作
+     */
+    case UNFREEZE = 2;
+
+    /**
+     * 获取操作类型名称
+     *
+     * @param int $value 操作类型值
+     * @return string 操作类型名称
+     */
+    public static function getName(int $value): string
+    {
+        $map = [
+            self::FREEZE->value => '冻结',
+            self::UNFREEZE->value => '解冻',
+        ];
+
+        return $map[$value] ?? '未知操作';
+    }
+
+    /**
+     * 获取所有操作类型
+     *
+     * @return array 操作类型映射数组
+     */
+    public static function getAll(): array
+    {
+        return [
+            self::FREEZE->value => self::FREEZE->getName(self::FREEZE->value),
+            self::UNFREEZE->value => self::UNFREEZE->getName(self::UNFREEZE->value),
+        ];
+    }
+
+    /**
+     * 判断是否为冻结操作
+     *
+     * @return bool
+     */
+    public function isFreeze(): bool
+    {
+        return $this === self::FREEZE;
+    }
+
+    /**
+     * 判断是否为解冻操作
+     *
+     * @return bool
+     */
+    public function isUnfreeze(): bool
+    {
+        return $this === self::UNFREEZE;
+    }
+}

+ 145 - 0
app/Module/GameItems/Enums/FREEZE_REASON_TYPE.php

@@ -0,0 +1,145 @@
+<?php
+
+namespace App\Module\GameItems\Enums;
+
+use UCore\Enum\EnumCore;
+use UCore\Enum\EnumExpression;
+use UCore\Enum\EnumToInt;
+
+/**
+ * 物品冻结原因类型枚举
+ *
+ * 定义了物品冻结的各种原因类型,用于标识冻结操作的业务场景。
+ * 不同的冻结原因可能有不同的处理逻辑和解冻条件,
+ * 该枚举帮助系统区分和管理不同类型的冻结操作。
+ */
+enum FREEZE_REASON_TYPE: int
+{
+    use EnumCore, EnumExpression, EnumToInt;
+
+    /**
+     * 交易订单冻结
+     * 用于匹配交易系统中卖出订单的物品冻结
+     */
+    case TRADE_ORDER = 1;
+
+    /**
+     * 管理员冻结
+     * 管理员手动冻结用户物品
+     */
+    case ADMIN_FREEZE = 2;
+
+    /**
+     * 系统冻结
+     * 系统自动冻结,如异常检测等
+     */
+    case SYSTEM_FREEZE = 3;
+
+    /**
+     * 拍卖冻结
+     * 拍卖系统中物品上架时的冻结
+     */
+    case AUCTION = 4;
+
+    /**
+     * 邮件附件冻结
+     * 邮件系统中附件物品的冻结
+     */
+    case MAIL_ATTACHMENT = 5;
+
+    /**
+     * 任务冻结
+     * 任务系统中需要提交物品时的冻结
+     */
+    case QUEST_FREEZE = 6;
+
+    /**
+     * 合成冻结
+     * 物品合成过程中材料的冻结
+     */
+    case CRAFT_FREEZE = 7;
+
+    /**
+     * 获取冻结原因名称
+     *
+     * @param int $value 冻结原因值
+     * @return string 冻结原因名称
+     */
+    public static function getName(int $value): string
+    {
+        $map = [
+            self::TRADE_ORDER->value => '交易订单',
+            self::ADMIN_FREEZE->value => '管理员冻结',
+            self::SYSTEM_FREEZE->value => '系统冻结',
+            self::AUCTION->value => '拍卖',
+            self::MAIL_ATTACHMENT->value => '邮件附件',
+            self::QUEST_FREEZE->value => '任务冻结',
+            self::CRAFT_FREEZE->value => '合成冻结',
+        ];
+
+        return $map[$value] ?? '未知原因';
+    }
+
+    /**
+     * 获取所有冻结原因
+     *
+     * @return array 冻结原因映射数组
+     */
+    public static function getAll(): array
+    {
+        return [
+            self::TRADE_ORDER->value => self::TRADE_ORDER->getName(self::TRADE_ORDER->value),
+            self::ADMIN_FREEZE->value => self::ADMIN_FREEZE->getName(self::ADMIN_FREEZE->value),
+            self::SYSTEM_FREEZE->value => self::SYSTEM_FREEZE->getName(self::SYSTEM_FREEZE->value),
+            self::AUCTION->value => self::AUCTION->getName(self::AUCTION->value),
+            self::MAIL_ATTACHMENT->value => self::MAIL_ATTACHMENT->getName(self::MAIL_ATTACHMENT->value),
+            self::QUEST_FREEZE->value => self::QUEST_FREEZE->getName(self::QUEST_FREEZE->value),
+            self::CRAFT_FREEZE->value => self::CRAFT_FREEZE->getName(self::CRAFT_FREEZE->value),
+        ];
+    }
+
+    /**
+     * 判断是否为用户操作相关的冻结
+     *
+     * @return bool
+     */
+    public function isUserOperation(): bool
+    {
+        return in_array($this, [
+            self::TRADE_ORDER,
+            self::AUCTION,
+            self::MAIL_ATTACHMENT,
+            self::QUEST_FREEZE,
+            self::CRAFT_FREEZE,
+        ]);
+    }
+
+    /**
+     * 判断是否为管理员操作
+     *
+     * @return bool
+     */
+    public function isAdminOperation(): bool
+    {
+        return in_array($this, [
+            self::ADMIN_FREEZE,
+            self::SYSTEM_FREEZE,
+        ]);
+    }
+
+    /**
+     * 判断是否需要自动解冻
+     * 某些类型的冻结可能需要在特定条件下自动解冻
+     *
+     * @return bool
+     */
+    public function needsAutoUnfreeze(): bool
+    {
+        return in_array($this, [
+            self::TRADE_ORDER,
+            self::AUCTION,
+            self::QUEST_FREEZE,
+            self::CRAFT_FREEZE,
+        ]);
+    }
+}

+ 7 - 3
app/Module/GameItems/Logics/Item.php

@@ -82,7 +82,7 @@ class Item
         // 获取来源信息
         $sourceType = $options['source_type'] ?? null;
         $sourceId = $options['source_id'] ?? null;
-            // 检查用户是否已有该物品且过期时间相同,并且未满堆叠
+            // 检查用户是否已有该物品且过期时间相同,并且未满堆叠(排除冻结的物品)
             $userItem = ItemUser::where('user_id', $userId)
                 ->where('item_id', $itemId)
                 ->where(function ($query) use ($expireAt) {
@@ -93,6 +93,7 @@ class Item
                     }
                 })
                 ->whereNull('instance_id')
+                ->where('is_frozen', false) // 排除冻结的物品
                 ->where(function ($query) use ($item) {
                     // 如果有最大堆叠限制,只查找未满的堆叠
                     if ($item->max_stack > 0) {
@@ -322,10 +323,12 @@ class Item
     {
         Helper::check_tr();
 
-        // 获取用户物品
+        // 获取用户物品(排除冻结的物品)
         $userItems = ItemUser::where('user_id', $userId)
             ->where('item_id', $itemId)
             ->whereNull('instance_id')
+            ->where('is_frozen', false) // 只获取未冻结的物品
+            ->where('quantity', '>', 0) // 确保数量大于0
             ->orderBy('expire_at') // 优先消耗即将过期的物品
             ->get();
 
@@ -443,10 +446,11 @@ class Item
     {
         Helper::check_tr();
 
-        // 获取用户物品
+        // 获取用户物品(确保未冻结)
         $userItem = ItemUser::where('user_id', $userId)
             ->where('item_id', $itemId)
             ->where('instance_id', $instanceId)
+            ->where('is_frozen', false) // 只获取未冻结的物品
             ->first();
 
         if (!$userItem) {

+ 528 - 0
app/Module/GameItems/Logics/ItemFreeze.php

@@ -0,0 +1,528 @@
+<?php
+
+namespace App\Module\GameItems\Logics;
+
+use App\Module\GameItems\Enums\FREEZE_ACTION_TYPE;
+use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
+use App\Module\GameItems\Models\Item;
+use App\Module\GameItems\Models\ItemFreezeLog;
+use App\Module\GameItems\Models\ItemUser;
+use Exception;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Log;
+use UCore\Db\Helper;
+
+/**
+ * 物品冻结逻辑类
+ *
+ * 处理物品冻结和解冻的核心业务逻辑,包括:
+ * - 统一属性物品的冻结/解冻(拆堆模式)
+ * - 单独属性物品的冻结/解冻
+ * - 冻结状态验证和数量查询
+ * - 批量冻结操作
+ */
+class ItemFreeze
+{
+    /**
+     * 冻结统一属性物品(拆堆模式)
+     *
+     * 实现逻辑:
+     * 1. 查找用户可用的物品堆叠(is_frozen=false)
+     * 2. 从可用堆叠中扣除冻结数量
+     * 3. 创建新的冻结堆叠记录(is_frozen=true)
+     * 4. 记录冻结日志
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int $quantity 冻结数量
+     * @param FREEZE_REASON_TYPE $reason 冻结原因
+     * @param int|null $sourceId 来源ID
+     * @param string|null $sourceType 来源类型
+     * @param int|null $operatorId 操作员ID
+     * @return array 冻结结果
+     * @throws Exception
+     */
+    public static function freezeNormalItem(
+        int $userId,
+        int $itemId,
+        int $quantity,
+        FREEZE_REASON_TYPE $reason,
+        ?int $sourceId = null,
+        ?string $sourceType = null,
+        ?int $operatorId = null
+    ): array {
+        // 检查事务
+        Helper::check_tr();
+
+        // 验证冻结操作的合法性
+        if (!self::validateFreezeOperation($userId, $itemId, $quantity)) {
+            throw new Exception("用户 {$userId} 的物品 {$itemId} 可用数量不足,无法冻结 {$quantity} 个");
+        }
+
+        // 获取用户可用的物品堆叠(按过期时间排序,优先冻结即将过期的)
+        $availableItems = ItemUser::where('user_id', $userId)
+            ->where('item_id', $itemId)
+            ->where('is_frozen', false)
+            ->whereNull('instance_id')
+            ->where('quantity', '>', 0)
+            ->orderBy('expire_at')
+            ->get();
+
+        $remainingQuantity = $quantity;
+        $frozenItems = [];
+
+        foreach ($availableItems as $userItem) {
+            if ($remainingQuantity <= 0) {
+                break;
+            }
+
+            $availableQuantity = $userItem->quantity;
+            $freezeQuantity = min($remainingQuantity, $availableQuantity);
+
+            // 创建冻结日志
+            $freezeLog = ItemFreezeLog::createLog(
+                $userId,
+                $itemId,
+                null,
+                $freezeQuantity,
+                FREEZE_ACTION_TYPE::FREEZE,
+                $reason->getName($reason->value),
+                $sourceId,
+                $sourceType,
+                $operatorId
+            );
+
+            if ($freezeQuantity == $availableQuantity) {
+                // 全部冻结,直接标记为冻结状态
+                $userItem->is_frozen = true;
+                $userItem->frozen_log_id = $freezeLog->id;
+                $userItem->save();
+
+                $frozenItems[] = [
+                    'user_item_id' => $userItem->id,
+                    'quantity' => $freezeQuantity,
+                    'freeze_log_id' => $freezeLog->id,
+                ];
+            } else {
+                // 部分冻结,需要拆堆
+                // 减少原堆叠数量
+                $userItem->quantity = $availableQuantity - $freezeQuantity;
+                $userItem->save();
+
+                // 创建新的冻结堆叠
+                $frozenItem = new ItemUser([
+                    'user_id' => $userId,
+                    'item_id' => $itemId,
+                    'instance_id' => null,
+                    'quantity' => $freezeQuantity,
+                    'expire_at' => $userItem->expire_at,
+                    'is_frozen' => true,
+                    'frozen_log_id' => $freezeLog->id,
+                ]);
+                $frozenItem->save();
+
+                $frozenItems[] = [
+                    'user_item_id' => $frozenItem->id,
+                    'quantity' => $freezeQuantity,
+                    'freeze_log_id' => $freezeLog->id,
+                ];
+            }
+
+            $remainingQuantity -= $freezeQuantity;
+        }
+
+        if ($remainingQuantity > 0) {
+            throw new Exception("冻结操作失败,剩余未冻结数量:{$remainingQuantity}");
+        }
+
+        return [
+            'success' => true,
+            'user_id' => $userId,
+            'item_id' => $itemId,
+            'frozen_quantity' => $quantity,
+            'frozen_items' => $frozenItems,
+        ];
+    }
+
+    /**
+     * 冻结单独属性物品
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int $instanceId 物品实例ID
+     * @param FREEZE_REASON_TYPE $reason 冻结原因
+     * @param int|null $sourceId 来源ID
+     * @param string|null $sourceType 来源类型
+     * @param int|null $operatorId 操作员ID
+     * @return array 冻结结果
+     * @throws Exception
+     */
+    public static function freezeUniqueItem(
+        int $userId,
+        int $itemId,
+        int $instanceId,
+        FREEZE_REASON_TYPE $reason,
+        ?int $sourceId = null,
+        ?string $sourceType = null,
+        ?int $operatorId = null
+    ): array {
+        // 检查事务
+        Helper::check_tr();
+
+        // 查找用户的单独属性物品
+        $userItem = ItemUser::where('user_id', $userId)
+            ->where('item_id', $itemId)
+            ->where('instance_id', $instanceId)
+            ->where('is_frozen', false)
+            ->first();
+
+        if (!$userItem) {
+            throw new Exception("用户 {$userId} 没有可冻结的物品实例 {$instanceId}");
+        }
+
+        // 创建冻结日志
+        $freezeLog = ItemFreezeLog::createLog(
+            $userId,
+            $itemId,
+            $instanceId,
+            1, // 单独属性物品数量始终为1
+            FREEZE_ACTION_TYPE::FREEZE,
+            $reason->getName($reason->value),
+            $sourceId,
+            $sourceType,
+            $operatorId
+        );
+
+        // 标记为冻结状态
+        $userItem->is_frozen = true;
+        $userItem->frozen_log_id = $freezeLog->id;
+        $userItem->save();
+
+        return [
+            'success' => true,
+            'user_id' => $userId,
+            'item_id' => $itemId,
+            'instance_id' => $instanceId,
+            'user_item_id' => $userItem->id,
+            'freeze_log_id' => $freezeLog->id,
+        ];
+    }
+
+    /**
+     * 解冻物品(通过冻结日志ID)
+     *
+     * @param int $freezeLogId 冻结日志ID
+     * @return array 解冻结果
+     * @throws Exception
+     */
+    public static function unfreezeByLogId(int $freezeLogId): array
+    {
+        // 检查事务
+        Helper::check_tr();
+
+        // 查找冻结日志
+        $freezeLog = ItemFreezeLog::find($freezeLogId);
+        if (!$freezeLog) {
+            throw new Exception("冻结日志 {$freezeLogId} 不存在");
+        }
+
+        if (!$freezeLog->isFreeze()) {
+            throw new Exception("日志 {$freezeLogId} 不是冻结操作记录");
+        }
+
+        // 查找对应的冻结物品
+        $frozenItem = ItemUser::where('frozen_log_id', $freezeLogId)
+            ->where('is_frozen', true)
+            ->first();
+
+        if (!$frozenItem) {
+            throw new Exception("未找到冻结日志 {$freezeLogId} 对应的冻结物品");
+        }
+
+        // 创建解冻日志
+        $unfreezeLog = ItemFreezeLog::createLog(
+            $frozenItem->user_id,
+            $frozenItem->item_id,
+            $frozenItem->instance_id,
+            $frozenItem->quantity,
+            FREEZE_ACTION_TYPE::UNFREEZE,
+            "解冻操作,原冻结日志ID: {$freezeLogId}",
+            $freezeLog->source_id,
+            $freezeLog->source_type,
+            $freezeLog->operator_id
+        );
+
+        // 解冻物品(保持独立,不合并堆叠)
+        $frozenItem->is_frozen = false;
+        $frozenItem->frozen_log_id = null;
+        $frozenItem->save();
+
+        return [
+            'success' => true,
+            'user_id' => $frozenItem->user_id,
+            'item_id' => $frozenItem->item_id,
+            'instance_id' => $frozenItem->instance_id,
+            'unfrozen_quantity' => $frozenItem->quantity,
+            'user_item_id' => $frozenItem->id,
+            'unfreeze_log_id' => $unfreezeLog->id,
+        ];
+    }
+
+    /**
+     * 检查用户可用物品数量(排除冻结堆叠)
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int|null $instanceId 实例ID
+     * @return int 可用数量
+     */
+    public static function getAvailableQuantity(
+        int $userId,
+        int $itemId,
+        ?int $instanceId = null
+    ): int {
+        return ItemUser::getAvailableQuantity($userId, $itemId, $instanceId);
+    }
+
+    /**
+     * 验证用户是否有足够的可用物品
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int $requiredQuantity 需要的数量
+     * @param int|null $instanceId 实例ID
+     * @return bool 是否有足够的可用物品
+     */
+    public static function checkAvailableQuantity(
+        int $userId,
+        int $itemId,
+        int $requiredQuantity,
+        ?int $instanceId = null
+    ): bool {
+        $availableQuantity = self::getAvailableQuantity($userId, $itemId, $instanceId);
+        return $availableQuantity >= $requiredQuantity;
+    }
+
+    /**
+     * 验证冻结操作的合法性
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int $quantity 冻结数量
+     * @param int|null $instanceId 实例ID
+     * @return bool 是否可以冻结
+     */
+    public static function validateFreezeOperation(
+        int $userId,
+        int $itemId,
+        int $quantity,
+        ?int $instanceId = null
+    ): bool {
+        // 检查物品是否存在
+        $item = Item::find($itemId);
+        if (!$item) {
+            return false;
+        }
+
+        // 检查数量是否合法
+        if ($quantity <= 0) {
+            return false;
+        }
+
+        // 检查可用数量是否足够
+        return self::checkAvailableQuantity($userId, $itemId, $quantity, $instanceId);
+    }
+
+    /**
+     * 批量冻结物品
+     *
+     * @param int $userId 用户ID
+     * @param array $items 物品列表 [['item_id' => 1, 'quantity' => 10], ...]
+     * @param FREEZE_REASON_TYPE $reason 冻结原因
+     * @param int|null $sourceId 来源ID
+     * @param string|null $sourceType 来源类型
+     * @param int|null $operatorId 操作员ID
+     * @return array 冻结结果
+     * @throws Exception
+     */
+    public static function batchFreezeItems(
+        int $userId,
+        array $items,
+        FREEZE_REASON_TYPE $reason,
+        ?int $sourceId = null,
+        ?string $sourceType = null,
+        ?int $operatorId = null
+    ): array {
+        // 检查事务
+        Helper::check_tr();
+
+        $results = [];
+        $errors = [];
+
+        foreach ($items as $itemData) {
+            $itemId = $itemData['item_id'];
+            $quantity = $itemData['quantity'] ?? 1;
+            $instanceId = $itemData['instance_id'] ?? null;
+
+            try {
+                if ($instanceId) {
+                    // 单独属性物品
+                    $result = self::freezeUniqueItem(
+                        $userId,
+                        $itemId,
+                        $instanceId,
+                        $reason,
+                        $sourceId,
+                        $sourceType,
+                        $operatorId
+                    );
+                } else {
+                    // 统一属性物品
+                    $result = self::freezeNormalItem(
+                        $userId,
+                        $itemId,
+                        $quantity,
+                        $reason,
+                        $sourceId,
+                        $sourceType,
+                        $operatorId
+                    );
+                }
+                $results[] = $result;
+            } catch (Exception $e) {
+                $errors[] = [
+                    'item_id' => $itemId,
+                    'instance_id' => $instanceId,
+                    'quantity' => $quantity,
+                    'error' => $e->getMessage(),
+                ];
+            }
+        }
+
+        if (!empty($errors)) {
+            throw new Exception("批量冻结操作部分失败:" . json_encode($errors, JSON_UNESCAPED_UNICODE));
+        }
+
+        return [
+            'success' => true,
+            'user_id' => $userId,
+            'frozen_items_count' => count($results),
+            'results' => $results,
+        ];
+    }
+
+    /**
+     * 检查冻结物品是否过期并处理
+     *
+     * @param int $userId 用户ID
+     * @return int 处理的过期冻结物品数量
+     */
+    public static function handleExpiredFrozenItems(int $userId): int
+    {
+        // 查找过期的冻结物品
+        $expiredFrozenItems = ItemUser::where('user_id', $userId)
+            ->where('is_frozen', true)
+            ->where('expire_at', '<', now())
+            ->whereNotNull('expire_at')
+            ->get();
+
+        $processedCount = 0;
+
+        foreach ($expiredFrozenItems as $frozenItem) {
+            try {
+                // 先解冻再处理过期
+                if ($frozenItem->frozen_log_id) {
+                    self::unfreezeByLogId($frozenItem->frozen_log_id);
+                }
+
+                // 处理过期逻辑(这里可以根据业务需求决定是删除还是其他处理)
+                // 暂时设置数量为0,不删除记录
+                $frozenItem->quantity = 0;
+                $frozenItem->save();
+
+                $processedCount++;
+            } catch (Exception $e) {
+                // 记录错误日志,但不中断处理
+                Log::error("处理过期冻结物品失败", [
+                    'user_id' => $userId,
+                    'item_user_id' => $frozenItem->id,
+                    'error' => $e->getMessage(),
+                ]);
+            }
+        }
+
+        return $processedCount;
+    }
+
+    /**
+     * 获取用户的冻结物品列表
+     *
+     * @param int $userId 用户ID
+     * @param array $filters 过滤条件
+     * @return Collection 冻结物品集合
+     */
+    public static function getFrozenItems(int $userId, array $filters = []): Collection
+    {
+        $query = ItemUser::where('user_id', $userId)
+            ->where('is_frozen', true)
+            ->with(['item', 'instance', 'freezeLog']);
+
+        // 应用过滤条件
+        if (isset($filters['item_id'])) {
+            $query->where('item_id', $filters['item_id']);
+        }
+
+        if (isset($filters['source_type'])) {
+            $query->whereHas('freezeLog', function ($q) use ($filters) {
+                $q->where('source_type', $filters['source_type']);
+            });
+        }
+
+        if (isset($filters['reason'])) {
+            $query->whereHas('freezeLog', function ($q) use ($filters) {
+                $q->where('reason', 'like', '%' . $filters['reason'] . '%');
+            });
+        }
+
+        return $query->get();
+    }
+
+    /**
+     * 获取冻结统计信息
+     *
+     * @param int $userId 用户ID
+     * @return array 统计信息
+     */
+    public static function getFreezeStatistics(int $userId): array
+    {
+        $frozenItems = ItemUser::where('user_id', $userId)
+            ->where('is_frozen', true)
+            ->with(['item', 'freezeLog'])
+            ->get();
+
+        $statistics = [
+            'total_frozen_items' => $frozenItems->count(),
+            'total_frozen_quantity' => $frozenItems->sum('quantity'),
+            'frozen_by_reason' => [],
+            'frozen_by_source_type' => [],
+        ];
+
+        // 按原因分组统计
+        $frozenItems->groupBy('freezeLog.reason')->each(function ($items, $reason) use (&$statistics) {
+            $statistics['frozen_by_reason'][$reason] = [
+                'count' => $items->count(),
+                'quantity' => $items->sum('quantity'),
+            ];
+        });
+
+        // 按来源类型分组统计
+        $frozenItems->groupBy('freezeLog.source_type')->each(function ($items, $sourceType) use (&$statistics) {
+            $statistics['frozen_by_source_type'][$sourceType] = [
+                'count' => $items->count(),
+                'quantity' => $items->sum('quantity'),
+            ];
+        });
+
+        return $statistics;
+    }
+}

+ 181 - 0
app/Module/GameItems/Models/ItemFreezeLog.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace App\Module\GameItems\Models;
+
+use App\Module\GameItems\Enums\FREEZE_ACTION_TYPE;
+use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use UCore\ModelCore;
+
+/**
+ * 物品冻结记录
+ *
+ * field start 
+ * @property  int  $id  记录ID,主键
+ * @property  int  $user_id  用户ID
+ * @property  int  $item_id  物品ID
+ * @property  int  $instance_id  物品实例ID(单独属性物品)
+ * @property  int  $quantity  冻结数量
+ * @property  \App\Module\GameItems\Enums\FREEZE_ACTION_TYPE  $action_type  操作类型(1:冻结, 2:解冻)
+ * @property  string  $reason  操作原因
+ * @property  int  $source_id  操作方记录ID
+ * @property  string  $source_type  操作类型(如:order, admin, system等)
+ * @property  int  $operator_id  操作员ID(系统操作为NULL)
+ * @property  \Carbon\Carbon  $created_at  操作时间
+ * @property  \Carbon\Carbon  $updated_at  更新时间
+ * field end
+ */
+class ItemFreezeLog extends ModelCore
+{
+    /**
+     * 与模型关联的表名
+     *
+     * @var string
+     */
+    protected $table = 'item_freeze_logs';
+
+    // attrlist start 
+    protected $fillable = [
+        'id',
+        'user_id',
+        'item_id',
+        'instance_id',
+        'quantity',
+        'action_type',
+        'reason',
+        'source_id',
+        'source_type',
+        'operator_id',
+    ];
+    // attrlist end
+
+    /**
+     * 应该被转换为原生类型的属性
+     *
+     * @var array
+     */
+    protected $casts = [
+        'action_type' => FREEZE_ACTION_TYPE::class,
+        'quantity' => 'integer',
+        'user_id' => 'integer',
+        'item_id' => 'integer',
+        'instance_id' => 'integer',
+        'source_id' => 'integer',
+        'operator_id' => 'integer',
+    ];
+
+    /**
+     * 获取关联的物品
+     *
+     * @return BelongsTo
+     */
+    public function item(): BelongsTo
+    {
+        return $this->belongsTo(Item::class, 'item_id');
+    }
+
+    /**
+     * 获取关联的物品实例(如果有)
+     *
+     * @return BelongsTo
+     */
+    public function instance(): BelongsTo
+    {
+        return $this->belongsTo(ItemInstance::class, 'instance_id');
+    }
+
+    /**
+     * 获取关联的用户物品记录
+     *
+     * @return BelongsTo
+     */
+    public function itemUser(): BelongsTo
+    {
+        return $this->hasOne(ItemUser::class, 'frozen_log_id', 'id');
+    }
+
+    /**
+     * 检查是否为冻结操作
+     *
+     * @return bool
+     */
+    public function isFreeze(): bool
+    {
+        return $this->action_type === FREEZE_ACTION_TYPE::FREEZE;
+    }
+
+    /**
+     * 检查是否为解冻操作
+     *
+     * @return bool
+     */
+    public function isUnfreeze(): bool
+    {
+        return $this->action_type === FREEZE_ACTION_TYPE::UNFREEZE;
+    }
+
+    /**
+     * 获取操作类型名称
+     *
+     * @return string
+     */
+    public function getActionTypeName(): string
+    {
+        return $this->action_type->getName($this->action_type->value);
+    }
+
+    /**
+     * 获取格式化的操作描述
+     *
+     * @return string
+     */
+    public function getOperationDescription(): string
+    {
+        $actionName = $this->getActionTypeName();
+        $itemName = $this->item->name ?? "物品ID:{$this->item_id}";
+        
+        if ($this->instance_id) {
+            return "{$actionName}单独属性物品: {$itemName} (实例ID: {$this->instance_id})";
+        } else {
+            return "{$actionName}统一属性物品: {$itemName} x{$this->quantity}";
+        }
+    }
+
+    /**
+     * 创建冻结记录
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int|null $instanceId 物品实例ID
+     * @param int $quantity 数量
+     * @param FREEZE_ACTION_TYPE $actionType 操作类型
+     * @param string $reason 操作原因
+     * @param int|null $sourceId 来源ID
+     * @param string|null $sourceType 来源类型
+     * @param int|null $operatorId 操作员ID
+     * @return static
+     */
+    public static function createLog(
+        int $userId,
+        int $itemId,
+        ?int $instanceId,
+        int $quantity,
+        FREEZE_ACTION_TYPE $actionType,
+        string $reason,
+        ?int $sourceId = null,
+        ?string $sourceType = null,
+        ?int $operatorId = null
+    ): self {
+        return self::create([
+            'user_id' => $userId,
+            'item_id' => $itemId,
+            'instance_id' => $instanceId,
+            'quantity' => $quantity,
+            'action_type' => $actionType,
+            'reason' => $reason,
+            'source_id' => $sourceId,
+            'source_type' => $sourceType,
+            'operator_id' => $operatorId,
+        ]);
+    }
+}

+ 114 - 2
app/Module/GameItems/Models/ItemUser.php

@@ -8,13 +8,15 @@ use UCore\ModelCore;
 /**
  * 用户物品关联
  *
- * field start 
+ * field start
  * @property  int  $id  记录ID,主键
  * @property  int  $user_id  用户ID
  * @property  int  $item_id  统一属性物品ID,外键关联kku_item_items表
  * @property  int  $instance_id  单独属性物品ID,外键关联kku_item_instances表(可为空)
  * @property  int  $quantity  数量(对于单独属性物品,该值始终为1)
  * @property  string  $expire_at  用户物品过期时间(可为空)
+ * @property  bool  $is_frozen  是否冻结(0:未冻结, 1:已冻结)
+ * @property  int  $frozen_log_id  冻结日志ID,关联kku_item_freeze_logs表
  * @property  \Carbon\Carbon  $created_at  获取时间
  * @property  \Carbon\Carbon  $updated_at  更新时间
  * field end
@@ -28,7 +30,7 @@ class ItemUser extends ModelCore
      */
     protected $table = 'item_users';
 
-    // attrlist start 
+    // attrlist start
     protected $fillable = [
         'id',
         'user_id',
@@ -36,6 +38,8 @@ class ItemUser extends ModelCore
         'instance_id',
         'quantity',
         'expire_at',
+        'is_frozen',
+        'frozen_log_id',
     ];
     // attrlist end
 
@@ -50,6 +54,16 @@ class ItemUser extends ModelCore
         'updated_at',
     ];
 
+    /**
+     * 应该被转换为原生类型的属性
+     *
+     * @var array
+     */
+    protected $casts = [
+        'is_frozen' => 'boolean',
+        'frozen_log_id' => 'integer',
+    ];
+
     /**
      * 获取关联的物品
      *
@@ -70,6 +84,16 @@ class ItemUser extends ModelCore
         return $this->belongsTo(ItemInstance::class, 'instance_id');
     }
 
+    /**
+     * 获取关联的冻结日志
+     *
+     * @return BelongsTo
+     */
+    public function freezeLog(): BelongsTo
+    {
+        return $this->belongsTo(ItemFreezeLog::class, 'frozen_log_id');
+    }
+
 
 
 
@@ -82,4 +106,92 @@ class ItemUser extends ModelCore
     {
         return !empty($this->instance_id);
     }
+
+    /**
+     * 检查是否为冻结状态
+     *
+     * @return bool
+     */
+    public function isFrozen(): bool
+    {
+        return $this->is_frozen;
+    }
+
+    /**
+     * 检查是否可用(未冻结)
+     *
+     * @return bool
+     */
+    public function isAvailable(): bool
+    {
+        return !$this->is_frozen;
+    }
+
+    /**
+     * 获取用户指定物品的可用数量(排除冻结的堆叠)
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int|null $instanceId 实例ID
+     * @return int 可用数量
+     */
+    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');
+    }
+
+    /**
+     * 获取用户指定物品的冻结数量
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int|null $instanceId 实例ID
+     * @return int 冻结数量
+     */
+    public static function getFrozenQuantity(int $userId, int $itemId, ?int $instanceId = null): int
+    {
+        $query = static::where('user_id', $userId)
+            ->where('item_id', $itemId)
+            ->where('is_frozen', true);
+
+        if ($instanceId) {
+            $query->where('instance_id', $instanceId);
+        } else {
+            $query->whereNull('instance_id');
+        }
+
+        return $query->sum('quantity');
+    }
+
+    /**
+     * 获取用户指定物品的总数量(包括冻结的)
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int|null $instanceId 实例ID
+     * @return int 总数量
+     */
+    public static function getTotalQuantity(int $userId, int $itemId, ?int $instanceId = null): int
+    {
+        $query = static::where('user_id', $userId)
+            ->where('item_id', $itemId);
+
+        if ($instanceId) {
+            $query->where('instance_id', $instanceId);
+        } else {
+            $query->whereNull('instance_id');
+        }
+
+        return $query->sum('quantity');
+    }
 }

+ 146 - 0
app/Module/GameItems/Services/ItemService.php

@@ -4,7 +4,9 @@ namespace App\Module\GameItems\Services;
 
 use App\Module\GameItems\Dtos\ItemDto;
 use App\Module\GameItems\Dtos\ItemUserDto;
+use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
 use App\Module\GameItems\Logics\Item as ItemLogic;
+use App\Module\GameItems\Logics\ItemFreeze;
 use App\Module\GameItems\Logics\ItemQuantity;
 use App\Module\GameItems\Models\Item;
 use App\Module\GameItems\Models\ItemUser;
@@ -300,4 +302,148 @@ class ItemService
             return [];
         }
     }
+
+    /**
+     * 冻结物品
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int|null $instanceId 物品实例ID(单独属性物品)
+     * @param int $quantity 数量
+     * @param string $reason 冻结原因
+     * @param array $options 选项
+     * @return array 冻结结果
+     * @throws Exception
+     */
+    public static function freezeItem(
+        int $userId,
+        int $itemId,
+        ?int $instanceId,
+        int $quantity,
+        string $reason,
+        array $options = []
+    ): array {
+        Helper::check_tr();
+
+        // 解析冻结原因
+        $reasonEnum = FREEZE_REASON_TYPE::tryFrom($options['reason_type'] ?? FREEZE_REASON_TYPE::SYSTEM_FREEZE->value);
+        if (!$reasonEnum) {
+            $reasonEnum = FREEZE_REASON_TYPE::SYSTEM_FREEZE;
+        }
+
+        $sourceId = $options['source_id'] ?? null;
+        $sourceType = $options['source_type'] ?? null;
+        $operatorId = $options['operator_id'] ?? null;
+
+        if ($instanceId) {
+            // 冻结单独属性物品
+            return ItemFreeze::freezeUniqueItem(
+                $userId,
+                $itemId,
+                $instanceId,
+                $reasonEnum,
+                $sourceId,
+                $sourceType,
+                $operatorId
+            );
+        } else {
+            // 冻结统一属性物品
+            return ItemFreeze::freezeNormalItem(
+                $userId,
+                $itemId,
+                $quantity,
+                $reasonEnum,
+                $sourceId,
+                $sourceType,
+                $operatorId
+            );
+        }
+    }
+
+    /**
+     * 解冻物品
+     *
+     * @param int $freezeLogId 冻结日志ID
+     * @return array 解冻结果
+     * @throws Exception
+     */
+    public static function unfreezeItem(int $freezeLogId): array
+    {
+        Helper::check_tr();
+        return ItemFreeze::unfreezeByLogId($freezeLogId);
+    }
+
+    /**
+     * 获取用户可用物品数量(排除冻结的)
+     *
+     * @param int $userId 用户ID
+     * @param int $itemId 物品ID
+     * @param int|null $instanceId 实例ID
+     * @return int 可用数量
+     */
+    public static function getAvailableQuantity(int $userId, int $itemId, ?int $instanceId = null): int
+    {
+        return ItemFreeze::getAvailableQuantity($userId, $itemId, $instanceId);
+    }
+
+    /**
+     * 获取用户冻结物品列表
+     *
+     * @param int $userId 用户ID
+     * @param array $filters 过滤条件
+     * @return SupportCollection 冻结物品集合
+     */
+    public static function getFrozenItems(int $userId, array $filters = []): SupportCollection
+    {
+        return ItemFreeze::getFrozenItems($userId, $filters);
+    }
+
+    /**
+     * 批量冻结物品
+     *
+     * @param int $userId 用户ID
+     * @param array $items 物品列表
+     * @param string $reason 冻结原因
+     * @param array $options 选项
+     * @return array 冻结结果
+     * @throws Exception
+     */
+    public static function batchFreezeItems(
+        int $userId,
+        array $items,
+        string $reason,
+        array $options = []
+    ): array {
+        Helper::check_tr();
+
+        // 解析冻结原因
+        $reasonEnum = FREEZE_REASON_TYPE::tryFrom($options['reason_type'] ?? FREEZE_REASON_TYPE::SYSTEM_FREEZE->value);
+        if (!$reasonEnum) {
+            $reasonEnum = FREEZE_REASON_TYPE::SYSTEM_FREEZE;
+        }
+
+        $sourceId = $options['source_id'] ?? null;
+        $sourceType = $options['source_type'] ?? null;
+        $operatorId = $options['operator_id'] ?? null;
+
+        return ItemFreeze::batchFreezeItems(
+            $userId,
+            $items,
+            $reasonEnum,
+            $sourceId,
+            $sourceType,
+            $operatorId
+        );
+    }
+
+    /**
+     * 获取冻结统计信息
+     *
+     * @param int $userId 用户ID
+     * @return array 统计信息
+     */
+    public static function getFreezeStatistics(int $userId): array
+    {
+        return ItemFreeze::getFreezeStatistics($userId);
+    }
 }

+ 296 - 0
app/Module/GameItems/Tests/ItemFreezeTest.php

@@ -0,0 +1,296 @@
+<?php
+
+namespace App\Module\GameItems\Tests;
+
+use App\Module\GameItems\Enums\FREEZE_ACTION_TYPE;
+use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
+use App\Module\GameItems\Logics\ItemFreeze;
+use App\Module\GameItems\Models\Item;
+use App\Module\GameItems\Models\ItemFreezeLog;
+use App\Module\GameItems\Models\ItemUser;
+use App\Module\GameItems\Services\ItemService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Tests\TestCase;
+use UCore\Db\Helper;
+
+/**
+ * 物品冻结功能测试
+ */
+class ItemFreezeTest extends TestCase
+{
+    use RefreshDatabase;
+
+    private int $testUserId = 1001;
+    private int $testItemId = 2001;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        
+        // 创建测试物品
+        Item::create([
+            'id' => $this->testItemId,
+            'name' => '测试物品',
+            'description' => '用于测试冻结功能的物品',
+            'category_id' => 1,
+            'type' => 1,
+            'is_unique' => false,
+            'max_stack' => 100,
+            'sell_price' => 10,
+            'tradable' => true,
+            'dismantlable' => true,
+            'default_expire_seconds' => 0,
+        ]);
+    }
+
+    /**
+     * 测试冻结统一属性物品
+     */
+    public function testFreezeNormalItem()
+    {
+        Helper::begin_tr();
+
+        try {
+            // 先给用户添加物品
+            ItemService::addItem($this->testUserId, $this->testItemId, 50);
+
+            // 验证添加成功
+            $availableQuantity = ItemFreeze::getAvailableQuantity($this->testUserId, $this->testItemId);
+            $this->assertEquals(50, $availableQuantity);
+
+            // 冻结部分物品
+            $result = ItemFreeze::freezeNormalItem(
+                $this->testUserId,
+                $this->testItemId,
+                20,
+                FREEZE_REASON_TYPE::TRADE_ORDER,
+                123,
+                'order'
+            );
+
+            // 验证冻结结果
+            $this->assertTrue($result['success']);
+            $this->assertEquals(20, $result['frozen_quantity']);
+
+            // 验证可用数量减少
+            $availableQuantity = ItemFreeze::getAvailableQuantity($this->testUserId, $this->testItemId);
+            $this->assertEquals(30, $availableQuantity);
+
+            // 验证冻结记录
+            $freezeLog = ItemFreezeLog::where('user_id', $this->testUserId)
+                ->where('item_id', $this->testItemId)
+                ->where('action_type', FREEZE_ACTION_TYPE::FREEZE)
+                ->first();
+
+            $this->assertNotNull($freezeLog);
+            $this->assertEquals(20, $freezeLog->quantity);
+            $this->assertEquals('交易订单', $freezeLog->reason);
+
+            Helper::commit_tr();
+        } catch (\Exception $e) {
+            Helper::rollback_tr();
+            throw $e;
+        }
+    }
+
+    /**
+     * 测试解冻物品
+     */
+    public function testUnfreezeItem()
+    {
+        Helper::begin_tr();
+
+        try {
+            // 先添加物品并冻结
+            ItemService::addItem($this->testUserId, $this->testItemId, 50);
+            $freezeResult = ItemFreeze::freezeNormalItem(
+                $this->testUserId,
+                $this->testItemId,
+                20,
+                FREEZE_REASON_TYPE::TRADE_ORDER,
+                123,
+                'order'
+            );
+
+            $freezeLogId = $freezeResult['frozen_items'][0]['freeze_log_id'];
+
+            // 验证冻结状态
+            $availableQuantity = ItemFreeze::getAvailableQuantity($this->testUserId, $this->testItemId);
+            $this->assertEquals(30, $availableQuantity);
+
+            // 解冻物品
+            $unfreezeResult = ItemFreeze::unfreezeByLogId($freezeLogId);
+
+            // 验证解冻结果
+            $this->assertTrue($unfreezeResult['success']);
+            $this->assertEquals(20, $unfreezeResult['unfrozen_quantity']);
+
+            // 验证可用数量恢复
+            $availableQuantity = ItemFreeze::getAvailableQuantity($this->testUserId, $this->testItemId);
+            $this->assertEquals(50, $availableQuantity);
+
+            // 验证解冻记录
+            $unfreezeLog = ItemFreezeLog::where('user_id', $this->testUserId)
+                ->where('item_id', $this->testItemId)
+                ->where('action_type', FREEZE_ACTION_TYPE::UNFREEZE)
+                ->first();
+
+            $this->assertNotNull($unfreezeLog);
+            $this->assertEquals(20, $unfreezeLog->quantity);
+
+            Helper::commit_tr();
+        } catch (\Exception $e) {
+            Helper::rollback_tr();
+            throw $e;
+        }
+    }
+
+    /**
+     * 测试消耗物品时排除冻结物品
+     */
+    public function testConsumeExcludesFrozenItems()
+    {
+        Helper::begin_tr();
+
+        try {
+            // 添加物品
+            ItemService::addItem($this->testUserId, $this->testItemId, 50);
+
+            // 冻结部分物品
+            ItemFreeze::freezeNormalItem(
+                $this->testUserId,
+                $this->testItemId,
+                20,
+                FREEZE_REASON_TYPE::TRADE_ORDER,
+                123,
+                'order'
+            );
+
+            // 尝试消耗物品,应该只能消耗未冻结的30个
+            $result = ItemService::consumeItem($this->testUserId, $this->testItemId, null, 25);
+
+            // 验证消耗成功
+            $this->assertTrue($result['success']);
+            $this->assertEquals(25, $result['quantity']);
+
+            // 验证剩余可用数量
+            $availableQuantity = ItemFreeze::getAvailableQuantity($this->testUserId, $this->testItemId);
+            $this->assertEquals(5, $availableQuantity);
+
+            // 尝试消耗超过可用数量的物品,应该失败
+            $this->expectException(\Exception::class);
+            ItemService::consumeItem($this->testUserId, $this->testItemId, null, 10);
+
+            Helper::commit_tr();
+        } catch (\Exception $e) {
+            Helper::rollback_tr();
+            throw $e;
+        }
+    }
+
+    /**
+     * 测试批量冻结物品
+     */
+    public function testBatchFreezeItems()
+    {
+        Helper::begin_tr();
+
+        try {
+            // 添加多种物品
+            ItemService::addItem($this->testUserId, $this->testItemId, 100);
+
+            // 创建另一个测试物品
+            $testItemId2 = $this->testItemId + 1;
+            Item::create([
+                'id' => $testItemId2,
+                'name' => '测试物品2',
+                'description' => '用于测试批量冻结的物品',
+                'category_id' => 1,
+                'type' => 1,
+                'is_unique' => false,
+                'max_stack' => 100,
+                'sell_price' => 10,
+                'tradable' => true,
+                'dismantlable' => true,
+                'default_expire_seconds' => 0,
+            ]);
+
+            ItemService::addItem($this->testUserId, $testItemId2, 50);
+
+            // 批量冻结
+            $items = [
+                ['item_id' => $this->testItemId, 'quantity' => 30],
+                ['item_id' => $testItemId2, 'quantity' => 20],
+            ];
+
+            $result = ItemFreeze::batchFreezeItems(
+                $this->testUserId,
+                $items,
+                FREEZE_REASON_TYPE::SYSTEM_FREEZE,
+                null,
+                'system'
+            );
+
+            // 验证批量冻结结果
+            $this->assertTrue($result['success']);
+            $this->assertEquals(2, $result['frozen_items_count']);
+
+            // 验证各物品的可用数量
+            $availableQuantity1 = ItemFreeze::getAvailableQuantity($this->testUserId, $this->testItemId);
+            $availableQuantity2 = ItemFreeze::getAvailableQuantity($this->testUserId, $testItemId2);
+
+            $this->assertEquals(70, $availableQuantity1);
+            $this->assertEquals(30, $availableQuantity2);
+
+            Helper::commit_tr();
+        } catch (\Exception $e) {
+            Helper::rollback_tr();
+            throw $e;
+        }
+    }
+
+    /**
+     * 测试获取冻结统计信息
+     */
+    public function testGetFreezeStatistics()
+    {
+        Helper::begin_tr();
+
+        try {
+            // 添加物品并冻结
+            ItemService::addItem($this->testUserId, $this->testItemId, 100);
+            
+            ItemFreeze::freezeNormalItem(
+                $this->testUserId,
+                $this->testItemId,
+                30,
+                FREEZE_REASON_TYPE::TRADE_ORDER,
+                123,
+                'order'
+            );
+
+            ItemFreeze::freezeNormalItem(
+                $this->testUserId,
+                $this->testItemId,
+                20,
+                FREEZE_REASON_TYPE::ADMIN_FREEZE,
+                null,
+                'admin'
+            );
+
+            // 获取统计信息
+            $statistics = ItemFreeze::getFreezeStatistics($this->testUserId);
+
+            // 验证统计结果
+            $this->assertEquals(2, $statistics['total_frozen_items']);
+            $this->assertEquals(50, $statistics['total_frozen_quantity']);
+            $this->assertArrayHasKey('frozen_by_reason', $statistics);
+            $this->assertArrayHasKey('frozen_by_source_type', $statistics);
+
+            Helper::commit_tr();
+        } catch (\Exception $e) {
+            Helper::rollback_tr();
+            throw $e;
+        }
+    }
+}

+ 139 - 0
app/Module/GameItems/Tests/freeze_test_manual.php

@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * 手动测试冻结功能
+ * 
+ * 运行方式:php artisan tinker
+ * 然后执行:include 'app/Module/GameItems/Tests/freeze_test_manual.php';
+ */
+
+use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
+use App\Module\GameItems\Logics\ItemFreeze;
+use App\Module\GameItems\Models\Item;
+use App\Module\GameItems\Models\ItemUser;
+use App\Module\GameItems\Services\ItemService;
+use UCore\Db\Helper;
+
+echo "开始测试物品冻结功能...\n";
+
+try {
+    // 测试用户ID和物品ID
+    $testUserId = 9999;
+    $testItemId = 8888;
+
+    // 清理测试数据
+    ItemUser::where('user_id', $testUserId)->delete();
+    Item::where('id', $testItemId)->delete();
+
+    // 创建测试物品
+    $item = Item::create([
+        'id' => $testItemId,
+        'name' => '测试冻结物品',
+        'description' => '用于测试冻结功能',
+        'category_id' => 1,
+        'type' => 1,
+        'is_unique' => false,
+        'max_stack' => 100,
+        'sell_price' => 10,
+        'tradable' => true,
+        'dismantlable' => true,
+        'default_expire_seconds' => 0,
+    ]);
+
+    echo "✓ 创建测试物品成功: {$item->name}\n";
+
+    // 开始事务
+    Helper::begin_tr();
+
+    // 1. 测试添加物品
+    $result = ItemService::addItem($testUserId, $testItemId, 100);
+    echo "✓ 添加物品成功: 数量 {$result['quantity']}\n";
+
+    // 2. 验证可用数量
+    $availableQuantity = ItemService::getAvailableQuantity($testUserId, $testItemId);
+    echo "✓ 当前可用数量: {$availableQuantity}\n";
+
+    // 3. 测试冻结物品
+    $freezeResult = ItemService::freezeItem(
+        $testUserId,
+        $testItemId,
+        null,
+        30,
+        '测试冻结',
+        [
+            'reason_type' => FREEZE_REASON_TYPE::TRADE_ORDER->value,
+            'source_id' => 123,
+            'source_type' => 'test_order'
+        ]
+    );
+    echo "✓ 冻结物品成功: 冻结数量 {$freezeResult['frozen_quantity']}\n";
+
+    // 4. 验证冻结后的可用数量
+    $availableQuantity = ItemService::getAvailableQuantity($testUserId, $testItemId);
+    echo "✓ 冻结后可用数量: {$availableQuantity}\n";
+
+    // 5. 测试获取冻结物品列表
+    $frozenItems = ItemService::getFrozenItems($testUserId);
+    echo "✓ 冻结物品数量: {$frozenItems->count()}\n";
+
+    // 6. 测试消耗物品(应该只消耗未冻结的)
+    $consumeResult = ItemService::consumeItem($testUserId, $testItemId, null, 20);
+    echo "✓ 消耗物品成功: 消耗数量 {$consumeResult['quantity']}\n";
+
+    // 7. 验证消耗后的可用数量
+    $availableQuantity = ItemService::getAvailableQuantity($testUserId, $testItemId);
+    echo "✓ 消耗后可用数量: {$availableQuantity}\n";
+
+    // 8. 测试解冻物品
+    $freezeLogId = $freezeResult['frozen_items'][0]['freeze_log_id'];
+    $unfreezeResult = ItemService::unfreezeItem($freezeLogId);
+    echo "✓ 解冻物品成功: 解冻数量 {$unfreezeResult['unfrozen_quantity']}\n";
+
+    // 9. 验证解冻后的可用数量
+    $availableQuantity = ItemService::getAvailableQuantity($testUserId, $testItemId);
+    echo "✓ 解冻后可用数量: {$availableQuantity}\n";
+
+    // 10. 测试批量冻结
+    $batchItems = [
+        ['item_id' => $testItemId, 'quantity' => 20],
+    ];
+    $batchResult = ItemService::batchFreezeItems(
+        $testUserId,
+        $batchItems,
+        '批量测试冻结',
+        ['reason_type' => FREEZE_REASON_TYPE::SYSTEM_FREEZE->value]
+    );
+    echo "✓ 批量冻结成功: 冻结物品种类 {$batchResult['frozen_items_count']}\n";
+
+    // 11. 获取冻结统计
+    $statistics = ItemService::getFreezeStatistics($testUserId);
+    echo "✓ 冻结统计: 总冻结物品 {$statistics['total_frozen_items']} 个,总数量 {$statistics['total_frozen_quantity']}\n";
+
+    // 提交事务
+    Helper::commit_tr();
+
+    echo "\n🎉 所有测试通过!冻结功能工作正常。\n";
+
+    // 清理测试数据
+    ItemUser::where('user_id', $testUserId)->delete();
+    Item::where('id', $testItemId)->delete();
+    echo "✓ 清理测试数据完成\n";
+
+} catch (Exception $e) {
+    // 回滚事务
+    Helper::rollback_tr();
+    
+    echo "\n❌ 测试失败: " . $e->getMessage() . "\n";
+    echo "错误位置: " . $e->getFile() . ":" . $e->getLine() . "\n";
+    
+    // 清理测试数据
+    try {
+        ItemUser::where('user_id', $testUserId)->delete();
+        Item::where('id', $testItemId)->delete();
+        echo "✓ 清理测试数据完成\n";
+    } catch (Exception $cleanupError) {
+        echo "⚠️ 清理测试数据失败: " . $cleanupError->getMessage() . "\n";
+    }
+}
+
+echo "\n测试完成。\n";