接入小游

This commit is contained in:
zhx 2026-06-09 14:27:06 +08:00
parent 0bb719d1b8
commit b2362eefe8
2 changed files with 126 additions and 0 deletions

View File

@ -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))
// 小游没有列表 APIgame_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 {

View File

@ -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"},