doge 12 timmar sedan
förälder
incheckning
68db96a03a

+ 33 - 4
app/Http/Controllers/admin/ThirdGameOrder.php

@@ -4,14 +4,43 @@ namespace App\Http\Controllers\admin;
 
 use App\Constants\HttpStatus;
 use App\Http\Controllers\Controller;
-use App\Services\ThirdGameBalanceService;
+use App\Services\ThirdGameOrderService;
 use Exception;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Validation\ValidationException;
 
 class ThirdGameOrder extends Controller
 {
-    public function index(ThirdGameBalanceService $service): JsonResponse
+    public function index(ThirdGameOrderService $service): JsonResponse
+    {
+        try {
+            $params = request()->validate([
+                'page' => ['nullable', 'integer', 'min:1'],
+                'limit' => ['nullable', 'integer', 'min:1', 'max:200'],
+                'user_id' => ['nullable', 'integer', 'min:1'],
+                'member_id' => ['nullable', 'string', 'max:64'],
+                'username' => ['nullable', 'string', 'max:128'],
+                'first_name' => ['nullable', 'string', 'max:128'],
+                'player_id' => ['nullable', 'string', 'max:32'],
+                'platform' => ['nullable', 'string', 'max:32'],
+                'game_order_id' => ['nullable', 'string', 'max:128'],
+                'game_type' => ['nullable', 'integer', 'in:1,2,3,4,5,6,7'],
+                'status' => ['nullable', 'integer', 'in:0,1,2,3'],
+                'start_time' => ['nullable', 'date_format:Y-m-d H:i:s', 'required_with:end_time'],
+                'end_time' => [
+                    'nullable', 'date_format:Y-m-d H:i:s', 'required_with:start_time', 'after_or_equal:start_time',
+                ],
+            ]);
+
+            return $this->success($service->paginate($params));
+        } catch (ValidationException $e) {
+            return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first());
+        } catch (Exception $e) {
+            return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
+        }
+    }
+
+    public function sync(ThirdGameOrderService $service): JsonResponse
     {
         try {
             $params = request()->validate([
@@ -20,11 +49,11 @@ class ThirdGameOrder extends Controller
                 'page' => ['nullable', 'integer', 'min:1'],
                 'limit' => ['nullable', 'integer', 'min:1', 'max:2000'],
             ]);
-            $result = $service->historyOrders(
+            $result = $service->sync(
                 (string) $params['start_time'],
                 (string) $params['end_time'],
                 (int) ($params['page'] ?? 1),
-                (int) ($params['limit'] ?? 200)
+                (int) ($params['limit'] ?? 2000)
             );
             if (!$result['ok']) {
                 throw new Exception($result['msg'], HttpStatus::CUSTOM_ERROR);

+ 45 - 0
app/Models/ThirdGameOrder.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace App\Models;
+
+class ThirdGameOrder extends BaseModel
+{
+    protected $table = 'third_game_orders';
+
+    protected $hidden = [];
+
+    protected $fillable = [
+        'order_key',
+        'user_id',
+        'member_id',
+        'username',
+        'first_name',
+        'player_id',
+        'platform',
+        'currency',
+        'game_type',
+        'game_name',
+        'round_no',
+        'table_no',
+        'seat',
+        'bet_amount',
+        'valid_amount',
+        'settled_amount',
+        'bet_content',
+        'status',
+        'game_order_id',
+        'bet_time',
+        'last_update_time',
+    ];
+
+    protected $casts = [
+        'user_id' => 'integer',
+        'game_type' => 'integer',
+        'bet_amount' => 'decimal:10',
+        'valid_amount' => 'decimal:10',
+        'settled_amount' => 'decimal:10',
+        'status' => 'integer',
+        'bet_time' => 'datetime',
+        'last_update_time' => 'datetime',
+    ];
+}

+ 6 - 1
app/Services/ThirdGameBalanceService.php

@@ -193,6 +193,7 @@ class ThirdGameBalanceService
         if (!is_array($body) || (int) ($body['code'] ?? 0) !== self::CODE_SUCCESS) {
             return ['ok' => false, 'msg' => $this->responseMessage($response)];
         }
+        $this->rememberHistorySession($startTime, $endTime, $limit);
 
         $providerData = is_array($body['data'] ?? null) ? $body['data'] : [];
         $providerList = is_array($providerData['list'] ?? null) ? $providerData['list'] : [];
@@ -513,7 +514,6 @@ class ThirdGameBalanceService
 
                 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);
@@ -574,6 +574,11 @@ class ThirdGameBalanceService
         return $this->historyRatePrefix() . ':session:' . md5($startTime . '|' . $endTime . '|' . $limit);
     }
 
+    private function rememberHistorySession(string $startTime, string $endTime, int $limit): void
+    {
+        Cache::put($this->historySessionKey($startTime, $endTime, $limit), true, 3600);
+    }
+
     private function clearCache(string $memberId): void
     {
         Cache::forget($this->cacheKey($memberId));

+ 380 - 0
app/Services/ThirdGameOrderService.php

@@ -0,0 +1,380 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\ThirdGameOrder as ThirdGameOrderModel;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+
+class ThirdGameOrderService
+{
+    private const GAME_TYPE_NAMES = [
+        1 => '视讯',
+        2 => '老虎机',
+        3 => '彩票',
+        4 => '体育',
+        5 => '电竞',
+        6 => '捕猎',
+        7 => '棋牌',
+    ];
+
+    private const STATUS_NAMES = [
+        0 => '未完成',
+        1 => '已完成',
+        2 => '已取消',
+        3 => '已撤单',
+    ];
+
+    private ThirdGameBalanceService $provider;
+
+    public function __construct(ThirdGameBalanceService $provider)
+    {
+        $this->provider = $provider;
+    }
+
+    public function sync(
+        string $startTime,
+        string $endTime,
+        int $page = 1,
+        int $limit = 2000
+    ): array {
+        $result = $this->provider->historyOrders($startTime, $endTime, $page, $limit);
+        if (empty($result['ok'])) {
+            return $result;
+        }
+
+        $orders = is_array($result['list'] ?? null) ? $result['list'] : [];
+        $users = $this->usersByPlayerId(array_values(array_unique(array_filter(array_map(
+            static fn(array $order): string => trim((string) ($order['player_id'] ?? '')),
+            $orders
+        )))));
+        $existing = $this->existingSnapshots($orders);
+        $now = Carbon::now(config('app.timezone', 'Asia/Shanghai'))->format('Y-m-d H:i:s');
+        $rows = [];
+        $matchedUserIds = [];
+
+        foreach ($orders as $order) {
+            $platform = trim((string) ($order['platform'] ?? ''));
+            $gameOrderId = trim((string) ($order['game_order_id'] ?? ''));
+            if ($gameOrderId === '') {
+                continue;
+            }
+
+            $playerId = trim((string) ($order['player_id'] ?? ''));
+            $identity = $this->identity($platform, $playerId, $gameOrderId);
+            $user = $users[$playerId] ?? null;
+            $previous = $existing[$identity] ?? null;
+            $userId = $user->id ?? $previous->user_id ?? null;
+            if ($userId !== null) {
+                $matchedUserIds[(int) $userId] = true;
+            }
+
+            $rows[$identity] = [
+                'order_key' => $identity,
+                'user_id' => $userId,
+                'member_id' => $user->member_id ?? $previous->member_id ?? null,
+                'username' => (string) ($user->username ?? $previous->username ?? ''),
+                'first_name' => (string) ($user->first_name ?? $previous->first_name ?? ''),
+                'player_id' => $playerId,
+                'platform' => $platform,
+                'currency' => trim((string) ($order['currency'] ?? '')),
+                'game_type' => min(255, max(0, (int) ($order['game_type'] ?? 0))),
+                'game_name' => (string) ($order['game_name'] ?? ''),
+                'round_no' => (string) ($order['round'] ?? ''),
+                'table_no' => (string) ($order['table'] ?? ''),
+                'seat' => (string) ($order['seat'] ?? ''),
+                'bet_amount' => $this->decimal($order['bet_amount'] ?? null),
+                'valid_amount' => $this->decimal($order['valid_amount'] ?? null),
+                'settled_amount' => $this->decimal($order['settled_amount'] ?? null),
+                'bet_content' => $this->text($order['bet_content'] ?? ''),
+                'status' => min(255, max(0, (int) ($order['status'] ?? 0))),
+                'game_order_id' => $gameOrderId,
+                'bet_time' => $this->dateTime($order['bet_time'] ?? null),
+                'last_update_time' => $this->dateTime($order['last_update_time'] ?? null),
+                'created_at' => $previous->created_at ?? $now,
+                'updated_at' => $now,
+            ];
+        }
+
+        if ($rows !== []) {
+            ThirdGameOrderModel::query()->upsert(
+                array_values($rows),
+                ['order_key'],
+                [
+                    'user_id', 'member_id', 'username', 'first_name', 'player_id', 'currency',
+                    'game_type', 'game_name', 'round_no', 'table_no', 'seat', 'bet_amount',
+                    'valid_amount', 'settled_amount', 'bet_content', 'status', 'bet_time',
+                    'last_update_time', 'updated_at',
+                ]
+            );
+        }
+
+        $pageNo = (int) ($result['page_no'] ?? $page);
+        $pageSize = (int) ($result['page_size'] ?? $limit);
+        $total = (int) ($result['total'] ?? count($orders));
+
+        return [
+            'ok' => true,
+            'received' => count($orders),
+            'synced' => count($rows),
+            'matched_users' => count($matchedUserIds),
+            'unmatched_orders' => count(array_filter($rows, static fn(array $row): bool => $row['user_id'] === null)),
+            'total' => $total,
+            'page_no' => $pageNo,
+            'page_size' => $pageSize,
+            'has_more' => $pageSize > 0 && $pageNo * $pageSize < $total,
+        ];
+    }
+
+    public function paginate(array $params): array
+    {
+        $page = max(1, (int) ($params['page'] ?? 1));
+        $limit = min(200, max(1, (int) ($params['limit'] ?? 20)));
+        $query = ThirdGameOrderModel::query()
+            ->leftJoin('users', 'users.id', '=', 'third_game_orders.user_id');
+
+        if (!empty($params['user_id'])) {
+            $userId = (int) $params['user_id'];
+            $query->where(function ($query) use ($userId) {
+                $query->where('users.id', $userId)
+                    ->orWhere(function ($query) use ($userId) {
+                        $query->whereNull('users.id')
+                            ->where('third_game_orders.user_id', $userId);
+                    });
+            });
+        }
+        if (!empty($params['member_id'])) {
+            $memberId = (string) $params['member_id'];
+            $query->where(function ($query) use ($memberId) {
+                $query->where('users.member_id', $memberId)
+                    ->orWhere(function ($query) use ($memberId) {
+                        $query->whereNull('users.id')
+                            ->where('third_game_orders.member_id', $memberId);
+                    });
+            });
+        }
+        if (!empty($params['username'])) {
+            $username = '%' . $params['username'] . '%';
+            $query->where(function ($query) use ($username) {
+                $query->where('users.username', 'like', $username)
+                    ->orWhere(function ($query) use ($username) {
+                        $query->whereNull('users.id')
+                            ->where('third_game_orders.username', 'like', $username);
+                    });
+            });
+        }
+        if (!empty($params['first_name'])) {
+            $firstName = '%' . $params['first_name'] . '%';
+            $query->where(function ($query) use ($firstName) {
+                $query->where('users.first_name', 'like', $firstName)
+                    ->orWhere(function ($query) use ($firstName) {
+                        $query->whereNull('users.id')
+                            ->where('third_game_orders.first_name', 'like', $firstName);
+                    });
+            });
+        }
+        if (!empty($params['player_id'])) {
+            $query->where('third_game_orders.player_id', (string) $params['player_id']);
+        }
+        if (!empty($params['platform'])) {
+            $query->where('third_game_orders.platform', (string) $params['platform']);
+        }
+        if (!empty($params['game_order_id'])) {
+            $query->where('third_game_orders.game_order_id', (string) $params['game_order_id']);
+        }
+        if (isset($params['game_type']) && $params['game_type'] !== '') {
+            $query->where('third_game_orders.game_type', (int) $params['game_type']);
+        }
+        if (isset($params['status']) && $params['status'] !== '') {
+            $query->where('third_game_orders.status', (int) $params['status']);
+        }
+        if (!empty($params['start_time']) && !empty($params['end_time'])) {
+            $query->whereBetween('third_game_orders.last_update_time', [
+                $params['start_time'],
+                $params['end_time'],
+            ]);
+        }
+
+        $total = (clone $query)->count('third_game_orders.id');
+        $list = $query
+            ->select('third_game_orders.*')
+            ->addSelect([
+                'users.id as current_user_id',
+                'users.member_id as current_member_id',
+                'users.username as current_username',
+                'users.first_name as current_first_name',
+            ])
+            ->orderByDesc('third_game_orders.last_update_time')
+            ->orderByDesc('third_game_orders.id')
+            ->forPage($page, $limit)
+            ->get()
+            ->map(fn(ThirdGameOrderModel $order): array => $this->format($order))
+            ->all();
+
+        return [
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+            'list' => $list,
+            'options' => [
+                'game_types' => $this->options(self::GAME_TYPE_NAMES),
+                'statuses' => $this->options(self::STATUS_NAMES),
+            ],
+        ];
+    }
+
+    /**
+     * @return array<string, object>
+     */
+    private function usersByPlayerId(array $playerIds): array
+    {
+        if ($playerIds === []) {
+            return [];
+        }
+
+        $sn = (string) config('third_game.sn');
+        $placeholders = implode(',', array_fill(0, count($playerIds), '?'));
+        $expression = "CONCAT('p', SUBSTRING(MD5(CONCAT(?, '_', `member_id`)), 1, 10))";
+        $rows = DB::table('users')
+            ->select(['id', 'member_id', 'username', 'first_name'])
+            ->selectRaw($expression . ' AS third_game_player_id', [$sn])
+            ->whereRaw($expression . " IN ({$placeholders})", array_merge([$sn], $playerIds))
+            ->get();
+
+        $result = [];
+        foreach ($rows as $row) {
+            $result[(string) $row->third_game_player_id] = $row;
+        }
+
+        return $result;
+    }
+
+    /**
+     * @return array<string, ThirdGameOrderModel>
+     */
+    private function existingSnapshots(array $orders): array
+    {
+        $orderKeys = [];
+        foreach ($orders as $order) {
+            $gameOrderId = trim((string) ($order['game_order_id'] ?? ''));
+            if ($gameOrderId === '') {
+                continue;
+            }
+            $orderKeys[] = $this->identity(
+                trim((string) ($order['platform'] ?? '')),
+                trim((string) ($order['player_id'] ?? '')),
+                $gameOrderId
+            );
+        }
+        $orderKeys = array_values(array_unique($orderKeys));
+        if ($orderKeys === []) {
+            return [];
+        }
+
+        $rows = ThirdGameOrderModel::query()
+            ->whereIn('order_key', $orderKeys)
+            ->get(['order_key', 'user_id', 'member_id', 'username', 'first_name', 'created_at']);
+        $result = [];
+        foreach ($rows as $row) {
+            $result[(string) $row->order_key] = $row;
+        }
+
+        return $result;
+    }
+
+    private function format(ThirdGameOrderModel $order): array
+    {
+        $userId = $order->current_user_id ?? $order->user_id;
+        $memberId = $order->current_member_id ?? $order->member_id;
+        $username = $order->current_username ?? $order->username;
+        $firstName = $order->current_first_name ?? $order->first_name;
+        $gameType = (int) $order->game_type;
+        $status = (int) $order->status;
+
+        return [
+            'id' => (int) $order->id,
+            'user_id' => $userId === null ? null : (int) $userId,
+            'member_id' => $memberId === null ? null : (string) $memberId,
+            'username' => (string) ($username ?? ''),
+            'first_name' => (string) ($firstName ?? ''),
+            'player_id' => (string) $order->player_id,
+            'platform' => (string) $order->platform,
+            'currency' => (string) $order->currency,
+            'game_type' => $gameType,
+            'game_type_text' => self::GAME_TYPE_NAMES[$gameType] ?? '未知',
+            'game_name' => (string) $order->game_name,
+            'round' => (string) $order->round_no,
+            'table' => (string) $order->table_no,
+            'seat' => (string) $order->seat,
+            'bet_amount' => $this->number($order->bet_amount),
+            'valid_amount' => $this->number($order->valid_amount),
+            'settled_amount' => $this->number($order->settled_amount),
+            'bet_content' => (string) ($order->bet_content ?? ''),
+            'status' => $status,
+            'status_text' => self::STATUS_NAMES[$status] ?? '未知',
+            'game_order_id' => (string) $order->game_order_id,
+            'bet_time' => $this->formatTime($order->bet_time),
+            'last_update_time' => $this->formatTime($order->last_update_time),
+        ];
+    }
+
+    private function identity(string $platform, string $playerId, string $gameOrderId): string
+    {
+        return hash('sha256', $platform . "\0" . $playerId . "\0" . $gameOrderId);
+    }
+
+    private function decimal($value): ?string
+    {
+        if (!is_numeric($value)) {
+            return null;
+        }
+        $value = trim((string) $value);
+        if (preg_match('/^-?\d+(?:\.\d+)?$/D', $value)) {
+            return bcadd($value, '0', 10);
+        }
+
+        return number_format((float) $value, 10, '.', '');
+    }
+
+    private function text($value): string
+    {
+        if (is_array($value) || is_object($value)) {
+            return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '';
+        }
+
+        return is_scalar($value) ? (string) $value : '';
+    }
+
+    private function dateTime($value): ?string
+    {
+        if ($value === null || $value === '') {
+            return null;
+        }
+
+        try {
+            return Carbon::parse($value, config('app.timezone', 'Asia/Shanghai'))->format('Y-m-d H:i:s');
+        } catch (\Throwable $e) {
+            return null;
+        }
+    }
+
+    private function formatTime($value): ?string
+    {
+        return $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null;
+    }
+
+    private function number($value)
+    {
+        return $value === null || $value === '' ? null : (float) $value;
+    }
+
+    private function options(array $items): array
+    {
+        $options = [];
+        foreach ($items as $value => $label) {
+            $options[] = ['label' => $label, 'value' => $value];
+        }
+
+        return $options;
+    }
+}

+ 36 - 0
database/migrations/2026_08_21_120000_create_third_game_orders.sql

@@ -0,0 +1,36 @@
+-- Safe standalone deployment script. Execute this file only once on the target database.
+CREATE TABLE IF NOT EXISTS `bot_third_game_orders` (
+  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+  `order_key` char(64) NOT NULL,
+  `user_id` bigint unsigned NULL DEFAULT NULL,
+  `member_id` varchar(64) NULL DEFAULT NULL,
+  `username` varchar(128) NOT NULL DEFAULT '',
+  `first_name` varchar(128) NOT NULL DEFAULT '',
+  `player_id` varchar(32) NOT NULL,
+  `platform` varchar(32) NOT NULL DEFAULT '',
+  `currency` varchar(16) NOT NULL DEFAULT '',
+  `game_type` tinyint unsigned NOT NULL DEFAULT 0,
+  `game_name` varchar(255) NOT NULL DEFAULT '',
+  `round_no` varchar(128) NOT NULL DEFAULT '',
+  `table_no` varchar(128) NOT NULL DEFAULT '',
+  `seat` varchar(128) NOT NULL DEFAULT '',
+  `bet_amount` decimal(30, 10) NULL DEFAULT NULL,
+  `valid_amount` decimal(30, 10) NULL DEFAULT NULL,
+  `settled_amount` decimal(30, 10) NULL DEFAULT NULL,
+  `bet_content` longtext NULL,
+  `status` tinyint unsigned NOT NULL DEFAULT 0,
+  `game_order_id` varchar(128) NOT NULL,
+  `bet_time` datetime NULL DEFAULT NULL,
+  `last_update_time` datetime NULL DEFAULT NULL,
+  `created_at` timestamp NULL DEFAULT NULL,
+  `updated_at` timestamp NULL DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uniq_third_game_order_key` (`order_key`),
+  KEY `idx_third_game_order_user_time` (`user_id`, `last_update_time`),
+  KEY `idx_third_game_order_member_time` (`member_id`, `last_update_time`),
+  KEY `idx_third_game_order_player_time` (`player_id`, `last_update_time`),
+  KEY `idx_third_game_order_status_time` (`status`, `last_update_time`),
+  KEY `idx_third_game_order_type_time` (`game_type`, `last_update_time`),
+  KEY `idx_third_game_order_order_id` (`game_order_id`),
+  KEY `idx_third_game_order_update_time` (`last_update_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

+ 54 - 0
database/migrations/2026_08_21_120000_create_third_game_orders_table.php

@@ -0,0 +1,54 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration {
+    public function up()
+    {
+        if (Schema::hasTable('third_game_orders')) {
+            return;
+        }
+
+        Schema::create('third_game_orders', function (Blueprint $table) {
+            $table->id();
+            $table->char('order_key', 64);
+            $table->unsignedBigInteger('user_id')->nullable();
+            $table->string('member_id', 64)->nullable();
+            $table->string('username', 128)->default('');
+            $table->string('first_name', 128)->default('');
+            $table->string('player_id', 32);
+            $table->string('platform', 32)->default('');
+            $table->string('currency', 16)->default('');
+            $table->unsignedTinyInteger('game_type')->default(0);
+            $table->string('game_name')->default('');
+            $table->string('round_no', 128)->default('');
+            $table->string('table_no', 128)->default('');
+            $table->string('seat', 128)->default('');
+            $table->decimal('bet_amount', 30, 10)->nullable();
+            $table->decimal('valid_amount', 30, 10)->nullable();
+            $table->decimal('settled_amount', 30, 10)->nullable();
+            $table->longText('bet_content')->nullable();
+            $table->unsignedTinyInteger('status')->default(0);
+            $table->string('game_order_id', 128);
+            $table->dateTime('bet_time')->nullable();
+            $table->dateTime('last_update_time')->nullable();
+            $table->timestamps();
+
+            $table->unique('order_key', 'uniq_third_game_order_key');
+            $table->index(['user_id', 'last_update_time'], 'idx_third_game_order_user_time');
+            $table->index(['member_id', 'last_update_time'], 'idx_third_game_order_member_time');
+            $table->index(['player_id', 'last_update_time'], 'idx_third_game_order_player_time');
+            $table->index(['status', 'last_update_time'], 'idx_third_game_order_status_time');
+            $table->index(['game_type', 'last_update_time'], 'idx_third_game_order_type_time');
+            $table->index('game_order_id', 'idx_third_game_order_order_id');
+            $table->index('last_update_time', 'idx_third_game_order_update_time');
+        });
+    }
+
+    public function down()
+    {
+        Schema::dropIfExists('third_game_orders');
+    }
+};

+ 64 - 20
docs/admin-third-game-order-api.md

@@ -1,39 +1,48 @@
 # 后台三方游戏订单
 
-## GET `/admin/thirdGame/orders`
-
-请求头:
+请求头统一使用:
 
 ```http
 Authorization: Bearer <token>
 ```
 
-参数:
+## 1. 订单列表
+
+### GET `/admin/thirdGame/orders`
+
+列表读取系统数据库,不会直接请求三方。
 
 | 参数 | 必填 | 说明 |
 | --- | --- | --- |
-| `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` |
+| `page` / `limit` | 否 | 默认 `1/20`,`limit` 最大 `200` |
+| `user_id` | 否 | 系统数据库用户主键,精确匹配 |
+| `member_id` | 否 | 系统会员 ID,精确匹配 |
+| `username` | 否 | 系统用户名,模糊匹配 |
+| `first_name` | 否 | 系统昵称,模糊匹配 |
+| `player_id` | 否 | 三方玩家 ID,精确匹配 |
+| `platform` | 否 | 三方游戏平台,如 `ag`、`pg` |
+| `game_order_id` | 否 | 三方订单号,精确匹配 |
+| `game_type` | 否 | `1`视讯、`2`老虎机、`3`彩票、`4`体育、`5`电竞、`6`捕猎、`7`棋牌 |
+| `status` | 否 | `0`未完成、`1`已完成、`2`已取消、`3`已撤单 |
+| `start_time` / `end_time` | 否 | 订单更新时间范围,必须同时传,格式 `Y-m-d H:i:s` |
 
-开始时间不能大于结束时间,单次范围不能超过 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,
+    "page": 1,
+    "limit": 20,
     "list": [
       {
-        "player_id": "abc123456",
+        "id": 1,
+        "user_id": 123,
+        "member_id": "10001",
+        "username": "test01",
+        "first_name": "测试用户",
+        "player_id": "p1234567890",
         "platform": "ag",
         "currency": "CNY",
         "game_type": 1,
@@ -52,11 +61,46 @@ Authorization: Bearer <token>
         "bet_time": "2026-08-21 01:00:00",
         "last_update_time": "2026-08-21 01:05:00"
       }
-    ]
+    ],
+    "options": {
+      "game_types": [],
+      "statuses": []
+    }
+  }
+}
+```
+
+## 2. 同步三方订单
+
+### POST `/admin/thirdGame/orders/sync`
+
+```json
+{
+  "start_time": "2026-08-21 00:00:00",
+  "end_time": "2026-08-21 06:00:00",
+  "page": 1,
+  "limit": 2000
+}
+```
+
+单次时间范围最多 6 小时,只能同步最近 15 天。返回:
+
+```json
+{
+  "code": 0,
+  "data": {
+    "received": 100,
+    "synced": 100,
+    "matched_users": 20,
+    "unmatched_orders": 0,
+    "total": 100,
+    "page_no": 1,
+    "page_size": 2000,
+    "has_more": false
   }
 }
 ```
 
-`code = 0` 表示成功;失败时读取 `msg`。
+同步成功后重新请求订单列表。`has_more = true` 时等待至少 10 秒,再使用相同时间范围请求下一页
 
-三方限制:普通查询至少间隔 1 分钟且每小时最多 5 次;翻页请求至少间隔 10 秒。前端不要自动高频刷新。
+普通同步至少间隔 1 分钟且每小时最多 5 次;翻页同步至少间隔 10 秒。失败时 `code != 0`,原因读取 `msg`

+ 1 - 0
routes/admin.php

@@ -221,6 +221,7 @@ Route::middleware(['admin.jwt'])->group(function () {
 
         Route::prefix('/thirdGame')->group(function () {
             Route::get('/orders', [ThirdGameOrder::class, 'index']);
+            Route::post('/orders/sync', [ThirdGameOrder::class, 'sync']);
         });
 
         Route::prefix('/wallet')->group(function () {