diff --git a/services/game-service/internal/service/game/service_test.go b/services/game-service/internal/service/game/service_test.go index 61b0fbdf..18aaed61 100644 --- a/services/game-service/internal/service/game/service_test.go +++ b/services/game-service/internal/service/game/service_test.go @@ -1260,6 +1260,68 @@ func TestHandleZeeOneAccessTokenIgnoresStaleLaunchSessionGameID(t *testing.T) { } } +func TestHandleZeeOneExpiredAccessTokenUsesScopedLaunchSession(t *testing.T) { + secret := "zeeone-test-secret" + userID := int64(328141720820654080) + token := leaderccTestAccessToken(t, "lalu", userID, "1010", 1700000300) + repo := &fakeRepository{ + platform: gamedomain.Platform{ + PlatformCode: "zeeone", + AdapterType: gamedomain.AdapterZeeOneV1, + CallbackSecretCiphertext: secret, + AdapterConfigJSON: `{"merchant_id":307715,"platform_id":2032,"uid_mode":"display_user_id"}`, + }, + sessions: []gamedomain.LaunchSession{ + { + AppCode: "lalu", + SessionID: "launch_session_1035", + UserID: userID, + DisplayUserID: "1010", + PlatformCode: "zeeone", + GameID: "zeeone_1035", + ProviderGameID: "1035", + LaunchTokenHash: stableHash(token), + Status: gamedomain.SessionActive, + ExpiresAtMS: 1700086800000, + CreatedAtMS: 1700000000000, + }, + { + AppCode: "lalu", + SessionID: "newer_wrong_game_session", + UserID: userID, + DisplayUserID: "1010", + PlatformCode: "zeeone", + GameID: "zeeone_1021", + ProviderGameID: "1021", + LaunchTokenHash: stableHash(token), + Status: gamedomain.SessionActive, + ExpiresAtMS: 1700086800000, + CreatedAtMS: 1700000100000, + }, + }, + } + wallet := &fakeWallet{balanceAfter: 777} + svc := New(Config{}, repo, wallet, &fakeUser{}) + svc.now = func() time.Time { return time.UnixMilli(1700000400000) } + + changeBody := `{"appId":"M307715_P2032","userId":"1010","sessionId":"` + token + `","currency_diff":-50000,"diff_msg":"bet","game_id":1035,"room_id":"3255","change_time_at":1700000400,"order_id":"zee_order_expired_token_scoped"}` + raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{ + Meta: &gamev1.RequestMeta{RequestId: "req-zeeone-expired-scoped", AppCode: "lalu"}, + PlatformCode: "zeeone", + Operation: "change_balance", + RawBody: zeeoneTestEnvelope(t, secret, changeBody, 1700000400000), + }) + if err != nil { + t.Fatalf("HandleCallback change failed: %v", err) + } + if !strings.Contains(string(raw), `"code":0`) || strings.Contains(string(raw), "sessionId invalid") { + t.Fatalf("zeeone expired JWT must fall back to scoped launch session: %s", raw) + } + if wallet.lastApply.GetUserId() != userID || wallet.lastApply.GetGameId() != "zeeone_1035" || wallet.lastApply.GetCoinAmount() != 50000 { + t.Fatalf("zeeone scoped launch session must drive wallet command, got %+v", wallet.lastApply) + } +} + func TestHandleZeeOneAllowsDebitAndCreditWithSameProviderOrderID(t *testing.T) { secret := "zeeone-test-secret" token := leaderccTestAccessToken(t, "lalu", 123456, "123456", 1700003600) diff --git a/services/game-service/internal/service/game/zeeone_adapter.go b/services/game-service/internal/service/game/zeeone_adapter.go index 2692757c..0a2600d0 100644 --- a/services/game-service/internal/service/game/zeeone_adapter.go +++ b/services/game-service/internal/service/game/zeeone_adapter.go @@ -143,7 +143,7 @@ func (s *Service) handleZeeOneOperation(ctx context.Context, app string, platfor case "change_balance", "change_coin", "update_coin": return s.handleZeeOneChangeBalance(ctx, app, config, req, body, requestHash) case "update_session", "refresh_session": - return s.handleZeeOneUpdateSession(ctx, app, config, body) + return s.handleZeeOneUpdateSession(ctx, app, req.GetPlatformCode(), config, body) case "repair_order", "repair": return s.handleZeeOneRepairOrder(ctx, app, platform, body) default: @@ -232,7 +232,7 @@ func validateZeeOneAppID(config zeeoneAdapterConfig, appID string) error { } func (s *Service) handleZeeOneUserInfo(ctx context.Context, app string, config zeeoneAdapterConfig, req *gamev1.CallbackRequest, body zeeoneBusinessBody) ([]byte, string, string, bool, string, error) { - session, err := s.validZeeOneSession(ctx, app, body.SessionID) + session, err := s.validZeeOneSession(ctx, app, req.GetPlatformCode(), body.SessionID, body.GameID) if err != nil { return zeeoneAdapterError(zeeoneCodeFromError(err), zeeoneMessageFromError(err), true, "") } @@ -262,7 +262,7 @@ func (s *Service) handleZeeOneChangeBalance(ctx context.Context, app string, con if body.OrderID == "" || body.UserID == "" || body.SessionID == "" || body.GameID <= 0 || body.DiffMsg == "" { return zeeoneAdapterError(zeeoneCodeInvalidArgument, "invalid argument", true, body.OrderID) } - session, err := s.validZeeOneSession(ctx, app, body.SessionID) + session, err := s.validZeeOneSession(ctx, app, req.GetPlatformCode(), body.SessionID, body.GameID) if err != nil { return zeeoneAdapterError(zeeoneCodeFromError(err), zeeoneMessageFromError(err), true, body.OrderID) } @@ -288,8 +288,8 @@ func (s *Service) handleZeeOneChangeBalance(ctx context.Context, app string, con return raw, contentType, strconv.Itoa(zeeoneCodeOK), true, body.OrderID, nil } -func (s *Service) handleZeeOneUpdateSession(ctx context.Context, app string, config zeeoneAdapterConfig, body zeeoneBusinessBody) ([]byte, string, string, bool, string, error) { - session, err := s.validZeeOneSession(ctx, app, body.SessionID) +func (s *Service) handleZeeOneUpdateSession(ctx context.Context, app string, platformCode string, config zeeoneAdapterConfig, body zeeoneBusinessBody) ([]byte, string, string, bool, string, error) { + session, err := s.validZeeOneSession(ctx, app, platformCode, body.SessionID, body.GameID) if err != nil { return zeeoneAdapterError(zeeoneCodeFromError(err), zeeoneMessageFromError(err), true, "") } @@ -321,7 +321,7 @@ func (s *Service) handleZeeOneRepairOrder(ctx context.Context, app string, platf return raw, contentType, strconv.Itoa(zeeoneCodeOK), true, body.OrderID, nil } -func (s *Service) validZeeOneSession(ctx context.Context, app string, sessionID string) (gamedomain.LaunchSession, error) { +func (s *Service) validZeeOneSession(ctx context.Context, app string, platformCode string, sessionID string, gameID int64) (gamedomain.LaunchSession, error) { sessionID = strings.TrimSpace(sessionID) if sessionID == "" { return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "sessionId not found") @@ -333,7 +333,13 @@ func (s *Service) validZeeOneSession(ctx context.Context, app string, sessionID return session, nil } } - if session, err := s.validYomiSession(ctx, app, sessionID); err == nil { + providerGameID := "" + if gameID > 0 { + providerGameID = strconv.FormatInt(gameID, 10) + } + // App access token 过期后仍可能处于三方游戏局中;启动链路已经把同一 token 的厂商、游戏和 24h TTL + // 落到 game_launch_sessions。这里必须按平台和游戏收窄,避免同一 access token 打开多款 ZeeOne 游戏时拿到旧会话。 + if session, err := s.validScopedYomiSession(ctx, app, platformCode, sessionID, providerGameID); err == nil { return session, nil } if strings.Count(sessionID, ".") == 2 {