| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585 |
- <?php
- namespace App\Module\GameItems\Logics;
- use App\Module\GameItems\Enums\ITEM_TYPE;
- use App\Module\GameItems\Enums\TRANSACTION_TYPE;
- use App\Module\GameItems\Events\ItemAcquired;
- use App\Module\GameItems\Events\ItemConsumed;
- use App\Module\GameItems\Events\ItemQuantityChanged;
- use App\Module\GameItems\Models\Item as ItemModel;
- use App\Module\GameItems\Models\ItemInstance;
- use App\Module\GameItems\Models\ItemTransactionLog;
- use App\Module\GameItems\Models\ItemUser;
- use Exception;
- use Illuminate\Support\Facades\Event;
- use UCore\Db\Helper;
- use UCore\Dto\Res;
- /**
- * 物品逻辑类
- */
- class Item
- {
- /**
- * 判断物品是否为宝箱
- *
- * @param ItemModel $item 物品模型
- * @return bool
- */
- public static function isChest(ItemModel $item): bool
- {
- return $item->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,
- ]);
- }
- }
|