doge 1 月之前
父節點
當前提交
9245c09a0f

+ 41 - 0
app/Http/Controllers/admin/ThirdGameOrder.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Http\Controllers\admin;
+
+use App\Constants\HttpStatus;
+use App\Http\Controllers\Controller;
+use App\Services\ThirdGameBalanceService;
+use Exception;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Validation\ValidationException;
+
+class ThirdGameOrder extends Controller
+{
+    public function index(ThirdGameBalanceService $service): JsonResponse
+    {
+        try {
+            $params = request()->validate([
+                'start_time' => ['required', 'date_format:Y-m-d H:i:s'],
+                'end_time' => ['required', 'date_format:Y-m-d H:i:s', 'after_or_equal:start_time'],
+                'page' => ['nullable', 'integer', 'min:1'],
+                'limit' => ['nullable', 'integer', 'min:1', 'max:2000'],
+            ]);
+            $result = $service->historyOrders(
+                (string) $params['start_time'],
+                (string) $params['end_time'],
+                (int) ($params['page'] ?? 1),
+                (int) ($params['limit'] ?? 200)
+            );
+            if (!$result['ok']) {
+                throw new Exception($result['msg'], HttpStatus::CUSTOM_ERROR);
+            }
+            unset($result['ok']);
+
+            return $this->success($result);
+        } catch (ValidationException $e) {
+            return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first());
+        } catch (Exception $e) {
+            return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
+        }
+    }
+}

+ 197 - 0
app/Services/ThirdGameBalanceService.php

@@ -2,16 +2,41 @@
 
 namespace App\Services;
 
+use Carbon\Carbon;
 use Illuminate\Http\Client\Pool;
 use Illuminate\Http\Client\Response;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\RateLimiter;
 
 class ThirdGameBalanceService
 {
     private const CODE_SUCCESS = 10000;
 
+    private const HISTORY_QUERY_INTERVAL_SECONDS = 60;
+
+    private const HISTORY_PAGE_INTERVAL_SECONDS = 10;
+
+    private const HISTORY_HOURLY_LIMIT = 5;
+
+    private const GAME_TYPE_NAMES = [
+        1 => '视讯',
+        2 => '老虎机',
+        3 => '彩票',
+        4 => '体育',
+        5 => '电竞',
+        6 => '捕猎',
+        7 => '棋牌',
+    ];
+
+    private const ORDER_STATUS_NAMES = [
+        0 => '未完成',
+        1 => '已完成',
+        2 => '已取消',
+        3 => '已撤单',
+    ];
+
     /**
      * 批量查询用户的三方游戏总余额,三方与主钱包金额按 1:1 处理。
      *
@@ -129,6 +154,69 @@ class ThirdGameBalanceService
         return $this->detailResult($memberId, $balances);
     }
 
+    /**
+     * 查询三方游戏历史订单,按订单更新时间正序返回。
+     */
+    public function historyOrders(
+        string $startTime,
+        string $endTime,
+        int $page = 1,
+        int $limit = 200
+    ): array
+    {
+        if (!$this->configured()) {
+            return ['ok' => false, 'msg' => '三方游戏配置缺失'];
+        }
+
+        $rangeError = $this->historyRangeError($startTime, $endTime);
+        if ($rangeError !== null) {
+            return ['ok' => false, 'msg' => $rangeError];
+        }
+
+        $rateError = $this->acquireHistoryQuota($startTime, $endTime, $page, $limit);
+        if ($rateError !== null) {
+            return ['ok' => false, 'msg' => $rateError];
+        }
+
+        $response = $this->request('/api/server/recordHistory', [
+            'currency' => (string) config('third_game.currency', 'CNY'),
+            'startTime' => $startTime,
+            'endTime' => $endTime,
+            'pageNo' => (string) $page,
+            'pageSize' => (string) $limit,
+        ]);
+        if (!$response instanceof Response) {
+            return ['ok' => false, 'msg' => $this->responseMessage($response)];
+        }
+
+        $body = $response->json();
+        if (!is_array($body) || (int) ($body['code'] ?? 0) !== self::CODE_SUCCESS) {
+            return ['ok' => false, 'msg' => $this->responseMessage($response)];
+        }
+
+        $providerData = is_array($body['data'] ?? null) ? $body['data'] : [];
+        $providerList = is_array($providerData['list'] ?? null) ? $providerData['list'] : [];
+        $list = [];
+        foreach ($providerList as $row) {
+            if (is_array($row)) {
+                $list[] = $this->formatOrder($row);
+            }
+        }
+
+        $result = [
+            'ok' => true,
+            'currency' => (string) config('third_game.currency', 'CNY'),
+            'start_time' => $startTime,
+            'end_time' => $endTime,
+            'total' => (int) ($providerData['total'] ?? count($list)),
+            'page_no' => (int) ($providerData['pageNo'] ?? $page),
+            'page_size' => (int) ($providerData['pageSize'] ?? $limit),
+            'list' => $list,
+        ];
+
+        return $result;
+    }
+
     /**
      * 一键把用户在所有三方游戏平台的余额转出。
      */
@@ -337,6 +425,105 @@ class ThirdGameBalanceService
         return (string) ($response->json('msg') ?: '三方游戏接口异常');
     }
 
