type == ITEM_TYPE::CHEST; // 使用枚举代替魔法数字 } /** * 检查物品是否已过期(全局过期) * * @param ItemModel $item 物品模型 * @return bool */ public static function isExpired(ItemModel $item): bool { if (empty($item->global_expire_at)) { return false; } // 确保 global_expire_at 是 Carbon 实例 $expireAt = $item->global_expire_at; if (is_string($expireAt)) { $expireAt = \Carbon\Carbon::parse($expireAt); } return $expireAt->isPast(); } /** * 添加统一属性物品 * * @param int $userId 用户ID * @param int $itemId 物品ID * @param int $quantity 数量 * @param array $options 选项 * @return array 添加结果 * @throws Exception */ public static function addNormalItem(int $userId, int $itemId, int $quantity, array $options = []): array { // 获取物品信息 $item = ItemModel::findOrFail($itemId); // 计算过期时间 $expireAt = null; if (!empty($options['expire_at'])) { $expireAt = $options['expire_at']; } elseif ($item->default_expire_seconds > 0) { $expireAt = now()->addSeconds($item->default_expire_seconds); } // 检查事务是否已开启 Helper::check_tr(); // 获取来源信息 $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) { if ($expireAt === null) { $query->whereNull('expire_at'); } else { $query->where('expire_at', $expireAt); } }) ->whereNull('instance_id') ->where('is_frozen', false) // 排除冻结的物品 ->where(function ($query) use ($item) { // 如果有最大堆叠限制,只查找未满的堆叠 if ($item->max_stack > 0) { $query->where('quantity', '<', $item->max_stack); } }) ->first(); $addedQuantity = $quantity; $currentQuantity = 0; if ($userItem) { // 已有物品,增加数量 $currentQuantity = $userItem->quantity; $newQuantity = $currentQuantity + $quantity; // 检查最大堆叠限制 if ($item->max_stack > 0 && $newQuantity > $item->max_stack) { // 超过最大堆叠,先填满当前堆叠 $canAddToCurrent = $item->max_stack - $currentQuantity; $userItem->quantity = $item->max_stack; $userItem->save(); // 触发物品数量变更事件(更新现有堆叠) Event::dispatch(new ItemQuantityChanged( $userId, $itemId, null, $currentQuantity, $item->max_stack, $userItem->id, $options )); // 剩余数量递归添加到新堆叠 $remainingQuantity = $quantity - $canAddToCurrent; if ($remainingQuantity > 0) { self::addNormalItem($userId, $itemId, $remainingQuantity, $options); } $addedQuantity = $quantity; $currentQuantity = $item->max_stack; } else { // 未超过最大堆叠,直接更新数量 $oldQuantity = $userItem->quantity; $userItem->quantity = $newQuantity; $userItem->save(); $currentQuantity = $newQuantity; // 触发物品数量变更事件 Event::dispatch(new ItemQuantityChanged( $userId, $itemId, null, $oldQuantity, $newQuantity, $userItem->id, $options )); } } else { // 没有该物品,创建新记录 $createQuantity = min($quantity, $item->max_stack > 0 ? $item->max_stack : $quantity); $userItem = new ItemUser([ 'user_id' => $userId, 'item_id' => $itemId, 'quantity' => $createQuantity, 'expire_at' => $expireAt, ]); $userItem->save(); // 触发物品数量变更事件(新增物品) Event::dispatch(new ItemQuantityChanged( $userId, $itemId, null, 0, // 旧数量为0 $createQuantity, $userItem->id, $options )); // 如果数量超过最大堆叠,递归添加剩余数量 if ($item->max_stack > 0 && $quantity > $item->max_stack) { $remainingQuantity = $quantity - $item->max_stack; self::addNormalItem($userId, $itemId, $remainingQuantity, $options); $addedQuantity = $quantity; // 总添加数量 } else { $addedQuantity = $createQuantity; } $currentQuantity = $createQuantity; } // 记录交易日志 self::logTransaction( $userId, $itemId, null, $addedQuantity, TRANSACTION_TYPE::ACQUIRE, $sourceType, $sourceId, $options['details'] ?? null, $expireAt, $options['ip_address'] ?? null, $options['device_info'] ?? null ); // 触发物品获取事件 Event::dispatch(new ItemAcquired($userId, $itemId, null, $addedQuantity, $options)); return [ 'success' => true, 'item_id' => $itemId, 'quantity' => $addedQuantity, 'current_quantity' => $currentQuantity, 'user_item_id' => $userItem->id, ]; } /** * 添加单独属性物品 * * @param int $userId 用户ID * @param int $itemId 物品ID * @param array $options 选项 * @return array 添加结果 * @throws Exception */ public static function addUniqueItem(int $userId, int $itemId, array $options = []): array { // 获取物品信息 $item = ItemModel::findOrFail($itemId); // 确保物品是单独属性物品 if (!$item->is_unique) { throw new Exception("物品 {$itemId} 不是单独属性物品"); } // 计算过期时间 $expireAt = null; if (!empty($options['expire_at'])) { $expireAt = $options['expire_at']; } elseif ($item->default_expire_seconds > 0) { $expireAt = now()->addSeconds($item->default_expire_seconds); } // 检查事务是否已开启 Helper::check_tr(); // 获取来源信息 $sourceType = $options['source_type'] ?? null; $sourceId = $options['source_id'] ?? null; if(!$sourceType || !$sourceId){ throw new Exception("物品 {$itemId} ,缺少来源类型."); } // 创建物品实例 $instance = new ItemInstance([ 'item_id' => $itemId, 'name' => $options['name'] ?? $item->name, 'display_attributes' => $options['display_attributes'] ?? $item->display_attributes, 'numeric_attributes' => $options['numeric_attributes'] ?? $item->numeric_attributes, 'tradable' => $options['tradable'] ?? $item->tradable, 'is_bound' => $options['is_bound'] ?? false, 'bound_to' => $options['bound_to'] ?? null, 'bind_exp_time' => $options['bind_exp_time'] ?? null, 'expire_at' => $expireAt, ]); $instance->save(); // 关联到用户 $userItem = new ItemUser([ 'user_id' => $userId, 'item_id' => $itemId, 'instance_id' => $instance->id, 'quantity' => 1, // 单独属性物品数量始终为1 'expire_at' => $expireAt, ]); $userItem->save(); // 记录交易日志 self::logTransaction( $userId, $itemId, $instance->id, 1, TRANSACTION_TYPE::ACQUIRE, $sourceType, $sourceId, $options['details'] ?? null, $expireAt, $options['ip_address'] ?? null, $options['device_info'] ?? null ); // 触发物品获取事件 Event::dispatch(new ItemAcquired($userId, $itemId, $instance->id, 1, $options)); // 触发物品数量变更事件(新增物品) Event::dispatch(new ItemQuantityChanged( $userId, $itemId, $instance->id, 0, // 旧数量为0 1, // 新数量为1 $userItem->id, $options )); return [ 'success' => true, 'item_id' => $itemId, 'instance_id' => $instance->id, 'user_item_id' => $userItem->id, ]; } /** * 消耗统一属性物品 * * @param int $userId 用户ID * @param int $itemId 物品ID * @param int $quantity 数量 * @param array $options 选项 * @return array 消耗结果 * @throws Exception */ public static function consumeNormalItem(int $userId, int $itemId, int $quantity, array $options = []): array { Helper::check_tr(); // 检查是否包含冻结物品 $includeFrozen = $options['include_frozen'] ?? false; // 构建查询条件 $query = ItemUser::where('user_id', $userId) ->where('item_id', $itemId) ->whereNull('instance_id') ->where('quantity', '>', 0); // 确保数量大于0 // 根据include_frozen参数决定是否包含冻结物品 if (!$includeFrozen) { $query->where('is_frozen', false); // 只获取未冻结的物品 } // 获取用户物品(优先消耗冻结物品,然后按过期时间排序) 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'); if ($totalQuantity < $quantity) { throw new Exception("用户 {$userId} 的物品 {$itemId} 数量不足,需要 {$quantity},实际 {$totalQuantity}"); } // 获取来源信息 $sourceType = $options['source_type'] ?? null; $sourceId = $options['source_id'] ?? null; // 开始消耗物品 $remainingQuantity = $quantity; foreach ($userItems as $userItem) { if ($remainingQuantity <= 0) { break; } if ($userItem->quantity <= $remainingQuantity) { // 当前堆叠数量不足,全部消耗 $consumedQuantity = $userItem->quantity; $remainingQuantity -= $consumedQuantity; $oldQuantity = $userItem->quantity; // 记录交易日志 self::logTransaction( $userId, $itemId, null, -$consumedQuantity, TRANSACTION_TYPE::CONSUME, $sourceType, $sourceId, $options['details'] ?? null, null, $options['ip_address'] ?? null, $options['device_info'] ?? null ); // 将数量设置为0,不删除记录 $userItem->quantity = 0; $userItem->save(); // 触发物品数量变更事件 Event::dispatch(new ItemQuantityChanged( $userId, $itemId, null, $oldQuantity, 0, $userItem->id, $options )); } else { // 当前堆叠数量足够,部分消耗 $consumedQuantity = $remainingQuantity; $oldQuantity = $userItem->quantity; $newQuantity = $oldQuantity - $consumedQuantity; $userItem->quantity = $newQuantity; $userItem->save(); $remainingQuantity = 0; // 记录交易日志 self::logTransaction( $userId, $itemId, null, -$consumedQuantity, TRANSACTION_TYPE::CONSUME, $sourceType, $sourceId, $options['details'] ?? null, null, $options['ip_address'] ?? null, $options['device_info'] ?? null ); // 触发物品数量变更事件 Event::dispatch(new ItemQuantityChanged( $userId, $itemId, null, $oldQuantity, $newQuantity, $userItem->id, $options )); } // 触发物品消耗事件 Event::dispatch(new ItemConsumed($userId, $itemId, null, $consumedQuantity, $options)); } return [ 'success' => true, 'item_id' => $itemId, 'quantity' => $quantity, 'remaining_quantity' => $totalQuantity - $quantity, ]; } /** * 消耗单独属性物品 * * @param int $userId 用户ID * @param int $itemId 物品ID * @param int $instanceId 物品实例ID * @param array $options 选项 * @return array 消耗结果 * @throws Exception */ public static function consumeUniqueItem(int $userId, int $itemId, int $instanceId, array $options = []): Res { Helper::check_tr(); // 检查是否包含冻结物品 $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); // 只获取未冻结的物品 } $userItem = $query->first(); if (!$userItem) { $frozenText = $includeFrozen ? '' : '(未冻结)'; throw new Exception("用户 {$userId} 没有物品实例 {$instanceId}{$frozenText}"); } // 获取来源信息 $sourceType = $options['source_type'] ?? null; $sourceId = $options['source_id'] ?? null; // 记录交易日志 self::logTransaction( $userId, $itemId, $instanceId, -1, TRANSACTION_TYPE::CONSUME, $sourceType, $sourceId, $options['details'] ?? null, null, $options['ip_address'] ?? null, $options['device_info'] ?? null ); // 删除用户物品记录 $userItem->delete(); // 是否删除物品实例 if (!empty($options['delete_instance'])) { ItemInstance::where('id', $instanceId)->delete(); } // 触发物品消耗事件 Event::dispatch(new ItemConsumed($userId, $itemId, $instanceId, 1, $options)); return Res::success('', [ 'item_id' => $itemId, 'instance_id' => $instanceId, ]); } /** * 记录物品交易日志 * * @param int $userId 用户ID * @param int $itemId 物品ID * @param int|null $instanceId 物品实例ID * @param int $quantity 数量 * @param int $transactionType 交易类型 * @param mixed $sourceType 来源类型(支持字符串或枚举类型) * @param int|null $sourceId 来源ID * @param array|null $details 详细信息 * @param string|null $expireAt 过期时间 * @param string|null $ipAddress IP地址 * @param string|null $deviceInfo 设备信息 * @return ItemTransactionLog */ public static function logTransaction( int $userId, int $itemId, ?int $instanceId, int $quantity, int $transactionType, $sourceType = null, ?int $sourceId = null, ?array $details = null, ?string $expireAt = null, ?string $ipAddress = null, ?string $deviceInfo = null ): ItemTransactionLog { // 处理枚举类型的sourceType $sourceTypeValue = null; if ($sourceType !== null) { if (is_object($sourceType) && method_exists($sourceType, 'value')) { // 如果是枚举类型,获取其值 $sourceTypeValue = $sourceType->value; } elseif (is_string($sourceType)) { // 如果是字符串,直接使用 $sourceTypeValue = $sourceType; } else { // 其他类型转换为字符串 $sourceTypeValue = (string)$sourceType; } } return ItemTransactionLog::create([ 'user_id' => $userId, 'item_id' => $itemId, 'instance_id' => $instanceId, 'quantity' => $quantity, 'transaction_type' => $transactionType, 'source_type' => $sourceTypeValue, 'source_id' => $sourceId, 'details' => $details, 'expire_at' => $expireAt, 'ip_address' => $ipAddress, 'device_info' => $deviceInfo, ]); } }