diff --git a/services/game-service/internal/service/game/service.go b/services/game-service/internal/service/game/service.go index ebd1d084..b9442c70 100644 --- a/services/game-service/internal/service/game/service.go +++ b/services/game-service/internal/service/game/service.go @@ -301,6 +301,10 @@ func (s *Service) LaunchGame(ctx context.Context, command LaunchCommand) (Launch } token = "gt_" + randomHex(24) } + if strings.EqualFold(game.AdapterType, gamedomain.AdapterZGameV1) { + // ZGame 文档要求 js_code 是客户服务器派发的短期凭证;不能把 App access token 泄漏到三方 H5 URL。 + token = "gt_" + randomHex(24) + } sessionTTL := s.config.LaunchSessionTTL // 三方游戏会在 H5 运行期间持续回调 token;厂商适配器可以把默认 15 分钟启动 TTL 拉长。 if strings.EqualFold(game.AdapterType, gamedomain.AdapterYomiV4) { @@ -995,24 +999,15 @@ func buildLaunchURL(game gamedomain.LaunchableGame, session gamedomain.LaunchSes return parsed.String(), nil } if strings.EqualFold(game.AdapterType, gamedomain.AdapterZGameV1) { - // ZGame 默认 channelName=zgame_demo 会走 iframe parent postMessage;App WebView 必须切到 wh 模式,让 SDK 从 URL 读取 openId/js_code。 - // token/code/js_code/session 四个别名仍指向同一个短期启动令牌,兼容厂商不同入口读取字段。 + // laluparty 是厂商文档约定通道;H5 通过 App 注入的 ThirdPlatformBridge 取用户信息和 login js_code。 + // 启动 URL 仍带 open_id/js_code,供 App Bridge 复用同一份短期凭证,避免客户端再发起一次派码请求。 config := zgameConfigFromPlatform(game) openID := externalUserID(session, config.UIDMode) - query.Set("channelName", "wh") - query.Set("channel_name", "wh") - query.Set("cv", "Android") - query.Set("openId", openID) + query.Set("channelName", "laluparty") query.Set("open_id", openID) - query.Set("userId", openID) - query.Set("uid", openID) - query.Set("userName", openID) - query.Set("user_name", openID) - query.Set("balance", "0") - query.Set("token", token) - query.Set("code", token) + query.Set("openId", openID) query.Set("js_code", token) - query.Set("session", token) + query.Set("jscode", token) query.Set("gameId", strings.TrimSpace(session.ProviderGameID)) query.Set("game_id", strings.TrimSpace(session.ProviderGameID)) callbackBase := config.CallbackBaseValue() diff --git a/services/game-service/internal/service/game/service_test.go b/services/game-service/internal/service/game/service_test.go index daace3d8..d3f686a4 100644 --- a/services/game-service/internal/service/game/service_test.go +++ b/services/game-service/internal/service/game/service_test.go @@ -598,21 +598,19 @@ func TestLaunchGameBuildsZGameURL(t *testing.T) { if parsed.Scheme+"://"+parsed.Host+parsed.Path != "https://zgame.example/h5/1001" { t.Fatalf("zgame launch url must use per-game configured url: %s", result.LaunchURL) } + jsCode := parsed.Query().Get("js_code") + if jsCode == "" || !strings.HasPrefix(jsCode, "gt_") || jsCode == token { + t.Fatalf("zgame launch must expose a scoped js_code instead of app token: %q", jsCode) + } + if repo.session.LaunchTokenHash != stableHash(jsCode) { + t.Fatalf("zgame launch session must be bound to scoped js_code") + } expected := map[string]string{ - "channelName": "wh", - "channel_name": "wh", - "cv": "Android", + "channelName": "laluparty", "openId": "420001", "open_id": "420001", - "userId": "420001", - "uid": "420001", - "userName": "420001", - "user_name": "420001", - "balance": "0", - "token": token, - "code": token, - "js_code": token, - "session": token, + "js_code": jsCode, + "jscode": jsCode, "gameId": "1001", "game_id": "1001", "callbackBase": "https://api.example.test/api/v1/zgame/callback", @@ -632,6 +630,11 @@ func TestLaunchGameBuildsZGameURL(t *testing.T) { t.Fatalf("zgame launch query %s mismatch: got %q want %q url=%s", key, got, want, result.LaunchURL) } } + for _, key := range []string{"token", "code", "session", "cv", "channel_name"} { + if got := parsed.Query().Get(key); got != "" { + t.Fatalf("zgame launch query must not expose %s=%q url=%s", key, got, result.LaunchURL) + } + } } func TestLaunchGameRequiresBaishunLaunchURL(t *testing.T) { diff --git a/services/game-service/internal/service/game/zgame_adapter.go b/services/game-service/internal/service/game/zgame_adapter.go index 541ae5ef..359473f8 100644 --- a/services/game-service/internal/service/game/zgame_adapter.go +++ b/services/game-service/internal/service/game/zgame_adapter.go @@ -59,6 +59,10 @@ func zgameConfigFromPlatform(value any) zgameAdapterConfig { var config zgameAdapterConfig _ = json.Unmarshal([]byte(strings.TrimSpace(raw)), &config) config.UIDMode = strings.ToLower(strings.TrimSpace(config.UIDMode)) + if config.UIDMode == "" { + // laluparty 游戏侧展示和签名都用短号作为 open_id;历史配置未写 uid_mode 时按文档默认走 display_user_id。 + config.UIDMode = "display_user_id" + } config.DefaultLang = strings.TrimSpace(config.DefaultLang) config.LaunchURLTemplate = strings.TrimSpace(config.LaunchURLTemplate) config.CallbackBase = strings.TrimSpace(firstNonEmpty(config.CallbackBase, config.CallbackBaseAlias)) @@ -159,7 +163,7 @@ func (s *Service) handleZGameLogin(ctx context.Context, app string, config zgame if err != nil { return zgameAdapterError(zgameCodeFromError(err), zgameMessageFromError(err), true, "") } - raw, contentType := zgameJSON(zgameCodeOK, "success", zgameLoginData(session, config, jsCode, snapshot, s.now())) + raw, contentType := zgameJSON(zgameCodeOK, "success", zgameLoginData(session, config, openID, jsCode, snapshot, s.now())) return raw, contentType, strconv.Itoa(zgameCodeOK), true, "", nil } @@ -339,7 +343,7 @@ func (s *Service) validZGameSession(ctx context.Context, app string, platformCod if token == "" { return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session invalid") } - // 当前 ZGame 不要求 App 原生生成一次性 jscode;优先消费 App access token,远程桥接脚本即可免发版接入。 + // 新版启动链路使用服务端短期 js_code;保留 JWT 兼容旧测试链接,避免已打开的 H5 立即失效。 if strings.Count(token, ".") == 2 { if session, err := s.appSessionFromAccessToken(app, token); err == nil { return session, nil @@ -377,8 +381,11 @@ func zgameSignatureValid(openID string, session string, signature string, secret return subtle.ConstantTimeCompare([]byte(strings.ToLower(signature)), []byte(expected)) == 1 } -func zgameLoginData(session gamedomain.LaunchSession, config zgameAdapterConfig, token string, snapshot yomiUserSnapshot, now time.Time) map[string]any { - openID := externalUserID(session, config.UIDMode) +func zgameLoginData(session gamedomain.LaunchSession, config zgameAdapterConfig, requestedOpenID string, token string, snapshot yomiUserSnapshot, now time.Time) map[string]any { + openID := strings.TrimSpace(requestedOpenID) + if openID == "" { + openID = externalUserID(session, config.UIDMode) + } return map[string]any{ "session": token, "session_expired": zgameSessionTTLMS(session, now),