灵仙游戏和百顺的兼容
This commit is contained in:
parent
24525bb4f8
commit
a9ab2b8589
@ -230,7 +230,7 @@ func Load() Config {
|
||||
},
|
||||
Lingxian: LingxianConfig{
|
||||
GameListURL: getEnvAny([]string{"CHATAPP_LINGXIAN_GAME_LIST_URL", "LINGXIAN_GAME_LIST_URL"}, "https://sg-test.leadercc.com/yumichat_games/test_game_list.json"),
|
||||
AppKey: getEnvAny([]string{"CHATAPP_LINGXIAN_APP_KEY", "LINGXIAN_APP_KEY", "GAME_HKYS_SIGN_KEY"}, ""),
|
||||
AppKey: getEnvAny([]string{"CHATAPP_LINGXIAN_APP_KEY", "LINGXIAN_APP_KEY", "GAME_HKYS_SIGN_KEY", "LIKEI_GAME_HKYS_SIGN_KEY"}, ""),
|
||||
},
|
||||
GameOpen: GameOpenConfig{
|
||||
PublicBaseURL: getEnvAny([]string{"CHATAPP_GAME_OPEN_PUBLIC_BASE_URL", "GAME_OPEN_PUBLIC_BASE_URL"}, ""),
|
||||
|
||||
@ -57,6 +57,15 @@ func TestGetEnvBoolAliasSupportsLegacyLuckyGiftKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadLingxianConfigSupportsLikeiHkysSignKey(t *testing.T) {
|
||||
t.Setenv("LIKEI_GAME_HKYS_SIGN_KEY", "likei-hkys-key")
|
||||
|
||||
cfg := Load()
|
||||
if got := cfg.Lingxian.AppKey; got != "likei-hkys-key" {
|
||||
t.Fatalf("expected legacy likei hkys sign key, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadLuckyGiftConfigsSupportsLegacyDefaultEnv(t *testing.T) {
|
||||
oldJSON, hadJSON := os.LookupEnv("LUCKY_GIFT_CONFIGS_JSON")
|
||||
oldList, hadList := os.LookupEnv("LUCKY_GIFT_CONFIGS")
|
||||
|
||||
@ -112,6 +112,7 @@ type LingxianProviderConfig struct {
|
||||
AppKey string `gorm:"column:app_key;size:255"`
|
||||
TestUID string `gorm:"column:test_uid;size:64"`
|
||||
TestToken string `gorm:"column:test_token;size:1024"`
|
||||
TestRoomID string `gorm:"column:test_room_id;size:64"`
|
||||
CreateTime time.Time `gorm:"column:create_time"`
|
||||
UpdateTime time.Time `gorm:"column:update_time;index:idx_lingxian_provider_active,priority:3"`
|
||||
}
|
||||
|
||||
@ -204,7 +204,11 @@ func registerBaishunRoutes(
|
||||
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
return
|
||||
}
|
||||
resp, err := baishunService.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs)
|
||||
defaultShowcase := true
|
||||
if req.DefaultShowcase != nil {
|
||||
defaultShowcase = *req.DefaultShowcase
|
||||
}
|
||||
resp, err := baishunService.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs, defaultShowcase)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
|
||||
@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"chatapp3-golang/internal/common"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
@ -83,7 +84,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, gin.H{"items": resp})
|
||||
writeOK(c, gin.H{"items": publicRoomGameItems(resp)})
|
||||
})
|
||||
providerGroup.GET("/:provider/room/list", func(c *gin.Context) {
|
||||
provider, ok := resolveGameProvider(c, registry)
|
||||
@ -95,7 +96,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
writeOK(c, publicRoomGameListResponse(resp))
|
||||
})
|
||||
providerGroup.GET("/:provider/state", func(c *gin.Context) {
|
||||
provider, ok := resolveGameProvider(c, registry)
|
||||
@ -107,7 +108,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
writeOK(c, publicRoomState(resp))
|
||||
})
|
||||
providerGroup.POST("/:provider/launch", func(c *gin.Context) {
|
||||
provider, ok := resolveGameProvider(c, registry)
|
||||
@ -124,7 +125,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
writeOK(c, publicLaunch(resp))
|
||||
})
|
||||
providerGroup.POST("/:provider/close", func(c *gin.Context) {
|
||||
provider, ok := resolveGameProvider(c, registry)
|
||||
@ -141,7 +142,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
writeOK(c, publicRoomState(resp))
|
||||
})
|
||||
}
|
||||
|
||||
@ -214,12 +215,14 @@ func publicRoomGameItems(items []gameprovider.RoomGameListItem) []publicRoomGame
|
||||
}
|
||||
result := make([]publicRoomGameItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
provider := appCompatProviderName(item.Provider)
|
||||
gameType := appCompatProviderName(item.GameType)
|
||||
result = append(result, publicRoomGameItem{
|
||||
ID: item.ID,
|
||||
GameID: item.GameID,
|
||||
GameType: item.GameType,
|
||||
Provider: item.Provider,
|
||||
VendorType: item.Provider,
|
||||
GameType: gameType,
|
||||
Provider: provider,
|
||||
VendorType: provider,
|
||||
ProviderGameID: item.ProviderGameID,
|
||||
VendorGameID: item.ProviderGameID,
|
||||
Name: item.Name,
|
||||
@ -233,12 +236,29 @@ func publicRoomGameItems(items []gameprovider.RoomGameListItem) []publicRoomGame
|
||||
Orientation: item.Orientation,
|
||||
PackageVersion: item.PackageVersion,
|
||||
Status: item.Status,
|
||||
LaunchParams: item.LaunchParams,
|
||||
LaunchParams: publicLaunchParams(item.LaunchParams),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func publicLaunchParams(params map[string]any) map[string]any {
|
||||
if len(params) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]any, len(params))
|
||||
for key, value := range params {
|
||||
if strings.EqualFold(strings.TrimSpace(key), "gameType") {
|
||||
if text, ok := value.(string); ok {
|
||||
result[key] = appCompatProviderName(text)
|
||||
continue
|
||||
}
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func publicRoomGameListResponse(resp *gameprovider.RoomGameListResponse) *publicRoomGameList {
|
||||
if resp == nil {
|
||||
return &publicRoomGameList{Items: []publicRoomGameItem{}}
|
||||
@ -255,8 +275,8 @@ func publicRoomState(resp *gameprovider.RoomStateResponse) *publicRoomStateRespo
|
||||
return &publicRoomStateResponse{
|
||||
RoomID: resp.RoomID,
|
||||
State: resp.State,
|
||||
Provider: resp.Provider,
|
||||
VendorType: resp.Provider,
|
||||
Provider: appCompatProviderName(resp.Provider),
|
||||
VendorType: appCompatProviderName(resp.Provider),
|
||||
GameSessionID: resp.GameSessionID,
|
||||
CurrentGameID: resp.CurrentGameID,
|
||||
CurrentProviderGameID: resp.CurrentProviderGameID,
|
||||
@ -278,8 +298,8 @@ func publicLaunch(resp *gameprovider.LaunchResponse) *publicLaunchResponse {
|
||||
return &publicLaunchResponse{
|
||||
ID: resp.ID,
|
||||
GameSessionID: resp.GameSessionID,
|
||||
Provider: resp.Provider,
|
||||
VendorType: resp.Provider,
|
||||
Provider: appCompatProviderName(resp.Provider),
|
||||
VendorType: appCompatProviderName(resp.Provider),
|
||||
GameID: resp.GameID,
|
||||
ProviderGameID: resp.ProviderGameID,
|
||||
VendorGameID: resp.ProviderGameID,
|
||||
@ -289,3 +309,10 @@ func publicLaunch(resp *gameprovider.LaunchResponse) *publicLaunchResponse {
|
||||
RoomState: *roomState,
|
||||
}
|
||||
}
|
||||
|
||||
func appCompatProviderName(provider string) string {
|
||||
if strings.EqualFold(strings.TrimSpace(provider), "LINGXIAN") {
|
||||
return "LEADER"
|
||||
}
|
||||
return provider
|
||||
}
|
||||
|
||||
@ -38,6 +38,42 @@ func TestPublicRoomGameListResponseKeepsLegacyProviderFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicRoomGameListResponseMapsLingxianToLeaderForOldClients(t *testing.T) {
|
||||
resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{
|
||||
Items: []gameprovider.RoomGameListItem{
|
||||
{
|
||||
ID: 9528,
|
||||
GameID: "1090",
|
||||
GameType: "LINGXIAN",
|
||||
Provider: "LINGXIAN",
|
||||
ProviderGameID: "1090",
|
||||
Name: "Lingxian Game",
|
||||
LaunchParams: map[string]any{"gameType": "LINGXIAN", "screenMode": "half"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if len(resp.Items) != 1 {
|
||||
t.Fatalf("items = %d, want 1", len(resp.Items))
|
||||
}
|
||||
item := resp.Items[0]
|
||||
if item.Provider != "LEADER" {
|
||||
t.Fatalf("provider = %q, want LEADER", item.Provider)
|
||||
}
|
||||
if item.GameType != "LEADER" {
|
||||
t.Fatalf("gameType = %q, want LEADER", item.GameType)
|
||||
}
|
||||
if item.VendorType != "LEADER" {
|
||||
t.Fatalf("vendorType = %q, want LEADER", item.VendorType)
|
||||
}
|
||||
if got := item.LaunchParams["gameType"]; got != "LEADER" {
|
||||
t.Fatalf("launchParams.gameType = %v, want LEADER", got)
|
||||
}
|
||||
if got := item.LaunchParams["screenMode"]; got != "half" {
|
||||
t.Fatalf("launchParams.screenMode = %v, want half", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicLaunchKeepsLegacyBridgeAndProviderFields(t *testing.T) {
|
||||
launchConfig := map[string]any{"code": "launch-code"}
|
||||
resp := publicLaunch(&gameprovider.LaunchResponse{
|
||||
@ -78,3 +114,30 @@ func TestPublicLaunchKeepsLegacyBridgeAndProviderFields(t *testing.T) {
|
||||
t.Fatalf("roomState.currentVendorGameId = %q, want 1046", resp.RoomState.CurrentVendorGameID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicLaunchMapsLingxianToLeaderForOldClients(t *testing.T) {
|
||||
resp := publicLaunch(&gameprovider.LaunchResponse{
|
||||
ID: 9528,
|
||||
GameSessionID: "lx_1_1090",
|
||||
Provider: "LINGXIAN",
|
||||
GameID: "1090",
|
||||
ProviderGameID: "1090",
|
||||
LaunchConfig: map[string]any{"uid": "user-1"},
|
||||
RoomState: gameprovider.RoomStateResponse{
|
||||
RoomID: "room-1",
|
||||
State: "PLAYING",
|
||||
Provider: "LINGXIAN",
|
||||
CurrentProviderGameID: "1090",
|
||||
},
|
||||
})
|
||||
|
||||
if resp.Provider != "LEADER" {
|
||||
t.Fatalf("provider = %q, want LEADER", resp.Provider)
|
||||
}
|
||||
if resp.VendorType != "LEADER" {
|
||||
t.Fatalf("vendorType = %q, want LEADER", resp.VendorType)
|
||||
}
|
||||
if resp.RoomState.Provider != "LEADER" {
|
||||
t.Fatalf("roomState.provider = %q, want LEADER", resp.RoomState.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,7 +87,11 @@ func registerLingxianRoutes(engine *gin.Engine, javaClient authGateway, service
|
||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
return
|
||||
}
|
||||
resp, err := service.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs)
|
||||
defaultShowcase := true
|
||||
if req.DefaultShowcase != nil {
|
||||
defaultShowcase = *req.DefaultShowcase
|
||||
}
|
||||
resp, err := service.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs, defaultShowcase)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
|
||||
@ -68,6 +68,16 @@ func registerSpecifiedGiftWeeklyRankRoutes(group *gin.RouterGroup, javaClient au
|
||||
writeOK(c, resp)
|
||||
})
|
||||
|
||||
group.GET("/current-rank", func(c *gin.Context) {
|
||||
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||
resp, err := weekStarService.CurrentRanking(c.Request.Context(), c.Query("sysOrigin"), limit)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeOK(c, resp)
|
||||
})
|
||||
|
||||
group.GET("/snapshot/page", func(c *gin.Context) {
|
||||
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||
|
||||
@ -386,7 +386,7 @@ func (s *BaishunService) DeleteAdminGame(ctx context.Context, sysOrigin string,
|
||||
}
|
||||
|
||||
// ImportCatalogGames 把本地目录中尚未进入后台列表的百顺游戏补齐成可配置记录。
|
||||
func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin string, profile string, vendorGameIDs []int) (*ImportCatalogResponse, error) {
|
||||
func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin string, profile string, vendorGameIDs []int, defaultShowcase ...bool) (*ImportCatalogResponse, error) {
|
||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
@ -396,7 +396,7 @@ func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin strin
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
resp, err := s.importCatalogGamesTx(tx, sysOrigin, profile, buildVendorGameIDFilter(vendorGameIDs))
|
||||
resp, err := s.importCatalogGamesTx(tx, sysOrigin, profile, buildVendorGameIDFilter(vendorGameIDs), resolveImportDefaultShowcase(defaultShowcase...))
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
@ -414,7 +414,7 @@ func (s *BaishunService) SyncAdminGames(ctx context.Context, req SyncCatalogRequ
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.Profile, req.VendorGameIDs)
|
||||
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.Profile, req.VendorGameIDs, resolveBoolPtrDefault(req.DefaultShowcase, true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -589,7 +589,7 @@ func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, p
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, profile string, filter map[int]struct{}) (*ImportCatalogResponse, error) {
|
||||
func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, profile string, filter map[int]struct{}, defaultShowcase bool) (*ImportCatalogResponse, error) {
|
||||
ctx := tx.Statement.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
@ -647,7 +647,7 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, pro
|
||||
Category: baishunRoomCategory,
|
||||
GameCode: vendorGameID,
|
||||
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
||||
Showcase: true,
|
||||
Showcase: defaultShowcase,
|
||||
Sort: int64(catalog.VendorGameID),
|
||||
FullScreen: true,
|
||||
ClientOrigin: "COMMON",
|
||||
@ -716,6 +716,20 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, pro
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func resolveImportDefaultShowcase(values ...bool) bool {
|
||||
if len(values) == 0 {
|
||||
return true
|
||||
}
|
||||
return values[0]
|
||||
}
|
||||
|
||||
func resolveBoolPtrDefault(value *bool, fallback bool) bool {
|
||||
if value == nil {
|
||||
return fallback
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func (r adminGameRow) toAdminItem() AdminBaishunGameItem {
|
||||
return AdminBaishunGameItem{
|
||||
ID: r.ID,
|
||||
|
||||
@ -141,6 +141,9 @@ func TestImportAndListRoomGamesUseActiveProfile(t *testing.T) {
|
||||
if prodConfig.Category != baishunRoomCategory {
|
||||
t.Fatalf("imported prod category = %q, want %q", prodConfig.Category, baishunRoomCategory)
|
||||
}
|
||||
if !prodConfig.Showcase {
|
||||
t.Fatalf("imported prod showcase = false, want true by default")
|
||||
}
|
||||
if resp, err := service.ImportCatalogGames(ctx, "LIKEI", baishunProfileTest, []int{9001}); err != nil {
|
||||
t.Fatalf("import test catalog: %v", err)
|
||||
} else if resp.Imported != 1 {
|
||||
@ -171,6 +174,57 @@ func TestImportAndListRoomGamesUseActiveProfile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCatalogGamesCanDefaultToHidden(t *testing.T) {
|
||||
service, db := newTestBaishunService(t)
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
if err := db.Create(&model.BaishunProviderConfig{
|
||||
ID: 1101,
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: baishunProfileProd,
|
||||
Active: true,
|
||||
PlatformBaseURL: "https://prod.example.com",
|
||||
AppID: 111,
|
||||
AppChannel: "prod",
|
||||
AppKey: "prod-key",
|
||||
GSP: 101,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create provider config: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.BaishunGameCatalog{
|
||||
ID: 2101,
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: baishunProfileProd,
|
||||
InternalGameID: "bs_9101",
|
||||
VendorGameID: 9101,
|
||||
Name: "Hidden Game",
|
||||
Status: baishunCatalogEnabled,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create catalog: %v", err)
|
||||
}
|
||||
|
||||
resp, err := service.ImportCatalogGames(ctx, "LIKEI", baishunProfileProd, []int{9101}, false)
|
||||
if err != nil {
|
||||
t.Fatalf("import hidden catalog: %v", err)
|
||||
}
|
||||
if resp.Imported != 1 {
|
||||
t.Fatalf("imported = %d, want 1", resp.Imported)
|
||||
}
|
||||
|
||||
var cfg model.SysGameListConfig
|
||||
if err := db.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", "LIKEI", baishunVendorType, "bs_9101").First(&cfg).Error; err != nil {
|
||||
t.Fatalf("load imported config: %v", err)
|
||||
}
|
||||
if cfg.Showcase {
|
||||
t.Fatalf("imported showcase = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogAddedRequiresExistingGameConfigAndImportRepairsDanglingExt(t *testing.T) {
|
||||
service, db := newTestBaishunService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
@ -174,10 +174,11 @@ type BaishunLaunchAppResponse struct {
|
||||
|
||||
// SyncCatalogRequest 是目录同步入参。
|
||||
type SyncCatalogRequest struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
VendorGameIDs []int `json:"vendorGameIds"`
|
||||
Force bool `json:"force"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
VendorGameIDs []int `json:"vendorGameIds"`
|
||||
Force bool `json:"force"`
|
||||
DefaultShowcase *bool `json:"defaultShowcase"`
|
||||
}
|
||||
|
||||
// SyncCatalogResponse 是目录同步结果统计。
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -45,6 +46,31 @@ func verifySign(actual, expected string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(actual), strings.TrimSpace(expected))
|
||||
}
|
||||
|
||||
func normalizeCallbackToken(token string) string {
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return ""
|
||||
}
|
||||
if decoded, err := url.QueryUnescape(token); err == nil {
|
||||
token = strings.TrimSpace(decoded)
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(token), "bearer ") {
|
||||
token = strings.TrimSpace(token[len("bearer "):])
|
||||
}
|
||||
|
||||
parts := strings.SplitN(token, ".", 2)
|
||||
if len(parts) != 2 || parts[1] == "" {
|
||||
return token
|
||||
}
|
||||
switch len(parts[1]) % 4 {
|
||||
case 2:
|
||||
parts[1] += "=="
|
||||
case 3:
|
||||
parts[1] += "="
|
||||
}
|
||||
return parts[0] + "." + parts[1]
|
||||
}
|
||||
|
||||
func receiptTypeFromType(typed int) string {
|
||||
if typed == 2 {
|
||||
return "INCOME"
|
||||
|
||||
@ -25,6 +25,7 @@ func (s *GameOpenService) HandleQueryUser(ctx context.Context, req QueryUserRequ
|
||||
return resp
|
||||
}
|
||||
|
||||
req.Token = normalizeCallbackToken(req.Token)
|
||||
credential, err := s.java.AuthenticateToken(ctx, req.Token)
|
||||
if err != nil {
|
||||
resp := failResponse(gameOpenCodeTokenInvalid, "token invalid")
|
||||
@ -79,6 +80,7 @@ func (s *GameOpenService) HandleUpdateCoin(ctx context.Context, req UpdateCoinRe
|
||||
return resp
|
||||
}
|
||||
|
||||
req.Token = normalizeCallbackToken(req.Token)
|
||||
credential, err := s.java.AuthenticateToken(ctx, req.Token)
|
||||
if err != nil {
|
||||
resp := failResponse(gameOpenCodeTokenInvalid, "token invalid")
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
type stubGateway struct {
|
||||
credential integration.UserCredential
|
||||
token string
|
||||
authToken string
|
||||
profile integration.UserProfile
|
||||
accountMap map[string]integration.UserProfile
|
||||
balance int64
|
||||
@ -17,7 +18,8 @@ type stubGateway struct {
|
||||
cmd integration.GoldReceiptCommand
|
||||
}
|
||||
|
||||
func (s *stubGateway) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
||||
func (s *stubGateway) AuthenticateToken(_ context.Context, token string) (integration.UserCredential, error) {
|
||||
s.authToken = token
|
||||
return s.credential, nil
|
||||
}
|
||||
|
||||
@ -103,6 +105,48 @@ func TestHandleQueryUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleQueryUserNormalizesCallbackTokenAfterSignVerify(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
want string
|
||||
}{
|
||||
{name: "missing_padding", token: "token-sign.YWJjZA", want: "token-sign.YWJjZA=="},
|
||||
{name: "encoded_padding", token: "token-sign.YWJjZA%3D%3D", want: "token-sign.YWJjZA=="},
|
||||
{name: "encoded_bearer_missing_padding", token: "Bearer%20token-sign.YWJjZA", want: "token-sign.YWJjZA=="},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gateway := &stubGateway{
|
||||
credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"},
|
||||
profile: integration.UserProfile{
|
||||
ID: 1234567,
|
||||
Account: "1234567",
|
||||
},
|
||||
balance: 88,
|
||||
}
|
||||
service := NewGameOpenService(config.Config{
|
||||
GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"},
|
||||
}, nil, gateway)
|
||||
req := QueryUserRequest{
|
||||
GameID: "101",
|
||||
UID: "1234567",
|
||||
Token: tt.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)
|
||||
}
|
||||
if gateway.authToken != tt.want {
|
||||
t.Fatalf("AuthenticateToken() token = %q, want %q", gateway.authToken, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateCoin(t *testing.T) {
|
||||
gateway := &stubGateway{
|
||||
credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"},
|
||||
@ -141,6 +185,41 @@ func TestHandleUpdateCoin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateCoinNormalizesCallbackTokenAfterSignVerify(t *testing.T) {
|
||||
gateway := &stubGateway{
|
||||
credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"},
|
||||
profile: integration.UserProfile{
|
||||
ID: 1234567,
|
||||
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: "Bearer%20token-sign.YWJjZA",
|
||||
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)
|
||||
}
|
||||
if gateway.authToken != "token-sign.YWJjZA==" {
|
||||
t.Fatalf("AuthenticateToken() token = %q, want token-sign.YWJjZA==", gateway.authToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateCoinDuplicateOrderReturns10003(t *testing.T) {
|
||||
gateway := &stubGateway{
|
||||
credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"},
|
||||
|
||||
@ -47,7 +47,11 @@ func (r *Registry) Resolve(key string) (Provider, error) {
|
||||
if r == nil {
|
||||
return nil, common.NewAppError(http.StatusNotFound, "provider_not_found", "game provider not found")
|
||||
}
|
||||
provider, ok := r.providers[normalizeKey(key)]
|
||||
normalizedKey := normalizeKey(key)
|
||||
provider, ok := r.providers[normalizedKey]
|
||||
if !ok {
|
||||
provider, ok = r.providers[normalizeProviderAlias(normalizedKey)]
|
||||
}
|
||||
if !ok {
|
||||
return nil, common.NewAppError(http.StatusNotFound, "provider_not_found", "game provider not found")
|
||||
}
|
||||
@ -227,3 +231,12 @@ func sortRoomGameListItems(items []RoomGameListItem) {
|
||||
func normalizeKey(key string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(key))
|
||||
}
|
||||
|
||||
func normalizeProviderAlias(key string) string {
|
||||
switch normalizeKey(key) {
|
||||
case "LEADER":
|
||||
return "LINGXIAN"
|
||||
default:
|
||||
return normalizeKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,6 +68,18 @@ func TestRegistryResolveIsCaseInsensitive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryResolveMapsLeaderAliasToLingxian(t *testing.T) {
|
||||
registry := NewRegistry(&stubProvider{key: "LINGXIAN", name: "灵仙"})
|
||||
|
||||
provider, err := registry.Resolve("LEADER")
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
if provider.Key() != "LINGXIAN" {
|
||||
t.Fatalf("Resolve() key = %q, want %q", provider.Key(), "LINGXIAN")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryListKeepsRegistrationOrder(t *testing.T) {
|
||||
registry := NewRegistry(
|
||||
&stubProvider{key: "BAISHUN", name: "百顺"},
|
||||
|
||||
@ -67,6 +67,11 @@ func (p *AppProvider) LaunchGame(ctx context.Context, user gameprovider.AuthUser
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uid := launchUID(runtimeCfg, user)
|
||||
token := launchToken(runtimeCfg, user)
|
||||
roomID := launchRoomID(runtimeCfg, req)
|
||||
lang := launchLang(req)
|
||||
sign := launchSign(runtimeCfg, row.VendorGameID, uid, token, roomID)
|
||||
entryURL := buildLaunchURL(row.entryURL(), runtimeCfg, user, req, row.VendorGameID)
|
||||
sessionID := fmt.Sprintf("lx_%d_%s", user.UserID, row.VendorGameID)
|
||||
return &gameprovider.LaunchResponse{
|
||||
@ -81,13 +86,17 @@ func (p *AppProvider) LaunchGame(ctx context.Context, user gameprovider.AuthUser
|
||||
PreviewURL: row.previewURL(),
|
||||
DownloadURL: row.entryURL(),
|
||||
PackageVersion: row.packageVersion(),
|
||||
Orientation: row.orientation(),
|
||||
SafeHeight: row.safeHeight(),
|
||||
},
|
||||
LaunchConfig: map[string]any{
|
||||
"uid": launchUID(runtimeCfg, user),
|
||||
"token": launchToken(runtimeCfg, user),
|
||||
"uid": uid,
|
||||
"token": token,
|
||||
"lang": lang,
|
||||
"gameId": row.VendorGameID,
|
||||
"roomId": req.RoomID,
|
||||
"sign": launchSign(runtimeCfg, row.VendorGameID, launchUID(runtimeCfg, user), launchToken(runtimeCfg, user), req.RoomID),
|
||||
"roomId": roomID,
|
||||
"roomid": roomID,
|
||||
"sign": sign,
|
||||
},
|
||||
RoomState: gameprovider.RoomStateResponse{
|
||||
RoomID: req.RoomID,
|
||||
@ -170,6 +179,8 @@ func (s *Service) baseGameQuery(ctx context.Context, sysOrigin, profile string)
|
||||
ext.package_version AS ext_package_version,
|
||||
ext.preview_url AS ext_preview_url,
|
||||
ext.package_url AS package_url,
|
||||
ext.orientation AS ext_orientation,
|
||||
ext.safe_height AS ext_safe_height,
|
||||
ext.extra_json AS ext_extra_json,
|
||||
cat.name AS catalog_name,
|
||||
cat.cover AS catalog_cover,
|
||||
@ -196,10 +207,14 @@ func (r gameRow) toListItem() gameprovider.RoomGameListItem {
|
||||
LaunchMode: defaultIfBlank(r.LaunchMode, launchModeH5),
|
||||
FullScreen: r.FullScreen,
|
||||
GameMode: 3,
|
||||
SafeHeight: r.safeHeight(),
|
||||
Orientation: r.orientation(),
|
||||
PackageVersion: r.packageVersion(),
|
||||
Status: defaultIfBlank(r.CatalogStatus, catalogEnabled),
|
||||
LaunchParams: map[string]any{
|
||||
"gameType": vendorType,
|
||||
"gameType": vendorType,
|
||||
"lang": defaultLaunchLang,
|
||||
"screenMode": r.screenMode(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -217,6 +232,11 @@ func (r gameRow) previewURL() string {
|
||||
}
|
||||
|
||||
func (r gameRow) entryURL() string {
|
||||
if !r.FullScreen {
|
||||
if halfURL := r.previewURL(); halfURL != "" {
|
||||
return halfURL
|
||||
}
|
||||
}
|
||||
return defaultIfBlank(r.PackageURL, defaultIfBlank(r.CatalogDownloadURL, r.ExtPreviewURL))
|
||||
}
|
||||
|
||||
@ -224,6 +244,24 @@ func (r gameRow) packageVersion() string {
|
||||
return defaultIfBlank(r.ExtPackageVersion, r.CatalogPackageVersion)
|
||||
}
|
||||
|
||||
func (r gameRow) orientation() int {
|
||||
if r.ExtOrientation != nil {
|
||||
return *r.ExtOrientation
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r gameRow) safeHeight() int {
|
||||
return r.ExtSafeHeight
|
||||
}
|
||||
|
||||
func (r gameRow) screenMode() string {
|
||||
if r.FullScreen {
|
||||
return "full"
|
||||
}
|
||||
return "half"
|
||||
}
|
||||
|
||||
func buildLaunchURL(entry string, cfg runtimeConfig, user gameprovider.AuthUser, req gameprovider.LaunchRequest, gameID string) string {
|
||||
parsed, err := url.Parse(strings.TrimSpace(entry))
|
||||
if err != nil {
|
||||
@ -231,12 +269,16 @@ func buildLaunchURL(entry string, cfg runtimeConfig, user gameprovider.AuthUser,
|
||||
}
|
||||
uid := launchUID(cfg, user)
|
||||
token := launchToken(cfg, user)
|
||||
roomID := launchRoomID(cfg, req)
|
||||
lang := launchLang(req)
|
||||
query := parsed.Query()
|
||||
query.Set("uid", uid)
|
||||
query.Set("token", token)
|
||||
query.Set("lang", lang)
|
||||
query.Set("gameId", gameID)
|
||||
query.Set("roomId", req.RoomID)
|
||||
query.Set("sign", launchSign(cfg, gameID, uid, token, req.RoomID))
|
||||
query.Set("roomId", roomID)
|
||||
query.Set("roomid", roomID)
|
||||
query.Set("sign", launchSign(cfg, gameID, uid, token, roomID))
|
||||
parsed.RawQuery = query.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
@ -249,6 +291,22 @@ func launchToken(cfg runtimeConfig, user gameprovider.AuthUser) string {
|
||||
return defaultIfBlank(cfg.TestToken, user.Token)
|
||||
}
|
||||
|
||||
func launchRoomID(cfg runtimeConfig, req gameprovider.LaunchRequest) string {
|
||||
return defaultIfBlank(cfg.TestRoomID, req.RoomID)
|
||||
}
|
||||
|
||||
func launchLang(req gameprovider.LaunchRequest) string {
|
||||
for _, key := range []string{"lang", "language", "locale"} {
|
||||
if req.Params == nil {
|
||||
break
|
||||
}
|
||||
if value := strings.TrimSpace(fmt.Sprint(req.Params[key])); value != "" && value != "<nil>" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return defaultLaunchLang
|
||||
}
|
||||
|
||||
func launchSign(cfg runtimeConfig, gameID, uid, token, roomID string) string {
|
||||
sum := md5.Sum([]byte(gameID + uid + token + roomID + strings.TrimSpace(cfg.AppKey)))
|
||||
return hex.EncodeToString(sum[:])
|
||||
|
||||
@ -207,7 +207,7 @@ func (s *Service) buildCatalogQuery(ctx context.Context, sysOrigin, profile, key
|
||||
return query
|
||||
}
|
||||
|
||||
func (s *Service) ImportCatalogGames(ctx context.Context, sysOrigin, profile string, vendorGameIDs []string) (*ImportCatalogResponse, error) {
|
||||
func (s *Service) ImportCatalogGames(ctx context.Context, sysOrigin, profile string, vendorGameIDs []string, defaultShowcase ...bool) (*ImportCatalogResponse, error) {
|
||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
@ -217,7 +217,7 @@ func (s *Service) ImportCatalogGames(ctx context.Context, sysOrigin, profile str
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
resp, err := s.importCatalogGamesTx(tx, sysOrigin, profile, buildVendorGameIDFilter(vendorGameIDs))
|
||||
resp, err := s.importCatalogGamesTx(tx, sysOrigin, profile, buildVendorGameIDFilter(vendorGameIDs), resolveImportDefaultShowcase(defaultShowcase...))
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
@ -234,7 +234,7 @@ func (s *Service) SyncAdminGames(ctx context.Context, req SyncCatalogRequest) (*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.Profile, req.VendorGameIDs)
|
||||
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.Profile, req.VendorGameIDs, resolveBoolPtrDefault(req.DefaultShowcase, true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -247,7 +247,7 @@ func (s *Service) SyncAdminGames(ctx context.Context, req SyncCatalogRequest) (*
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) importCatalogGamesTx(tx *gorm.DB, sysOrigin, profile string, filter map[string]struct{}) (*ImportCatalogResponse, error) {
|
||||
func (s *Service) importCatalogGamesTx(tx *gorm.DB, sysOrigin, profile string, filter map[string]struct{}, defaultShowcase bool) (*ImportCatalogResponse, error) {
|
||||
query := tx.Where("sys_origin = ? AND profile = ? AND status = ?", sysOrigin, profile, catalogEnabled)
|
||||
if len(filter) > 0 {
|
||||
ids := make([]string, 0, len(filter))
|
||||
@ -300,7 +300,7 @@ func (s *Service) importCatalogGamesTx(tx *gorm.DB, sysOrigin, profile string, f
|
||||
Category: roomCategory,
|
||||
GameCode: vendorGameID,
|
||||
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
||||
Showcase: true,
|
||||
Showcase: defaultShowcase,
|
||||
Sort: parseSort(vendorGameID),
|
||||
FullScreen: true,
|
||||
ClientOrigin: "COMMON",
|
||||
@ -339,6 +339,20 @@ func (s *Service) importCatalogGamesTx(tx *gorm.DB, sysOrigin, profile string, f
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func resolveImportDefaultShowcase(values ...bool) bool {
|
||||
if len(values) == 0 {
|
||||
return true
|
||||
}
|
||||
return values[0]
|
||||
}
|
||||
|
||||
func resolveBoolPtrDefault(value *bool, fallback bool) bool {
|
||||
if value == nil {
|
||||
return fallback
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func buildVendorGameIDFilter(vendorGameIDs []string) map[string]struct{} {
|
||||
filter := make(map[string]struct{}, len(vendorGameIDs))
|
||||
for _, id := range vendorGameIDs {
|
||||
|
||||
@ -3,9 +3,12 @@ package lingxian
|
||||
import (
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/model"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -68,4 +71,259 @@ func TestSyncAdminGamesFetchesCatalogAndImportsUnifiedList(t *testing.T) {
|
||||
if ext.VendorGameID != "1" || ext.PackageURL != "https://example.test/full.html" {
|
||||
t.Fatalf("vendor ext = %#v", ext)
|
||||
}
|
||||
if !cfg.Showcase {
|
||||
t.Fatalf("showcase = false, want true by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCatalogGamesCanDefaultToHidden(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(
|
||||
&model.LingxianProviderConfig{},
|
||||
&model.LingxianGameCatalog{},
|
||||
&model.SysGameListConfig{},
|
||||
&model.SysGameListVendorExt{},
|
||||
); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := db.Create(&model.LingxianProviderConfig{
|
||||
ID: 11,
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: profileProd,
|
||||
Active: true,
|
||||
AppKey: "test-key",
|
||||
TestUID: "test-uid",
|
||||
TestToken: "test-token",
|
||||
TestRoomID: "test-room",
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed provider: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.LingxianGameCatalog{
|
||||
ID: 12,
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: profileProd,
|
||||
InternalGameID: "lx_2",
|
||||
VendorGameID: "2",
|
||||
Name: "Hidden Lingxian",
|
||||
Status: catalogEnabled,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed catalog: %v", err)
|
||||
}
|
||||
|
||||
service := NewService(config.Config{HTTP: config.HTTPConfig{Timeout: time.Second}}, db, nil)
|
||||
resp, err := service.ImportCatalogGames(ctx, "LIKEI", profileProd, []string{"2"}, false)
|
||||
if err != nil {
|
||||
t.Fatalf("ImportCatalogGames: %v", err)
|
||||
}
|
||||
if resp.Imported != 1 {
|
||||
t.Fatalf("imported = %d, want 1", resp.Imported)
|
||||
}
|
||||
|
||||
var cfg model.SysGameListConfig
|
||||
if err := db.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", "LIKEI", vendorType, "lx_2").First(&cfg).Error; err != nil {
|
||||
t.Fatalf("unified config missing: %v", err)
|
||||
}
|
||||
if cfg.Showcase {
|
||||
t.Fatalf("showcase = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveProviderConfigPersistsTestRoomID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.LingxianProviderConfig{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
service := NewService(config.Config{HTTP: config.HTTPConfig{Timeout: time.Second}}, db, nil)
|
||||
saved, err := service.SaveProviderConfig(ctx, SaveProviderConfigRequest{
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: profileProd,
|
||||
GameListURL: "https://example.test/list.json",
|
||||
AppKey: "test-key",
|
||||
TestUID: "test-uid",
|
||||
TestToken: "test-token",
|
||||
TestRoomID: "test-room",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveProviderConfig: %v", err)
|
||||
}
|
||||
if saved.TestRoomID != "test-room" {
|
||||
t.Fatalf("saved testRoomId = %q, want test-room", saved.TestRoomID)
|
||||
}
|
||||
|
||||
runtimeCfg, err := service.resolveRuntimeConfigForProfile(ctx, "LIKEI", profileProd)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveRuntimeConfigForProfile: %v", err)
|
||||
}
|
||||
if runtimeCfg.TestRoomID != "test-room" {
|
||||
t.Fatalf("runtime testRoomId = %q, want test-room", runtimeCfg.TestRoomID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppProviderUsesHalfURLForHalfScreenGames(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(
|
||||
&model.LingxianProviderConfig{},
|
||||
&model.LingxianGameCatalog{},
|
||||
&model.SysGameListConfig{},
|
||||
&model.SysGameListVendorExt{},
|
||||
); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := db.Create(&model.LingxianProviderConfig{
|
||||
ID: 21,
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: profileProd,
|
||||
Active: true,
|
||||
AppKey: "test-key",
|
||||
TestUID: "test-uid",
|
||||
TestToken: "test-token",
|
||||
TestRoomID: "test-room",
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed provider: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.LingxianGameCatalog{
|
||||
ID: 22,
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: profileProd,
|
||||
InternalGameID: "1",
|
||||
VendorGameID: "1",
|
||||
Name: "Half Lingxian",
|
||||
PreviewURL: "https://example.test/half.html",
|
||||
DownloadURL: "https://example.test/full.html",
|
||||
PackageVersion: "11",
|
||||
Status: catalogEnabled,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed catalog: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.SysGameListConfig{
|
||||
ID: 23,
|
||||
SysOrigin: "LIKEI",
|
||||
GameOrigin: vendorType,
|
||||
GameID: "1",
|
||||
Name: "Half Lingxian",
|
||||
Category: roomCategory,
|
||||
GameCode: "1",
|
||||
Showcase: true,
|
||||
Sort: 1,
|
||||
FullScreen: false,
|
||||
ClientOrigin: "COMMON",
|
||||
GameMode: "[3]",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed config: %v", err)
|
||||
}
|
||||
orientation := 1
|
||||
if err := db.Create(&model.SysGameListVendorExt{
|
||||
ID: 24,
|
||||
GameListConfigID: 23,
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: profileProd,
|
||||
VendorType: vendorType,
|
||||
VendorGameID: "1",
|
||||
LaunchMode: launchModeH5,
|
||||
PackageVersion: "11",
|
||||
PackageURL: "https://example.test/full.html",
|
||||
PreviewURL: "https://example.test/half.html",
|
||||
Orientation: &orientation,
|
||||
SafeHeight: 640,
|
||||
BridgeSchemaVersion: "1.0",
|
||||
ExtraJSON: `{"gameType":"LINGXIAN"}`,
|
||||
Enabled: true,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("seed vendor ext: %v", err)
|
||||
}
|
||||
|
||||
service := NewService(config.Config{HTTP: config.HTTPConfig{Timeout: time.Second}}, db, nil)
|
||||
provider := NewAppProvider(service)
|
||||
user := gameprovider.AuthUser{UserID: 1001, SysOrigin: "LIKEI", Token: "user-token"}
|
||||
|
||||
list, err := provider.ListRoomGames(ctx, user, "room-1", roomCategory)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRoomGames: %v", err)
|
||||
}
|
||||
if len(list.Items) != 1 {
|
||||
t.Fatalf("items = %d, want 1", len(list.Items))
|
||||
}
|
||||
item := list.Items[0]
|
||||
if item.FullScreen {
|
||||
t.Fatalf("fullScreen = true, want false")
|
||||
}
|
||||
if item.SafeHeight != 640 {
|
||||
t.Fatalf("safeHeight = %d, want 640", item.SafeHeight)
|
||||
}
|
||||
if item.Orientation != 1 {
|
||||
t.Fatalf("orientation = %d, want 1", item.Orientation)
|
||||
}
|
||||
if got := item.LaunchParams["screenMode"]; got != "half" {
|
||||
t.Fatalf("screenMode = %v, want half", got)
|
||||
}
|
||||
|
||||
launch, err := provider.LaunchGame(ctx, user, gameprovider.LaunchRequest{RoomID: "room-1", GameID: "1"}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("LaunchGame: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(launch.Entry.EntryURL, "https://example.test/half.html?") {
|
||||
t.Fatalf("entryUrl = %q, want half url with query", launch.Entry.EntryURL)
|
||||
}
|
||||
if strings.Contains(launch.Entry.EntryURL, "full.html") {
|
||||
t.Fatalf("entryUrl = %q, should not use full url", launch.Entry.EntryURL)
|
||||
}
|
||||
if launch.Entry.SafeHeight != 640 {
|
||||
t.Fatalf("launch safeHeight = %d, want 640", launch.Entry.SafeHeight)
|
||||
}
|
||||
parsed, err := url.Parse(launch.Entry.EntryURL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse entry url: %v", err)
|
||||
}
|
||||
query := parsed.Query()
|
||||
if got := query.Get("uid"); got != "test-uid" {
|
||||
t.Fatalf("query uid = %q, want test-uid", got)
|
||||
}
|
||||
if got := query.Get("token"); got != "test-token" {
|
||||
t.Fatalf("query token = %q, want test-token", got)
|
||||
}
|
||||
if got := query.Get("lang"); got != defaultLaunchLang {
|
||||
t.Fatalf("query lang = %q, want %s", got, defaultLaunchLang)
|
||||
}
|
||||
if got := query.Get("roomid"); got != "test-room" {
|
||||
t.Fatalf("query roomid = %q, want test-room", got)
|
||||
}
|
||||
if got := query.Get("roomId"); got != "test-room" {
|
||||
t.Fatalf("query roomId = %q, want test-room", got)
|
||||
}
|
||||
if got := launch.LaunchConfig.(map[string]any)["roomid"]; got != "test-room" {
|
||||
t.Fatalf("launchConfig.roomid = %v, want test-room", got)
|
||||
}
|
||||
if got := launch.LaunchConfig.(map[string]any)["lang"]; got != defaultLaunchLang {
|
||||
t.Fatalf("launchConfig.lang = %v, want %s", got, defaultLaunchLang)
|
||||
}
|
||||
if got := launch.RoomState.RoomID; got != "room-1" {
|
||||
t.Fatalf("roomState.roomId = %q, want real room room-1", got)
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,6 +108,7 @@ func (s *Service) GetProviderConfig(ctx context.Context, sysOrigin, profile stri
|
||||
AppKey: runtimeCfg.AppKey,
|
||||
TestUID: runtimeCfg.TestUID,
|
||||
TestToken: runtimeCfg.TestToken,
|
||||
TestRoomID: runtimeCfg.TestRoomID,
|
||||
UpdateTime: formatTime(row.UpdateTime),
|
||||
}, nil
|
||||
}
|
||||
@ -137,6 +138,7 @@ func (s *Service) SaveProviderConfig(ctx context.Context, req SaveProviderConfig
|
||||
AppKey: appKey,
|
||||
TestUID: strings.TrimSpace(req.TestUID),
|
||||
TestToken: strings.TrimSpace(req.TestToken),
|
||||
TestRoomID: strings.TrimSpace(req.TestRoomID),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
@ -153,7 +155,7 @@ func (s *Service) SaveProviderConfig(ctx context.Context, req SaveProviderConfig
|
||||
if err := s.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "profile"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"game_list_url", "app_key", "test_uid", "test_token", "update_time",
|
||||
"game_list_url", "app_key", "test_uid", "test_token", "test_room_id", "update_time",
|
||||
}),
|
||||
}).Create(&row).Error; err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -14,13 +14,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
vendorType = "LINGXIAN"
|
||||
profileProd = "PROD"
|
||||
profileTest = "TEST"
|
||||
launchModeH5 = "H5_REMOTE"
|
||||
catalogEnabled = "ENABLED"
|
||||
roomCategory = "CHAT_ROOM"
|
||||
defaultListURL = "https://sg-test.leadercc.com/yumichat_games/test_game_list.json"
|
||||
vendorType = "LINGXIAN"
|
||||
profileProd = "PROD"
|
||||
profileTest = "TEST"
|
||||
launchModeH5 = "H5_REMOTE"
|
||||
catalogEnabled = "ENABLED"
|
||||
roomCategory = "CHAT_ROOM"
|
||||
defaultLaunchLang = "en-US"
|
||||
defaultListURL = "https://sg-test.leadercc.com/yumichat_games/test_game_list.json"
|
||||
)
|
||||
|
||||
type dbHandle interface {
|
||||
@ -52,6 +53,7 @@ type runtimeConfig struct {
|
||||
AppKey string
|
||||
TestUID string
|
||||
TestToken string
|
||||
TestRoomID string
|
||||
}
|
||||
|
||||
type ProviderConfigItem struct {
|
||||
@ -64,6 +66,7 @@ type ProviderConfigItem struct {
|
||||
AppKey string `json:"appKey"`
|
||||
TestUID string `json:"testUid"`
|
||||
TestToken string `json:"testToken"`
|
||||
TestRoomID string `json:"testRoomId"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
}
|
||||
|
||||
@ -74,6 +77,7 @@ type SaveProviderConfigRequest struct {
|
||||
AppKey string `json:"appKey"`
|
||||
TestUID string `json:"testUid"`
|
||||
TestToken string `json:"testToken"`
|
||||
TestRoomID string `json:"testRoomId"`
|
||||
}
|
||||
|
||||
type ActivateProviderProfileRequest struct {
|
||||
@ -82,10 +86,11 @@ type ActivateProviderProfileRequest struct {
|
||||
}
|
||||
|
||||
type SyncCatalogRequest struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
VendorGameIDs []string `json:"vendorGameIds"`
|
||||
Force bool `json:"force"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
VendorGameIDs []string `json:"vendorGameIds"`
|
||||
Force bool `json:"force"`
|
||||
DefaultShowcase *bool `json:"defaultShowcase"`
|
||||
}
|
||||
|
||||
type SyncCatalogResponse struct {
|
||||
@ -153,6 +158,8 @@ type gameRow struct {
|
||||
ExtPackageVersion string
|
||||
ExtPreviewURL string
|
||||
PackageURL string
|
||||
ExtOrientation *int
|
||||
ExtSafeHeight int
|
||||
ExtExtraJSON string
|
||||
CatalogName string
|
||||
CatalogCover string
|
||||
@ -223,5 +230,6 @@ func configToRuntime(base runtimeConfig, row model.LingxianProviderConfig) runti
|
||||
base.AppKey = defaultIfBlank(row.AppKey, base.AppKey)
|
||||
base.TestUID = strings.TrimSpace(row.TestUID)
|
||||
base.TestToken = strings.TrimSpace(row.TestToken)
|
||||
base.TestRoomID = strings.TrimSpace(row.TestRoomID)
|
||||
return base
|
||||
}
|
||||
|
||||
@ -107,6 +107,52 @@ func (s *WeekStarService) GetPublicPage(ctx context.Context, sysOrigin string) (
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CurrentRanking 返回后台查看用的当前实时榜,实时榜只在当前有生效配置时读取。
|
||||
func (s *WeekStarService) CurrentRanking(ctx context.Context, sysOrigin string, limit int) (*WeekStarCurrentRankResponse, error) {
|
||||
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
now := time.Now().In(s.location)
|
||||
configRow, giftRows, _, err := s.loadActiveConfigBundle(ctx, sysOrigin, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if configRow == nil {
|
||||
return &WeekStarCurrentRankResponse{
|
||||
SysOrigin: sysOrigin,
|
||||
ActivityStatus: weekStarActivityStatusDisabled,
|
||||
Timezone: s.cfg.WeekStar.Timezone,
|
||||
GiftConfigs: []WeekStarGiftConfigPayload{},
|
||||
Records: []WeekStarRankingUserView{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
cycleStart, cycleEnd := s.cycleBounds(now)
|
||||
periodStart := maxTime(cycleStart, s.fromStorageWallClock(configRow.StartAt))
|
||||
periodEnd := minTime(cycleEnd, s.fromStorageWallClock(configRow.EndAt))
|
||||
records, err := s.loadLiveRanking(ctx, configRow.ID, s.cycleKey(cycleStart), int64(limit))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &WeekStarCurrentRankResponse{
|
||||
ConfigID: configRow.ID,
|
||||
SysOrigin: sysOrigin,
|
||||
ActivityStatus: weekStarActivityStatusOngoing,
|
||||
CycleKey: s.cycleKey(cycleStart),
|
||||
PeriodStartAt: formatDateTime(periodStart),
|
||||
PeriodEndAt: formatDateTime(periodEnd),
|
||||
Timezone: configRow.Timezone,
|
||||
GiftConfigs: buildGiftPayloads(giftRows),
|
||||
Records: records,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PageSnapshots 按系统或配置分页查询历史快照,支持跨多周活动查看冻结榜单。
|
||||
func (s *WeekStarService) PageSnapshots(ctx context.Context, sysOrigin string, configID int64, cycleKey string, cursor, limit int) (map[string]any, error) {
|
||||
page, size := normalizePage(cursor, limit)
|
||||
|
||||
102
internal/service/weekstar/page_test.go
Normal file
102
internal/service/weekstar/page_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
package weekstar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chatapp3-golang/internal/model"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func TestCurrentRankingReturnsLiveRank(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
service, db := newTestWeekStarService(t)
|
||||
if err := db.AutoMigrate(&model.UserBaseInfo{}); err != nil {
|
||||
t.Fatalf("auto migrate user_base_info: %v", err)
|
||||
}
|
||||
redisServer := miniredis.RunT(t)
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: redisServer.Addr()})
|
||||
defer redisClient.Close()
|
||||
service.repo.Redis = redisClient
|
||||
|
||||
now := time.Now().In(service.location)
|
||||
cycleStart, cycleEnd := service.cycleBounds(now)
|
||||
configRow := model.WeekStarActivityConfig{
|
||||
ID: 101,
|
||||
SysOrigin: "LIKEI",
|
||||
Enabled: true,
|
||||
StartAt: service.toStorageWallClock(cycleStart.Add(-time.Hour)),
|
||||
EndAt: service.toStorageWallClock(cycleEnd.Add(time.Hour)),
|
||||
Timezone: "Asia/Riyadh",
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
if err := db.Create(&configRow).Error; err != nil {
|
||||
t.Fatalf("create config: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.WeekStarActivityGiftConfig{
|
||||
ConfigID: configRow.ID,
|
||||
GiftID: 2001,
|
||||
GiftName: "Gift A",
|
||||
GiftCandy: 100,
|
||||
Sort: 1,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create gift config: %v", err)
|
||||
}
|
||||
if err := db.Create([]model.UserBaseInfo{
|
||||
{ID: 3001, Account: "3001", UserNickname: "Alice", CountryCode: "SA", CountryName: "Saudi Arabia"},
|
||||
{ID: 3002, Account: "3002", UserNickname: "Bob", CountryCode: "AE", CountryName: "UAE"},
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create users: %v", err)
|
||||
}
|
||||
if err := redisClient.ZAdd(ctx, service.rankRedisKey(configRow.ID, service.cycleKey(cycleStart)),
|
||||
redis.Z{Member: "3001", Score: 250},
|
||||
redis.Z{Member: "3002", Score: 500},
|
||||
).Err(); err != nil {
|
||||
t.Fatalf("zadd rank: %v", err)
|
||||
}
|
||||
|
||||
resp, err := service.CurrentRanking(ctx, "likei", 20)
|
||||
if err != nil {
|
||||
t.Fatalf("CurrentRanking() error = %v", err)
|
||||
}
|
||||
if resp.ActivityStatus != weekStarActivityStatusOngoing {
|
||||
t.Fatalf("ActivityStatus = %s, want %s", resp.ActivityStatus, weekStarActivityStatusOngoing)
|
||||
}
|
||||
if resp.ConfigID != configRow.ID {
|
||||
t.Fatalf("ConfigID = %d, want %d", resp.ConfigID, configRow.ID)
|
||||
}
|
||||
if resp.CycleKey != service.cycleKey(cycleStart) {
|
||||
t.Fatalf("CycleKey = %s, want %s", resp.CycleKey, service.cycleKey(cycleStart))
|
||||
}
|
||||
if len(resp.GiftConfigs) != 1 || resp.GiftConfigs[0].GiftID != 2001 {
|
||||
t.Fatalf("GiftConfigs = %#v", resp.GiftConfigs)
|
||||
}
|
||||
if len(resp.Records) != 2 {
|
||||
t.Fatalf("Records length = %d, want 2", len(resp.Records))
|
||||
}
|
||||
if resp.Records[0].UserID != 3002 || resp.Records[0].Rank != 1 || resp.Records[0].ScoreGold != 500 {
|
||||
t.Fatalf("top record = %#v", resp.Records[0])
|
||||
}
|
||||
if resp.Records[1].UserID != 3001 || resp.Records[1].Rank != 2 || resp.Records[1].ScoreGold != 250 {
|
||||
t.Fatalf("second record = %#v", resp.Records[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentRankingReturnsDisabledWhenNoActiveConfig(t *testing.T) {
|
||||
service, _ := newTestWeekStarService(t)
|
||||
|
||||
resp, err := service.CurrentRanking(context.Background(), "LIKEI", 20)
|
||||
if err != nil {
|
||||
t.Fatalf("CurrentRanking() error = %v", err)
|
||||
}
|
||||
if resp.ActivityStatus != weekStarActivityStatusDisabled {
|
||||
t.Fatalf("ActivityStatus = %s, want %s", resp.ActivityStatus, weekStarActivityStatusDisabled)
|
||||
}
|
||||
if len(resp.Records) != 0 {
|
||||
t.Fatalf("Records length = %d, want 0", len(resp.Records))
|
||||
}
|
||||
}
|
||||
@ -197,6 +197,19 @@ type WeekStarPageResponse struct {
|
||||
RewardPreview []WeekStarRewardConfigPayload `json:"rewardPreview"`
|
||||
}
|
||||
|
||||
// WeekStarCurrentRankResponse 是后台当前实时榜展示对象。
|
||||
type WeekStarCurrentRankResponse struct {
|
||||
ConfigID int64 `json:"configId,string"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
ActivityStatus string `json:"activityStatus"`
|
||||
CycleKey string `json:"cycleKey"`
|
||||
PeriodStartAt string `json:"periodStartAt"`
|
||||
PeriodEndAt string `json:"periodEndAt"`
|
||||
Timezone string `json:"timezone"`
|
||||
GiftConfigs []WeekStarGiftConfigPayload `json:"giftConfigs"`
|
||||
Records []WeekStarRankingUserView `json:"records"`
|
||||
}
|
||||
|
||||
// WeekStarSnapshotView 是后台快照列表的展示对象。
|
||||
type WeekStarSnapshotView struct {
|
||||
ID int64 `json:"id,string"`
|
||||
|
||||
@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS lingxian_provider_config (
|
||||
app_key VARCHAR(255) NOT NULL DEFAULT '',
|
||||
test_uid VARCHAR(64) DEFAULT NULL,
|
||||
test_token VARCHAR(1024) DEFAULT NULL,
|
||||
test_room_id VARCHAR(64) DEFAULT NULL,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_lingxian_provider_sys_origin_profile (sys_origin, profile),
|
||||
|
||||
17
migrations/018_lingxian_test_room_id.sql
Normal file
17
migrations/018_lingxian_test_room_id.sql
Normal file
@ -0,0 +1,17 @@
|
||||
SET @column_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'lingxian_provider_config'
|
||||
AND COLUMN_NAME = 'test_room_id'
|
||||
);
|
||||
|
||||
SET @ddl := IF(
|
||||
@column_exists = 0,
|
||||
'ALTER TABLE lingxian_provider_config ADD COLUMN test_room_id VARCHAR(64) DEFAULT NULL AFTER test_token',
|
||||
'SELECT 1'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
Loading…
x
Reference in New Issue
Block a user