diff --git a/internal/integration/gateways.go b/internal/integration/gateways.go index f09225b..256fba2 100644 --- a/internal/integration/gateways.go +++ b/internal/integration/gateways.go @@ -88,6 +88,16 @@ func (g *Gateways) GetUserProfile(ctx context.Context, userID int64) (UserProfil return g.Profile.GetUserProfile(ctx, userID) } +// GetUserProfileByAccount 透传到资料网关。 +func (g *Gateways) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) { + return g.Profile.GetUserProfileByAccount(ctx, sysOrigin, account) +} + +// GetUserProfileByAccountOne 透传到资料网关。 +func (g *Gateways) GetUserProfileByAccountOne(ctx context.Context, account string) (UserProfile, error) { + return g.Profile.GetUserProfileByAccountOne(ctx, account) +} + // GetUserLanguage 透传到资料网关。 func (g *Gateways) GetUserLanguage(ctx context.Context, userID int64) (string, error) { return g.Profile.GetUserLanguage(ctx, userID) @@ -198,6 +208,16 @@ func (g *ProfileGateway) GetUserProfile(ctx context.Context, userID int64) (User return g.client.GetUserProfile(ctx, userID) } +// GetUserProfileByAccount 根据账号查询用户资料。 +func (g *ProfileGateway) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) { + return g.client.GetUserProfileByAccount(ctx, sysOrigin, account) +} + +// GetUserProfileByAccountOne 根据账号查询单个用户资料。 +func (g *ProfileGateway) GetUserProfileByAccountOne(ctx context.Context, account string) (UserProfile, error) { + return g.client.GetUserProfileByAccountOne(ctx, account) +} + // GetUserLanguage 查询用户语言。 func (g *ProfileGateway) GetUserLanguage(ctx context.Context, userID int64) (string, error) { return g.client.GetUserLanguage(ctx, userID) diff --git a/internal/integration/java.go b/internal/integration/java.go index a910580..df65ea3 100644 --- a/internal/integration/java.go +++ b/internal/integration/java.go @@ -38,6 +38,7 @@ type ConsoleAccount struct { type UserProfile struct { ID Int64Value `json:"id"` + Account string `json:"account"` UserAvatar string `json:"userAvatar"` UserNickname string `json:"userNickname"` } @@ -349,6 +350,32 @@ func (c *Client) GetUserProfile(ctx context.Context, userID int64) (UserProfile, return resp.Body, nil } +func (c *Client) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) { + endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByAccount?sysOrigin=" + + url.QueryEscape(strings.TrimSpace(sysOrigin)) + "&account=" + url.QueryEscape(strings.TrimSpace(account)) + var resp resultResponse[UserProfile] + if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil { + return UserProfile{}, err + } + if int64(resp.Body.ID) == 0 { + return UserProfile{}, fmt.Errorf("empty user profile response") + } + return resp.Body, nil +} + +func (c *Client) GetUserProfileByAccountOne(ctx context.Context, account string) (UserProfile, error) { + endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByAccountOne?account=" + + url.QueryEscape(strings.TrimSpace(account)) + var resp resultResponse[UserProfile] + if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil { + return UserProfile{}, err + } + if int64(resp.Body.ID) == 0 { + return UserProfile{}, fmt.Errorf("empty user profile response") + } + return resp.Body, nil +} + func (c *Client) rewardBridgeBaseURL() string { if baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.Java.ConsoleBaseURL), "/"); baseURL != "" { return baseURL diff --git a/internal/service/gameopen/service.go b/internal/service/gameopen/service.go index 7610bea..67fb383 100644 --- a/internal/service/gameopen/service.go +++ b/internal/service/gameopen/service.go @@ -31,14 +31,19 @@ func (s *GameOpenService) HandleQueryUser(ctx context.Context, req QueryUserRequ s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } - userID := int64(credential.UserID) - if req.UID != strconv.FormatInt(userID, 10) { + resolved, err := s.resolveUserWithToken(ctx, credential, req.UID) + if err != nil { resp := failResponse(gameOpenCodeTokenInvalid, "uid token mismatch") + userID := int64(credential.UserID) s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } + userID := resolved.userID - profile, err := s.java.GetUserProfile(ctx, userID) + profile := resolved.profile + if int64(profile.ID) == 0 { + profile, err = s.java.GetUserProfile(ctx, userID) + } if err != nil { resp := failResponse(gameOpenCodeServerError, "profile load failed") s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) @@ -80,12 +85,14 @@ func (s *GameOpenService) HandleUpdateCoin(ctx context.Context, req UpdateCoinRe s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } - userID := int64(credential.UserID) - if req.UID != strconv.FormatInt(userID, 10) { + resolved, err := s.resolveUserWithToken(ctx, credential, req.UID) + if err != nil { resp := failResponse(gameOpenCodeTokenInvalid, "uid token mismatch") + userID := int64(credential.UserID) s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed) return resp } + userID := resolved.userID eventID := "GAME_OPEN:" + strings.TrimSpace(req.OrderID) exists, err := s.java.ExistsGoldEvent(ctx, eventID) @@ -138,7 +145,12 @@ func (s *GameOpenService) HandleSupplement(ctx context.Context, req SupplementRe return resp } - userID := parseInt64Default(req.UID) + userID, err := s.resolveUserID(ctx, req.UID) + if err != nil { + resp := failResponse(gameOpenCodeTokenInvalid, "uid invalid") + s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) + return resp + } balance, err := s.readUserBalance(ctx, userID) if err != nil { resp := failResponse(gameOpenCodeServerError, "balance load failed") @@ -187,6 +199,49 @@ func (s *GameOpenService) readUserBalance(ctx context.Context, userID int64) (in return balances[userID], nil } +type resolvedGameOpenUser struct { + userID int64 + profile integration.UserProfile +} + +func (s *GameOpenService) resolveUserWithToken(ctx context.Context, credential integration.UserCredential, uid string) (resolvedGameOpenUser, error) { + userID := int64(credential.UserID) + uid = strings.TrimSpace(uid) + if uid == strconv.FormatInt(userID, 10) { + return resolvedGameOpenUser{userID: userID}, nil + } + + profile, err := s.java.GetUserProfileByAccount(ctx, credential.SysOrigin, uid) + if err == nil { + if int64(profile.ID) == userID { + return resolvedGameOpenUser{userID: userID, profile: profile}, nil + } + return resolvedGameOpenUser{}, fmt.Errorf("uid token mismatch") + } + + profile, err = s.java.GetUserProfile(ctx, userID) + if err != nil { + return resolvedGameOpenUser{}, err + } + if strings.EqualFold(strings.TrimSpace(profile.Account), uid) { + return resolvedGameOpenUser{userID: userID, profile: profile}, nil + } + return resolvedGameOpenUser{}, fmt.Errorf("uid token mismatch") +} + +func (s *GameOpenService) resolveUserID(ctx context.Context, uid string) (int64, error) { + uid = strings.TrimSpace(uid) + profile, err := s.java.GetUserProfileByAccountOne(ctx, uid) + if err == nil && int64(profile.ID) > 0 { + return int64(profile.ID), nil + } + + if userID := parseInt64Default(uid); userID > 0 { + return userID, nil + } + return 0, fmt.Errorf("uid invalid") +} + func (s *GameOpenService) saveCallbackLog(ctx context.Context, requestType, rawJSON string, resp CallbackResponse, sysOrigin string, req any, userID *int64, status string) { if s == nil || s.repo.DB == nil { return diff --git a/internal/service/gameopen/service_test.go b/internal/service/gameopen/service_test.go index 39cdfde..7f8d55b 100644 --- a/internal/service/gameopen/service_test.go +++ b/internal/service/gameopen/service_test.go @@ -11,6 +11,7 @@ type stubGateway struct { credential integration.UserCredential token string profile integration.UserProfile + accountMap map[string]integration.UserProfile balance int64 exists bool cmd integration.GoldReceiptCommand @@ -28,8 +29,34 @@ func (s *stubGateway) GetUserProfile(context.Context, int64) (integration.UserPr return s.profile, nil } +func (s *stubGateway) GetUserProfileByAccount(_ context.Context, _ string, account string) (integration.UserProfile, error) { + if profile, ok := s.accountMap[account]; ok { + return profile, nil + } + return integration.UserProfile{}, context.Canceled +} + +func (s *stubGateway) GetUserProfileByAccountOne(_ context.Context, account string) (integration.UserProfile, error) { + if profile, ok := s.accountMap[account]; ok { + return profile, nil + } + return integration.UserProfile{}, context.Canceled +} + func (s *stubGateway) MapGoldBalance(context.Context, []int64) (map[int64]int64, error) { - return map[int64]int64{int64(s.credential.UserID): s.balance}, nil + result := map[int64]int64{} + if int64(s.credential.UserID) > 0 { + result[int64(s.credential.UserID)] = s.balance + } + if int64(s.profile.ID) > 0 { + result[int64(s.profile.ID)] = s.balance + } + for _, profile := range s.accountMap { + if int64(profile.ID) > 0 { + result[int64(profile.ID)] = s.balance + } + } + return result, nil } func (s *stubGateway) ExistsGoldEvent(context.Context, string) (bool, error) { @@ -46,6 +73,7 @@ func TestHandleQueryUser(t *testing.T) { credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, profile: integration.UserProfile{ ID: 1234567, + Account: "1234567", UserNickname: "tester", UserAvatar: "https://example.com/avatar.png", }, @@ -78,7 +106,11 @@ func TestHandleQueryUser(t *testing.T) { func TestHandleUpdateCoin(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, - balance: 99, + profile: integration.UserProfile{ + ID: 1234567, + Account: "1234567", + }, + balance: 99, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, @@ -109,6 +141,114 @@ func TestHandleUpdateCoin(t *testing.T) { } } +func TestHandleQueryUserAcceptsAccountUID(t *testing.T) { + gateway := &stubGateway{ + credential: integration.UserCredential{UserID: 4569421795454615552, SysOrigin: "LIKEI"}, + profile: integration.UserProfile{ + ID: 4569421795454615552, + Account: "1234567", + UserNickname: "tester", + UserAvatar: "https://example.com/avatar.png", + }, + accountMap: map[string]integration.UserProfile{ + "1234567": { + ID: 4569421795454615552, + Account: "1234567", + UserNickname: "tester", + UserAvatar: "https://example.com/avatar.png", + }, + }, + balance: 88, + } + service := NewGameOpenService(config.Config{ + GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, + }, nil, gateway) + req := QueryUserRequest{ + GameID: "101", + UID: "1234567", + Token: "token", + RoomID: "room-1", + } + req.Sign = buildQueryUserSign(req, "game-open-test-key") + + resp := service.HandleQueryUser(context.Background(), req, "{}") + if resp.ErrorCode != 0 { + t.Fatalf("HandleQueryUser() error = %+v", resp) + } +} + +func TestHandleUpdateCoinAcceptsAccountUID(t *testing.T) { + gateway := &stubGateway{ + credential: integration.UserCredential{UserID: 4569421795454615552, SysOrigin: "LIKEI"}, + accountMap: map[string]integration.UserProfile{ + "1234567": { + ID: 4569421795454615552, + Account: "1234567", + }, + }, + balance: 99, + } + service := NewGameOpenService(config.Config{ + GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, + }, nil, gateway) + req := UpdateCoinRequest{ + OrderID: "order-1", + GameID: "101", + RoundID: "round-1", + UID: "1234567", + Coin: 15, + Type: 2, + RewardType: 2, + Token: "token", + WinID: "win-1", + RoomID: "room-1", + } + req.Sign = buildUpdateCoinSign(req, "game-open-test-key") + + resp := service.HandleUpdateCoin(context.Background(), req, "{}") + if resp.ErrorCode != 0 { + t.Fatalf("HandleUpdateCoin() error = %+v", resp) + } +} + +func TestHandleSupplementAcceptsAccountUID(t *testing.T) { + gateway := &stubGateway{ + accountMap: map[string]integration.UserProfile{ + "1234567": { + ID: 4569421795454615552, + Account: "1234567", + }, + }, + balance: 66, + } + service := NewGameOpenService(config.Config{ + GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, + }, nil, gateway) + req := SupplementRequest{ + OrderID: "order-1", + GameID: "101", + RoundID: "round-1", + UID: "1234567", + Coin: 1, + RewardType: 1, + WinID: "win-1", + RoomID: "room-1", + } + req.Sign = buildSupplementSign(req, "game-open-test-key") + + resp := service.HandleSupplement(context.Background(), req, "{}") + if resp.ErrorCode != 0 { + t.Fatalf("HandleSupplement() error = %+v", resp) + } + data, ok := resp.Data.(map[string]any) + if !ok { + t.Fatalf("HandleSupplement() data type = %T", resp.Data) + } + if got := data["coin"]; got != int64(66) { + t.Fatalf("HandleSupplement() coin = %v, want 66", got) + } +} + func TestGetIntegrationInfo(t *testing.T) { gateway := &stubGateway{token: "token-123"} service := NewGameOpenService(config.Config{ diff --git a/internal/service/gameopen/types.go b/internal/service/gameopen/types.go index 14ecc58..7ecb361 100644 --- a/internal/service/gameopen/types.go +++ b/internal/service/gameopen/types.go @@ -29,6 +29,8 @@ type gameOpenGateway interface { AuthenticateToken(ctx context.Context, token string) (integration.UserCredential, error) CreateIfAbsentUserCredentialToken(ctx context.Context, userID int64) (string, error) GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error) + GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (integration.UserProfile, error) + GetUserProfileByAccountOne(ctx context.Context, account string) (integration.UserProfile, error) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) ExistsGoldEvent(ctx context.Context, eventID string) (bool, error) ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error