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}"); } // 触发物品变更事件(冻结状态变更) foreach ($frozenItems as $frozenItem) { event(new ItemQuantityChanged( $userId, $itemId, null, // 统一属性物品没有实例ID $frozenItem['quantity'], // 旧数量(冻结前的数量) $frozenItem['quantity'], // 新数量(数量未变) $frozenItem['user_item_id'], false, // 旧冻结状态:未冻结 true, // 新冻结状态:已冻结 [ 'freeze_action' => 'freeze', 'freeze_log_id' => $frozenItem['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} 对应的冻结物品"); } // 创建解冻日志 $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(); // 触发物品变更事件(解冻状态变更) event(new ItemQuantityChanged( $frozenItem->user_id, $frozenItem->item_id, $frozenItem->instance_id, $frozenItem->quantity, // 旧数量(数量未变) $frozenItem->quantity, // 新数量(数量未变) $frozenItem->id, true, // 旧冻结状态:已冻结 false, // 新冻结状态:未冻结 [ 'freeze_action' => 'unfreeze', 'unfreeze_log_id' => $unfreezeLog->id, 'original_freeze_log_id' => $freezeLogId, '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' => $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; } }