doge 22 ore în urmă
părinte
comite
ab29ee7b5c

+ 64 - 0
app/Console/Commands/RechargeSyncMember.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Services\RechargeService;
+use Illuminate\Console\Command;
+
+class RechargeSyncMember extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'recharge:sync-member {member_id : 用户 member_id} {--confirm-only : 只确认已有待确认记录,不重新拉链上充值}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '按指定用户重新同步并确认 USDT 自动充值';
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $memberId = $this->argument('member_id');
+        $confirmOnly = (bool) $this->option('confirm-only');
+
+        $this->info("开始处理用户 {$memberId} 的充值记录...");
+
+        $result = RechargeService::syncAndConfirmMemberRecharge($memberId, !$confirmOnly, true);
+        if (empty($result['success'])) {
+            $this->error($result['message'] ?? '处理失败');
+            return Command::FAILURE;
+        }
+
+        $this->table(
+            ['member_id', 'address', 'synced', 'checked', 'confirmed', 'remaining_pending'],
+            [[
+                $result['member_id'],
+                $result['address'],
+                $result['synced'],
+                $result['checked'],
+                $result['confirmed'],
+                $result['remaining_pending'],
+            ]]
+        );
+
+        if (!empty($result['confirmed_txids'])) {
+            $this->line('已确认交易:');
+            foreach ($result['confirmed_txids'] as $txid) {
+                $this->line($txid);
+            }
+        }
+
+        $this->info('处理完成');
+        return Command::SUCCESS;
+    }
+}

+ 8 - 4
app/Helpers/TronHelper.php

@@ -427,7 +427,7 @@ class TronHelper
         $url = "/v1/accounts/{$address}/transactions/trc20?limit={$limit}";
 
         
