| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- <?php
- namespace App\Services\Agent;
- use App\Helpers\TronHelper;
- use App\Models\Agent\Agent;
- use App\Models\Agent\AgentRecharge;
- use App\Models\Agent\AgentWallet;
- use App\Services\CollectService;
- use App\Services\UsdtRechargeConfigService;
- use App\Services\WalletService;
- use Illuminate\Support\Facades\Cache;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Str;
- use Throwable;
- /**
- * 代理 USDT 充值,沿用会员前台的 USDT 充值逻辑(config.receiving_type):
- * - 1 自动到账:每个代理一个固定 TRC20 地址,轮询链上转账,确认数达标后入账并生成归集记录;
- * - 2 人工确认:展示平台收款地址,代理提交转账凭证,总后台审核后入账。
- * 入账金额 = USDT 数量 × exchange_rate_rmb。合营代理的充值全部计入保证金。
- * 记录写在 agent_recharges,不进会员 recharges,避免被统计成会员充值。
- */
- class AgentRechargeService
- {
- private const PENDING_KEY = 'agent_usdt_recharge_pending';
- private const PENDING_TTL = 7200;
- public function __construct(private AgentWalletService $wallets, private AgentEquityService $equity)
- {
- }
- public function address(Agent $agent, string $net): array
- {
- $net = strtolower($net);
- if (!in_array($net, ['trc20', 'erc20'], true)) throw new \InvalidArgumentException('充值类型错误');
- $config = [
- 'exchange_rate' => AgentExchangeRate::recharge(),
- ] + (new UsdtRechargeConfigService())->get();
- if ($this->mode() === AgentRecharge::TYPE_AUTO) {
- if ($net !== 'trc20') throw new \InvalidArgumentException('自动到账仅支持 TRC20');
- $wallet = $this->wallet($agent);
- $this->markPending((int) $agent->id);
- $qr = WalletService::getPlatformImageAddress($wallet->address);
- return ['mode' => AgentRecharge::TYPE_AUTO, 'net' => 'TRC20', 'address' => $wallet->address, 'qrcode' => $qr['full_path']] + $config;
- }
- $field = $net === 'trc20' ? 'receiving_address' : 'receiving_address_erc20';
- $address = (string) DB::table('config')->where('field', $field)->value('val');
- if ($address === '') throw new \RuntimeException('平台暂未配置收款地址');
- $qr = WalletService::getPlatformImageAddress($address);
- return ['mode' => AgentRecharge::TYPE_HAND, 'net' => strtoupper($net), 'address' => $address, 'qrcode' => $qr['full_path']] + $config;
- }
- /** 人工确认模式:提交转账凭证。 */
- public function submitProof(Agent $agent, array $data): AgentRecharge
- {
- if ($this->mode() !== AgentRecharge::TYPE_HAND) throw new \RuntimeException('当前为自动到账模式,转账后无需提交凭证');
- return AgentRecharge::query()->create([
- 'order_no' => $this->orderNo(), 'agent_id' => $agent->id, 'type' => AgentRecharge::TYPE_HAND,
- 'net' => strtoupper((string) $data['net']), 'to_address' => trim((string) $data['to_address']),
- 'usdt_amount' => bcadd((string) $data['usdt_amount'], '0', 4), 'image' => (string) $data['image'],
- 'status' => AgentRecharge::STATUS_STAY,
- ]);
- }
- /** 总后台审核凭证充值:1 通过入账,2 驳回。 */
- public function audit(int $id, int $status, int $adminId, string $remark = ''): AgentRecharge
- {
- if ($status === AgentRecharge::STATUS_FAIL) {
- return DB::transaction(function () use ($id, $adminId, $remark) {
- $recharge = AgentRecharge::query()->where('type', AgentRecharge::TYPE_HAND)->lockForUpdate()->findOrFail($id);
- if ((int) $recharge->status !== AgentRecharge::STATUS_STAY) throw new \RuntimeException('充值记录已处理');
- if (trim($remark) === '') throw new \InvalidArgumentException('请填写驳回原因');
- $recharge->forceFill(['status' => AgentRecharge::STATUS_FAIL, 'remark' => trim($remark), 'audit_admin_id' => $adminId])->save();
- return $recharge;
- });
- }
- $recharge = AgentRecharge::query()->where('type', AgentRecharge::TYPE_HAND)->findOrFail($id);
- if (!$this->credit($recharge, (string) $recharge->usdt_amount, ['audit_admin_id' => $adminId, 'remark' => trim($remark)])) {
- throw new \RuntimeException('充值记录已处理');
- }
- return $recharge->fresh();
- }
- /** 自动模式:同步待查询代理的链上充值并确认入账。由 /admin/sync/recharge 和 agent:recharge-sync 调用。 */
- public function sync(?int $agentId = null): array
- {
- $result = ['agents' => 0, 'found' => 0, 'credited' => 0, 'ignored' => 0];
- $agentIds = $agentId ? [$agentId] : array_keys($this->pendingQueue());
- foreach (AgentWallet::query()->whereIn('agent_id', $agentIds)->get() as $wallet) {
- ++$result['agents'];
- try {
- foreach (TronHelper::getTrc20UsdtRecharges($wallet->address) as $transfer) {
- if (strcasecmp((string) $transfer['to_address'], (string) $wallet->address) !== 0) continue;
- $created = AgentRecharge::query()->firstOrCreate(['txid' => $transfer['txid']], [
- 'order_no' => $this->orderNo(), 'agent_id' => $wallet->agent_id, 'type' => AgentRecharge::TYPE_AUTO,
- 'net' => 'TRC20', 'from_address' => (string) $transfer['from_address'],
- 'to_address' => (string) $transfer['to_address'], 'usdt_amount' => bcadd((string) $transfer['amount'], '0', 4),
- 'status' => AgentRecharge::STATUS_STAY,
- ]);
- if ($created->wasRecentlyCreated) ++$result['found'];
- }
- } catch (Throwable $e) {
- Log::warning('agent_usdt_recharge_sync_failed', ['agent_id' => $wallet->agent_id, 'error' => $e->getMessage()]);
- }
- }
- $pending = AgentRecharge::query()->where('type', AgentRecharge::TYPE_AUTO)->where('status', AgentRecharge::STATUS_STAY)
- ->when($agentId, fn ($q) => $q->where('agent_id', $agentId))->orderBy('id')->limit(200)->get();
- foreach ($pending as $recharge) {
- $outcome = $this->confirm($recharge);
- if ($outcome !== null) ++$result[$outcome];
- }
- return $result;
- }
- private function confirm(AgentRecharge $recharge): ?string
- {
- $wallet = AgentWallet::query()->where('agent_id', $recharge->agent_id)->first();
- if (!$wallet || strcasecmp($wallet->address, (string) $recharge->to_address) !== 0) {
- $recharge->forceFill(['status' => AgentRecharge::STATUS_IGNORE, 'remark' => '收款地址不是该代理的充值地址'])->save();
- return 'ignored';
- }
- $result = TronHelper::getUsdtTransferConfirmation((string) $recharge->txid, $wallet->address);
- if (empty($result['success'])) {
- if (!empty($result['invalid'])) {
- $recharge->forceFill(['status' => AgentRecharge::STATUS_IGNORE, 'remark' => (string) ($result['message'] ?? '无效USDT充值交易')])->save();
- return 'ignored';
- }
- return null;
- }
- if ((int) $result['confirmations'] < TronHelper::CONFIRMED_NUMBER) {
- $recharge->forceFill(['confirmations' => (int) $result['confirmations']])->save();
- return null;
- }
- $credited = $this->credit($recharge, (string) $result['amount'], [
- 'confirmations' => (int) $result['confirmations'], 'from_address' => (string) $result['from_address'],
- ]);
- if ($credited) {
- try {
- CollectService::createCollect($wallet->address, 'USDT', 'TRC20');
- } catch (Throwable $e) {
- Log::warning('agent_usdt_collect_create_failed', ['agent_id' => $recharge->agent_id, 'error' => $e->getMessage()]);
- }
- }
- return $credited ? 'credited' : null;
- }
- /** 入账:记录改为成功、代理加款(合营计入保证金),再尝试完成入股待付款订单。 */
- public function credit(AgentRecharge $recharge, string $usdtAmount, array $extra = []): bool
- {
- $credited = DB::transaction(function () use ($recharge, $usdtAmount, $extra) {
- $recharge = AgentRecharge::query()->lockForUpdate()->find($recharge->id);
- if (!$recharge || (int) $recharge->status !== AgentRecharge::STATUS_STAY) return false;
- $rate = AgentExchangeRate::recharge();
- $amount = bcmul($usdtAmount, $rate, 4);
- if (bccomp($amount, '0', 4) <= 0) throw new \RuntimeException('充值金额必须大于0');
- $recharge->forceFill($extra + [
- 'usdt_amount' => bcadd($usdtAmount, '0', 4), 'exchange_rate' => $rate, 'amount' => $amount,
- 'status' => AgentRecharge::STATUS_SUCCESS, 'confirmed_at' => now(),
- ])->save();
- $this->wallets->adjust((int) $recharge->agent_id, $amount, 'recharge',
- 'USDT充值 ' . $recharge->usdt_amount . ' USDT,汇率 ' . $rate,
- 'agent-recharge:' . $recharge->id, $recharge->type === AgentRecharge::TYPE_HAND ? 'admin' : 'system',
- $extra['audit_admin_id'] ?? null, 'agent_recharge', (int) $recharge->id);
- return true;
- }, 3);
- if ($credited) $this->equity->fulfillPending((int) $recharge->agent_id);
- return $credited;
- }
- public function mode(): int
- {
- return (int) DB::table('config')->where('field', 'receiving_type')->value('val') === 1
- ? AgentRecharge::TYPE_AUTO : AgentRecharge::TYPE_HAND;
- }
- private function wallet(Agent $agent): AgentWallet
- {
- return DB::transaction(function () use ($agent) {
- Agent::query()->lockForUpdate()->findOrFail($agent->id);
- $wallet = AgentWallet::query()->where('agent_id', $agent->id)->first();
- if ($wallet) return $wallet;
- $tron = TronHelper::createAddress('AG' . $agent->id);
- if (empty($tron['address']) || empty($tron['private_key'])) throw new \RuntimeException('充值地址生成失败,请稍后重试');
- return AgentWallet::query()->create([
- 'agent_id' => $agent->id, 'address' => $tron['address'], 'private_key' => $tron['private_key'], 'net' => 'TRC20',
- ]);
- });
- }
- private function markPending(int $agentId): void
- {
- $queue = $this->pendingQueue();
- $queue[$agentId] = time() + self::PENDING_TTL;
- Cache::put(self::PENDING_KEY, $queue, self::PENDING_TTL);
- }
- private function pendingQueue(): array
- {
- $queue = Cache::get(self::PENDING_KEY, []);
- $now = time();
- return array_filter(is_array($queue) ? $queue : [], fn ($expires) => (int) $expires > $now);
- }
- private function orderNo(): string
- {
- return 'AD' . now()->format('YmdHis') . strtoupper(Str::random(8));
- }
- }
|