+    private function formatOrder(array $row): array
+    {
+        $gameType = (int) ($row['gameType'] ?? 0);
+        $status = (int) ($row['status'] ?? -1);
+
+        return [
+            'player_id' => (string) ($row['playerId'] ?? ''),
+            'platform' => (string) ($row['platType'] ?? ''),
+            'currency' => (string) ($row['currency'] ?? ''),
+            'game_type' => $gameType,
+            'game_type_text' => self::GAME_TYPE_NAMES[$gameType] ?? '未知',
+            'game_name' => (string) ($row['gameName'] ?? ''),
+            'round' => (string) ($row['round'] ?? ''),
+            'table' => (string) ($row['table'] ?? ''),
+            'seat' => (string) ($row['seat'] ?? ''),
+            'bet_amount' => $this->numericValue($row['betAmount'] ?? null),
+            'valid_amount' => $this->numericValue($row['validAmount'] ?? null),
+            'settled_amount' => $this->numericValue($row['settledAmount'] ?? null),
+            'bet_content' => $row['betContent'] ?? '',
+            'status' => $status,
+            'status_text' => self::ORDER_STATUS_NAMES[$status] ?? '未知',
+            'game_order_id' => (string) ($row['gameOrderId'] ?? ''),
+            'bet_time' => (string) ($row['betTime'] ?? ''),
+            'last_update_time' => (string) ($row['lastUpdateTime'] ?? ''),
+        ];
+    }
+
+    private function numericValue($value)
+    {
+        return is_numeric($value) ? $value + 0 : null;
+    }
+
+    private function historyRangeError(string $startTime, string $endTime): ?string
+    {
+        $timezone = config('app.timezone', 'Asia/Shanghai');
+        $start = Carbon::createFromFormat('Y-m-d H:i:s', $startTime, $timezone);
+        $end = Carbon::createFromFormat('Y-m-d H:i:s', $endTime, $timezone);
+        if ($start->gt($end)) {
+            return '开始时间不能大于结束时间';
+        }
+        if ($start->diffInSeconds($end) > 6 * 60 * 60) {
+            return '查询时间范围不能超过6小时';
+        }
+        if ($start->lt(Carbon::now($timezone)->subDays(15))) {
+            return '只能查询最近15天内的订单';
+        }
+
+        return null;
+    }
+
+    private function acquireHistoryQuota(
+        string $startTime,
+        string $endTime,
+        int $page,
+        int $limit
+    ): ?string
+    {
+        $prefix = $this->historyRatePrefix();
+        $sessionKey = $this->historySessionKey($startTime, $endTime, $limit);
+        $lock = Cache::lock($prefix . ':lock', 5);
+        if (!$lock->get()) {
+            return '订单查询正在处理中,请稍后重试';
+        }
+
+        try {
+            $now = time();
+            $lastRequestAt = (int) Cache::get($prefix . ':last_request', 0);
+            if ($lastRequestAt > 0 && $now - $lastRequestAt < self::HISTORY_PAGE_INTERVAL_SECONDS) {
+                $retryAfter = self::HISTORY_PAGE_INTERVAL_SECONDS - ($now - $lastRequestAt);
+                return "订单请求至少间隔10秒,请在{$retryAfter}秒后重试";
+            }
+
+            $isPagination = $page > 1 && Cache::has($sessionKey);
+            if (!$isPagination) {
+                $lastQueryAt = (int) Cache::get($prefix . ':last_query', 0);
+                if ($lastQueryAt > 0 && $now - $lastQueryAt < self::HISTORY_QUERY_INTERVAL_SECONDS) {
+                    $retryAfter = self::HISTORY_QUERY_INTERVAL_SECONDS - ($now - $lastQueryAt);
+                    return "订单查询至少间隔1分钟,请在{$retryAfter}秒后重试";
+                }
+
+                $hourlyKey = $prefix . ':hourly';
+                if (RateLimiter::tooManyAttempts($hourlyKey, self::HISTORY_HOURLY_LIMIT)) {
+                    $retryAfter = max(1, RateLimiter::availableIn($hourlyKey));
+                    return "订单查询每小时最多5次,请在{$retryAfter}秒后重试";
+                }
+
+                RateLimiter::hit($hourlyKey, 3600);
+                Cache::put($prefix . ':last_query', $now, self::HISTORY_QUERY_INTERVAL_SECONDS);
+                Cache::put($sessionKey, true, 3600);
+            }
+
+            Cache::put($prefix . ':last_request', $now, self::HISTORY_PAGE_INTERVAL_SECONDS);
+
+            return null;
+        } finally {
+            $lock->release();
+        }
+    }
+
     private function configured(): bool
     {
         return config('third_game.api_url') !== ''
@@ -377,6 +564,16 @@ class ThirdGameBalanceService
         return 'third_game_balance_detail:' . $memberId;
     }
 