-        // try {
+        try {
             $response = self::$client->get($url, [
                 'headers' => [
                     'Accept' => 'application/json',
@@ -457,9 +457,13 @@ class TronHelper
 
             }
             return $recharges;
-        // } catch (\Exception $e) {
-        //     return ['error' => $e->getMessage()];
-        // }
+        } catch (\Throwable $e) {
+            Log::warning('获取TRC20 USDT充值记录失败', [
+                'address' => $address,
+                'error' => $e->getMessage(),
+            ]);
+            return [];
+        }
     }
 
 

+ 2 - 2
app/Http/Controllers/admin/Sync.php

@@ -21,9 +21,9 @@ class Sync extends Controller
 
     public function recharge()
     {
-        $synced = RechargeService::syncAllUsdtRechargeRecords();
+        $synced = RechargeService::syncPendingUsdtRechargeRecords();
         RechargeService::syncRechargeStay();
-        return $this->success(['synced' => $synced]);
+        return $this->success($synced);
     }
 
 

+ 1 - 0
app/Services/KeyboardService.php

@@ -230,6 +230,7 @@ class KeyboardService extends BaseService
                 // 删除个人缓存
                 Util::delCache($chatId);
                 // 查看余额 同步充值记录
+                RechargeService::markUsdtRechargePending($chatId);
                 RechargeService::syncUsdtRechargeRecords($chatId);
                 return WalletService::getBalance($chatId);
             case 'selectLanguage': // 选择语言

+ 216 - 11
app/Services/RechargeService.php

@@ -5,9 +5,11 @@ namespace App\Services;
 
 use App\Services\BaseService;
 use App\Models\Recharge;
+use App\Models\User;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
 use App\Services\WalletService;
 use App\Services\CollectService;
 use App\Services\UserService;
@@ -21,6 +23,17 @@ use App\Models\Config;
 class RechargeService extends BaseService
 {
     public static string $MODEL = Recharge::class;
+    const PENDING_SYNC_CACHE_KEY = 'usdt_recharge_pending_sync_members';
+    const RECENT_ACTIVE_SYNC_CACHE_KEY = 'usdt_recharge_recent_active_synced_at';
+    const TRONGRID_SYNC_LOCK_KEY = 'usdt_recharge_trongrid_sync_lock';
+    const ADDRESS_SYNC_CACHE_PREFIX = 'usdt_recharge_address_synced_at:';
+    const PENDING_SYNC_TTL = 7200;
+    const ADDRESS_SYNC_COOLDOWN = 60;
+    const TRONGRID_SYNC_LOCK_SECONDS = 2;
+    const SYNC_BATCH_SIZE = 1;
+    const RECENT_ACTIVE_MINUTES = 30;
+    const RECENT_ACTIVE_LIMIT = 20;
+    const RECENT_ACTIVE_ENQUEUE_INTERVAL = 300;
     /**
      * @description: 模型
      * @return {string}
@@ -132,14 +145,28 @@ class RechargeService extends BaseService
      * @param {*} $memberId
      * @return {*}
      */
-    public static function syncUsdtRechargeRecords($memberId, $walletInfo = null)
+    public static function syncUsdtRechargeRecords($memberId, $walletInfo = null, $force = false)
     {
         $walletInfo = $walletInfo ?: WalletService::findOne(['member_id' => $memberId, 'coin' => 'USDT']);
         if (empty($walletInfo) || empty($walletInfo->address)) {
             return 0;
         }
 
-        $data = TronHelper::getTrc20UsdtRecharges($walletInfo->address);
+        if (!$force && !self::canSyncAddress($walletInfo->address)) {
+            return 0;
+        }
+
+        try {
+            $data = TronHelper::getTrc20UsdtRecharges($walletInfo->address);
+        } catch (\Throwable $e) {
+            Log::warning('同步USDT充值记录失败', [
+                'member_id' => $memberId,
+                'address' => $walletInfo->address,
+                'error' => $e->getMessage(),
+            ]);
+            return 0;
+        }
+
         if (empty($data)) {
             return 0;
         }
@@ -158,23 +185,201 @@ class RechargeService extends BaseService
     }
 
     /**
-     * @description: 同步所有USDT钱包的新充值记录
-     * @return int
+     * @description: 强制同步并确认单个会员的USDT充值记录
+     * @param {*} $memberId
+     * @param bool $syncChain 是否先拉链上新充值
+     * @param bool $force 是否忽略地址同步节流
+     * @return array
      */
-    public static function syncAllUsdtRechargeRecords()
+    public static function syncAndConfirmMemberRecharge($memberId, $syncChain = true, $force = false)
     {
-        $wallets = WalletService::findAll(['coin' => 'USDT']);
-        $total = 0;
+        $walletInfo = WalletService::findOne(['member_id' => $memberId, 'coin' => 'USDT']);
+        if (empty($walletInfo) || empty($walletInfo->address)) {
+            return [
+                'success' => false,
+                'message' => '未找到该用户的USDT钱包地址',
+                'member_id' => $memberId,
+            ];
+        }
+
+        $synced = 0;
+        if ($syncChain) {
+            $synced = self::syncUsdtRechargeRecords($memberId, $walletInfo, $force);
+        }
+
+        $pendingList = self::model()::where(self::getWhere([
+            'member_id' => $memberId,
+            'status' => self::model()::STATUS_STAY,
+            'type' => self::model()::TYPE_AUTO,
+        ]))->orderBy('id')->get();
+
+        $checked = 0;
+        $confirmed = 0;
+        $confirmedTxids = [];
+
+        foreach ($pendingList as $item) {
+            $checked++;
+            self::handleRechargeConfirmation($item->txid);
+            $fresh = self::findOne(['id' => $item->id]);
+            if ($fresh && intval($fresh->status) === self::model()::STATUS_SUCCESS) {
+                $confirmed++;
+                $confirmedTxids[] = $fresh->txid;
+            }
+        }
+
+        $remainingPending = self::model()::where(self::getWhere([
+            'member_id' => $memberId,
+            'status' => self::model()::STATUS_STAY,
+            'type' => self::model()::TYPE_AUTO,
+        ]))->count();
+
+        return [
+            'success' => true,
+            'member_id' => $memberId,
+            'address' => $walletInfo->address,
+            'synced' => $synced,
+            'checked' => $checked,
+            'confirmed' => $confirmed,
+            'remaining_pending' => $remainingPending,
+            'confirmed_txids' => $confirmedTxids,
+        ];
+    }
+
+    /**
+     * @description: 标记用户进入USDT充值同步队列
+     * @param {*} $memberId
+     * @return void
+     */
+    public static function markUsdtRechargePending($memberId)
+    {
+        if (empty($memberId)) {
+            return;
+        }
+
+        $queue = self::getPendingSyncQueue();
+        $key = strval($memberId);
+        $now = time();
+        $queue[$key] = [
+            'member_id' => $memberId,
+            'last_synced_at' => $queue[$key]['last_synced_at'] ?? 0,
+            'updated_at' => $now,
+            'expires_at' => $now + self::PENDING_SYNC_TTL,
+        ];
+        self::putPendingSyncQueue($queue);
+    }
+
+    /**
+     * @description: 同步待处理队列中的USDT充值记录
+     * @param int $limit
+     * @return array
+     */
+    public static function syncPendingUsdtRechargeRecords($limit = self::SYNC_BATCH_SIZE)
+    {
+        self::markRecentActiveMembersPending();
+
+        $queue = self::getPendingSyncQueue();
+        uasort($queue, function ($a, $b) {
+            return ($a['last_synced_at'] ?? 0) <=> ($b['last_synced_at'] ?? 0);
+        });
+
+        $synced = 0;
+        $checked = 0;
+        $now = time();
+
+        foreach ($queue as $key => $item) {
+            if ($checked >= $limit) {
+                break;
+            }
 
-        foreach ($wallets as $walletInfo) {
-            if (empty($walletInfo->member_id) || empty($walletInfo->address)) {
+            if (!empty($item['last_synced_at']) && $now - $item['last_synced_at'] < self::ADDRESS_SYNC_COOLDOWN) {
                 continue;
             }
 
-            $total += self::syncUsdtRechargeRecords($walletInfo->member_id, $walletInfo);
+            $walletInfo = WalletService::findOne(['member_id' => $item['member_id'] ?? 0, 'coin' => 'USDT']);
+            if (empty($walletInfo) || empty($walletInfo->address)) {
+                unset($queue[$key]);
+                continue;
+            }
+
+            $synced += self::syncUsdtRechargeRecords($walletInfo->member_id, $walletInfo);
+            $checked++;
+            $queue[$key]['last_synced_at'] = time();
+            $queue[$key]['updated_at'] = time();
+        }
+
+        self::putPendingSyncQueue($queue);
+
+        return [
+            'synced' => $synced,
+            'checked' => $checked,
+            'queued' => count($queue),
+        ];
+    }
+
+    /**
+     * @description: 兼容旧调用,避免再次全量扫描钱包
+     * @return array
+     */
+    public static function syncAllUsdtRechargeRecords()
+    {
+        return self::syncPendingUsdtRechargeRecords();
+    }
+
+    private static function canSyncAddress($address)
+    {
+        $addressKey = self::ADDRESS_SYNC_CACHE_PREFIX . $address;
+        if (Cache::has($addressKey)) {
+            return false;
+        }
+
+        if (!Cache::add(self::TRONGRID_SYNC_LOCK_KEY, time(), self::TRONGRID_SYNC_LOCK_SECONDS)) {
+            return false;
         }
 
-        return $total;
+        Cache::put($addressKey, time(), self::ADDRESS_SYNC_COOLDOWN);
+        return true;
+    }
+
+    private static function getPendingSyncQueue()
+    {
+        $queue = Cache::get(self::PENDING_SYNC_CACHE_KEY, []);
+        if (!is_array($queue)) {
+            return [];
+        }
+
+        $now = time();
+        foreach ($queue as $key => $item) {
+            if (!is_array($item) || empty($item['member_id']) || ($item['expires_at'] ?? 0) < $now) {
+                unset($queue[$key]);
+            }
+        }
+
+        return $queue;
+    }
+
+    private static function putPendingSyncQueue(array $queue)
+    {
+        Cache::put(self::PENDING_SYNC_CACHE_KEY, $queue, self::PENDING_SYNC_TTL);
+    }
+
+    private static function markRecentActiveMembersPending()
+    {
+        if (Cache::has(self::RECENT_ACTIVE_SYNC_CACHE_KEY)) {
+            return;
+        }
+
+        Cache::put(self::RECENT_ACTIVE_SYNC_CACHE_KEY, time(), self::RECENT_ACTIVE_ENQUEUE_INTERVAL);
+
+        $since = date('Y-m-d H:i:s', time() - self::RECENT_ACTIVE_MINUTES * 60);
+        $members = User::whereNotNull('last_active_time')
+            ->where('last_active_time', '>=', $since)
+            ->orderByDesc('last_active_time')
+            ->limit(self::RECENT_ACTIVE_LIMIT)
+            ->pluck('member_id');
+
+        foreach ($members as $memberId) {
+            self::markUsdtRechargePending($memberId);
+        }
     }
 
     /**

+ 2 - 0
app/Services/TopUpService.php

@@ -107,6 +107,7 @@ class TopUpService
 
     public static function bill($chatId, $firstName, $messageId = null, $page = 1, $limit = 5)
     {
+        RechargeService::markUsdtRechargePending($chatId);
         RechargeService::syncUsdtRechargeRecords($chatId);
         $list = BalanceLog::where('member_id', $chatId)
 //            ->where('change_type', '充值')
@@ -244,6 +245,7 @@ class TopUpService
     //用户点击我已付款
     public static function done($chatId)
     {
+        RechargeService::markUsdtRechargePending($chatId);
         RechargeService::syncUsdtRechargeRecords($chatId);
         $keyboard = [
             [['text' => lang("↩️返回"), 'callback_data' => "topUp@@home"]]

+ 2 - 0
app/Services/WalletService.php

@@ -280,6 +280,8 @@ class WalletService extends BaseService
             $path = self::createRechargeQrCode($address);
         }
 
+        RechargeService::markUsdtRechargePending($memberId);
+
         // $host = config('app.url'); // 通常在 .env 中配置 APP_URL
         return [
             'coin' => $info->coin,