소스 검색

第三方用户ID

doge 1 일 전
부모
커밋
b525db4b5c

+ 16 - 6
app/Services/ThirdGameBalanceService.php

@@ -144,15 +144,25 @@ class ThirdGameBalanceService
     }
 
     /**
-     * 新规则:直接 u + users.id,不补零、不做哈希。
+     * 使用 users.id;数字部分不足4位左补零,保持已有合规账号不变。
      */
     public function playerIdFromUserId(int $userId): string
     {
-        if ($userId <= 0) {
-            throw new \InvalidArgumentException('users.id 必须是正整数');
+        $prefix = $this->playerPrefix();
+        if ($userId <= 0 || strlen((string) $userId) > 11 - strlen($prefix)) {
+            throw new \InvalidArgumentException('users.id 超出三方玩家账号允许的长度');
         }
 
-        return 'u' . $userId;
+        return $prefix . str_pad((string) $userId, 4, '0', STR_PAD_LEFT);
+    }
+
+    public function playerPrefix(): string
+    {
+        $prefix = trim((string) config('third_game.player_prefix', 'u'));
+        if (!in_array($prefix, ['u', 'tu'], true)) {
+            throw new \InvalidArgumentException('GAME_PLAYER_PREFIX 只能配置为u或tu');
+        }
+        return $prefix;
     }
 
     public function agentPlayerId(int $agentId): string
@@ -727,12 +737,12 @@ class ThirdGameBalanceService
 
     private function cacheKey(string $memberId): string
     {
-        return 'third_game_total_balance:' . $memberId;
+        return 'third_game_total_balance:' . ($this->playerPrefix() === 'u' ? '' : 'tu:') . $memberId;
     }
 
     private function detailCacheKey(string $memberId): string
     {
-        return 'third_game_balance_detail:' . $memberId;
+        return 'third_game_balance_detail:' . ($this->playerPrefix() === 'u' ? '' : 'tu:') . $memberId;
     }
 
     private function historyRatePrefix(): string

+ 17 - 9
app/Services/ThirdGameOrderService.php

@@ -55,6 +55,12 @@ class ThirdGameOrderService
         }
 
         $orders = is_array($result['list'] ?? null) ? $result['list'] : [];
