| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- <?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;
- /**
- * 代理提现,仅 USDT。代理填写 USDT 数量,按提现汇率(汇率 + 汇率差)折算后从余额冻结;
- * 合营代理只能提取超出保证金的盈利部分。打款由总后台人工完成。
- */
- class AgentWithdrawalService
- {
- public function __construct(private ?AgentWalletService $wallets = null)
- {
- $this->wallets ??= new AgentWalletService();
- }
- public function request(Agent $agent, int $accountId, string $usdtAmount, string $withdrawPassword, string $requestId, string $remark = ''): AgentWithdrawal
- {
- if (bccomp($usdtAmount, '0', 4) <= 0) {
- throw new \InvalidArgumentException('提现金额必须大于0');
- }
- $requestKey = 'agent:' . $agent->id . ':' . $requestId;
- return DB::transaction(function () use ($agent, $accountId, $usdtAmount, $withdrawPassword, $remark, $requestKey): AgentWithdrawal {
- $lockedAgent = Agent::query()->lockForUpdate()->findOrFail($agent->id);
- if ((int) $lockedAgent->status !== Agent::STATUS_ENABLED) {
- throw new \RuntimeException('代理账号已禁用');
- }
- if ($lockedAgent->withdraw_password === '' || !Hash::check($withdrawPassword, (string) $lockedAgent->withdraw_password)) {
- throw new \InvalidArgumentException('提现密码错误');
- }
- // First consistent read is after the agent lock, so concurrent retries see the committed request.
- $existing = AgentWithdrawal::query()->where('request_key', $requestKey)->first();
- if ($existing) {
- $this->assertSameRequest($existing, $accountId, $usdtAmount);
- return $existing;
- }
- $account = AgentWithdrawAccount::query()
- ->where('agent_id', $lockedAgent->id)
- ->where('status', 1)
- ->lockForUpdate()
- ->findOrFail($accountId);
- if ($account->type !== 'usdt') {
- throw new \RuntimeException('仅支持提现到 USDT 地址');
- }
- $rate = AgentExchangeRate::withdraw();
- $amount = bcmul($usdtAmount, $rate, 4);
- if (bccomp($this->wallets->withdrawable($lockedAgent), $amount, 4) < 0) {
- throw new \RuntimeException($lockedAgent->isJoint() ? '可提现盈利不足(保证金不可提现)' : '代理余额不足');
- }
- $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->realtime_balance = bcsub((string) $lockedAgent->realtime_balance, $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,
- 'network' => $account->network,
- ],
- 'amount' => $amount,
- 'fee' => $fee,
- 'actual_amount' => $actual,
- 'usdt_amount' => $usdtAmount,
- 'actual_usdt_amount' => bcdiv($actual, $rate, 4),
- 'exchange_rate' => $rate,
- '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->realtime_balance = bcadd((string) $agent->realtime_balance, (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;
- $this->wallets->evaluateActivation($agent);
- } 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 $usdtAmount): void
- {
- if ((int) $withdrawal->account_id !== $accountId || bccomp((string) $withdrawal->usdt_amount, $usdtAmount, 4) !== 0) {
- throw new \RuntimeException('request_id 已用于其他提现申请');
- }
- }
- }
|