| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- <?php
- namespace App\Services\Agent;
- use App\Models\Agent\Agent;
- use App\Models\Agent\AgentBalanceLog;
- use App\Models\Agent\AgentWithdrawAccount;
- use App\Models\Agent\AgentWithdrawal;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Hash;
- use Illuminate\Support\Str;
- class AgentWithdrawalService
- {
- public function request(Agent $agent, int $accountId, string $amount, string $withdrawPassword, string $requestId, string $remark = ''): AgentWithdrawal
- {
- if (bccomp($amount, '0', 4) <= 0) {
- throw new \InvalidArgumentException('提现金额必须大于0');
- }
- if ($agent->withdraw_password === '' || !Hash::check($withdrawPassword, (string) $agent->withdraw_password)) {
- throw new \InvalidArgumentException('提现密码错误');
- }
- $requestKey = 'agent:' . $agent->id . ':' . $requestId;
- return DB::transaction(function () use ($agent, $accountId, $amount, $remark, $requestKey): AgentWithdrawal {
- $existing = AgentWithdrawal::query()->where('request_key', $requestKey)->first();
- if ($existing) {
- $this->assertSameRequest($existing, $accountId, $amount);
- return $existing;
- }
- $lockedAgent = Agent::query()->lockForUpdate()->findOrFail($agent->id);
- $existing = AgentWithdrawal::query()->where('request_key', $requestKey)->first();
- if ($existing) {
- $this->assertSameRequest($existing, $accountId, $amount);
- return $existing;
- }
- $account = AgentWithdrawAccount::query()
- ->where('agent_id', $lockedAgent->id)
- ->where('status', 1)
- ->findOrFail($accountId);
- if (bccomp((string) $lockedAgent->balance, $amount, 4) < 0) {
- throw new \RuntimeException('代理余额不足');
- }
- $fee = bcdiv(bcmul($amount, (string) $lockedAgent->withdraw_fee_rate, 8), '100', 4);
- $actual = bcsub($amount, $fee, 4);
- if (bccomp($actual, '0', 4) <= 0) {
- throw new \RuntimeException('扣除手续费后到账金额必须大于0');
- }
- $before = bcadd((string) $lockedAgent->balance, '0', 4);
- $lockedAgent->balance = bcsub($before, $amount, 4);
- $lockedAgent->frozen_balance = bcadd((string) $lockedAgent->frozen_balance, $amount, 4);
- $lockedAgent->save();
- $withdrawal = AgentWithdrawal::query()->create([
- 'order_no' => 'AW' . now()->format('YmdHis') . strtoupper(Str::random(8)),
- 'request_key' => $requestKey,
- 'agent_id' => $lockedAgent->id,
- 'account_id' => $account->id,
- 'account_type' => $account->type,
- 'account_snapshot' => [
- 'type' => $account->type,
- 'name' => $account->name,
- 'account' => $account->account,
- 'bank_name' => $account->bank_name,
- 'network' => $account->network,
- 'details' => $account->details,
- ],
- 'amount' => $amount,
- 'fee' => $fee,
- 'actual_amount' => $actual,
- 'status' => 'pending',
- 'remark' => $remark,
- ]);
- AgentBalanceLog::query()->create([
- 'agent_id' => $lockedAgent->id,
- 'type' => 'withdraw_freeze',
- 'amount' => bcsub('0', $amount, 4),
- 'before_balance' => $before,
- 'after_balance' => $lockedAgent->balance,
- 'reference_type' => 'agent_withdrawal',
- 'reference_id' => $withdrawal->id,
- 'operator_type' => 'agent',
- 'operator_id' => $lockedAgent->id,
- 'idempotency_key' => 'withdraw-freeze:' . $withdrawal->id,
- 'remark' => $remark,
- ]);
- return $withdrawal->fresh();
- });
- }
- public function audit(int $withdrawalId, string $action, int $adminId, string $remark = ''): AgentWithdrawal
- {
- if (!in_array($action, ['approve', 'reject', 'paid'], true)) {
- throw new \InvalidArgumentException('审核动作错误');
- }
- return DB::transaction(function () use ($withdrawalId, $action, $adminId, $remark): AgentWithdrawal {
- $withdrawal = AgentWithdrawal::query()->lockForUpdate()->findOrFail($withdrawalId);
- $agent = Agent::query()->lockForUpdate()->findOrFail($withdrawal->agent_id);
- if ($action === 'approve') {
- if ($withdrawal->status !== 'pending') {
- throw new \RuntimeException('只有待审核订单可以通过');
- }
- $withdrawal->status = 'approved';
- $withdrawal->audited_at = now();
- $withdrawal->audit_admin_id = $adminId;
- } elseif ($action === 'reject') {
- if (!in_array($withdrawal->status, ['pending', 'approved'], true)) {
- throw new \RuntimeException('当前订单不能驳回');
- }
- $before = bcadd((string) $agent->balance, '0', 4);
- if (bccomp((string) $agent->frozen_balance, (string) $withdrawal->amount, 4) < 0) {
- throw new \RuntimeException('代理冻结余额异常');
- }
- $agent->balance = bcadd($before, (string) $withdrawal->amount, 4);
- $agent->frozen_balance = bcsub((string) $agent->frozen_balance, (string) $withdrawal->amount, 4);
- $agent->save();
- AgentBalanceLog::query()->firstOrCreate(
- ['idempotency_key' => 'withdraw-refund:' . $withdrawal->id],
- [
- 'agent_id' => $agent->id,
- 'type' => 'withdraw_refund',
- 'amount' => $withdrawal->amount,
- 'before_balance' => $before,
- 'after_balance' => $agent->balance,
- 'reference_type' => 'agent_withdrawal',
- 'reference_id' => $withdrawal->id,
- 'operator_type' => 'admin',
- 'operator_id' => $adminId,
- 'remark' => $remark,
- ]
- );
- $withdrawal->status = 'rejected';
- $withdrawal->audited_at = now();
- $withdrawal->audit_admin_id = $adminId;
- } else {
- if ($withdrawal->status !== 'approved') {
- throw new \RuntimeException('只有已通过订单可以标记已付款');
- }
- if (bccomp((string) $agent->frozen_balance, (string) $withdrawal->amount, 4) < 0) {
- throw new \RuntimeException('代理冻结余额异常');
- }
- $agent->frozen_balance = bcsub((string) $agent->frozen_balance, (string) $withdrawal->amount, 4);
- $agent->save();
- $withdrawal->status = 'paid';
- $withdrawal->paid_at = now();
- }
- $withdrawal->audit_remark = $remark;
- $withdrawal->save();
- return $withdrawal->fresh();
- });
- }
- private function assertSameRequest(AgentWithdrawal $withdrawal, int $accountId, string $amount): void
- {
- if ((int) $withdrawal->account_id !== $accountId || bccomp((string) $withdrawal->amount, $amount, 4) !== 0) {
- throw new \RuntimeException('request_id 已用于其他提现申请');
- }
- }
- }
|