This commit is contained in:
zhx 2026-05-22 13:34:54 +08:00
parent 4af7844d1b
commit a5a53b390f
3 changed files with 152 additions and 6 deletions

View File

@ -564,6 +564,9 @@ func buildLaunchURL(game gamedomain.LaunchableGame, session gamedomain.LaunchSes
}
}
if base == "" {
if strings.EqualFold(game.AdapterType, gamedomain.AdapterYomiV4) {
return "", xerr.New(xerr.InvalidArgument, "yomi auth url is required")
}
if strings.EqualFold(game.AdapterType, gamedomain.AdapterZeeOneV1) {
return "", xerr.New(xerr.InvalidArgument, "zeeone launch url is required")
}
@ -587,11 +590,14 @@ func buildLaunchURL(game gamedomain.LaunchableGame, session gamedomain.LaunchSes
query.Set("platUserId", externalUserID(session, config.UIDMode))
query.Set("lang", lang)
// Yomi gameLevelUid 语义类似房间/场次,没有后台配置时必须显式传 0。
if config.GameLevelUID > 0 {
query.Set("gameLevelUid", strconv.FormatInt(config.GameLevelUID, 10))
if gameLevelUID := config.GameLevelUIDFor(game); gameLevelUID > 0 {
query.Set("gameLevelUid", strconv.FormatInt(gameLevelUID, 10))
} else {
query.Set("gameLevelUid", "0")
}
if config.PlatPayload != "" {
query.Set("platPayload", config.PlatPayload)
}
if session.RoomID != "" {
query.Set("platRoomId", session.RoomID)
}

View File

@ -8,6 +8,7 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/url"
"strconv"
"strings"
"testing"
@ -82,7 +83,7 @@ func TestLaunchGameBuildsYomiAuthURL(t *testing.T) {
PlatformStatus: gamedomain.StatusActive,
APIBaseURL: "https://api-testv2.yomigame.games/gate/gateway/auth",
AdapterType: gamedomain.AdapterYomiV4,
AdapterConfigJSON: `{"default_lang":"en","token_ttl_seconds":86400,"uid_mode":"user_id"}`,
AdapterConfigJSON: `{"default_lang":"en","token_ttl_seconds":86400,"uid_mode":"user_id","game_level_uids":{"33":17},"plat_payload":"{\"env\":\"test\"}"}`,
},
}
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
@ -102,13 +103,57 @@ func TestLaunchGameBuildsYomiAuthURL(t *testing.T) {
if result.ExpiresAtMS != 1700086400000 {
t.Fatalf("yomi launch must use adapter token ttl, got %+v", result)
}
for _, part := range []string{"gameUid=33", "platUserId=42", "lang=en", "gameLevelUid=0", "platRoomId=room_1", "platAuthCode=app_access_token_yomi"} {
if !strings.Contains(result.LaunchURL, part) {
t.Fatalf("yomi launch url missing %s: %s", part, result.LaunchURL)
parsed, err := url.Parse(result.LaunchURL)
if err != nil {
t.Fatalf("parse yomi launch url failed: %v", err)
}
query := parsed.Query()
expected := map[string]string{
"gameUid": "33",
"platUserId": "42",
"lang": "en",
"gameLevelUid": "17",
"platRoomId": "room_1",
"platAuthCode": "app_access_token_yomi",
"platPayload": `{"env":"test"}`,
}
for key, want := range expected {
if got := query.Get(key); got != want {
t.Fatalf("yomi launch query %s mismatch: got %q want %q url=%s", key, got, want, result.LaunchURL)
}
}
}
func TestLaunchGameRequiresYomiAuthURL(t *testing.T) {
repo := &fakeRepository{
launchable: gamedomain.LaunchableGame{
CatalogItem: gamedomain.CatalogItem{
AppCode: "lalu",
GameID: "yomi_fish_001",
PlatformCode: "yomi",
ProviderGameID: "33",
GameName: "Fish",
Status: gamedomain.StatusActive,
},
PlatformStatus: gamedomain.StatusActive,
AdapterType: gamedomain.AdapterYomiV4,
AdapterConfigJSON: `{"default_lang":"en"}`,
},
}
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
_, err := svc.LaunchGame(context.Background(), LaunchCommand{
AppCode: "lalu",
UserID: 42,
GameID: "yomi_fish_001",
AccessToken: "app_access_token_yomi",
})
if err == nil || !strings.Contains(xerr.MessageOf(err), "yomi auth url is required") {
t.Fatalf("LaunchGame must reject missing yomi auth url, got %v", err)
}
}
func TestLaunchGameBuildsLeaderCCURL(t *testing.T) {
repo := &fakeRepository{
launchable: gamedomain.LaunchableGame{
@ -464,6 +509,69 @@ func TestHandleLeaderCCChangeCoinAcceptsAppAccessTokenAndTokenOnlyProbe(t *testi
}
}
func TestHandleYomiGetTokenAndUserInfo(t *testing.T) {
secret := "kEdOjdxgOUCJjG1L6kl3fYEIgMFxQOza"
token := "app_access_token_yomi"
repo := &fakeRepository{
platform: gamedomain.Platform{
PlatformCode: "yomi",
AdapterType: gamedomain.AdapterYomiV4,
CallbackSecretCiphertext: secret,
AdapterConfigJSON: `{"uid_mode":"display_user_id"}`,
},
session: gamedomain.LaunchSession{
AppCode: "lalu",
SessionID: "game_sess_yomi",
UserID: 42,
DisplayUserID: "420001",
RoomID: "room_1",
PlatformCode: "yomi",
GameID: "yomi_fish_001",
ProviderGameID: "33",
LaunchTokenHash: stableHash(token),
Status: gamedomain.SessionActive,
ExpiresAtMS: 1700086400000,
},
}
wallet := &fakeWallet{balanceAfter: 1880}
user := &fakeUser{}
svc := New(Config{}, repo, wallet, user)
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
getTokenBody := encryptYomiTestBody(t, secret, `{"gameUid":33,"lang":"en","platAuthCode":"app_access_token_yomi","platUserId":"420001","platPayload":"","platRoomId":"room_1"}`)
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
Meta: &gamev1.RequestMeta{RequestId: "req-yomi-gettoken", AppCode: "lalu"},
PlatformCode: "yomi",
Operation: "gettoken",
RawBody: getTokenBody,
RemoteAddr: "3.1.174.194:443",
})
if err != nil {
t.Fatalf("HandleCallback gettoken failed: %v", err)
}
if contentType != "application/json" || !strings.Contains(string(raw), `"code":200`) || !strings.Contains(string(raw), `"platToken":"app_access_token_yomi"`) || !strings.Contains(string(raw), `"balance":1880`) {
t.Fatalf("yomi gettoken response mismatch: %s", raw)
}
if user.lastGet.GetUserId() != 42 {
t.Fatalf("yomi gettoken must query token user, got %+v", user.lastGet)
}
userInfoBody := encryptYomiTestBody(t, secret, `{"platUserId":"420001","platToken":"app_access_token_yomi","platPayload":"","platRoomId":"room_1"}`)
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
Meta: &gamev1.RequestMeta{RequestId: "req-yomi-userinfo", AppCode: "lalu"},
PlatformCode: "yomi",
Operation: "userinfo",
RawBody: userInfoBody,
RemoteAddr: "3.1.174.194:443",
})
if err != nil {
t.Fatalf("HandleCallback userinfo failed: %v", err)
}
if !strings.Contains(string(raw), `"code":200`) || !strings.Contains(string(raw), `"platUserId":"420001"`) || !strings.Contains(string(raw), `"avatar":"https://cdn.example/avatar.png"`) {
t.Fatalf("yomi userinfo response mismatch: %s", raw)
}
}
func TestHandleYomiChangeBalanceDecryptsAndAppliesWallet(t *testing.T) {
secret := "12345678901234567890123456789012"
token := "gt_test_token"

View File

@ -43,6 +43,10 @@ type yomiAdapterConfig struct {
TokenTTLSeconds int64 `json:"token_ttl_seconds"`
// game_level_uid 对应 Yomi 房间/场次,没有配置时按文档传 0。
GameLevelUID int64 `json:"game_level_uid"`
// game_level_uids 支持同一平台下不同游戏/房间使用不同场次 IDkey 可填 providerGameId 或内部 gameId。
GameLevelUIDs map[string]int64 `json:"game_level_uids"`
// plat_payload 透传给 Yomi适合联调时放环境、渠道或房间附加上下文不参与服务端业务判断。
PlatPayload string `json:"plat_payload"`
}
func yomiConfigFromPlatform(value any) yomiAdapterConfig {
@ -58,6 +62,8 @@ func yomiConfigFromPlatform(value any) yomiAdapterConfig {
_ = json.Unmarshal([]byte(strings.TrimSpace(raw)), &config)
config.UIDMode = strings.ToLower(strings.TrimSpace(config.UIDMode))
config.DefaultLang = strings.TrimSpace(config.DefaultLang)
config.PlatPayload = strings.TrimSpace(config.PlatPayload)
config.GameLevelUIDs = normalizeInt64Map(config.GameLevelUIDs)
return config
}
@ -69,6 +75,32 @@ func (c yomiAdapterConfig) TokenTTL() time.Duration {
return yomiDefaultTokenTTL
}
func (c yomiAdapterConfig) GameLevelUIDFor(game gamedomain.LaunchableGame) int64 {
for _, key := range []string{game.ProviderGameID, game.GameID, strings.ToLower(game.GameID)} {
if value := c.GameLevelUIDs[strings.TrimSpace(key)]; value > 0 {
return value
}
}
return c.GameLevelUID
}
func normalizeInt64Map(input map[string]int64) map[string]int64 {
if len(input) == 0 {
return nil
}
output := make(map[string]int64, len(input))
for key, value := range input {
key = strings.TrimSpace(key)
if key != "" && value > 0 {
output[key] = value
}
}
if len(output) == 0 {
return nil
}
return output
}
func externalUserID(session gamedomain.LaunchSession, uidMode string) string {
// 三方通常会把 uid 展示给玩家或写入订单,切换 uid_mode 后必须保持启动和回调校验一致。
if strings.EqualFold(uidMode, "display_user_id") && strings.TrimSpace(session.DisplayUserID) != "" {