+    private function historyRatePrefix(): string
+    {
+        return 'third_game_order_history:' . md5((string) config('third_game.sn'));
+    }
+
+    private function historySessionKey(string $startTime, string $endTime, int $limit): string
+    {
+        return $this->historyRatePrefix() . ':session:' . md5($startTime . '|' . $endTime . '|' . $limit);
+    }
+
     private function clearCache(string $memberId): void
     {
         Cache::forget($this->cacheKey($memberId));

+ 62 - 0
docs/admin-third-game-order-api.md

@@ -0,0 +1,62 @@
+# 后台三方游戏订单
+
+## GET `/admin/thirdGame/orders`
+
+请求头:
+
+```http
+Authorization: Bearer <token>
+```
+
+参数:
+
+| 参数 | 必填 | 说明 |
+| --- | --- | --- |
+| `start_time` | 是 | 订单更新时间开始,格式 `Y-m-d H:i:s`,UTC+8 |
+| `end_time` | 是 | 订单更新时间结束,格式 `Y-m-d H:i:s`,UTC+8 |
+| `page` | 否 | 默认 `1` |
+| `limit` | 否 | 默认 `200`,最大 `2000` |
+
+开始时间不能大于结束时间,单次范围不能超过 6 小时,只能查询最近 15 天。
+
+成功返回:
+
+```json
+{
+  "code": 0,
+  "data": {
+    "currency": "CNY",
+    "start_time": "2026-08-21 00:00:00",
+    "end_time": "2026-08-21 06:00:00",
+    "total": 1,
+    "page_no": 1,
+    "page_size": 200,
+    "list": [
+      {
+        "player_id": "abc123456",
+        "platform": "ag",
+        "currency": "CNY",
+        "game_type": 1,
+        "game_type_text": "视讯",
+        "game_name": "Baccarat",
+        "round": "88888888",
+        "table": "88888888",
+        "seat": "88888888",
+        "bet_amount": 100,
+        "valid_amount": 95,
+        "settled_amount": -100,
+        "bet_content": "Banker",
+        "status": 1,
+        "status_text": "已完成",
+        "game_order_id": "000001",
+        "bet_time": "2026-08-21 01:00:00",
+        "last_update_time": "2026-08-21 01:05:00"
+      }
+    ]
+  }
+}
+```
+
+`code = 0` 表示成功;失败时读取 `msg`。
+
+三方限制:普通查询至少间隔 1 分钟且每小时最多 5 次;翻页请求至少间隔 10 秒。前端不要自动高频刷新。

+ 5 - 0
routes/admin.php

@@ -48,6 +48,7 @@ use App\Http\Controllers\admin\Online;
 use App\Http\Controllers\admin\Funds;
 use App\Http\Controllers\admin\ManualAudit;
 use App\Http\Controllers\admin\ThirdPartyDeposit;
+use App\Http\Controllers\admin\ThirdGameOrder;
 
 Route::post('/login', [Admin::class, 'login']);
 Route::get('/test', [Wallet::class, 'test']);
@@ -218,6 +219,10 @@ Route::middleware(['admin.jwt'])->group(function () {
             Route::get('/detail', [ThirdPartyDeposit::class, 'detail']);
         });
 
+        Route::prefix('/thirdGame')->group(function () {
+            Route::get('/orders', [ThirdGameOrder::class, 'index']);
+        });
+
         Route::prefix('/wallet')->group(function () {
             Route::get('/', [Wallet::class, 'index']);
             Route::post('/topUp', [Wallet::class, 'topUp']);