ItemFreeze.php 16 KB

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