SimulationFundService.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. <?php
  2. namespace App\Services\Simulation;
  3. use App\Constants\HttpStatus;
  4. use App\Models\PaymentOrder;
  5. use App\Models\Wallet;
  6. use App\Services\BalanceLogService;
  7. use App\Services\PaymentOrderService;
  8. use App\Services\WageringService;
  9. use App\Services\WalletFreezeService;
  10. use App\Services\WithdrawFundsService;
  11. use Exception;
  12. use Illuminate\Support\Facades\DB;
  13. /**
  14. * 模拟会员的资金:订单、流水、冻结都写真实业务表并走真实入账代码,只是不向任何三方发请求。
  15. * 充值有两条路:公司入款(人工充值单审核通过)和三方充值(代收单按成功回调入账);提现走人工提现。
  16. */
  17. final class SimulationFundService
  18. {
  19. public const CHANNEL = 'simulation';
  20. /** @param string $channel company|third @param string $result success|pending|fail */
  21. public static function recharge($memberId, $amount, string $channel = 'third', string $result = 'success'): PaymentOrder
  22. {
  23. SimulationSettings::assertEnabled();
  24. $user = SimulationSettings::member($memberId);
  25. $amount = SimulationSettings::assertAmount($amount);
  26. $type = $channel === 'company' ? PaymentOrderService::TYPE_SELF_PAY : PaymentOrderService::TYPE_PAY;
  27. $order = PaymentOrder::create(['type' => $type, 'member_id' => $user->member_id, 'amount' => $amount, 'channel' => self::CHANNEL,
  28. 'order_no' => PaymentOrderService::createOrderNo('sim' . $type . '_', $user->member_id), 'remark' => '模拟充值',
  29. 'status' => $type === PaymentOrderService::TYPE_SELF_PAY ? PaymentOrderService::STATUS_AUDIT : PaymentOrderService::STATUS_PROCESS]
  30. + PaymentOrderService::feeFields($amount, 0));
  31. if ($result === 'fail') {
  32. $order->status = PaymentOrderService::STATUS_FAIL;
  33. $order->remark = '模拟充值失败';
  34. $order->save();
  35. } elseif ($result === 'success') {
  36. if ($type === PaymentOrderService::TYPE_PAY) {
  37. if (!PaymentOrderService::simulatePaySuccess($order)) throw new Exception('模拟充值入账失败', HttpStatus::CUSTOM_ERROR);
  38. } else {
  39. self::approveCompanyDeposit((int) $order->id);
  40. }
  41. }
  42. return $order->refresh();
  43. }
  44. /** 与后台“人工充值审核通过”相同的入账:订单置成功、加可用余额、写“充值”流水。 */
  45. private static function approveCompanyDeposit(int $id): void
  46. {
  47. DB::transaction(function () use ($id) {
  48. $order = PaymentOrder::where('id', $id)->where('type', PaymentOrderService::TYPE_SELF_PAY)->lockForUpdate()->first();
  49. if (!$order || (int) $order->status !== PaymentOrderService::STATUS_AUDIT) throw new Exception('订单状态不可审核', HttpStatus::CUSTOM_ERROR);
  50. $order->status = PaymentOrderService::STATUS_SUCCESS;
  51. $order->state = 1;
  52. $order->save();
  53. $wallet = Wallet::where('member_id', $order->member_id)->lockForUpdate()->first();
  54. if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR);
  55. $before = (string) $wallet->available_balance;
  56. $after = bcadd($before, (string) $order->amount, 10);
  57. $wallet->available_balance = $after;
  58. $wallet->save();
  59. $log = BalanceLogService::addLog($order->member_id, $order->amount, $before, $after, '充值', $order->id, '模拟充值');
  60. \App\Services\RechargeBonusService::grant('payment_order', $order, $log);
  61. });
  62. }
  63. /** 人工提现申请:校验打码量、冻结资金、建待审核提现单。 */
  64. public static function withdrawApply($memberId, $amount): PaymentOrder
  65. {
  66. SimulationSettings::assertEnabled();
  67. $user = SimulationSettings::member($memberId);
  68. $amount = SimulationSettings::assertAmount($amount);
  69. if (SimulationSettings::get('simulation_wager_bypass') === '1' && WageringService::enabled()) {
  70. $wallet = Wallet::where('member_id', $user->member_id)->first();
  71. if ($wallet) WageringService::resetCycle($wallet, 'simulation', null, '模拟提现跳过打码量');
  72. }
  73. $data = ['type' => PaymentOrderService::TYPE_SELF_PAYOUT, 'member_id' => $user->member_id, 'amount' => $amount, 'channel' => self::CHANNEL,
  74. 'order_no' => PaymentOrderService::createOrderNo('sim4_', $user->member_id), 'bank_name' => '模拟银行', 'account' => (string) $user->first_name,
  75. 'card_no' => 'SIM' . $user->member_id, 'status' => PaymentOrderService::STATUS_STAY] + PaymentOrderService::feeFields($amount, 0);
  76. [$order] = WithdrawFundsService::hold((string) $user->member_id, $amount, '人工提现', '模拟提现', WithdrawFundsService::PAYMENT_ORDERS,
  77. function () use ($data, $user) {
  78. $pending = PaymentOrder::where('type', PaymentOrderService::TYPE_SELF_PAYOUT)->where('member_id', $user->member_id)
  79. ->where('status', PaymentOrderService::STATUS_STAY)->lockForUpdate()->first(['id']);
  80. if ($pending) throw new Exception('该模拟会员已有一笔待审核的提现', HttpStatus::CUSTOM_ERROR);
  81. return PaymentOrder::create($data);
  82. });
  83. return $order->refresh();
  84. }
  85. /** 审核模拟提现:通过则扣除冻结余额,拒绝则退回可用余额。 */
  86. public static function withdrawAudit(int $id, bool $approve, string $remark = ''): PaymentOrder
  87. {
  88. SimulationSettings::assertEnabled();
  89. $order = PaymentOrder::where('id', $id)->where('type', PaymentOrderService::TYPE_SELF_PAYOUT)->first();
  90. if (!$order) throw new Exception('提现单不存在', HttpStatus::CUSTOM_ERROR);
  91. SimulationSettings::member($order->member_id);
  92. if ($approve) {
  93. DB::transaction(function () use ($id) {
  94. $locked = PaymentOrder::where('id', $id)->where('status', PaymentOrderService::STATUS_STAY)->lockForUpdate()->first();
  95. if (!$locked) throw new Exception('提现单已处理', HttpStatus::CUSTOM_ERROR);
  96. $locked->status = PaymentOrderService::STATUS_SUCCESS;
  97. $locked->save();
  98. WithdrawFundsService::settle(WithdrawFundsService::PAYMENT_ORDERS, $locked);
  99. });
  100. } else {
  101. $result = PaymentOrderService::withdrawalFailed($id, $remark !== '' ? $remark : '模拟提现拒绝');
  102. if ($result['code'] !== 0) throw new Exception($result['msg'], HttpStatus::CUSTOM_ERROR);
  103. }
  104. return $order->refresh();
  105. }
  106. /** 人工调账:加款记“人工充值”,扣款记“人工扣款”,与后台上下分一致。 */
  107. public static function adjust($memberId, $amount, string $direction, string $remark)
  108. {
  109. SimulationSettings::assertEnabled();
  110. $user = SimulationSettings::member($memberId);
  111. $amount = SimulationSettings::assertAmount($amount);
  112. return DB::transaction(function () use ($user, $amount, $direction, $remark) {
  113. $wallet = Wallet::where('member_id', $user->member_id)->lockForUpdate()->first();
  114. if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR);
  115. $delta = $direction === 'debit' ? bcsub('0', $amount, 2) : $amount;
  116. $before = (string) $wallet->available_balance;
  117. $after = bcadd($before, $delta, 10);
  118. if (bccomp($after, '0', 10) < 0) throw new Exception('可用余额不足', HttpStatus::CUSTOM_ERROR);
  119. $wallet->available_balance = $after;
  120. $wallet->save();
  121. return BalanceLogService::addLog($user->member_id, $delta, $before, $after, $direction === 'debit' ? '人工扣款' : '人工充值', null, $remark);
  122. });
  123. }
  124. public static function freeze($memberId, $amount, string $reason, int $adminId, bool $unfreeze = false): array
  125. {
  126. SimulationSettings::assertEnabled();
  127. $user = SimulationSettings::member($memberId);
  128. $amount = SimulationSettings::assertAmount($amount);
  129. $requestId = 'sim-' . bin2hex(random_bytes(8));
  130. return $unfreeze
  131. ? WalletFreezeService::unfreeze($requestId, $adminId, (string) $user->member_id, $amount, '0', $reason)
  132. : WalletFreezeService::freeze($requestId, $adminId, (string) $user->member_id, $amount, '0', $reason);
  133. }
  134. }