diff --git a/internal/service/gameopen/convert.go b/internal/service/gameopen/convert.go index 5c334d8..dde7382 100644 --- a/internal/service/gameopen/convert.go +++ b/internal/service/gameopen/convert.go @@ -2,6 +2,7 @@ package gameopen import ( "crypto/md5" + "encoding/base64" "encoding/hex" "fmt" "net/url" @@ -71,6 +72,27 @@ func normalizeCallbackToken(token string) string { return parts[0] + "." + parts[1] } +func callbackTokenSysOrigin(token string) string { + token = normalizeCallbackToken(token) + parts := strings.SplitN(token, ".", 2) + if len(parts) != 2 || strings.TrimSpace(parts[1]) == "" { + return "" + } + decoded, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return "" + } + payload := strings.TrimSpace(string(decoded)) + if unescaped, err := url.QueryUnescape(payload); err == nil { + payload = strings.TrimSpace(unescaped) + } + fields := strings.Split(payload, ":") + if len(fields) < 3 { + return "" + } + return strings.ToUpper(strings.TrimSpace(fields[2])) +} + func receiptTypeFromType(typed int) string { if typed == 2 { return "INCOME" diff --git a/internal/service/gameopen/service.go b/internal/service/gameopen/service.go index 35b2364..9b09743 100644 --- a/internal/service/gameopen/service.go +++ b/internal/service/gameopen/service.go @@ -21,7 +21,13 @@ func (s *GameOpenService) HandleQueryUser(ctx context.Context, req QueryUserRequ s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } - if !verifySign(req.Sign, buildQueryUserSign(req, s.cfg.GameOpen.AppKey)) { + appKey := s.resolveCallbackAppKey(ctx, req.GameID, req.Token) + if strings.TrimSpace(appKey) == "" { + resp := failResponse(gameOpenCodeBadRequest, "app key is blank") + s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) + return resp + } + if !verifySign(req.Sign, buildQueryUserSign(req, appKey)) { resp := failResponse(gameOpenCodeSignatureError, "signature error") s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp @@ -76,7 +82,13 @@ func (s *GameOpenService) HandleUpdateCoin(ctx context.Context, req UpdateCoinRe s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } - if !verifySign(req.Sign, buildUpdateCoinSign(req, s.cfg.GameOpen.AppKey)) { + appKey := s.resolveCallbackAppKey(ctx, req.GameID, req.Token) + if strings.TrimSpace(appKey) == "" { + resp := failResponse(gameOpenCodeBadRequest, "app key is blank") + s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) + return resp + } + if !verifySign(req.Sign, buildUpdateCoinSign(req, appKey)) { resp := failResponse(gameOpenCodeSignatureError, "signature error") s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp @@ -147,7 +159,13 @@ func (s *GameOpenService) HandleSupplement(ctx context.Context, req SupplementRe s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp } - if !verifySign(req.Sign, buildSupplementSign(req, s.cfg.GameOpen.AppKey)) { + appKey := s.resolveCallbackAppKey(ctx, req.GameID, "") + if strings.TrimSpace(appKey) == "" { + resp := failResponse(gameOpenCodeBadRequest, "app key is blank") + s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) + return resp + } + if !verifySign(req.Sign, buildSupplementSign(req, appKey)) { resp := failResponse(gameOpenCodeSignatureError, "signature error") s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed) return resp @@ -187,6 +205,7 @@ func (s *GameOpenService) GetIntegrationInfo(ctx context.Context, publicBaseURL if err != nil { return nil, err } + appKey := s.resolveCallbackAppKey(ctx, "", token) return &IntegrationInfoResponse{ Domain: baseURL, QueryUser: baseURL + "/game/open/user-info", @@ -195,10 +214,66 @@ func (s *GameOpenService) GetIntegrationInfo(ctx context.Context, publicBaseURL UID: defaultIfBlank(s.cfg.GameOpen.TestAccount, strconv.FormatInt(s.cfg.GameOpen.TestUserID, 10)), Password: s.cfg.GameOpen.TestPassword, Token: strings.TrimSpace(token), - Key: s.cfg.GameOpen.AppKey, + Key: appKey, }, nil } +func (s *GameOpenService) resolveCallbackAppKey(ctx context.Context, gameID, token string) string { + if s != nil && s.repo.DB != nil { + if sysOrigin := callbackTokenSysOrigin(token); sysOrigin != "" { + if appKey := s.resolveActiveLingxianAppKey(ctx, sysOrigin); appKey != "" { + return appKey + } + } + if appKey := s.resolveLingxianAppKeyByGame(ctx, gameID); appKey != "" { + return appKey + } + } + if s == nil { + return "" + } + return strings.TrimSpace(s.cfg.GameOpen.AppKey) +} + +func (s *GameOpenService) resolveLingxianAppKeyByGame(ctx context.Context, gameID string) string { + gameID = strings.TrimSpace(gameID) + if gameID == "" { + return "" + } + var row struct { + AppKey string + } + err := s.repo.DB.WithContext(ctx). + Table("lingxian_provider_config AS cfg"). + Select("cfg.app_key AS app_key"). + Joins("JOIN sys_game_list_vendor_ext ext ON ext.sys_origin = cfg.sys_origin AND ext.profile = cfg.profile AND ext.vendor_type = ? AND ext.enabled = ?", "LINGXIAN", true). + Where("cfg.active = ? AND ext.vendor_game_id = ?", true, gameID). + Order("cfg.update_time DESC"). + Limit(1). + Scan(&row).Error + if err != nil { + return "" + } + return strings.TrimSpace(row.AppKey) +} + +func (s *GameOpenService) resolveActiveLingxianAppKey(ctx context.Context, sysOrigin string) string { + sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin)) + if sysOrigin == "" { + return "" + } + var row model.LingxianProviderConfig + err := s.repo.DB.WithContext(ctx). + Where("sys_origin = ? AND active = ?", sysOrigin, true). + Order("update_time DESC"). + Limit(1). + First(&row).Error + if err != nil { + return "" + } + return strings.TrimSpace(row.AppKey) +} + func (s *GameOpenService) readUserBalance(ctx context.Context, userID int64) (int64, error) { balances, err := s.java.MapGoldBalance(ctx, []int64{userID}) if err != nil { @@ -327,8 +402,6 @@ func (s *GameOpenService) validateQueryUserRequest(req QueryUserRequest) error { return newBadRequest("token is required") case strings.TrimSpace(req.Sign) == "": return newBadRequest("sign is required") - case strings.TrimSpace(s.cfg.GameOpen.AppKey) == "": - return newBadRequest("app key is blank") default: return nil } @@ -352,8 +425,6 @@ func (s *GameOpenService) validateUpdateCoinRequest(req UpdateCoinRequest) error return newBadRequest("token is required") case strings.TrimSpace(req.Sign) == "": return newBadRequest("sign is required") - case strings.TrimSpace(s.cfg.GameOpen.AppKey) == "": - return newBadRequest("app key is blank") default: return nil } @@ -373,8 +444,6 @@ func (s *GameOpenService) validateSupplementRequest(req SupplementRequest) error return newBadRequest("coin is required") case strings.TrimSpace(req.Sign) == "": return newBadRequest("sign is required") - case strings.TrimSpace(s.cfg.GameOpen.AppKey) == "": - return newBadRequest("app key is blank") default: return nil } diff --git a/internal/service/gameopen/service_test.go b/internal/service/gameopen/service_test.go index d6fc34a..6553a97 100644 --- a/internal/service/gameopen/service_test.go +++ b/internal/service/gameopen/service_test.go @@ -3,8 +3,13 @@ package gameopen import ( "chatapp3-golang/internal/config" "chatapp3-golang/internal/integration" + "chatapp3-golang/internal/model" "context" "testing" + "time" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) type stubGateway struct { @@ -259,6 +264,109 @@ func TestHandleUpdateCoinDuplicateOrderReturns10003(t *testing.T) { } } +func TestCallbacksUseLingxianProviderConfigAppKey(t *testing.T) { + db := newGameOpenTestDB(t) + now := time.Now() + if err := db.Create(&model.LingxianProviderConfig{ + ID: 1, + SysOrigin: "LIKEI", + Profile: "PROD", + Active: true, + AppKey: "backend-lingxian-key", + CreateTime: now, + UpdateTime: now, + }).Error; err != nil { + t.Fatalf("create lingxian config: %v", err) + } + if err := db.Create(&model.SysGameListVendorExt{ + ID: 2, + GameListConfigID: 3, + SysOrigin: "LIKEI", + Profile: "PROD", + VendorType: "LINGXIAN", + VendorGameID: "101", + Enabled: true, + CreateTime: now, + UpdateTime: now, + }).Error; err != nil { + t.Fatalf("create vendor ext: %v", err) + } + + gateway := &stubGateway{ + credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, + profile: integration.UserProfile{ + ID: 1234567, + Account: "1234567", + }, + accountMap: map[string]integration.UserProfile{ + "1234567": {ID: 1234567, Account: "1234567"}, + }, + balance: 99, + } + service := NewGameOpenService(config.Config{ + GameOpen: config.GameOpenConfig{AppKey: "wrong-env-key"}, + }, db, gateway) + + queryReq := QueryUserRequest{ + GameID: "101", + UID: "1234567", + Token: "token", + RoomID: "room-1", + } + queryReq.Sign = buildQueryUserSign(queryReq, "backend-lingxian-key") + if resp := service.HandleQueryUser(context.Background(), queryReq, "{}"); resp.ErrorCode != 0 { + t.Fatalf("HandleQueryUser() error = %+v", resp) + } + + updateReq := 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", + } + updateReq.Sign = buildUpdateCoinSign(updateReq, "backend-lingxian-key") + if resp := service.HandleUpdateCoin(context.Background(), updateReq, "{}"); resp.ErrorCode != 0 { + t.Fatalf("HandleUpdateCoin() error = %+v", resp) + } + + supplementReq := SupplementRequest{ + OrderID: "order-2", + GameID: "101", + RoundID: "round-2", + UID: "1234567", + Coin: 1, + RewardType: 1, + WinID: "win-2", + RoomID: "room-1", + } + supplementReq.Sign = buildSupplementSign(supplementReq, "backend-lingxian-key") + if resp := service.HandleSupplement(context.Background(), supplementReq, "{}"); resp.ErrorCode != 0 { + t.Fatalf("HandleSupplement() error = %+v", resp) + } +} + +func newGameOpenTestDB(t *testing.T) *gorm.DB { + t.Helper() + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if err := db.AutoMigrate( + &model.LingxianProviderConfig{}, + &model.SysGameListVendorExt{}, + &model.GameOpenCallbackLog{}, + ); err != nil { + t.Fatalf("migrate sqlite: %v", err) + } + return db +} + func TestHandleQueryUserAcceptsAccountUID(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 4569421795454615552, SysOrigin: "LIKEI"},