ConsumeService.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. <?php
  2. namespace App\Module\Game\Services;
  3. use App\Module\Fund\Logic\User as FundLogic;
  4. use App\Module\Fund\Enums\LOG_TYPE as FUND_LOG_TYPE;
  5. use App\Module\Game\Enums\CONSUME_TYPE;
  6. use App\Module\Game\Models\GameConsumeGroup;
  7. use App\Module\Game\Models\GameConsumeItem;
  8. use App\Module\GameItems\Services\ItemService;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Log;
  11. use UCore\Db\Helper;
  12. use UCore\Dto\Res;
  13. /**
  14. * 消耗服务类
  15. *
  16. * 提供消耗组相关的服务,包括检查用户是否满足消耗条件、执行消耗等功能
  17. */
  18. class ConsumeService
  19. {
  20. /**
  21. * 检查用户是否满足消耗组的条件
  22. *
  23. * @param int $userId 用户ID
  24. * @param string|int $consumeGroupCode 消耗组编码或ID
  25. * @return array 检查结果,包含success字段表示是否满足条件,message字段表示错误信息
  26. */
  27. public static function checkConsume(int $userId, $consumeGroupCode): Res
  28. {
  29. try {
  30. // 获取消耗组
  31. /**
  32. * @var GameConsumeGroup $consumeGroup
  33. */
  34. $consumeGroup = is_numeric($consumeGroupCode)
  35. ? GameConsumeGroup::find($consumeGroupCode)
  36. : GameConsumeGroup::where('code', $consumeGroupCode)->first();
  37. if (!$consumeGroup) {
  38. return Res::error("消耗组不存在: {$consumeGroupCode}");
  39. }
  40. // 获取消耗组中的所有消耗项
  41. $consumeItems = $consumeGroup->consumeItems;
  42. if ($consumeItems->isEmpty()) {
  43. return Res::success();
  44. }
  45. // 检查每个消耗项
  46. foreach ($consumeItems as $item) {
  47. $checkResult = self::checkConsumeItem($userId, $item);
  48. if (!$checkResult['success']) {
  49. return Res::error($checkResult['message'], $checkResult);
  50. }
  51. }
  52. // 所有条件都满足,返回成功; 返回消耗组信息
  53. return Res::success('',
  54. ConsumeGroupService::consumeItems2Array($consumeItems)
  55. );
  56. } catch (\Exception $e) {
  57. Log::error('检查消耗条件失败', [
  58. 'user_id' => $userId,
  59. 'consume_group' => $consumeGroupCode,
  60. 'error' => $e->getMessage()
  61. ]);
  62. return Res::error('检查消耗条件时发生错误: ' . $e->getMessage());
  63. }
  64. }
  65. /**
  66. * 执行消耗
  67. *
  68. * @param int $userId 用户ID
  69. * @param string|int $consumeGroupCode 消耗组编码或ID
  70. * @param string $source 消耗来源
  71. * @param int $sourceId 消耗来源ID
  72. * @return Res 执行结果
  73. */
  74. public static function executeConsume(int $userId, $consumeGroupCode, string $source, int $sourceId = 0, $check = true): Res
  75. {
  76. if ($check) {
  77. // 先检查是否满足消耗条件
  78. $checkResult = self::checkConsume($userId, $consumeGroupCode);
  79. if (!$checkResult->success) {
  80. return $checkResult;
  81. }
  82. }
  83. try {
  84. Helper::check_tr();
  85. // 获取消耗组
  86. /**
  87. * @var GameConsumeGroup $consumeGroup
  88. */
  89. $consumeGroup = is_numeric($consumeGroupCode)
  90. ? GameConsumeGroup::find($consumeGroupCode)
  91. : GameConsumeGroup::where('code', $consumeGroupCode)->first();
  92. // 获取消耗组中的所有消耗项
  93. $consumeItems = $consumeGroup->consumeItems;
  94. // 开始事务
  95. // 执行每个消耗项
  96. foreach ($consumeItems as $item) {
  97. $consumeResult = self::executeConsumeItem($userId, $item, $source, $sourceId);
  98. if (!$consumeResult['success']) {
  99. DB::rollBack();
  100. return Res::error($consumeResult['message'], $consumeResult);
  101. }
  102. }
  103. return Res::success('消耗执行成功', [
  104. 'consume_group' => [
  105. 'id' => $consumeGroup->id,
  106. 'code' => $consumeGroup->code,
  107. 'name' => $consumeGroup->name
  108. ],
  109. 'list' => ConsumeGroupService::consumeItems2Array($consumeItems)
  110. ]);
  111. } catch (\Exception $e) {
  112. Log::error('执行消耗失败', [
  113. 'user_id' => $userId,
  114. 'consume_group' => $consumeGroupCode,
  115. 'source' => $source,
  116. 'source_id' => $sourceId,
  117. 'error' => $e->getMessage()
  118. ]);
  119. return Res::error('执行消耗时发生错误: ' . $e->getMessage());
  120. }
  121. }
  122. /**
  123. * 检查单个消耗项
  124. *
  125. * @param int $userId 用户ID
  126. * @param GameConsumeItem $consumeItem 消耗项
  127. * @return array 检查结果
  128. */
  129. protected static function checkConsumeItem(int $userId, GameConsumeItem $consumeItem): array
  130. {
  131. switch ($consumeItem->consume_type) {
  132. case CONSUME_TYPE::ITEM->value:
  133. $result = self::checkItemConsume($userId, $consumeItem);
  134. // 将 Res 对象转换为数组格式
  135. return [
  136. 'success' => $result->success,
  137. 'message' => $result->message,
  138. 'item_id' => $result->data['item_id'] ?? null,
  139. 'required' => $result->data['required'] ?? null,
  140. 'actual' => $result->data['actual'] ?? null
  141. ];
  142. case CONSUME_TYPE::FUND_CONFIG->value:
  143. $result = self::checkFundConfigConsume($userId, $consumeItem);
  144. // 将 Res 对象转换为数组格式
  145. return [
  146. 'success' => $result->success,
  147. 'message' => $result->message,
  148. 'fund_config_id' => $result->data['fund_config_id'] ?? null,
  149. 'required' => $result->data['required'] ?? null,
  150. 'actual' => $result->data['actual'] ?? null
  151. ];
  152. case CONSUME_TYPE::CURRENCY->value:
  153. return self::checkCurrencyConsume($userId, $consumeItem);
  154. default:
  155. return [
  156. 'success' => false,
  157. 'message' => "不支持的消耗类型: {$consumeItem->consume_type}"
  158. ];
  159. }
  160. }
  161. /**
  162. * 执行单个消耗项
  163. *
  164. * @param int $userId 用户ID
  165. * @param GameConsumeItem $consumeItem 消耗项
  166. * @param string $source 消耗来源
  167. * @param int $sourceId 消耗来源ID
  168. * @return array 执行结果
  169. */
  170. protected static function executeConsumeItem(int $userId, GameConsumeItem $consumeItem, string $source, int $sourceId): array
  171. {
  172. switch ($consumeItem->consume_type) {
  173. case CONSUME_TYPE::ITEM->value:
  174. $result = self::executeItemConsume($userId, $consumeItem, $source, $sourceId);
  175. // 将 Res 对象转换为数组格式
  176. return [
  177. 'success' => $result->success,
  178. 'message' => $result->message,
  179. 'item_id' => $result->data['item_id'] ?? null,
  180. 'quantity' => $result->data['quantity'] ?? null
  181. ];
  182. case CONSUME_TYPE::FUND_CONFIG->value:
  183. $result = self::executeFundConfigConsume($userId, $consumeItem, $source, $sourceId);
  184. // 将 Res 对象转换为数组格式
  185. return [
  186. 'success' => $result->success,
  187. 'message' => $result->message,
  188. 'fund_config_id' => $result->data['fund_config_id'] ?? null,
  189. 'amount' => $result->data['amount'] ?? null
  190. ];
  191. case CONSUME_TYPE::CURRENCY->value:
  192. return self::executeCurrencyConsume($userId, $consumeItem, $source, $sourceId);
  193. default:
  194. return [
  195. 'success' => false,
  196. 'message' => "不支持的消耗类型: {$consumeItem->consume_type}"
  197. ];
  198. }
  199. }
  200. /**
  201. * 检查物品消耗
  202. *
  203. * @param int $userId 用户ID
  204. * @param GameConsumeItem $consumeItem 消耗项
  205. * @return Res 检查结果
  206. */
  207. protected static function checkItemConsume(int $userId, GameConsumeItem $consumeItem): Res
  208. {
  209. $itemId = $consumeItem->target_id;
  210. $quantity = $consumeItem->quantity;
  211. // 获取用户物品
  212. $userItems = ItemService::getUserItems($userId, [ 'item_id' => $itemId ]);
  213. // 计算用户拥有的物品总数
  214. $totalQuantity = 0;
  215. foreach ($userItems as $userItem) {
  216. $totalQuantity += $userItem->quantity;
  217. }
  218. // 检查数量是否足够
  219. if ($totalQuantity < $quantity) {
  220. return Res::error("物品数量不足,需要 {$quantity},实际 {$totalQuantity}", [
  221. 'item_id' => $itemId,
  222. 'required' => $quantity,
  223. 'actual' => $totalQuantity
  224. ]);
  225. }
  226. return Res::success('物品数量足够');
  227. }
  228. /**
  229. * 检查账户种类消耗
  230. *
  231. * 注意:这里的target_id指向fund_config表的id(账户种类ID)
  232. *
  233. * @param int $userId 用户ID
  234. * @param GameConsumeItem $consumeItem 消耗项
  235. * @return Res 检查结果
  236. */
  237. protected static function checkFundConfigConsume(int $userId, GameConsumeItem $consumeItem): Res
  238. {
  239. $fundConfigId = $consumeItem->target_id;
  240. $amount = $consumeItem->quantity;
  241. // 获取用户账户
  242. $account = FundLogic::get_account($userId, $fundConfigId);
  243. // 检查账户是否存在
  244. if ($account === false) {
  245. return Res::error("用户没有该账户种类", [
  246. 'fund_config_id' => $fundConfigId
  247. ]);
  248. }
  249. // 检查余额是否足够
  250. if ($account->balance < $amount) {
  251. return Res::error("账户余额不足,需要 {$amount},实际 {$account->balance}", [
  252. 'fund_config_id' => $fundConfigId,
  253. 'required' => $amount,
  254. 'actual' => $account->balance
  255. ]);
  256. }
  257. return Res::success('账户余额足够');
  258. }
  259. /**
  260. * 检查代币账户消耗
  261. *
  262. * @param int $userId 用户ID
  263. * @param GameConsumeItem $consumeItem 消耗项
  264. * @return array 检查结果
  265. */
  266. protected static function checkFundConsume(int $userId, GameConsumeItem $consumeItem): array
  267. {
  268. $fundId = $consumeItem->target_id;
  269. $amount = $consumeItem->quantity;
  270. try {
  271. // 获取用户代币账户
  272. $fundService = new \App\Module\Fund\Services\FundService($userId, $fundId);
  273. $account = $fundService->getAccount();
  274. // 检查账户是否存在
  275. if (!$account) {
  276. return [
  277. 'success' => false,
  278. 'message' => "用户没有该代币账户",
  279. 'fund_id' => $fundId
  280. ];
  281. }
  282. // 检查余额是否足够
  283. $balance = $account->balance;
  284. if ($balance < $amount) {
  285. return [
  286. 'success' => false,
  287. 'message' => "代币账户余额不足,需要 {$amount},实际 {$balance}",
  288. 'fund_id' => $fundId,
  289. 'required' => $amount,
  290. 'actual' => $balance
  291. ];
  292. }
  293. return [
  294. 'success' => true,
  295. 'message' => '代币账户余额足够'
  296. ];
  297. } catch (\Exception $e) {
  298. return [
  299. 'success' => false,
  300. 'message' => '检查代币账户异常: ' . $e->getMessage(),
  301. 'fund_id' => $fundId
  302. ];
  303. }
  304. }
  305. /**
  306. * 执行物品消耗
  307. *
  308. * @param int $userId 用户ID
  309. * @param GameConsumeItem $consumeItem 消耗项
  310. * @param string $source 消耗来源
  311. * @param int $sourceId 消耗来源ID
  312. * @return Res 执行结果
  313. */
  314. protected static function executeItemConsume(int $userId, GameConsumeItem $consumeItem, string $source, int $sourceId): Res
  315. {
  316. $itemId = $consumeItem->target_id;
  317. $quantity = $consumeItem->quantity;
  318. // 消耗物品
  319. $result = ItemService::consumeItem($userId, $itemId, null, $quantity, [
  320. 'source_type' => $source,
  321. 'source_id' => $sourceId,
  322. 'details' => [
  323. 'consume_item_id' => $consumeItem->id,
  324. 'consume_group_id' => $consumeItem->group_id
  325. ]
  326. ]);
  327. if (!$result['success']) {
  328. return Res::error($result['message'] ?? '物品消耗失败', [
  329. 'item_id' => $itemId,
  330. 'quantity' => $quantity
  331. ]);
  332. }
  333. return Res::success('物品消耗成功', [
  334. 'item_id' => $itemId,
  335. 'quantity' => $quantity
  336. ]);
  337. }
  338. /**
  339. * 执行账户种类消耗
  340. *
  341. * 注意:这里的target_id指向fund_config表的id(账户种类ID)
  342. *
  343. * @param int $userId 用户ID
  344. * @param GameConsumeItem $consumeItem 消耗项
  345. * @param string $source 消耗来源
  346. * @param int $sourceId 消耗来源ID
  347. * @return Res 执行结果
  348. */
  349. protected static function executeFundConfigConsume(int $userId, GameConsumeItem $consumeItem, string $source, int $sourceId): Res
  350. {
  351. $fundConfigId = $consumeItem->target_id;
  352. $amount = -$consumeItem->quantity; // 负数表示消耗
  353. // 构建备注
  354. $remark = "消耗组:{$consumeItem->group_id},来源:{$source}";
  355. if ($sourceId > 0) {
  356. $remark .= ",ID:{$sourceId}";
  357. }
  358. // 消耗账户资金
  359. $result = FundLogic::handle(
  360. $userId,
  361. $fundConfigId,
  362. $amount,
  363. FUND_LOG_TYPE::TRADE, // 使用TRADE类型,因为CONSUME可能不存在
  364. $sourceId,
  365. $remark
  366. );
  367. if ($result !== true) {
  368. $errorMessage = is_string($result) ? $result : '账户资金消耗失败';
  369. return Res::error($errorMessage, [
  370. 'fund_config_id' => $fundConfigId,
  371. 'amount' => abs($amount)
  372. ]);
  373. }
  374. return Res::success('账户资金消耗成功', [
  375. 'fund_config_id' => $fundConfigId,
  376. 'amount' => abs($amount)
  377. ]);
  378. }
  379. /**
  380. * 检查币种消耗
  381. *
  382. * 注意:这里的target_id指向fund_currency表的id(币种ID)
  383. * 这个方法会检查用户所有与该币种相关的账户,并计算总余额
  384. *
  385. * @param int $userId 用户ID
  386. * @param GameConsumeItem $consumeItem 消耗项
  387. * @return array 检查结果
  388. */
  389. protected static function checkCurrencyConsume(int $userId, GameConsumeItem $consumeItem): array
  390. {
  391. $currencyId = $consumeItem->target_id;
  392. $amount = $consumeItem->quantity;
  393. try {
  394. // 获取该币种的所有账户种类
  395. $fundConfigs = \App\Module\Fund\Models\FundConfigModel::where('currency_id', $currencyId)->get();
  396. if ($fundConfigs->isEmpty()) {
  397. return [
  398. 'success' => false,
  399. 'message' => "币种不存在或没有关联的账户种类",
  400. 'currency_id' => $currencyId
  401. ];
  402. }
  403. // 获取用户所有与该币种相关的账户
  404. $fundConfigIds = $fundConfigs->pluck('id')->toArray();
  405. $accounts = \App\Module\Fund\Models\FundModel::where('user_id', $userId)
  406. ->whereIn('fund_id', $fundConfigIds)
  407. ->get();
  408. if ($accounts->isEmpty()) {
  409. return [
  410. 'success' => false,
  411. 'message' => "用户没有该币种的账户",
  412. 'currency_id' => $currencyId
  413. ];
  414. }
  415. // 计算总余额
  416. $totalBalance = $accounts->sum('balance');
  417. // 检查余额是否足够
  418. if ($totalBalance < $amount) {
  419. return [
  420. 'success' => false,
  421. 'message' => "币种总余额不足,需要 {$amount},实际 {$totalBalance}",
  422. 'currency_id' => $currencyId,
  423. 'required' => $amount,
  424. 'actual' => $totalBalance
  425. ];
  426. }
  427. return [
  428. 'success' => true,
  429. 'message' => '币种总余额足够',
  430. 'accounts' => $accounts->toArray()
  431. ];
  432. } catch (\Exception $e) {
  433. return [
  434. 'success' => false,
  435. 'message' => '检查币种消耗异常: ' . $e->getMessage(),
  436. 'currency_id' => $currencyId
  437. ];
  438. }
  439. }
  440. /**
  441. * 执行币种消耗
  442. *
  443. * 注意:这里的target_id指向fund_currency表的id(币种ID)
  444. * 这个方法会优先从用户的可用账户中扣除,如果不足则依次从其他账户扣除
  445. *
  446. * @param int $userId 用户ID
  447. * @param GameConsumeItem $consumeItem 消耗项
  448. * @param string $source 消耗来源
  449. * @param int $sourceId 消耗来源ID
  450. * @return array 执行结果
  451. */
  452. protected static function executeCurrencyConsume(int $userId, GameConsumeItem $consumeItem, string $source, int $sourceId): array
  453. {
  454. // todo 需要优化,迁移到Fund中
  455. $currencyId = $consumeItem->target_id;
  456. $amountToConsume = $consumeItem->quantity;
  457. try {
  458. // 先检查是否有足够的余额
  459. $checkResult = self::checkCurrencyConsume($userId, $consumeItem);
  460. if (!$checkResult['success']) {
  461. return $checkResult;
  462. }
  463. // 获取该币种的所有账户种类
  464. $fundConfigs = \App\Module\Fund\Models\FundConfigModel::where('currency_id', $currencyId)->get();
  465. $fundConfigIds = $fundConfigs->pluck('id')->toArray();
  466. // 获取用户所有与该币种相关的账户
  467. $accounts = \App\Module\Fund\Models\FundModel::where('user_id', $userId)
  468. ->whereIn('fund_id', $fundConfigIds)
  469. ->orderBy('fund_id') // 按账户种类ID排序,通常可用账户ID较小
  470. ->get();
  471. // 开始事务
  472. \Illuminate\Support\Facades\DB::beginTransaction();
  473. $remainingAmount = $amountToConsume;
  474. $consumedAccounts = [];
  475. // 依次从各个账户中扣除
  476. foreach ($accounts as $account) {
  477. if ($remainingAmount <= 0) {
  478. break;
  479. }
  480. $accountBalance = $account->balance;
  481. $amountToDeduct = min($accountBalance, $remainingAmount);
  482. if ($amountToDeduct > 0) {
  483. // 构建备注
  484. $remark = "币种消耗:{$currencyId},消耗组:{$consumeItem->group_id},来源:{$source}";
  485. if ($sourceId > 0) {
  486. $remark .= ",ID:{$sourceId}";
  487. }
  488. // 从当前账户扣除
  489. $result = FundLogic::handle(
  490. $userId,
  491. $account->fund_id->value,
  492. -$amountToDeduct, // 负数表示消耗
  493. FUND_LOG_TYPE::TRADE,
  494. $sourceId,
  495. $remark
  496. );
  497. if ($result !== true) {
  498. \Illuminate\Support\Facades\DB::rollBack();
  499. return [
  500. 'success' => false,
  501. 'message' => is_string($result) ? $result : "从账户 {$account->fund_id} 扣除失败",
  502. 'currency_id' => $currencyId,
  503. 'fund_config_id' => $account->fund_id,
  504. 'amount' => $amountToDeduct
  505. ];
  506. }
  507. $consumedAccounts[] = [
  508. 'fund_config_id' => $account->fund_id,
  509. 'amount' => $amountToDeduct
  510. ];
  511. $remainingAmount -= $amountToDeduct;
  512. }
  513. }
  514. // 提交事务
  515. \Illuminate\Support\Facades\DB::commit();
  516. return [
  517. 'success' => true,
  518. 'message' => '币种消耗成功',
  519. 'currency_id' => $currencyId,
  520. 'amount' => $amountToConsume,
  521. 'consumed_accounts' => $consumedAccounts
  522. ];
  523. } catch (\Exception $e) {
  524. // 回滚事务
  525. if (\Illuminate\Support\Facades\DB::transactionLevel() > 0) {
  526. \Illuminate\Support\Facades\DB::rollBack();
  527. }
  528. return [
  529. 'success' => false,
  530. 'message' => '币种消耗异常: ' . $e->getMessage(),
  531. 'currency_id' => $currencyId,
  532. 'amount' => $amountToConsume
  533. ];
  534. }
  535. }
  536. }