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 已用于其他提现申请'); } } }