From b2362eefe8f2b6262e36e723897c0c7f43790561 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 9 Jun 2026 14:27:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E5=B0=8F=E6=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/gamemanagement/sync_games.go | 90 +++++++++++++++++++ .../modules/gamemanagement/sync_games_test.go | 36 ++++++++ 2 files changed, 126 insertions(+) diff --git a/server/admin/internal/modules/gamemanagement/sync_games.go b/server/admin/internal/modules/gamemanagement/sync_games.go index 14f80450..ab3f695b 100644 --- a/server/admin/internal/modules/gamemanagement/sync_games.go +++ b/server/admin/internal/modules/gamemanagement/sync_games.go @@ -28,6 +28,7 @@ const ( adapterZeeOneV1 = "zeeone_v1" adapterVivaGamesV1 = "vivagames_v1" adapterReyouV1 = "reyou_v1" + adapterYomiV4 = "yomi_v4" defaultGameStatus = "disabled" defaultGameCategory = "casino" defaultLaunchMode = "full_screen" @@ -137,6 +138,8 @@ func fetchProviderGameSyncPlan(ctx context.Context, platform *gamev1.GamePlatfor return fetchVivaGamesGameSyncPlan(platform, req) case adapterReyouV1: return fetchReyouGameSyncPlan(platform, req) + case adapterYomiV4: + return fetchYomiGameSyncPlan(platform, req) default: // 不在配置文件写死游戏:每家厂商新增“列表 API”时只加一个 adapter fetcher,后台入口保持不变。 return providerGameSyncPlan{}, fmt.Errorf("当前适配器暂未接入厂商游戏列表 API: %s", platform.GetAdapterType()) @@ -199,6 +202,14 @@ type reyouSyncConfig struct { GameCovers map[string]string `json:"game_covers"` } +type yomiSyncConfig struct { + // Yomi V4 文档只提供启动鉴权地址,没有游戏列表 API;后台通过 game_names 维护截图里的 gameUid 清单。 + GameNames map[string]string `json:"game_names"` + GameURLs map[string]string `json:"game_urls"` + GameIcons map[string]string `json:"game_icons"` + GameCovers map[string]string `json:"game_covers"` +} + func fetchZeeOneGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) { config, err := decodeZeeOneSyncConfig(platform.GetAdapterConfigJson()) if err != nil { @@ -355,6 +366,21 @@ func fetchReyouGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) }, nil } +func fetchYomiGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) { + config, err := decodeYomiSyncConfig(platform.GetAdapterConfigJson()) + if err != nil { + return providerGameSyncPlan{}, err + } + games, gameURLs := yomiCatalogItems(platform.GetPlatformCode(), config, req) + if len(games) == 0 { + return providerGameSyncPlan{}, fmt.Errorf("小游游戏列表为空:请在 adapterConfigJson.game_names 配置游戏 ID 到名称的映射") + } + return providerGameSyncPlan{ + Games: games, + GameURLs: gameURLs, + }, nil +} + func decodeReyouSyncConfig(raw string) (reyouSyncConfig, error) { if strings.TrimSpace(raw) == "" { return reyouSyncConfig{}, nil @@ -366,6 +392,17 @@ func decodeReyouSyncConfig(raw string) (reyouSyncConfig, error) { return config, nil } +func decodeYomiSyncConfig(raw string) (yomiSyncConfig, error) { + if strings.TrimSpace(raw) == "" { + return yomiSyncConfig{}, nil + } + var config yomiSyncConfig + if err := json.Unmarshal([]byte(raw), &config); err != nil { + return yomiSyncConfig{}, fmt.Errorf("小游适配器配置 JSON 不合法") + } + return config, nil +} + func reyouCatalogItems(platformCode string, config reyouSyncConfig, req syncGamesRequest) ([]catalogRequest, map[string]string) { keys := make([]string, 0, len(config.GameURLs)) for providerGameID, launchURL := range config.GameURLs { @@ -401,6 +438,59 @@ func reyouCatalogItems(platformCode string, config reyouSyncConfig, req syncGame return items, gameURLs } +func yomiCatalogItems(platformCode string, config yomiSyncConfig, req syncGamesRequest) ([]catalogRequest, map[string]string) { + keys := make([]string, 0, len(config.GameNames)+len(config.GameURLs)) + seen := make(map[string]struct{}, len(config.GameNames)+len(config.GameURLs)) + // 小游没有列表 API,game_names 是本地候选游戏的事实来源;game_urls 只用于后台预览,不参与启动拼参。 + for providerGameID := range config.GameNames { + key := strings.TrimSpace(providerGameID) + if key == "" { + continue + } + seen[key] = struct{}{} + keys = append(keys, key) + } + for providerGameID, launchURL := range config.GameURLs { + key := strings.TrimSpace(providerGameID) + if key == "" || strings.TrimSpace(launchURL) == "" { + continue + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + keys = append(keys, key) + } + sort.Strings(keys) + items := make([]catalogRequest, 0, len(keys)) + gameURLs := make(map[string]string, len(keys)) + for index, providerGameID := range keys { + launchURL := strings.TrimSpace(config.GameURLs[providerGameID]) + if launchURL != "" { + gameURLs[providerGameID] = launchURL + } + name := firstNonEmpty(config.GameNames[providerGameID], zeeoneGameNameFromURL(launchURL), providerGameID) + iconURL := strings.TrimSpace(config.GameIcons[providerGameID]) + coverURL := firstNonEmpty(config.GameCovers[providerGameID], iconURL) + items = append(items, catalogRequest{ + GameID: stableGameID(platformCode, providerGameID), + PlatformCode: strings.TrimSpace(platformCode), + ProviderGameID: providerGameID, + GameName: name, + Category: defaulted(req.Category, defaultGameCategory), + IconURL: iconURL, + CoverURL: coverURL, + LaunchMode: defaulted(req.LaunchMode, defaultLaunchMode), + Orientation: defaultOrientation, + MinCoin: req.MinCoin, + Status: defaulted(req.Status, defaultGameStatus), + SortOrder: int32((index + 1) * 10), + Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterYomiV4}, req.Tags...)), + }) + } + return items, gameURLs +} + func titleGameName(value string) string { tokens := splitGameNameTokens(value) if len(tokens) == 0 { diff --git a/server/admin/internal/modules/gamemanagement/sync_games_test.go b/server/admin/internal/modules/gamemanagement/sync_games_test.go index b61bbd01..5b65645b 100644 --- a/server/admin/internal/modules/gamemanagement/sync_games_test.go +++ b/server/admin/internal/modules/gamemanagement/sync_games_test.go @@ -167,6 +167,42 @@ func TestFetchReyouGameSyncPlanReadsConfiguredGameURLs(t *testing.T) { } } +func TestFetchYomiGameSyncPlanReadsConfiguredGameNames(t *testing.T) { + plan, err := fetchProviderGameSyncPlan(t.Context(), &gamev1.GamePlatform{ + PlatformCode: "yomi", + AdapterType: adapterYomiV4, + AdapterConfigJson: `{ + "game_names": { + "481": "MahjongWays", + "480": "ChickenPlinko" + }, + "game_urls": { + "481": "https://api-testv2.yomigame.games/gate/gateway/auth" + } + }`, + }, syncGamesRequest{Status: "active"}) + if err != nil { + t.Fatalf("fetchProviderGameSyncPlan failed: %v", err) + } + if len(plan.Games) != 2 { + t.Fatalf("games len = %d, want 2", len(plan.Games)) + } + first := plan.Games[0] + if first.GameID != "yomi_480" || first.ProviderGameID != "480" || first.GameName != "ChickenPlinko" { + t.Fatalf("first yomi game mismatch: %+v", first) + } + second := plan.Games[1] + if second.GameID != "yomi_481" || second.ProviderGameID != "481" || second.GameName != "MahjongWays" { + t.Fatalf("second yomi game mismatch: %+v", second) + } + if plan.GameURLs["481"] != "https://api-testv2.yomigame.games/gate/gateway/auth" { + t.Fatalf("game url mismatch: %+v", plan.GameURLs) + } + if first.Status != "active" || first.LaunchMode != "full_screen" || first.Orientation != "portrait" { + t.Fatalf("default fields mismatch: %+v", first) + } +} + func TestSelectProviderGamesKeepsOnlySelectedIDs(t *testing.T) { games := []catalogRequest{ {ProviderGameID: "1001", GameName: "one"},