Item.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. <?php
  2. namespace App\Module\GameItems\Logics;
  3. use App\Module\GameItems\Enums\ITEM_TYPE;
  4. use App\Module\GameItems\Enums\TRANSACTION_TYPE;
  5. use App\Module\GameItems\Events\ItemAcquired;
  6. use App\Module\GameItems\Events\ItemConsumed;
  7. use App\Module\GameItems\Events\ItemQuantityChanged;
  8. use App\Module\GameItems\Models\Item as ItemModel;
  9. use App\Module\GameItems\Models\ItemInstance;
  10. use App\Module\GameItems\Models\ItemTransactionLog;
  11. use App\Module\GameItems\Models\ItemUser;
  12. use Exception;
  13. use Illuminate\Support\Facades\DB;
  14. use Illuminate\Support\Facades\Event;
  15. /**
  16. * 物品逻辑类
  17. */
  18. class Item
  19. {
  20. /**
  21. * 判断物品是否为宝箱
  22. *
  23. * @param ItemModel $item 物品模型
  24. * @return bool
  25. */
  26. public static function isChest(ItemModel $item): bool
  27. {
  28. return $item->type == ITEM_TYPE::CHEST; // 使用枚举代替魔法数字
  29. }
  30. /**
  31. * 检查物品是否已过期(全局过期)
  32. *
  33. * @param ItemModel $item 物品模型
  34. * @return bool
  35. */
  36. public static function isExpired(ItemModel $item): bool
  37. {
  38. if (empty($item->global_expire_at)) {
  39. return false;
  40. }
  41. return $item->global_expire_at->isPast();
  42. }
  43. /**
  44. * 添加统一属性物品
  45. *
  46. * @param int $userId 用户ID
  47. * @param int $itemId 物品ID
  48. * @param int $quantity 数量
  49. * @param array $options 选项
  50. * @return array 添加结果
  51. * @throws Exception
  52. */
  53. public static function addNormalItem(int $userId, int $itemId, int $quantity, array $options = []): array
  54. {
  55. // 获取物品信息
  56. $item = ItemModel::findOrFail($itemId);
  57. // 计算过期时间
  58. $expireAt = null;
  59. if (!empty($options['expire_at'])) {
  60. $expireAt = $options['expire_at'];
  61. } elseif ($item->default_expire_seconds > 0) {
  62. $expireAt = now()->addSeconds($item->default_expire_seconds);
  63. }
  64. // 获取来源信息
  65. $sourceType = $options['source_type'] ?? null;
  66. $sourceId = $options['source_id'] ?? null;
  67. // 开始事务
  68. DB::beginTransaction();
  69. try {
  70. // 检查用户是否已有该物品且过期时间相同
  71. $userItem = ItemUser::where('user_id', $userId)
  72. ->where('item_id', $itemId)
  73. ->where(function ($query) use ($expireAt) {
  74. if ($expireAt === null) {
  75. $query->whereNull('expire_at');
  76. } else {
  77. $query->where('expire_at', $expireAt);
  78. }
  79. })
  80. ->whereNull('instance_id')
  81. ->first();
  82. $addedQuantity = $quantity;
  83. $currentQuantity = 0;
  84. if ($userItem) {
  85. // 已有物品,增加数量
  86. $currentQuantity = $userItem->quantity;
  87. $newQuantity = $currentQuantity + $quantity;
  88. // 检查最大堆叠限制
  89. if ($item->max_stack > 0 && $newQuantity > $item->max_stack) {
  90. // 超过最大堆叠,创建新堆叠
  91. $userItem->quantity = $item->max_stack;
  92. $userItem->save();
  93. // 剩余数量
  94. $remainingQuantity = $newQuantity - $item->max_stack;
  95. // 递归添加剩余数量
  96. self::addNormalItem($userId, $itemId, $remainingQuantity, $options);
  97. $addedQuantity = $quantity - $remainingQuantity;
  98. $currentQuantity = $item->max_stack;
  99. } else {
  100. // 未超过最大堆叠,直接更新数量
  101. $oldQuantity = $userItem->quantity;
  102. $userItem->quantity = $newQuantity;
  103. $userItem->save();
  104. $currentQuantity = $newQuantity;
  105. // 触发物品数量变更事件
  106. Event::dispatch(new ItemQuantityChanged(
  107. $userId,
  108. $itemId,
  109. null,
  110. $oldQuantity,
  111. $newQuantity,
  112. $userItem->id,
  113. $options
  114. ));
  115. }
  116. } else {
  117. // 没有该物品,创建新记录
  118. $userItem = new ItemUser([
  119. 'user_id' => $userId,
  120. 'item_id' => $itemId,
  121. 'quantity' => min($quantity, $item->max_stack > 0 ? $item->max_stack : $quantity),
  122. 'expire_at' => $expireAt,
  123. ]);
  124. $userItem->save();
  125. // 如果数量超过最大堆叠,递归添加剩余数量
  126. if ($item->max_stack > 0 && $quantity > $item->max_stack) {
  127. $remainingQuantity = $quantity - $item->max_stack;
  128. self::addNormalItem($userId, $itemId, $remainingQuantity, $options);
  129. $addedQuantity = $item->max_stack;
  130. }
  131. $currentQuantity = $userItem->quantity;
  132. }
  133. // 记录交易日志
  134. self::logTransaction(
  135. $userId,
  136. $itemId,
  137. null,
  138. $addedQuantity,
  139. TRANSACTION_TYPE::ACQUIRE,
  140. $sourceType,
  141. $sourceId,
  142. $options['details'] ?? null,
  143. $expireAt,
  144. $options['ip_address'] ?? null,
  145. $options['device_info'] ?? null
  146. );
  147. // 触发物品获取事件
  148. Event::dispatch(new ItemAcquired($userId, $itemId, null, $addedQuantity, $options));
  149. DB::commit();
  150. return [
  151. 'success' => true,
  152. 'item_id' => $itemId,
  153. 'quantity' => $addedQuantity,
  154. 'current_quantity' => $currentQuantity,
  155. 'user_item_id' => $userItem->id,
  156. ];
  157. } catch (Exception $e) {
  158. DB::rollBack();
  159. throw $e;
  160. }
  161. }
  162. /**
  163. * 添加单独属性物品
  164. *
  165. * @param int $userId 用户ID
  166. * @param int $itemId 物品ID
  167. * @param array $options 选项
  168. * @return array 添加结果
  169. * @throws Exception
  170. */
  171. public static function addUniqueItem(int $userId, int $itemId, array $options = []): array
  172. {
  173. // 获取物品信息
  174. $item = ItemModel::findOrFail($itemId);
  175. // 确保物品是单独属性物品
  176. if (!$item->is_unique) {
  177. throw new Exception("物品 {$itemId} 不是单独属性物品");
  178. }
  179. // 计算过期时间
  180. $expireAt = null;
  181. if (!empty($options['expire_at'])) {
  182. $expireAt = $options['expire_at'];
  183. } elseif ($item->default_expire_seconds > 0) {
  184. $expireAt = now()->addSeconds($item->default_expire_seconds);
  185. }
  186. // 获取来源信息
  187. $sourceType = $options['source_type'] ?? null;
  188. $sourceId = $options['source_id'] ?? null;
  189. // 开始事务
  190. DB::beginTransaction();
  191. try {
  192. // 创建物品实例
  193. $instance = new ItemInstance([
  194. 'item_id' => $itemId,
  195. 'name' => $options['name'] ?? $item->name,
  196. 'display_attributes' => $options['display_attributes'] ?? $item->display_attributes,
  197. 'numeric_attributes' => $options['numeric_attributes'] ?? $item->numeric_attributes,
  198. 'tradable' => $options['tradable'] ?? $item->tradable,
  199. 'is_bound' => $options['is_bound'] ?? false,
  200. 'bound_to' => $options['bound_to'] ?? null,
  201. 'bind_exp_time' => $options['bind_exp_time'] ?? null,
  202. 'expire_at' => $expireAt,
  203. ]);
  204. $instance->save();
  205. // 关联到用户
  206. $userItem = new ItemUser([
  207. 'user_id' => $userId,
  208. 'item_id' => $itemId,
  209. 'instance_id' => $instance->id,
  210. 'quantity' => 1, // 单独属性物品数量始终为1
  211. 'expire_at' => $expireAt,
  212. ]);
  213. $userItem->save();
  214. // 记录交易日志
  215. self::logTransaction(
  216. $userId,
  217. $itemId,
  218. $instance->id,
  219. 1,
  220. TRANSACTION_TYPE::ACQUIRE,
  221. $sourceType,
  222. $sourceId,
  223. $options['details'] ?? null,
  224. $expireAt,
  225. $options['ip_address'] ?? null,
  226. $options['device_info'] ?? null
  227. );
  228. // 触发物品获取事件
  229. Event::dispatch(new ItemAcquired($userId, $itemId, $instance->id, 1, $options));
  230. DB::commit();
  231. return [
  232. 'success' => true,
  233. 'item_id' => $itemId,
  234. 'instance_id' => $instance->id,
  235. 'user_item_id' => $userItem->id,
  236. ];
  237. } catch (Exception $e) {
  238. DB::rollBack();
  239. throw $e;
  240. }
  241. }
  242. /**
  243. * 消耗统一属性物品
  244. *
  245. * @param int $userId 用户ID
  246. * @param int $itemId 物品ID
  247. * @param int $quantity 数量
  248. * @param array $options 选项
  249. * @return array 消耗结果
  250. * @throws Exception
  251. */
  252. public static function consumeNormalItem(int $userId, int $itemId, int $quantity, array $options = []): array
  253. {
  254. // 获取用户物品
  255. $userItems = ItemUser::where('user_id', $userId)
  256. ->where('item_id', $itemId)
  257. ->whereNull('instance_id')
  258. ->orderBy('expire_at') // 优先消耗即将过期的物品
  259. ->get();
  260. // 检查物品数量是否足够
  261. $totalQuantity = $userItems->sum('quantity');
  262. if ($totalQuantity < $quantity) {
  263. throw new Exception("用户 {$userId} 的物品 {$itemId} 数量不足,需要 {$quantity},实际 {$totalQuantity}");
  264. }
  265. // 获取来源信息
  266. $sourceType = $options['source_type'] ?? null;
  267. $sourceId = $options['source_id'] ?? null;
  268. // 开始消耗物品
  269. $remainingQuantity = $quantity;
  270. foreach ($userItems as $userItem) {
  271. if ($remainingQuantity <= 0) {
  272. break;
  273. }
  274. if ($userItem->quantity <= $remainingQuantity) {
  275. // 当前堆叠数量不足,全部消耗
  276. $consumedQuantity = $userItem->quantity;
  277. $remainingQuantity -= $consumedQuantity;
  278. // 记录交易日志
  279. self::logTransaction(
  280. $userId,
  281. $itemId,
  282. null,
  283. -$consumedQuantity,
  284. TRANSACTION_TYPE::CONSUME,
  285. $sourceType,
  286. $sourceId,
  287. $options['details'] ?? null,
  288. null,
  289. $options['ip_address'] ?? null,
  290. $options['device_info'] ?? null
  291. );
  292. // 删除用户物品记录
  293. $userItem->delete();
  294. } else {
  295. // 当前堆叠数量足够,部分消耗
  296. $consumedQuantity = $remainingQuantity;
  297. $oldQuantity = $userItem->quantity;
  298. $newQuantity = $oldQuantity - $consumedQuantity;
  299. $userItem->quantity = $newQuantity;
  300. $userItem->save();
  301. $remainingQuantity = 0;
  302. // 记录交易日志
  303. self::logTransaction(
  304. $userId,
  305. $itemId,
  306. null,
  307. -$consumedQuantity,
  308. TRANSACTION_TYPE::CONSUME,
  309. $sourceType,
  310. $sourceId,
  311. $options['details'] ?? null,
  312. null,
  313. $options['ip_address'] ?? null,
  314. $options['device_info'] ?? null
  315. );
  316. // 触发物品数量变更事件
  317. Event::dispatch(new ItemQuantityChanged(
  318. $userId,
  319. $itemId,
  320. null,
  321. $oldQuantity,
  322. $newQuantity,
  323. $userItem->id,
  324. $options
  325. ));
  326. }
  327. // 触发物品消耗事件
  328. Event::dispatch(new ItemConsumed($userId, $itemId, null, $consumedQuantity, $options));
  329. }
  330. return [
  331. 'success' => true,
  332. 'item_id' => $itemId,
  333. 'quantity' => $quantity,
  334. 'remaining_quantity' => $totalQuantity - $quantity,
  335. ];
  336. }
  337. /**
  338. * 消耗单独属性物品
  339. *
  340. * @param int $userId 用户ID
  341. * @param int $itemId 物品ID
  342. * @param int $instanceId 物品实例ID
  343. * @param array $options 选项
  344. * @return array 消耗结果
  345. * @throws Exception
  346. */
  347. public static function consumeUniqueItem(int $userId, int $itemId, int $instanceId, array $options = []): array
  348. {
  349. // 获取用户物品
  350. $userItem = ItemUser::where('user_id', $userId)
  351. ->where('item_id', $itemId)
  352. ->where('instance_id', $instanceId)
  353. ->first();
  354. if (!$userItem) {
  355. throw new Exception("用户 {$userId} 没有物品实例 {$instanceId}");
  356. }
  357. // 获取来源信息
  358. $sourceType = $options['source_type'] ?? null;
  359. $sourceId = $options['source_id'] ?? null;
  360. // 记录交易日志
  361. self::logTransaction(
  362. $userId,
  363. $itemId,
  364. $instanceId,
  365. -1,
  366. TRANSACTION_TYPE::CONSUME,
  367. $sourceType,
  368. $sourceId,
  369. $options['details'] ?? null,
  370. null,
  371. $options['ip_address'] ?? null,
  372. $options['device_info'] ?? null
  373. );
  374. // 删除用户物品记录
  375. $userItem->delete();
  376. // 是否删除物品实例
  377. if (!empty($options['delete_instance'])) {
  378. ItemInstance::where('id', $instanceId)->delete();
  379. }
  380. // 触发物品消耗事件
  381. Event::dispatch(new ItemConsumed($userId, $itemId, $instanceId, 1, $options));
  382. return [
  383. 'success' => true,
  384. 'item_id' => $itemId,
  385. 'instance_id' => $instanceId,
  386. ];
  387. }
  388. /**
  389. * 记录物品交易日志
  390. *
  391. * @param int $userId 用户ID
  392. * @param int $itemId 物品ID
  393. * @param int|null $instanceId 物品实例ID
  394. * @param int $quantity 数量
  395. * @param int $transactionType 交易类型
  396. * @param string|null $sourceType 来源类型
  397. * @param int|null $sourceId 来源ID
  398. * @param array|null $details 详细信息
  399. * @param string|null $expireAt 过期时间
  400. * @param string|null $ipAddress IP地址
  401. * @param string|null $deviceInfo 设备信息
  402. * @return ItemTransactionLog
  403. */
  404. public static function logTransaction(
  405. int $userId,
  406. int $itemId,
  407. ?int $instanceId,
  408. int $quantity,
  409. int $transactionType,
  410. ?string $sourceType = null,
  411. ?int $sourceId = null,
  412. ?array $details = null,
  413. ?string $expireAt = null,
  414. ?string $ipAddress = null,
  415. ?string $deviceInfo = null
  416. ): ItemTransactionLog {
  417. return ItemTransactionLog::create([
  418. 'user_id' => $userId,
  419. 'item_id' => $itemId,
  420. 'instance_id' => $instanceId,
  421. 'quantity' => $quantity,
  422. 'transaction_type' => $transactionType,
  423. 'source_type' => $sourceType,
  424. 'source_id' => $sourceId,
  425. 'details' => $details,
  426. 'expire_at' => $expireAt,
  427. 'ip_address' => $ipAddress,
  428. 'device_info' => $deviceInfo,
  429. ]);
  430. }
  431. }