ItemFreeze.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. <?php
  2. namespace App\Module\GameItems\Logics;
  3. use App\Module\GameItems\Enums\FREEZE_ACTION_TYPE;
  4. use App\Module\GameItems\Enums\FREEZE_REASON_TYPE;
  5. use App\Module\GameItems\Events\ItemQuantityChanged;
  6. use App\Module\GameItems\Models\Item;
  7. use App\Module\GameItems\Models\ItemFreezeLog;
  8. use App\Module\GameItems\Models\ItemUser;
  9. use Exception;
  10. use Illuminate\Support\Collection;
  11. use Illuminate\Support\Facades\Log;
  12. use UCore\Db\Helper;
  13. /**
  14. * 物品冻结逻辑类
  15. *
  16. * 处理物品冻结和解冻的核心业务逻辑,包括:
  17. * - 统一属性物品的冻结/解冻(拆堆模式)
  18. * - 单独属性物品的冻结/解冻
  19. * - 冻结状态验证和数量查询
  20. * - 批量冻结操作
  21. */
  22. class ItemFreeze
  23. {
  24. /**
  25. * 冻结统一属性物品(拆堆模式)
  26. *
  27. * 实现逻辑:
  28. * 1. 查找用户可用的物品堆叠(is_frozen=false)
  29. * 2. 从可用堆叠中扣除冻结数量
  30. * 3. 创建新的冻结堆叠记录(is_frozen=true)
  31. * 4. 记录冻结日志
  32. *
  33. * @param int $userId 用户ID
  34. * @param int $itemId 物品ID
  35. * @param int $quantity 冻结数量
  36. * @param FREEZE_REASON_TYPE $reason 冻结原因
  37. * @param int|null $sourceId 来源ID
  38. * @param string|null $sourceType 来源类型
  39. * @param int|null $operatorId 操作员ID
  40. * @return array 冻结结果
  41. * @throws Exception
  42. */
  43. public static function freezeNormalItem(
  44. int $userId,
  45. int $itemId,
  46. int $quantity,
  47. FREEZE_REASON_TYPE $reason,
  48. ?int $sourceId = null,
  49. ?string $sourceType = null,
  50. ?int $operatorId = null
  51. ): array {
  52. // 检查事务
  53. Helper::check_tr();
  54. // 验证冻结操作的合法性
  55. if (!self::validateFreezeOperation($userId, $itemId, $quantity)) {
  56. throw new Exception("用户 {$userId} 的物品 {$itemId} 可用数量不足,无法冻结 {$quantity} 个");
  57. }
  58. // 获取用户可用的物品堆叠(按过期时间排序,优先冻结即将过期的)
  59. $availableItems = ItemUser::where('user_id', $userId)
  60. ->where('item_id', $itemId)
  61. ->where('is_frozen', false)
  62. ->whereNull('instance_id')
  63. ->where('quantity', '>', 0)
  64. ->orderBy('expire_at')
  65. ->get();
  66. $remainingQuantity = $quantity;
  67. $frozenItems = [];
  68. foreach ($availableItems as $userItem) {
  69. if ($remainingQuantity <= 0) {
  70. break;
  71. }
  72. $availableQuantity = $userItem->quantity;
  73. $freezeQuantity = min($remainingQuantity, $availableQuantity);
  74. // 创建冻结日志
  75. $freezeLog = ItemFreezeLog::createLog(
  76. $userId,
  77. $itemId,
  78. null,
  79. $freezeQuantity,
  80. FREEZE_ACTION_TYPE::FREEZE,
  81. $reason->getName($reason->value),
  82. $sourceId,
  83. $sourceType,
  84. $operatorId
  85. );
  86. if ($freezeQuantity == $availableQuantity) {
  87. // 全部冻结,直接标记为冻结状态
  88. $userItem->is_frozen = true;
  89. $userItem->frozen_log_id = $freezeLog->id;
  90. $userItem->save();
  91. $frozenItems[] = [
  92. 'user_item_id' => $userItem->id,
  93. 'quantity' => $freezeQuantity,
  94. 'freeze_log_id' => $freezeLog->id,
  95. ];
  96. } else {
  97. // 部分冻结,需要拆堆
  98. // 减少原堆叠数量
  99. $userItem->quantity = $availableQuantity - $freezeQuantity;
  100. $userItem->save();
  101. // 创建新的冻结堆叠
  102. $frozenItem = new ItemUser([
  103. 'user_id' => $userId,
  104. 'item_id' => $itemId,
  105. 'instance_id' => null,
  106. 'quantity' => $freezeQuantity,
  107. 'expire_at' => $userItem->expire_at,
  108. 'is_frozen' => true,
  109. 'frozen_log_id' => $freezeLog->id,
  110. ]);
  111. $frozenItem->save();
  112. $frozenItems[] = [
  113. 'user_item_id' => $frozenItem->id,
  114. 'quantity' => $freezeQuantity,
  115. 'freeze_log_id' => $freezeLog->id,
  116. ];
  117. }
  118. $remainingQuantity -= $freezeQuantity;
  119. }
  120. if ($remainingQuantity > 0) {
  121. throw new Exception("冻结操作失败,剩余未冻结数量:{$remainingQuantity}");
  122. }
  123. // 触发物品变更事件(冻结状态变更)
  124. foreach ($frozenItems as $frozenItem) {
  125. event(new ItemQuantityChanged(
  126. $userId,
  127. $itemId,
  128. null, // 统一属性物品没有实例ID
  129. $frozenItem['quantity'], // 旧数量(冻结前的数量)
  130. $frozenItem['quantity'], // 新数量(数量未变)
  131. $frozenItem['user_item_id'],
  132. false, // 旧冻结状态:未冻结
  133. true, // 新冻结状态:已冻结
  134. [
  135. 'freeze_action' => 'freeze',
  136. 'freeze_log_id' => $frozenItem['freeze_log_id'],
  137. 'reason' => $reason->getName($reason->value),
  138. 'source_id' => $sourceId,
  139. 'source_type' => $sourceType,
  140. 'operator_id' => $operatorId,
  141. ]
  142. ));
  143. }
  144. return [
  145. 'success' => true,
  146. 'user_id' => $userId,
  147. 'item_id' => $itemId,
  148. 'frozen_quantity' => $quantity,
  149. 'frozen_items' => $frozenItems,
  150. ];
  151. }
  152. /**
  153. * 冻结单独属性物品
  154. *
  155. * @param int $userId 用户ID
  156. * @param int $itemId 物品ID
  157. * @param int $instanceId 物品实例ID
  158. * @param FREEZE_REASON_TYPE $reason 冻结原因
  159. * @param int|null $sourceId 来源ID
  160. * @param string|null $sourceType 来源类型
  161. * @param int|null $operatorId 操作员ID
  162. * @return array 冻结结果
  163. * @throws Exception
  164. */
  165. public static function freezeUniqueItem(
  166. int $userId,
  167. int $itemId,
  168. int $instanceId,
  169. FREEZE_REASON_TYPE $reason,
  170. ?int $sourceId = null,
  171. ?string $sourceType = null,
  172. ?int $operatorId = null
  173. ): array {
  174. // 检查事务
  175. Helper::check_tr();
  176. // 查找用户的单独属性物品
  177. $userItem = ItemUser::where('user_id', $userId)
  178. ->where('item_id', $itemId)
  179. ->where('instance_id', $instanceId)
  180. ->where('is_frozen', false)
  181. ->first();
  182. if (!$userItem) {
  183. throw new Exception("用户 {$userId} 没有可冻结的物品实例 {$instanceId}");
  184. }
  185. // 创建冻结日志
  186. $freezeLog = ItemFreezeLog::createLog(
  187. $userId,
  188. $itemId,
  189. $instanceId,
  190. 1, // 单独属性物品数量始终为1
  191. FREEZE_ACTION_TYPE::FREEZE,
  192. $reason->getName($reason->value),
  193. $sourceId,
  194. $sourceType,
  195. $operatorId
  196. );
  197. // 标记为冻结状态
  198. $userItem->is_frozen = true;
  199. $userItem->frozen_log_id = $freezeLog->id;
  200. $userItem->save();
  201. // 触发物品变更事件(冻结状态变更)
  202. event(new ItemQuantityChanged(
  203. $userId,
  204. $itemId,
  205. $instanceId,
  206. 1, // 旧数量(单独属性物品数量始终为1)
  207. 1, // 新数量(数量未变)
  208. $userItem->id,
  209. false, // 旧冻结状态:未冻结
  210. true, // 新冻结状态:已冻结
  211. [
  212. 'freeze_action' => 'freeze',
  213. 'freeze_log_id' => $freezeLog->id,
  214. 'reason' => $reason->getName($reason->value),
  215. 'source_id' => $sourceId,
  216. 'source_type' => $sourceType,
  217. 'operator_id' => $operatorId,
  218. ]
  219. ));
  220. return [
  221. 'success' => true,
  222. 'user_id' => $userId,
  223. 'item_id' => $itemId,
  224. 'instance_id' => $instanceId,
  225. 'user_item_id' => $userItem->id,
  226. 'freeze_log_id' => $freezeLog->id,
  227. ];
  228. }
  229. /**
  230. * 解冻物品(通过冻结日志ID)
  231. *
  232. * @param int $freezeLogId 冻结日志ID
  233. * @return array 解冻结果
  234. * @throws Exception
  235. */
  236. public static function unfreezeByLogId(int $freezeLogId): array
  237. {
  238. // 检查事务
  239. Helper::check_tr();
  240. // 查找冻结日志
  241. $freezeLog = ItemFreezeLog::find($freezeLogId);
  242. if (!$freezeLog) {
  243. throw new Exception("冻结日志 {$freezeLogId} 不存在");
  244. }
  245. if (!$freezeLog->isFreeze()) {
  246. throw new Exception("日志 {$freezeLogId} 不是冻结操作记录");
  247. }
  248. // 查找对应的冻结物品
  249. $frozenItem = ItemUser::where('frozen_log_id', $freezeLogId)
  250. ->where('is_frozen', true)
  251. ->first();
  252. if (!$frozenItem) {
  253. throw new Exception("未找到冻结日志 {$freezeLogId} 对应的冻结物品");
  254. }
  255. // 创建解冻日志
  256. $unfreezeLog = ItemFreezeLog::createLog(
  257. $frozenItem->user_id,
  258. $frozenItem->item_id,
  259. $frozenItem->instance_id,
  260. $frozenItem->quantity,
  261. FREEZE_ACTION_TYPE::UNFREEZE,
  262. "解冻操作,原冻结日志ID: {$freezeLogId}",
  263. $freezeLog->source_id,
  264. $freezeLog->source_type,
  265. $freezeLog->operator_id
  266. );
  267. // 解冻物品(保持独立,不合并堆叠)
  268. $frozenItem->is_frozen = false;
  269. $frozenItem->frozen_log_id = null;
  270. $frozenItem->save();
  271. // 触发物品变更事件(解冻状态变更)
  272. event(new ItemQuantityChanged(
  273. $frozenItem->user_id,
  274. $frozenItem->item_id,
  275. $frozenItem->instance_id,
  276. $frozenItem->quantity, // 旧数量(数量未变)
  277. $frozenItem->quantity, // 新数量(数量未变)
  278. $frozenItem->id,
  279. true, // 旧冻结状态:已冻结
  280. false, // 新冻结状态:未冻结
  281. [
  282. 'freeze_action' => 'unfreeze',
  283. 'unfreeze_log_id' => $unfreezeLog->id,
  284. 'original_freeze_log_id' => $freezeLogId,
  285. 'source_id' => $freezeLog->source_id,
  286. 'source_type' => $freezeLog->source_type,
  287. 'operator_id' => $freezeLog->operator_id,
  288. ]
  289. ));
  290. return [
  291. 'success' => true,
  292. 'user_id' => $frozenItem->user_id,
  293. 'item_id' => $frozenItem->item_id,
  294. 'instance_id' => $frozenItem->instance_id,
  295. 'unfrozen_quantity' => $frozenItem->quantity,
  296. 'user_item_id' => $frozenItem->id,
  297. 'unfreeze_log_id' => $unfreezeLog->id,
  298. ];
  299. }
  300. /**
  301. * 检查用户可用物品数量(排除冻结堆叠)
  302. *
  303. * @param int $userId 用户ID
  304. * @param int $itemId 物品ID
  305. * @param int|null $instanceId 实例ID
  306. * @return int 可用数量
  307. */
  308. public static function getAvailableQuantity(
  309. int $userId,
  310. int $itemId,
  311. ?int $instanceId = null
  312. ): int {
  313. return ItemUser::getAvailableQuantity($userId, $itemId, $instanceId);
  314. }
  315. /**
  316. * 验证用户是否有足够的可用物品
  317. *
  318. * @param int $userId 用户ID
  319. * @param int $itemId 物品ID
  320. * @param int $requiredQuantity 需要的数量
  321. * @param int|null $instanceId 实例ID
  322. * @return bool 是否有足够的可用物品
  323. */
  324. public static function checkAvailableQuantity(
  325. int $userId,
  326. int $itemId,
  327. int $requiredQuantity,
  328. ?int $instanceId = null
  329. ): bool {
  330. $availableQuantity = self::getAvailableQuantity($userId, $itemId, $instanceId);
  331. return $availableQuantity >= $requiredQuantity;
  332. }
  333. /**
  334. * 验证冻结操作的合法性
  335. *
  336. * @param int $userId 用户ID
  337. * @param int $itemId 物品ID
  338. * @param int $quantity 冻结数量
  339. * @param int|null $instanceId 实例ID
  340. * @return bool 是否可以冻结
  341. */
  342. public static function validateFreezeOperation(
  343. int $userId,
  344. int $itemId,
  345. int $quantity,
  346. ?int $instanceId = null
  347. ): bool {
  348. // 检查物品是否存在
  349. $item = Item::find($itemId);
  350. if (!$item) {
  351. return false;
  352. }
  353. // 检查数量是否合法
  354. if ($quantity <= 0) {
  355. return false;
  356. }
  357. // 检查可用数量是否足够
  358. return self::checkAvailableQuantity($userId, $itemId, $quantity, $instanceId);
  359. }
  360. /**
  361. * 批量冻结物品
  362. *
  363. * @param int $userId 用户ID
  364. * @param array $items 物品列表 [['item_id' => 1, 'quantity' => 10], ...]
  365. * @param FREEZE_REASON_TYPE $reason 冻结原因
  366. * @param int|null $sourceId 来源ID
  367. * @param string|null $sourceType 来源类型
  368. * @param int|null $operatorId 操作员ID
  369. * @return array 冻结结果
  370. * @throws Exception
  371. */
  372. public static function batchFreezeItems(
  373. int $userId,
  374. array $items,
  375. FREEZE_REASON_TYPE $reason,
  376. ?int $sourceId = null,
  377. ?string $sourceType = null,
  378. ?int $operatorId = null
  379. ): array {
  380. // 检查事务
  381. Helper::check_tr();
  382. $results = [];
  383. $errors = [];
  384. foreach ($items as $itemData) {
  385. $itemId = $itemData['item_id'];
  386. $quantity = $itemData['quantity'] ?? 1;
  387. $instanceId = $itemData['instance_id'] ?? null;
  388. try {
  389. if ($instanceId) {
  390. // 单独属性物品
  391. $result = self::freezeUniqueItem(
  392. $userId,
  393. $itemId,
  394. $instanceId,
  395. $reason,
  396. $sourceId,
  397. $sourceType,
  398. $operatorId
  399. );
  400. } else {
  401. // 统一属性物品
  402. $result = self::freezeNormalItem(
  403. $userId,
  404. $itemId,
  405. $quantity,
  406. $reason,
  407. $sourceId,
  408. $sourceType,
  409. $operatorId
  410. );
  411. }
  412. $results[] = $result;
  413. } catch (Exception $e) {
  414. $errors[] = [
  415. 'item_id' => $itemId,
  416. 'instance_id' => $instanceId,
  417. 'quantity' => $quantity,
  418. 'error' => $e->getMessage(),
  419. ];
  420. }
  421. }
  422. if (!empty($errors)) {
  423. throw new Exception("批量冻结操作部分失败:" . json_encode($errors, JSON_UNESCAPED_UNICODE));
  424. }
  425. return [
  426. 'success' => true,
  427. 'user_id' => $userId,
  428. 'frozen_items_count' => count($results),
  429. 'results' => $results,
  430. ];
  431. }
  432. /**
  433. * 检查冻结物品是否过期并处理
  434. *
  435. * @param int $userId 用户ID
  436. * @return int 处理的过期冻结物品数量
  437. */
  438. public static function handleExpiredFrozenItems(int $userId): int
  439. {
  440. // 查找过期的冻结物品
  441. $expiredFrozenItems = ItemUser::where('user_id', $userId)
  442. ->where('is_frozen', true)
  443. ->where('expire_at', '<', now())
  444. ->whereNotNull('expire_at')
  445. ->get();
  446. $processedCount = 0;
  447. foreach ($expiredFrozenItems as $frozenItem) {
  448. try {
  449. // 先解冻再处理过期
  450. if ($frozenItem->frozen_log_id) {
  451. self::unfreezeByLogId($frozenItem->frozen_log_id);
  452. }
  453. // 处理过期逻辑(这里可以根据业务需求决定是删除还是其他处理)
  454. // 暂时设置数量为0,不删除记录
  455. $frozenItem->quantity = 0;
  456. $frozenItem->save();
  457. $processedCount++;
  458. } catch (Exception $e) {
  459. // 记录错误日志,但不中断处理
  460. Log::error("处理过期冻结物品失败", [
  461. 'user_id' => $userId,
  462. 'item_user_id' => $frozenItem->id,
  463. 'error' => $e->getMessage(),
  464. ]);
  465. }
  466. }
  467. return $processedCount;
  468. }
  469. /**
  470. * 获取用户的冻结物品列表
  471. *
  472. * @param int $userId 用户ID
  473. * @param array $filters 过滤条件
  474. * @return Collection 冻结物品集合
  475. */
  476. public static function getFrozenItems(int $userId, array $filters = []): Collection
  477. {
  478. $query = ItemUser::where('user_id', $userId)
  479. ->where('is_frozen', true)
  480. ->with(['item', 'instance', 'freezeLog']);
  481. // 应用过滤条件
  482. if (isset($filters['item_id'])) {
  483. $query->where('item_id', $filters['item_id']);
  484. }
  485. if (isset($filters['source_type'])) {
  486. $query->whereHas('freezeLog', function ($q) use ($filters) {
  487. $q->where('source_type', $filters['source_type']);
  488. });
  489. }
  490. if (isset($filters['reason'])) {
  491. $query->whereHas('freezeLog', function ($q) use ($filters) {
  492. $q->where('reason', 'like', '%' . $filters['reason'] . '%');
  493. });
  494. }
  495. return $query->get();
  496. }
  497. /**
  498. * 获取冻结统计信息
  499. *
  500. * @param int $userId 用户ID
  501. * @return array 统计信息
  502. */
  503. public static function getFreezeStatistics(int $userId): array
  504. {
  505. $frozenItems = ItemUser::where('user_id', $userId)
  506. ->where('is_frozen', true)
  507. ->with(['item', 'freezeLog'])
  508. ->get();
  509. $statistics = [
  510. 'total_frozen_items' => $frozenItems->count(),
  511. 'total_frozen_quantity' => $frozenItems->sum('quantity'),
  512. 'frozen_by_reason' => [],
  513. 'frozen_by_source_type' => [],
  514. ];
  515. // 按原因分组统计
  516. $frozenItems->groupBy('freezeLog.reason')->each(function ($items, $reason) use (&$statistics) {
  517. $statistics['frozen_by_reason'][$reason] = [
  518. 'count' => $items->count(),
  519. 'quantity' => $items->sum('quantity'),
  520. ];
  521. });
  522. // 按来源类型分组统计
  523. $frozenItems->groupBy('freezeLog.source_type')->each(function ($items, $sourceType) use (&$statistics) {
  524. $statistics['frozen_by_source_type'][$sourceType] = [
  525. 'count' => $items->count(),
  526. 'quantity' => $items->sum('quantity'),
  527. ];
  528. });
  529. return $statistics;
  530. }
  531. }