AgentWithdrawalService.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. <?php
  2. namespace App\Services\Agent;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentBalanceLog;
  5. use App\Models\Agent\AgentWithdrawAccount;
  6. use App\Models\Agent\AgentWithdrawal;
  7. use Illuminate\Support\Facades\DB;
  8. use Illuminate\Support\Facades\Hash;
  9. use Illuminate\Support\Str;
  10. class AgentWithdrawalService
  11. {
  12. public function request(Agent $agent, int $accountId, string $amount, string $withdrawPassword, string $requestId, string $remark = ''): AgentWithdrawal
  13. {
  14. if (bccomp($amount, '0', 4) <= 0) {
  15. throw new \InvalidArgumentException('提现金额必须大于0');
  16. }
  17. if ($agent->withdraw_password === '' || !Hash::check($withdrawPassword, (string) $agent->withdraw_password)) {
  18. throw new \InvalidArgumentException('提现密码错误');
  19. }
  20. $requestKey = 'agent:' . $agent->id . ':' . $requestId;
  21. return DB::transaction(function () use ($agent, $accountId, $amount, $remark, $requestKey): AgentWithdrawal {
  22. $existing = AgentWithdrawal::query()->where('request_key', $requestKey)->first();
  23. if ($existing) {
  24. $this->assertSameRequest($existing, $accountId, $amount);
  25. return $existing;
  26. }
  27. $lockedAgent = Agent::query()->lockForUpdate()->findOrFail($agent->id);
  28. $existing = AgentWithdrawal::query()->where('request_key', $requestKey)->first();
  29. if ($existing) {
  30. $this->assertSameRequest($existing, $accountId, $amount);
  31. return $existing;
  32. }
  33. $account = AgentWithdrawAccount::query()
  34. ->where('agent_id', $lockedAgent->id)
  35. ->where('status', 1)
  36. ->findOrFail($accountId);
  37. if (bccomp((string) $lockedAgent->balance, $amount, 4) < 0) {
  38. throw new \RuntimeException('代理余额不足');
  39. }
  40. $fee = bcdiv(bcmul($amount, (string) $lockedAgent->withdraw_fee_rate, 8), '100', 4);
  41. $actual = bcsub($amount, $fee, 4);
  42. if (bccomp($actual, '0', 4) <= 0) {
  43. throw new \RuntimeException('扣除手续费后到账金额必须大于0');
  44. }
  45. $before = bcadd((string) $lockedAgent->balance, '0', 4);
  46. $lockedAgent->balance = bcsub($before, $amount, 4);
  47. $lockedAgent->frozen_balance = bcadd((string) $lockedAgent->frozen_balance, $amount, 4);
  48. $lockedAgent->save();
  49. $withdrawal = AgentWithdrawal::query()->create([
  50. 'order_no' => 'AW' . now()->format('YmdHis') . strtoupper(Str::random(8)),
  51. 'request_key' => $requestKey,
  52. 'agent_id' => $lockedAgent->id,
  53. 'account_id' => $account->id,
  54. 'account_type' => $account->type,
  55. 'account_snapshot' => [
  56. 'type' => $account->type,
  57. 'name' => $account->name,
  58. 'account' => $account->account,
  59. 'bank_name' => $account->bank_name,
  60. 'network' => $account->network,
  61. 'details' => $account->details,
  62. ],
  63. 'amount' => $amount,
  64. 'fee' => $fee,
  65. 'actual_amount' => $actual,
  66. 'status' => 'pending',
  67. 'remark' => $remark,
  68. ]);
  69. AgentBalanceLog::query()->create([
  70. 'agent_id' => $lockedAgent->id,
  71. 'type' => 'withdraw_freeze',
  72. 'amount' => bcsub('0', $amount, 4),
  73. 'before_balance' => $before,
  74. 'after_balance' => $lockedAgent->balance,
  75. 'reference_type' => 'agent_withdrawal',
  76. 'reference_id' => $withdrawal->id,
  77. 'operator_type' => 'agent',
  78. 'operator_id' => $lockedAgent->id,
  79. 'idempotency_key' => 'withdraw-freeze:' . $withdrawal->id,
  80. 'remark' => $remark,
  81. ]);
  82. return $withdrawal->fresh();
  83. });
  84. }
  85. public function audit(int $withdrawalId, string $action, int $adminId, string $remark = ''): AgentWithdrawal
  86. {
  87. if (!in_array($action, ['approve', 'reject', 'paid'], true)) {
  88. throw new \InvalidArgumentException('审核动作错误');
  89. }
  90. return DB::transaction(function () use ($withdrawalId, $action, $adminId, $remark): AgentWithdrawal {
  91. $withdrawal = AgentWithdrawal::query()->lockForUpdate()->findOrFail($withdrawalId);
  92. $agent = Agent::query()->lockForUpdate()->findOrFail($withdrawal->agent_id);
  93. if ($action === 'approve') {
  94. if ($withdrawal->status !== 'pending') {
  95. throw new \RuntimeException('只有待审核订单可以通过');
  96. }
  97. $withdrawal->status = 'approved';
  98. $withdrawal->audited_at = now();
  99. $withdrawal->audit_admin_id = $adminId;
  100. } elseif ($action === 'reject') {
  101. if (!in_array($withdrawal->status, ['pending', 'approved'], true)) {
  102. throw new \RuntimeException('当前订单不能驳回');
  103. }
  104. $before = bcadd((string) $agent->balance, '0', 4);
  105. if (bccomp((string) $agent->frozen_balance, (string) $withdrawal->amount, 4) < 0) {
  106. throw new \RuntimeException('代理冻结余额异常');
  107. }
  108. $agent->balance = bcadd($before, (string) $withdrawal->amount, 4);
  109. $agent->frozen_balance = bcsub((string) $agent->frozen_balance, (string) $withdrawal->amount, 4);
  110. $agent->save();
  111. AgentBalanceLog::query()->firstOrCreate(
  112. ['idempotency_key' => 'withdraw-refund:' . $withdrawal->id],
  113. [
  114. 'agent_id' => $agent->id,
  115. 'type' => 'withdraw_refund',
  116. 'amount' => $withdrawal->amount,
  117. 'before_balance' => $before,
  118. 'after_balance' => $agent->balance,
  119. 'reference_type' => 'agent_withdrawal',
  120. 'reference_id' => $withdrawal->id,
  121. 'operator_type' => 'admin',
  122. 'operator_id' => $adminId,
  123. 'remark' => $remark,
  124. ]
  125. );
  126. $withdrawal->status = 'rejected';
  127. $withdrawal->audited_at = now();
  128. $withdrawal->audit_admin_id = $adminId;
  129. } else {
  130. if ($withdrawal->status !== 'approved') {
  131. throw new \RuntimeException('只有已通过订单可以标记已付款');
  132. }
  133. if (bccomp((string) $agent->frozen_balance, (string) $withdrawal->amount, 4) < 0) {
  134. throw new \RuntimeException('代理冻结余额异常');
  135. }
  136. $agent->frozen_balance = bcsub((string) $agent->frozen_balance, (string) $withdrawal->amount, 4);
  137. $agent->save();
  138. $withdrawal->status = 'paid';
  139. $withdrawal->paid_at = now();
  140. }
  141. $withdrawal->audit_remark = $remark;
  142. $withdrawal->save();
  143. return $withdrawal->fresh();
  144. });
  145. }
  146. private function assertSameRequest(AgentWithdrawal $withdrawal, int $accountId, string $amount): void
  147. {
  148. if ((int) $withdrawal->account_id !== $accountId || bccomp((string) $withdrawal->amount, $amount, 4) !== 0) {
  149. throw new \RuntimeException('request_id 已用于其他提现申请');
  150. }
  151. }
  152. }