Item.php 17 KB

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