Item.php 18 KB

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