+        $received = count($orders);
+        $testNamespace = $this->provider->playerPrefix() === 'tu';
+        $orders = array_values(array_filter($orders, static function (array $order) use ($testNamespace): bool {
+            $isTestPlayer = str_starts_with(trim((string) ($order['player_id'] ?? '')), 'tu');
+            return $testNamespace === $isTestPlayer;
+        }));
         $users = $this->usersByPlayerId(array_values(array_unique(array_filter(array_map(
             static fn(array $order): string => trim((string) ($order['player_id'] ?? '')),
             $orders
@@ -122,11 +128,11 @@ class ThirdGameOrderService
 
         $pageNo = (int) ($result['page_no'] ?? $page);
         $pageSize = (int) ($result['page_size'] ?? $limit);
-        $total = (int) ($result['total'] ?? count($orders));
+        $total = (int) ($result['total'] ?? $received);
 
         return [
             'ok' => true,
-            'received' => count($orders),
+            'received' => $received,
             'synced' => count($rows),
             'matched_users' => count($matchedUserIds),
             'unmatched_orders' => count(array_filter($rows, static fn(array $row): bool => $row['user_id'] === null)),
@@ -166,6 +172,8 @@ class ThirdGameOrderService
         $limit = min(200, max(1, (int) ($params['limit'] ?? 20)));
         $query = ThirdGameOrderModel::query()
             ->leftJoin('users', 'users.id', '=', 'third_game_orders.user_id');
+        if ($this->provider->playerPrefix() === 'tu') $query->where('third_game_orders.player_id', 'like', 'tu%');
+        else $query->where('third_game_orders.player_id', 'not like', 'tu%');
 
         if (!empty($params['user_id'])) {
             $userId = (int) $params['user_id'];
@@ -262,17 +270,18 @@ class ThirdGameOrderService
      */
     private function usersByPlayerId(array $playerIds): array
     {
-        $requested = [];
+        $requested = []; $prefix = $this->provider->playerPrefix();
+        $pattern = '/^' . preg_quote($prefix, '/') . '(\d{1,' . (11 - strlen($prefix)) . '})$/D';
         foreach ($playerIds as $playerId) {
             $playerId = trim((string) $playerId);
-            if (!preg_match('/^u([1-9]\d*)$/D', $playerId, $matches)) {
+            if (!preg_match($pattern, $playerId, $matches)) {
                 continue;
             }
-            $userId = filter_var($matches[1], FILTER_VALIDATE_INT, [
+            $userId = filter_var(ltrim($matches[1], '0'), FILTER_VALIDATE_INT, [
                 'options' => ['min_range' => 1, 'max_range' => PHP_INT_MAX],
             ]);
-            if ($userId !== false) {
-                $requested[(int) $userId] = $playerId;
+            if ($userId !== false && ($playerId === $prefix . $userId || $playerId === $this->provider->playerIdFromUserId($userId))) {
+                $requested[(int) $userId][$playerId] = true;
             }
         }
         if ($requested === []) {
@@ -286,8 +295,7 @@ class ThirdGameOrderService
 
         $result = [];
         foreach ($rows as $row) {
-            $playerId = $requested[(int) $row->id] ?? null;
-            if ($playerId !== null) {
+            foreach (array_keys($requested[(int) $row->id] ?? []) as $playerId) {
                 $result[$playerId] = $row;
             }
         }

+ 2 - 0
config/third_game.php

@@ -4,6 +4,8 @@ return [
     'api_url' => rtrim((string) env('GAME_API_URL', 'https://ap.api-bet.net'), '/'),
     'sn' => trim((string) env('GAME_SN', '')),
     'key' => trim((string) env('GAME_KEY', '')),
+    // 会员账号前缀:正式服u,测试服tu;与会员API服务保持一致。
+    'player_prefix' => trim((string) env('GAME_PLAYER_PREFIX', 'u')),
     'proxy' => trim((string) env('GAME_PROXY', '')),
     'ca_bundle' => trim((string) env('GAME_CA_BUNDLE', '')),
     'currency' => trim((string) env('GAME_CURRENCY', 'CNY')),

+ 1 - 1
docs/admin-third-game-order-api.md

@@ -42,7 +42,7 @@ Authorization: Bearer <token>
         "member_id": "10001",
         "username": "test01",
         "first_name": "测试用户",
-        "player_id": "u123",
+        "player_id": "u0123",
         "platform": "ag",
         "currency": "CNY",
         "game_type": 1,

+ 1 - 1
docs/代理/所有会员.md

@@ -49,4 +49,4 @@ GET /agent/members?page=1&limit=20&keyword=9238&agent_id=12
 }
 ```
 
-`id` 是 `users.id`;`member_id` 是业务会员/TG ID。第三方游戏平台玩家号按现行规则为 `u + users.id`,例如该示例为 `u92385294`。
+`id` 是 `users.id`;`member_id` 是业务会员/TG ID。第三方游戏平台玩家号由接口返回;正式服前缀为 `u`,测试服前缀为 `tu`,数字部分不足4位时左补0。例如会员106分别为 `u0106`、`tu0106`。

+ 27 - 0
docs/后端/三方游戏账号前缀-环境配置.md

@@ -0,0 +1,27 @@
+# 三方游戏会员账号环境配置
+
+同一环境的 `bot-28` 和 `melbet_sport-api` 使用相同的 `GAME_PLAYER_PREFIX`。
+
+| 环境 | 配置 | 会员106的三方账号 |
+|---|---|---|
+| 正式服 | `GAME_PLAYER_PREFIX=u` | `u0106` |
+| 测试服 | `GAME_PLAYER_PREFIX=tu` | `tu0106` |
+
+`melbet_sport-api` 使用分组格式 `.env` 时,对应写法为:
+
+```ini
+[GAME]
+PLAYER_PREFIX = tu
+```
+
+`bot-28` 对应写法为:
+
+```dotenv
+GAME_PLAYER_PREFIX=tu
+```
+
+正式服默认 `u`;测试服需明确配置 `tu`。数字部分不足4位时左补0,总账号长度最多11位,正式服最多10位数字、测试服最多9位数字。已合规的正式服账号格式保持原样。
+
+余额查询、游戏登录、转入、回收均使用所配置的会员账号前缀。管理端会员余额缓存也按前缀区分,注单同步与查询区分 `u` 和 `tu` 账号。现有账号和余额不做自动改名或迁移。
+
+该配置区分会员账号;三方地址、商户SN及商户额度由各环境原有的三方商户配置决定。本次未修改服务器环境文件。

+ 111 - 0
tests/Integration/ThirdGamePlayerIdentityTest.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace Tests\Integration;
+
+use App\Services\ThirdGameBalanceService;
+use App\Services\ThirdGameOrderService;
+use Illuminate\Config\Repository;
+use Illuminate\Database\Capsule\Manager;
+use Illuminate\Foundation\Application;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Facade;
+use PHPUnit\Framework\TestCase;
+
+/** Provider order identities are resolved against an isolated SQLite database only. */
+class ThirdGamePlayerIdentityTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        Facade::clearResolvedInstances();
+        $app = new Application(dirname(__DIR__, 2));
+        $app->instance('config', new Repository(['app' => ['timezone' => 'Asia/Shanghai'], 'third_game' => ['player_prefix' => 'u']]));
+        Facade::setFacadeApplication($app);
+        $db = new Manager($app);
+        $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
+        $db->setAsGlobal(); $db->bootEloquent(); $app->instance('db', $db->getDatabaseManager());
+        DB::statement('CREATE TABLE bot_users (id INTEGER PRIMARY KEY,member_id TEXT,username TEXT,first_name TEXT)');
+        DB::statement('CREATE TABLE bot_third_game_orders (id INTEGER PRIMARY KEY AUTOINCREMENT,order_key TEXT UNIQUE,
+            user_id INTEGER,member_id TEXT,username TEXT,first_name TEXT,player_id TEXT,platform TEXT,currency TEXT,
+            game_type INTEGER,game_name TEXT,round_no TEXT,table_no TEXT,seat TEXT,bet_amount TEXT,valid_amount TEXT,
+            settled_amount TEXT,bet_content TEXT,status INTEGER,game_order_id TEXT,bet_time TEXT,last_update_time TEXT,
+            created_at TEXT,updated_at TEXT)');
+        DB::table('users')->insert([
+            ['id' => 1, 'member_id' => 'm1', 'username' => 'one', 'first_name' => ''],
+            ['id' => 106, 'member_id' => 'm106', 'username' => 'member106', 'first_name' => ''],
+            ['id' => 1000, 'member_id' => 'm1000', 'username' => 'member1000', 'first_name' => ''],
+        ]);
+    }
+
+    protected function tearDown(): void
+    {
+        DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown();
+    }
+
+    private function lookup(array $playerIds): array
+    {
+        $service = new ThirdGameOrderService(new ThirdGameBalanceService());
+        return (new \ReflectionMethod($service, 'usersByPlayerId'))->invoke($service, $playerIds);
+    }
+
+    public function test_new_padded_and_legacy_identifiers_in_one_batch_resolve_to_the_correct_member(): void
+    {
+        $rows = $this->lookup(['u106', 'u0106', 'u0001', 'u1', 'u1000']);
+        $this->assertCount(5, $rows);
+        $this->assertSame(106, $rows['u0106']->id);
+        $this->assertSame(106, $rows['u106']->id);
+        $this->assertSame('m106', $rows['u0106']->member_id);
+        $this->assertSame(1, $rows['u0001']->id);
+        $this->assertSame(1, $rows['u1']->id);
+        $this->assertSame(1000, $rows['u1000']->id);
+    }
+
+    public function test_noncanonical_zero_padding_and_unknown_accounts_do_not_get_bound_to_another_member(): void
+    {
+        $this->assertSame([], $this->lookup(['u000106', 'u01', 'u00001', 'u01000', 'u0000',
+            'u0', 'u10000000000', 'u0999', 'a0106', 'U0106', 'p1234567890']));
+        $this->assertSame([], $this->lookup([]));
+    }
+    public function test_test_namespace_does_not_associate_production_players_with_test_members(): void
+    {
+        config(['third_game.player_prefix' => 'tu']);
+        $rows = $this->lookup(['u106', 'u0106', 'u1000', 'tu106', 'tu0106', 'tu1000']);
+        $this->assertSame(['tu106', 'tu0106', 'tu1000'], array_keys($rows));
+        $this->assertSame(106, $rows['tu0106']->id);
+        $this->assertSame([], $this->lookup(['u0106', 'tu00106', 'tu0000', 'tu1000000000']));
+    }
+
+    public function test_sync_and_listing_keep_namespaces_separate_even_when_an_old_foreign_snapshot_exists(): void
+    {
+        config(['third_game.player_prefix' => 'tu']);
+        $foreign = ['player_id' => 'u0106', 'platform' => 'pg', 'game_order_id' => 'production-order',
+            'currency' => 'CNY', 'game_type' => 2, 'status' => 1, 'bet_amount' => '999',
+            'bet_time' => '2026-10-27 10:00:00', 'last_update_time' => '2026-10-27 10:01:00'];
+        $foreignKey = hash('sha256', "pg\0u0106\0production-order");
+        DB::table('third_game_orders')->insert($foreign + ['order_key' => $foreignKey, 'user_id' => 106,
+            'member_id' => 'm106', 'username' => 'member106', 'first_name' => '', 'created_at' => '2026-10-27 10:00:00']);
+        $before = (array) DB::table('third_game_orders')->where('order_key', $foreignKey)->first();
+        $local = array_replace($foreign, ['player_id' => 'tu0106', 'game_order_id' => 'test-order', 'bet_amount' => '10']);
+        $service = new ThirdGameOrderService(new ThirdGameBalanceService());
+        $persist = new \ReflectionMethod($service, 'persistProviderResult');
+        $result = $persist->invoke($service, ['ok' => true, 'list' => [$foreign, $local],
+            'page_no' => 1, 'page_size' => 2, 'total' => 4]);
+        $this->assertSame(2, $result['received']);
+        $this->assertSame(1, $result['synced']);
+        $this->assertSame(1, $result['matched_users']);
+        $this->assertSame(4, $result['total']);
+        $this->assertTrue($result['has_more']);
+        $this->assertSame($before, (array) DB::table('third_game_orders')->where('order_key', $foreignKey)->first());
+        $this->assertSame(106, DB::table('third_game_orders')->where('player_id', 'tu0106')->value('user_id'));
+        $list = $service->paginate([]);
+        $this->assertSame(1, $list['total']);
+        $this->assertSame('tu0106', $list['list'][0]['player_id']);
+        config(['third_game.player_prefix' => 'u']);
+        $production = $service->paginate([]);
+        $this->assertSame(1, $production['total']);
+        $this->assertSame('u0106', $production['list'][0]['player_id']);
+        $rejected = $persist->invoke($service, ['ok' => true, 'list' => [$local], 'page_no' => 1, 'page_size' => 1, 'total' => 1]);
+        $this->assertSame(0, $rejected['synced']);
+        $this->assertSame(2, DB::table('third_game_orders')->count());
+    }
+
+}

+ 59 - 7
tests/Unit/ThirdGamePlayerIdTest.php

@@ -4,24 +4,76 @@ namespace Tests\Unit;
 
 use App\Services\ThirdGameBalanceService;
 use InvalidArgumentException;
+use Illuminate\Config\Repository;
+use Illuminate\Foundation\Application;
+use Illuminate\Support\Facades\Facade;
 use PHPUnit\Framework\TestCase;
 
 class ThirdGamePlayerIdTest extends TestCase
 {
-    public function test_player_id_uses_users_primary_key_without_padding(): void
+    private Application $app;
+
+    protected function setUp(): void
     {
-        $service = new ThirdGameBalanceService();
+        Facade::clearResolvedInstances();
+        $this->app = new Application(dirname(__DIR__, 2));
+        $this->app->instance('config', new Repository(['third_game' => ['player_prefix' => 'u']]));
+        Facade::setFacadeApplication($this->app);
+    }
+
+    protected function tearDown(): void
+    {
+        Facade::clearResolvedInstances(); Facade::setFacadeApplication(null); parent::tearDown();
+    }
 
-        $this->assertSame('u1', $service->playerIdFromUserId(1));
-        $this->assertSame('u483', $service->playerIdFromUserId(483));
-        $this->assertSame('u751434', $service->playerIdFromUserId(751434));
+    /** @dataProvider playerIds */
+    public function test_member_player_ids_meet_provider_length_without_changing_valid_accounts(int $id, string $expected): void
+    {
+        $actual = (new ThirdGameBalanceService())->playerIdFromUserId($id);
+        $this->assertSame($expected, $actual);
+        $this->assertMatchesRegularExpression('/^u[0-9]{4,10}$/D', $actual);
+    }
+
+    public static function playerIds(): array
+    {
+        return [[1, 'u0001'], [106, 'u0106'], [483, 'u0483'], [999, 'u0999'],
+            [1000, 'u1000'], [751434, 'u751434'], [9999999999, 'u9999999999']];
     }
 
-    public function test_player_id_rejects_non_positive_user_id(): void
+    /** @dataProvider invalidIds */
+    public function test_invalid_user_ids_fail_locally_instead_of_producing_an_invalid_provider_account(int $id): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+        (new ThirdGameBalanceService())->playerIdFromUserId($id);
+    }
+
+    public static function invalidIds(): array { return [[0], [-1], [10000000000]]; }
+    public function test_test_namespace_never_reuses_production_member_identity_or_balance_cache(): void
     {
         $service = new ThirdGameBalanceService();
+        $cache = new \ReflectionMethod($service, 'cacheKey');
+        $detail = new \ReflectionMethod($service, 'detailCacheKey');
+        $this->assertSame('u0106', $service->playerIdFromUserId(106));
+        $this->assertSame('third_game_total_balance:m106', $cache->invoke($service, 'm106'));
+        $this->assertSame('third_game_balance_detail:m106', $detail->invoke($service, 'm106'));
+        $this->app['config']->set('third_game.player_prefix', 'tu');
+        $this->assertSame('tu0106', $service->playerIdFromUserId(106));
+        $this->assertSame('tu0001', $service->playerIdFromUserId(1));
+        $this->assertSame('tu999999999', $service->playerIdFromUserId(999999999));
+        $this->assertSame('third_game_total_balance:tu:m106', $cache->invoke($service, 'm106'));
+        $this->assertSame('third_game_balance_detail:tu:m106', $detail->invoke($service, 'm106'));
+        $this->expectException(InvalidArgumentException::class);
+        $service->playerIdFromUserId(1000000000);
+    }
 
+    /** @dataProvider invalidPrefixes */
+    public function test_invalid_prefixes_do_not_fall_back_to_production(string $prefix): void
+    {
+        $this->app['config']->set('third_game.player_prefix', $prefix);
         $this->expectException(InvalidArgumentException::class);
-        $service->playerIdFromUserId(0);
+        (new ThirdGameBalanceService())->playerIdFromUserId(106);
     }
+
+    public static function invalidPrefixes(): array { return [[''], ['U'], ['test'], ['a'], ['t_u']]; }
+
 }