|
|
@@ -4,30 +4,122 @@ namespace App\Services\Agent;
|
|
|
|
|
|
use App\Models\Agent\Agent;
|
|
|
use App\Models\Agent\AgentBalanceLog;
|
|
|
+use App\Models\Agent\AgentSetting;
|
|
|
use App\Models\Agent\AgentWithdrawAccount;
|
|
|
use App\Models\Agent\AgentWithdrawal;
|
|
|
+use App\Models\RechargeChannel;
|
|
|
+use App\Services\PaymentOrderService;
|
|
|
+use App\Services\WithdrawalDestination;
|
|
|
+use App\Services\WithdrawalName;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
use Illuminate\Support\Str;
|
|
|
|
|
|
/**
|
|
|
- * 代理提现,仅 USDT。代理填写 USDT 数量,按提现汇率(汇率 + 汇率差)折算后从余额冻结;
|
|
|
- * 合营代理只能提取超出保证金的盈利部分。打款由总后台人工完成。
|
|
|
+ * 代理提现,和会员前台一样使用总后台配置的提现通道(agent_settings.recharge_channel_group_id 对应组合的提现类型):
|
|
|
+ * - usdt:填 USDT 数量,按提现汇率(汇率 + 汇率差)折算后从余额冻结,总后台人工打款;
|
|
|
+ * - rgtx 人工提现:填人民币金额,总后台人工打款;
|
|
|
+ * - 三方代付(DF001/DF002/DF005 钱宝、JDpay、NOwithdraw、ZIMUwithdraw、ZIMUcash):填人民币金额,
|
|
|
+ * 总后台审核通过时向三方下单(PaymentOrderService::requestPayout),三方回调成功扣冻结、失败退回余额。
|
|
|
+ * 合营代理只能提取超出保证金的盈利部分。代理手续费按 agents.withdraw_fee_rate 从提现金额中扣除。
|
|
|
+ * 状态:pending 待审核 → approved 已通过(人工打款)/ processing 三方代付中 → paid 已打款;rejected 已驳回、failed 代付失败(均退回余额)。
|
|
|
*/
|
|
|
class AgentWithdrawalService
|
|
|
{
|
|
|
+ public const MANUAL_CHANNELS = ['usdt', 'rgtx'];
|
|
|
+ public const OPEN_STATUSES = ['pending', 'approved', 'processing'];
|
|
|
+ private const MAX_ACCOUNTS_PER_CHANNEL = 5;
|
|
|
+
|
|
|
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
|
|
|
+ public static function isProviderChannel(string $channel): bool
|
|
|
+ {
|
|
|
+ return !in_array($channel, self::MANUAL_CHANNELS, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 提现通道,格式同会员 GET /api/wallet/withdrawChannel。 */
|
|
|
+ public function channels(Agent $agent): array
|
|
|
+ {
|
|
|
+ $list = WithdrawalDestination::describeChannels(RechargeChannel::getFormatChannel(2, $this->groupId()));
|
|
|
+ foreach ($list as &$item) {
|
|
|
+ $item['requires_real_name'] = WithdrawalName::usesOrderName((string) $item['value']);
|
|
|
+ }
|
|
|
+ unset($item);
|
|
|
+ return ['list' => array_values($list), 'real_name' => (string) $agent->real_name];
|
|
|
+ }
|
|
|
+
|
|
|
+ public function accounts(Agent $agent, ?string $channel = null): array
|
|
|
+ {
|
|
|
+ $query = AgentWithdrawAccount::query()->where('agent_id', $agent->id);
|
|
|
+ if ($channel !== null && $channel !== '') $query->where('type', $channel);
|
|
|
+ return $query->orderByDesc('id')->get()->map(fn (AgentWithdrawAccount $account) => $this->present($account, $agent))->all();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 新增/修改提现账户。USDT:network + account(地址);其他通道:card_no、bank_name 等,规则同会员绑卡。 */
|
|
|
+ public function saveAccount(Agent $agent, array $data): array
|
|
|
{
|
|
|
- if (bccomp($usdtAmount, '0', 4) <= 0) {
|
|
|
+ $channel = (string) ($data['type'] ?? 'usdt');
|
|
|
+ if (RechargeChannel::checkWithdrawChannel($channel, $this->groupId()) === false) throw new \RuntimeException('不支持此提现方式');
|
|
|
+ if ($channel === 'usdt') {
|
|
|
+ $network = strtoupper(trim((string) ($data['network'] ?? '')));
|
|
|
+ $address = trim((string) ($data['account'] ?? ''));
|
|
|
+ if (!in_array($network, ['TRC20', 'ERC20'], true)) throw new \InvalidArgumentException('请选择公链 TRC20 或 ERC20');
|
|
|
+ $pattern = $network === 'ERC20' ? '/^0x[0-9a-fA-F]{40}$/D' : '/^T[1-9A-HJ-NP-Za-km-z]{33}$/D';
|
|
|
+ if (!preg_match($pattern, $address)) throw new \InvalidArgumentException('USDT 地址格式错误');
|
|
|
+ $fields = ['account' => $address, 'network' => $network, 'bank_name' => '', 'details' => null];
|
|
|
+ } else {
|
|
|
+ if (!isset($data['card_no']) && isset($data['account'])) $data['card_no'] = $data['account'];
|
|
|
+ try {
|
|
|
+ $destination = WithdrawalDestination::resolve($channel, $data);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ throw new \InvalidArgumentException($e->getMessage());
|
|
|
+ }
|
|
|
+ $details = array_filter([
|
|
|
+ 'bank_branch' => $destination['bank_branch'] ?? null,
|
|
|
+ 'qr_image' => $destination['qr_image'] ?? null,
|
|
|
+ 'payout_method' => $channel === 'rgtx' ? WithdrawalDestination::manualResponse($destination['bank_name'], $destination['card_no'], $destination['qr_image'] ?? null)['payout_method'] : null,
|
|
|
+ ], fn ($v) => $v !== null && $v !== '');
|
|
|
+ $fields = ['account' => $destination['card_no'], 'network' => '', 'bank_name' => $destination['bank_name'], 'details' => $details ?: null];
|
|
|
+ }
|
|
|
+
|
|
|
+ return DB::transaction(function () use ($agent, $data, $channel, $fields) {
|
|
|
+ $locked = Agent::query()->lockForUpdate()->findOrFail($agent->id);
|
|
|
+ if (WithdrawalName::usesOrderName($channel) && trim((string) $locked->real_name) === '') {
|
|
|
+ // 和会员一样:首次绑定需要填写真实姓名,之后不能自行修改(由总后台修改)。
|
|
|
+ $name = WithdrawalName::normalize((string) ($data['real_name'] ?? ''));
|
|
|
+ if ($name === '') throw new \InvalidArgumentException('请输入真实姓名');
|
|
|
+ $locked->real_name = $name;
|
|
|
+ $locked->save();
|
|
|
+ }
|
|
|
+ if (!empty($data['id'])) {
|
|
|
+ $account = AgentWithdrawAccount::query()->where('agent_id', $locked->id)->lockForUpdate()->findOrFail((int) $data['id']);
|
|
|
+ if ($account->type !== $channel) throw new \InvalidArgumentException('不能修改账户的提现方式');
|
|
|
+ } else {
|
|
|
+ $count = AgentWithdrawAccount::query()->where('agent_id', $locked->id)->where('type', $channel)->count();
|
|
|
+ if ($count >= self::MAX_ACCOUNTS_PER_CHANNEL) throw new \RuntimeException('已达添加上限');
|
|
|
+ $account = new AgentWithdrawAccount(['agent_id' => $locked->id, 'type' => $channel]);
|
|
|
+ }
|
|
|
+ $account->fill($fields + [
|
|
|
+ 'name' => trim((string) ($data['name'] ?? $account->name ?? '')),
|
|
|
+ 'status' => (int) ($data['status'] ?? $account->status ?? 1),
|
|
|
+ ])->save();
|
|
|
+ return $this->present($account->fresh(), $locked);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 申请提现。$amount:USDT 账户为 USDT 数量,其他通道为人民币金额(最多 2 位小数)。
|
|
|
+ */
|
|
|
+ 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');
|
|
|
}
|
|
|
$requestKey = 'agent:' . $agent->id . ':' . $requestId;
|
|
|
- return DB::transaction(function () use ($agent, $accountId, $usdtAmount, $withdrawPassword, $remark, $requestKey): AgentWithdrawal {
|
|
|
+ return DB::transaction(function () use ($agent, $accountId, $amount, $withdrawPassword, $remark, $requestKey): AgentWithdrawal {
|
|
|
$lockedAgent = Agent::query()->lockForUpdate()->findOrFail($agent->id);
|
|
|
if ((int) $lockedAgent->status !== Agent::STATUS_ENABLED) {
|
|
|
throw new \RuntimeException('代理账号已禁用');
|
|
|
@@ -38,7 +130,7 @@ class AgentWithdrawalService
|
|
|
// 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);
|
|
|
+ $this->assertSameRequest($existing, $accountId, $amount);
|
|
|
return $existing;
|
|
|
}
|
|
|
$account = AgentWithdrawAccount::query()
|
|
|
@@ -46,44 +138,71 @@ class AgentWithdrawalService
|
|
|
->where('status', 1)
|
|
|
->lockForUpdate()
|
|
|
->findOrFail($accountId);
|
|
|
- if ($account->type !== 'usdt') {
|
|
|
- throw new \RuntimeException('仅支持提现到 USDT 地址');
|
|
|
+ $channel = (string) $account->type;
|
|
|
+ $config = RechargeChannel::checkWithdrawChannel($channel, $this->groupId());
|
|
|
+ if ($config === false) throw new \RuntimeException('不支持此提现方式');
|
|
|
+ $min = (float) ($config->min ?? 0);
|
|
|
+ $max = (float) ($config->max ?? 0);
|
|
|
+ if ((float) $amount < $min || ($max > 0 && (float) $amount > $max)) {
|
|
|
+ throw new \RuntimeException("提现金额范围为{$min}-{$max}" . ($channel === 'usdt' ? ' USDT' : ''));
|
|
|
}
|
|
|
- $rate = AgentExchangeRate::withdraw();
|
|
|
- $amount = bcmul($usdtAmount, $rate, 4);
|
|
|
- if (bccomp($this->wallets->withdrawable($lockedAgent), $amount, 4) < 0) {
|
|
|
+ $holder = '';
|
|
|
+ if (WithdrawalName::usesOrderName($channel)) {
|
|
|
+ $holder = trim((string) $lockedAgent->real_name);
|
|
|
+ if ($holder === '') throw new \RuntimeException('请先在提现账户中填写真实姓名');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($channel === 'usdt') {
|
|
|
+ $rate = AgentExchangeRate::withdraw();
|
|
|
+ $usdtAmount = bcadd($amount, '0', 4);
|
|
|
+ $rmbAmount = bcmul($usdtAmount, $rate, 4);
|
|
|
+ $scale = 4;
|
|
|
+ } else {
|
|
|
+ if (!preg_match('/^\d+(\.\d{1,2})?$/D', $amount)) throw new \InvalidArgumentException('提现金额最多 2 位小数');
|
|
|
+ $rate = '0';
|
|
|
+ $usdtAmount = '0';
|
|
|
+ $rmbAmount = bcadd($amount, '0', 4);
|
|
|
+ $scale = 2; // 三方按分打款,手续费和到账金额保留 2 位
|
|
|
+ }
|
|
|
+ if (bccomp($this->wallets->withdrawable($lockedAgent), $rmbAmount, 4) < 0) {
|
|
|
throw new \RuntimeException($lockedAgent->isJoint() ? '可提现盈利不足(保证金不可提现)' : '代理余额不足');
|
|
|
}
|
|
|
|
|
|
- $fee = bcdiv(bcmul($amount, (string) $lockedAgent->withdraw_fee_rate, 8), '100', 4);
|
|
|
- $actual = bcsub($amount, $fee, 4);
|
|
|
+ $fee = bcdiv(bcmul($rmbAmount, (string) $lockedAgent->withdraw_fee_rate, 8), '100', $scale);
|
|
|
+ $actual = bcsub($rmbAmount, $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->balance = bcsub($before, $rmbAmount, 4);
|
|
|
+ $lockedAgent->realtime_balance = bcsub((string) $lockedAgent->realtime_balance, $rmbAmount, 4);
|
|
|
+ $lockedAgent->frozen_balance = bcadd((string) $lockedAgent->frozen_balance, $rmbAmount, 4);
|
|
|
$lockedAgent->save();
|
|
|
|
|
|
+ $details = (array) ($account->details ?? []);
|
|
|
$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,
|
|
|
+ 'account_type' => $channel,
|
|
|
+ 'account_snapshot' => array_filter([
|
|
|
+ 'type' => $channel,
|
|
|
'name' => $account->name,
|
|
|
'account' => $account->account,
|
|
|
'network' => $account->network,
|
|
|
- ],
|
|
|
- 'amount' => $amount,
|
|
|
+ 'bank_name' => $account->bank_name,
|
|
|
+ 'bank_branch' => $details['bank_branch'] ?? null,
|
|
|
+ 'qr_image' => $details['qr_image'] ?? null,
|
|
|
+ 'payout_method' => $details['payout_method'] ?? null,
|
|
|
+ 'holder_name' => $holder,
|
|
|
+ ], fn ($v) => $v !== null),
|
|
|
+ 'amount' => $rmbAmount,
|
|
|
'fee' => $fee,
|
|
|
'actual_amount' => $actual,
|
|
|
'usdt_amount' => $usdtAmount,
|
|
|
- 'actual_usdt_amount' => bcdiv($actual, $rate, 4),
|
|
|
+ 'actual_usdt_amount' => $channel === 'usdt' ? bcdiv($actual, $rate, 4) : '0',
|
|
|
'exchange_rate' => $rate,
|
|
|
'status' => 'pending',
|
|
|
'remark' => $remark,
|
|
|
@@ -92,7 +211,7 @@ class AgentWithdrawalService
|
|
|
AgentBalanceLog::query()->create([
|
|
|
'agent_id' => $lockedAgent->id,
|
|
|
'type' => 'withdraw_freeze',
|
|
|
- 'amount' => bcsub('0', $amount, 4),
|
|
|
+ 'amount' => bcsub('0', $rmbAmount, 4),
|
|
|
'before_balance' => $before,
|
|
|
'after_balance' => $lockedAgent->balance,
|
|
|
'reference_type' => 'agent_withdrawal',
|
|
|
@@ -107,6 +226,12 @@ class AgentWithdrawalService
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 总后台审核:
|
|
|
+ * - approve:USDT / 人工提现变为 approved,等人工打款后 paid;三方通道直接向三方下单,变为 processing,下单失败保持待审核并返回原因;
|
|
|
+ * - reject:pending / approved 驳回并退回余额;
|
|
|
+ * - paid:approved 的人工打款完成。
|
|
|
+ */
|
|
|
public function audit(int $withdrawalId, string $action, int $adminId, string $remark = ''): AgentWithdrawal
|
|
|
{
|
|
|
if (!in_array($action, ['approve', 'reject', 'paid'], true)) {
|
|
|
@@ -116,56 +241,42 @@ class AgentWithdrawalService
|
|
|
return DB::transaction(function () use ($withdrawalId, $action, $adminId, $remark): AgentWithdrawal {
|
|
|
$withdrawal = AgentWithdrawal::query()->lockForUpdate()->findOrFail($withdrawalId);
|
|
|
$agent = Agent::query()->lockForUpdate()->findOrFail($withdrawal->agent_id);
|
|
|
+ $channel = (string) ($withdrawal->account_type ?: 'usdt');
|
|
|
|
|
|
if ($action === 'approve') {
|
|
|
if ($withdrawal->status !== 'pending') {
|
|
|
throw new \RuntimeException('只有待审核订单可以通过');
|
|
|
}
|
|
|
- $withdrawal->status = 'approved';
|
|
|
+ if (self::isProviderChannel($channel)) {
|
|
|
+ $snapshot = (array) $withdrawal->account_snapshot;
|
|
|
+ try {
|
|
|
+ $ret = PaymentOrderService::requestPayout($channel, (string) $withdrawal->order_no, AgentPaymentService::payerId((int) $agent->id),
|
|
|
+ (string) $withdrawal->actual_amount, (string) ($snapshot['holder_name'] ?? ''), (string) ($snapshot['account'] ?? ''),
|
|
|
+ (string) ($snapshot['bank_name'] ?? ''), ['agent_withdrawal_id' => $withdrawal->id]);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ throw new \RuntimeException($e->getMessage() ?: '代付下单失败');
|
|
|
+ }
|
|
|
+ $withdrawal->pay_no = $ret['pay_no'];
|
|
|
+ $withdrawal->pay_data = $ret['pay_data'];
|
|
|
+ $withdrawal->status = 'processing';
|
|
|
+ } else {
|
|
|
+ $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,
|
|
|
- ]
|
|
|
- );
|
|
|
+ $this->refund($withdrawal, $agent, 'admin', $adminId, $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();
|
|
|
+ $this->settle($withdrawal, $agent);
|
|
|
}
|
|
|
|
|
|
$withdrawal->audit_remark = $remark;
|
|
|
@@ -174,9 +285,90 @@ class AgentWithdrawalService
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private function assertSameRequest(AgentWithdrawal $withdrawal, int $accountId, string $usdtAmount): void
|
|
|
+ /** 三方代付回调结果:true 成功、false 失败、null 处理中(只记录回调)。只处理 processing 状态,重复回调无副作用。 */
|
|
|
+ public function providerResult(int $withdrawalId, ?bool $success, array $params): void
|
|
|
+ {
|
|
|
+ DB::transaction(function () use ($withdrawalId, $success, $params) {
|
|
|
+ $withdrawal = AgentWithdrawal::query()->lockForUpdate()->find($withdrawalId);
|
|
|
+ if (!$withdrawal || $withdrawal->status !== 'processing') return;
|
|
|
+ $withdrawal->callback_data = json_encode($params, JSON_UNESCAPED_UNICODE);
|
|
|
+ if ($success !== null) {
|
|
|
+ $agent = Agent::query()->lockForUpdate()->findOrFail($withdrawal->agent_id);
|
|
|
+ if ($success) {
|
|
|
+ $this->settle($withdrawal, $agent);
|
|
|
+ } else {
|
|
|
+ $this->refund($withdrawal, $agent, 'system', null, '三方代付失败');
|
|
|
+ $withdrawal->status = 'failed';
|
|
|
+ $withdrawal->audit_remark = trim($withdrawal->audit_remark . ' 三方代付失败,已退回余额');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $withdrawal->save();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private function settle(AgentWithdrawal $withdrawal, Agent $agent): void
|
|
|
+ {
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+
|
|
|
+ private function refund(AgentWithdrawal $withdrawal, Agent $agent, string $operatorType, ?int $operatorId, string $remark): void
|
|
|
+ {
|
|
|
+ $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' => $operatorType,
|
|
|
+ 'operator_id' => $operatorId,
|
|
|
+ 'remark' => $remark,
|
|
|
+ ]
|
|
|
+ );
|
|
|
+ $this->wallets->evaluateActivation($agent);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function groupId(): int
|
|
|
+ {
|
|
|
+ return max(1, (int) AgentSetting::current()->recharge_channel_group_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 账户输出:details 展开到顶层;人工提现补 payout_method;需要实名的通道带上持卡人姓名。 */
|
|
|
+ private function present(AgentWithdrawAccount $account, Agent $agent): array
|
|
|
+ {
|
|
|
+ $row = $account->toArray();
|
|
|
+ $details = (array) ($row['details'] ?? []);
|
|
|
+ unset($row['details']);
|
|
|
+ $row['channel'] = $row['type'];
|
|
|
+ $row['card_no'] = $row['type'] === 'usdt' ? '' : (string) $row['account'];
|
|
|
+ $row['bank_branch'] = $details['bank_branch'] ?? null;
|
|
|
+ $row['qr_image'] = $details['qr_image'] ?? null;
|
|
|
+ $row['payout_method'] = $details['payout_method'] ?? null;
|
|
|
+ $row['holder_name'] = WithdrawalName::usesOrderName((string) $row['type']) ? (string) $agent->real_name : '';
|
|
|
+ return $row;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function assertSameRequest(AgentWithdrawal $withdrawal, int $accountId, string $amount): void
|
|
|
{
|
|
|
- if ((int) $withdrawal->account_id !== $accountId || bccomp((string) $withdrawal->usdt_amount, $usdtAmount, 4) !== 0) {
|
|
|
+ $submitted = ($withdrawal->account_type ?: 'usdt') === 'usdt' ? (string) $withdrawal->usdt_amount : (string) $withdrawal->amount;
|
|
|
+ if ((int) $withdrawal->account_id !== $accountId || bccomp($submitted, $amount, 4) !== 0) {
|
|
|
throw new \RuntimeException('request_id 已用于其他提现申请');
|
|
|
}
|
|
|
}
|