where('item_id', $itemId) ->where('is_frozen', false) ->whereNull('instance_id') ->where('quantity', '>', 0) ->orderBy('expire_at') ->get(); $remainingQuantity = $quantity; $frozenItems = []; $changedItems = []; // 记录所有变更的物品,包括数量减少和冻结创建 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) { Logger::debug('不拆堆'); // 全部冻结,直接标记为冻结状态 $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, ]; // 记录冻结状态变更(数量不变,但冻结状态从false变为true) $changedItems[] = [ 'type' => 'freeze_status_change', 'user_item_id' => $userItem->id, 'old_quantity' => $availableQuantity, 'new_quantity' => $availableQuantity, 'old_frozen_status' => false, 'new_frozen_status' => true, 'freeze_log_id' => $freezeLog->id, ]; } else { // 部分冻结,需要拆堆 Logger::debug('拆堆'); // 减少原堆叠数量 $userItem->quantity = $availableQuantity - $freezeQuantity; $userItem->save(); // 记录原堆叠数量减少(非冻结物品减少) $changedItems[] = [ 'type' => 'quantity_decrease', 'user_item_id' => $userItem->id, 'old_quantity' => $availableQuantity, 'new_quantity' => $availableQuantity - $freezeQuantity, 'old_frozen_status' => false, 'new_frozen_status' => false, // 冻结状态不变,仍为未冻结 'freeze_log_id' => null, ]; // 创建新的冻结堆叠 $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, ]; // 记录新冻结堆叠创建(冻结物品创建) $changedItems[] = [ 'type' => 'frozen_item_create', 'user_item_id' => $frozenItem->id, 'old_quantity' => 0, // 新创建的物品,旧数量为0 'new_quantity' => $freezeQuantity, 'old_frozen_status' => null, // 新创建的物品,旧冻结状态为null 'new_frozen_status' => true, 'freeze_log_id' => $freezeLog->id, ]; } $remainingQuantity -= $freezeQuantity; } if ($remainingQuantity > 0) { throw new Exception("冻结操作失败,剩余未冻结数量:{$remainingQuantity}"); } // 触发物品变更事件(包括非冻结减少和冻结创建) foreach ($changedItems as $changedItem) { event(new ItemQuantityChanged( $userId, $itemId, null, // 统一属性物品没有实例ID $changedItem['old_quantity'], $changedItem['new_quantity'], $changedItem['user_item_id'], $changedItem['old_frozen_status'], $changedItem['new_frozen_status'], [ 'freeze_action' => 'freeze', 'change_type' => $changedItem['type'], // 变更类型:quantity_decrease, frozen_item_create, freeze_status_change 'freeze_log_id' => $changedItem['freeze_log_id'], 'reason' => $reason->getName($reason->value), 'source_id' => $sourceId, 'source_type' => $sourceType, 'operator_id' => $operatorId, ] )); } 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(); // 触发物品变更事件(冻结状态变更) event(new ItemQuantityChanged( $userId, $itemId, $instanceId, 1, // 旧数量(单独属性物品数量始终为1) 1, // 新数量(数量未变) $userItem->id, false, // 旧冻结状态:未冻结 true, // 新冻结状态:已冻结 [ 'freeze_action' => 'freeze', 'freeze_log_id' => $freezeLog->id, 'reason' => $reason->getName($reason->value), 'source_id' => $sourceId, 'source_type' => $sourceType, 'operator_id' => $operatorId, ] )); 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} 对应的冻结物品"); } // 获取原始冻结数量和当前剩余数量 $originalFrozenQuantity = $freezeLog->quantity; $currentQuantity = $frozenItem->quantity; if ($currentQuantity <= 0) { // 冻结堆已被完全消耗,无法解冻 throw new Exception("冻结日志 {$freezeLogId} 对应的冻结物品已被完全消耗,无法解冻"); } // 检查是否需要补足差额 $shortageQuantity = $originalFrozenQuantity - $currentQuantity; if ($shortageQuantity > 0) { // 冻结堆被部分消耗,需要从用户其他可用物品中补足 $availableQuantity = self::getAvailableQuantity( $frozenItem->user_id, $frozenItem->item_id, $frozenItem->instance_id ); if ($availableQuantity < $shortageQuantity) { throw new Exception( "解冻失败:原始冻结数量 {$originalFrozenQuantity},当前冻结剩余 {$currentQuantity}," . "需要补足 {$shortageQuantity},但用户可用数量只有 {$availableQuantity}" ); } // 从用户可用物品中扣除差额,补足到冻结堆 $availableItems = ItemUser::where('user_id', $frozenItem->user_id) ->where('item_id', $frozenItem->item_id) ->where('is_frozen', false) ->whereNull('instance_id') ->where('quantity', '>', 0) ->orderBy('expire_at') ->get(); $remainingShortage = $shortageQuantity; $transferDetails = []; // 记录转移详情 foreach ($availableItems as $availableItem) { if ($remainingShortage <= 0) break; $deductQuantity = min($availableItem->quantity, $remainingShortage); $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, // 使用交易失去类型 'unfreeze_compensation', $freezeLogId, ["解冻补足转移:从可用物品转移 {$deductQuantity} 个到冻结堆"] ); // 触发物品数量变更事件(可用物品减少) event(new ItemQuantityChanged( $frozenItem->user_id, $frozenItem->item_id, null, $oldQuantity, $newQuantity, $availableItem->id, false, // 旧冻结状态:未冻结 false, // 新冻结状态:未冻结 [ 'action' => 'unfreeze_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, // 使用交易获得类型 'unfreeze_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' => 'unfreeze_compensation_transfer_in', 'freeze_log_id' => $freezeLogId, 'compensated_quantity' => $shortageQuantity, 'transfer_details' => $transferDetails, ] )); } // 创建解冻日志,记录原始冻结数量 $unfreezeLog = ItemFreezeLog::createLog( $frozenItem->user_id, $frozenItem->item_id, $frozenItem->instance_id, $originalFrozenQuantity, // 解冻原始数量 FREEZE_ACTION_TYPE::UNFREEZE, "解冻操作,原冻结日志ID: {$freezeLogId},原始冻结数量: {$originalFrozenQuantity}" . ($shortageQuantity > 0 ? ",补足差额: {$shortageQuantity}" : ""), $freezeLog->source_id, $freezeLog->source_type, $freezeLog->operator_id ); // 解冻物品(保持独立,不合并堆叠) $frozenItem->is_frozen = false; $frozenItem->frozen_log_id = null; $frozenItem->save(); // 触发物品变更事件(解冻状态变更) event(new ItemQuantityChanged( $frozenItem->user_id, $frozenItem->item_id, $frozenItem->instance_id, $originalFrozenQuantity, // 旧数量(恢复到原始数量) $originalFrozenQuantity, // 新数量(数量未变,只是解冻状态变更) $frozenItem->id, true, // 旧冻结状态:已冻结 false, // 新冻结状态:未冻结 [ 'freeze_action' => 'unfreeze', 'unfreeze_log_id' => $unfreezeLog->id, 'original_freeze_log_id' => $freezeLogId, 'original_frozen_quantity' => $originalFrozenQuantity, 'shortage_compensated' => $shortageQuantity, 'source_id' => $freezeLog->source_id, 'source_type' => $freezeLog->source_type, 'operator_id' => $freezeLog->operator_id, ] )); return [ 'success' => true, 'user_id' => $frozenItem->user_id, 'item_id' => $frozenItem->item_id, 'instance_id' => $frozenItem->instance_id, 'unfrozen_quantity' => $originalFrozenQuantity, // 返回原始冻结数量 'shortage_compensated' => $shortageQuantity, // 返回补足的差额 '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; } /** * 安全解冻物品(处理已被消耗的冻结堆) * * 与unfreezeByLogId不同,此方法会检查冻结物品是否已被消耗: * - 如果冻结物品数量为0,则标记为已处理,不抛出异常 * - 如果冻结物品数量大于0,则正常解冻 * - 返回详细的处理结果信息 * * @param int $freezeLogId 冻结日志ID * @return array 解冻结果 * @throws Exception */ public static function safeUnfreezeByLogId(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) { // 冻结物品不存在,可能已被删除或处理 return [ 'success' => true, 'status' => 'already_processed', 'message' => "冻结日志 {$freezeLogId} 对应的冻结物品不存在,可能已被处理", 'freeze_log_id' => $freezeLogId, 'unfrozen_quantity' => 0, ]; } // 获取原始冻结数量和当前剩余数量 $originalFrozenQuantity = $freezeLog->quantity; $currentQuantity = $frozenItem->quantity; if ($currentQuantity <= 0) { // 冻结堆已被完全消耗,尝试从用户其他可用物品中补足 $availableQuantity = self::getAvailableQuantity( $frozenItem->user_id, $frozenItem->item_id, $frozenItem->instance_id ); if ($availableQuantity < $originalFrozenQuantity) { // 用户可用数量不足,无法完全解冻 return [ 'success' => false, 'status' => 'insufficient_available', 'message' => "冻结物品已被完全消耗,用户可用数量不足以补足原始冻结数量", 'user_id' => $frozenItem->user_id, 'item_id' => $frozenItem->item_id, 'instance_id' => $frozenItem->instance_id, 'unfrozen_quantity' => 0, 'original_frozen_quantity' => $originalFrozenQuantity, 'current_frozen_quantity' => $currentQuantity, 'available_quantity' => $availableQuantity, 'shortage_quantity' => $originalFrozenQuantity - $availableQuantity, ]; } // 从用户可用物品中补足全部数量 $availableItems = ItemUser::where('user_id', $frozenItem->user_id) ->where('item_id', $frozenItem->item_id) ->where('is_frozen', false) ->whereNull('instance_id') ->where('quantity', '>', 0) ->orderBy('expire_at') ->get(); $remainingQuantity = $originalFrozenQuantity; $transferDetails = []; // 记录转移详情 foreach ($availableItems as $availableItem) { if ($remainingQuantity <= 0) break; $deductQuantity = min($availableItem->quantity, $remainingQuantity); $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 { // 检查是否需要补足差额 $shortageQuantity = $originalFrozenQuantity - $currentQuantity; if ($shortageQuantity > 0) { // 冻结堆被部分消耗,需要从用户其他可用物品中补足 $availableQuantity = self::getAvailableQuantity( $frozenItem->user_id, $frozenItem->item_id, $frozenItem->instance_id ); if ($availableQuantity < $shortageQuantity) { return [ 'success' => false, 'status' => 'insufficient_available', 'message' => "解冻失败:需要补足 {$shortageQuantity},但用户可用数量只有 {$availableQuantity}", 'user_id' => $frozenItem->user_id, 'item_id' => $frozenItem->item_id, 'instance_id' => $frozenItem->instance_id, 'unfrozen_quantity' => 0, 'original_frozen_quantity' => $originalFrozenQuantity, 'current_frozen_quantity' => $currentQuantity, 'shortage_quantity' => $shortageQuantity, 'available_quantity' => $availableQuantity, ]; } // 从用户可用物品中扣除差额,补足到冻结堆 $availableItems = ItemUser::where('user_id', $frozenItem->user_id) ->where('item_id', $frozenItem->item_id) ->where('is_frozen', false) ->whereNull('instance_id') ->where('quantity', '>', 0) ->orderBy('expire_at') ->get(); $remainingShortage = $shortageQuantity; $transferDetails = []; // 记录转移详情 foreach ($availableItems as $availableItem) { if ($remainingShortage <= 0) break; $deductQuantity = min($availableItem->quantity, $remainingShortage); $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, ] )); } } // 创建解冻日志 $unfreezeLog = ItemFreezeLog::createLog( $frozenItem->user_id, $frozenItem->item_id, $frozenItem->instance_id, $originalFrozenQuantity, FREEZE_ACTION_TYPE::UNFREEZE, "安全解冻操作,原冻结日志ID: {$freezeLogId},原始冻结数量: {$originalFrozenQuantity}" . ($shortageQuantity > 0 ? ",补足差额: {$shortageQuantity}" : ""), $freezeLog->source_id, $freezeLog->source_type, $freezeLog->operator_id ); // 解冻物品 $frozenItem->is_frozen = false; $frozenItem->frozen_log_id = null; $frozenItem->save(); // 触发物品变更事件 event(new ItemQuantityChanged( $frozenItem->user_id, $frozenItem->item_id, $frozenItem->instance_id, $originalFrozenQuantity, $originalFrozenQuantity, $frozenItem->id, true, // 旧冻结状态:已冻结 false, // 新冻结状态:未冻结 [ 'freeze_action' => 'safe_unfreeze', 'unfreeze_log_id' => $unfreezeLog->id, 'original_freeze_log_id' => $freezeLogId, 'original_frozen_quantity' => $originalFrozenQuantity, 'shortage_compensated' => $shortageQuantity, 'source_id' => $freezeLog->source_id, 'source_type' => $freezeLog->source_type, 'operator_id' => $freezeLog->operator_id, ] )); return [ 'success' => true, 'status' => 'unfrozen', 'message' => "成功解冻物品", 'user_id' => $frozenItem->user_id, 'item_id' => $frozenItem->item_id, 'instance_id' => $frozenItem->instance_id, 'unfrozen_quantity' => $originalFrozenQuantity, 'shortage_compensated' => $shortageQuantity, 'user_item_id' => $frozenItem->id, 'unfreeze_log_id' => $unfreezeLog->id, ]; } /** * 获取已被消耗的冻结物品统计 * * 查找所有数量为0的冻结物品记录并返回统计信息 * 注意:不删除记录,只提供统计信息 * * @param int|null $userId 用户ID,为null时统计所有用户 * @return array 统计结果 */ public static function getConsumedFrozenItemsStatistics(?int $userId = null): array { $query = ItemUser::where('is_frozen', true) ->where('quantity', '<=', 0); if ($userId) { $query->where('user_id', $userId); } $consumedFrozenItems = $query->get(); $statistics = [ 'total_count' => $consumedFrozenItems->count(), 'by_user' => [], 'by_item' => [], 'items' => [], ]; foreach ($consumedFrozenItems as $frozenItem) { // 按用户统计 if (!isset($statistics['by_user'][$frozenItem->user_id])) { $statistics['by_user'][$frozenItem->user_id] = 0; } $statistics['by_user'][$frozenItem->user_id]++; // 按物品统计 if (!isset($statistics['by_item'][$frozenItem->item_id])) { $statistics['by_item'][$frozenItem->item_id] = 0; } $statistics['by_item'][$frozenItem->item_id]++; // 详细记录 $statistics['items'][] = [ 'user_id' => $frozenItem->user_id, 'item_id' => $frozenItem->item_id, 'instance_id' => $frozenItem->instance_id, 'user_item_id' => $frozenItem->id, 'frozen_log_id' => $frozenItem->frozen_log_id, 'quantity' => $frozenItem->quantity, ]; } return $statistics; } }