diff --git a/internal/router/game_provider_routes.go b/internal/router/game_provider_routes.go index aa72ebc..447b8cc 100644 --- a/internal/router/game_provider_routes.go +++ b/internal/router/game_provider_routes.go @@ -23,7 +23,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi writeError(c, err) return } - writeOK(c, stripProvidersFromRoomGameItems(resp)) + writeOK(c, publicRoomGameItems(resp)) }) appGroup.GET("/room/list", func(c *gin.Context) { resp, err := registry.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category")) @@ -31,7 +31,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi writeError(c, err) return } - writeOK(c, stripProvidersFromRoomGameListResponse(resp)) + writeOK(c, publicRoomGameListResponse(resp)) }) appGroup.GET("/state", func(c *gin.Context) { resp, err := registry.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId")) @@ -39,7 +39,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi writeError(c, err) return } - writeOK(c, stripProviderFromRoomState(resp)) + writeOK(c, publicRoomState(resp)) }) appGroup.POST("/launch", func(c *gin.Context) { var req gameprovider.LaunchRequest @@ -52,7 +52,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi writeError(c, err) return } - writeOK(c, stripProviderFromLaunch(resp)) + writeOK(c, publicLaunch(resp)) }) appGroup.POST("/close", func(c *gin.Context) { var req gameprovider.CloseRequest @@ -65,7 +65,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi writeError(c, err) return } - writeOK(c, stripProviderFromRoomState(resp)) + writeOK(c, publicRoomState(resp)) }) providerGroup := engine.Group("/app/game/providers") @@ -154,42 +154,138 @@ func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gamep return provider, true } -func stripProvidersFromRoomGameItems(items []gameprovider.RoomGameListItem) []gameprovider.RoomGameListItem { +type publicRoomGameItem struct { + ID int64 `json:"id"` + GameID string `json:"gameId"` + GameType string `json:"gameType,omitempty"` + Provider string `json:"provider,omitempty"` + VendorType string `json:"vendorType,omitempty"` + ProviderGameID string `json:"providerGameId,omitempty"` + VendorGameID string `json:"vendorGameId,omitempty"` + Name string `json:"name"` + Cover string `json:"cover"` + Category string `json:"category,omitempty"` + Sort int64 `json:"sort,omitempty"` + LaunchMode string `json:"launchMode,omitempty"` + FullScreen bool `json:"fullScreen"` + GameMode int `json:"gameMode,omitempty"` + SafeHeight int `json:"safeHeight,omitempty"` + Orientation int `json:"orientation,omitempty"` + PackageVersion string `json:"packageVersion,omitempty"` + Status string `json:"status,omitempty"` + LaunchParams map[string]any `json:"launchParams,omitempty"` +} + +type publicRoomGameList struct { + Items []publicRoomGameItem `json:"items"` +} + +type publicRoomStateResponse struct { + RoomID string `json:"roomId"` + State string `json:"state"` + Provider string `json:"provider,omitempty"` + VendorType string `json:"vendorType,omitempty"` + GameSessionID string `json:"gameSessionId,omitempty"` + CurrentGameID string `json:"currentGameId,omitempty"` + CurrentProviderGameID string `json:"currentProviderGameId,omitempty"` + CurrentVendorGameID string `json:"currentVendorGameId,omitempty"` + CurrentGameName string `json:"currentGameName,omitempty"` + CurrentGameCover string `json:"currentGameCover,omitempty"` + HostUserID int64 `json:"hostUserId,omitempty"` +} + +type publicLaunchResponse struct { + ID int64 `json:"id,omitempty"` + GameSessionID string `json:"gameSessionId"` + Provider string `json:"provider,omitempty"` + VendorType string `json:"vendorType,omitempty"` + GameID string `json:"gameId"` + ProviderGameID string `json:"providerGameId,omitempty"` + VendorGameID string `json:"vendorGameId,omitempty"` + Entry gameprovider.LaunchEntry `json:"entry"` + LaunchConfig any `json:"launchConfig,omitempty"` + BridgeConfig any `json:"bridgeConfig,omitempty"` + RoomState publicRoomStateResponse `json:"roomState"` +} + +func publicRoomGameItems(items []gameprovider.RoomGameListItem) []publicRoomGameItem { if len(items) == 0 { - return []gameprovider.RoomGameListItem{} + return []publicRoomGameItem{} } - result := make([]gameprovider.RoomGameListItem, 0, len(items)) + result := make([]publicRoomGameItem, 0, len(items)) for _, item := range items { - item.Provider = "" - result = append(result, item) + result = append(result, publicRoomGameItem{ + ID: item.ID, + GameID: item.GameID, + GameType: item.GameType, + Provider: item.Provider, + VendorType: item.Provider, + ProviderGameID: item.ProviderGameID, + VendorGameID: item.ProviderGameID, + Name: item.Name, + Cover: item.Cover, + Category: item.Category, + Sort: item.Sort, + LaunchMode: item.LaunchMode, + FullScreen: item.FullScreen, + GameMode: item.GameMode, + SafeHeight: item.SafeHeight, + Orientation: item.Orientation, + PackageVersion: item.PackageVersion, + Status: item.Status, + LaunchParams: item.LaunchParams, + }) } return result } -func stripProvidersFromRoomGameListResponse(resp *gameprovider.RoomGameListResponse) *gameprovider.RoomGameListResponse { +func publicRoomGameListResponse(resp *gameprovider.RoomGameListResponse) *publicRoomGameList { if resp == nil { - return &gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}} + return &publicRoomGameList{Items: []publicRoomGameItem{}} } - return &gameprovider.RoomGameListResponse{ - Items: stripProvidersFromRoomGameItems(resp.Items), + return &publicRoomGameList{ + Items: publicRoomGameItems(resp.Items), } } -func stripProviderFromRoomState(resp *gameprovider.RoomStateResponse) *gameprovider.RoomStateResponse { +func publicRoomState(resp *gameprovider.RoomStateResponse) *publicRoomStateResponse { if resp == nil { return nil } - copied := *resp - copied.Provider = "" - return &copied + return &publicRoomStateResponse{ + RoomID: resp.RoomID, + State: resp.State, + Provider: resp.Provider, + VendorType: resp.Provider, + GameSessionID: resp.GameSessionID, + CurrentGameID: resp.CurrentGameID, + CurrentProviderGameID: resp.CurrentProviderGameID, + CurrentVendorGameID: resp.CurrentProviderGameID, + CurrentGameName: resp.CurrentGameName, + CurrentGameCover: resp.CurrentGameCover, + HostUserID: resp.HostUserID, + } } -func stripProviderFromLaunch(resp *gameprovider.LaunchResponse) *gameprovider.LaunchResponse { +func publicLaunch(resp *gameprovider.LaunchResponse) *publicLaunchResponse { if resp == nil { return nil } - copied := *resp - copied.Provider = "" - copied.RoomState = *stripProviderFromRoomState(&resp.RoomState) - return &copied + roomState := publicRoomState(&resp.RoomState) + if roomState == nil { + roomState = &publicRoomStateResponse{} + } + return &publicLaunchResponse{ + ID: resp.ID, + GameSessionID: resp.GameSessionID, + Provider: resp.Provider, + VendorType: resp.Provider, + GameID: resp.GameID, + ProviderGameID: resp.ProviderGameID, + VendorGameID: resp.ProviderGameID, + Entry: resp.Entry, + LaunchConfig: resp.LaunchConfig, + BridgeConfig: resp.LaunchConfig, + RoomState: *roomState, + } } diff --git a/internal/router/game_provider_routes_test.go b/internal/router/game_provider_routes_test.go new file mode 100644 index 0000000..6ccdd58 --- /dev/null +++ b/internal/router/game_provider_routes_test.go @@ -0,0 +1,80 @@ +package router + +import ( + "testing" + + "chatapp3-golang/internal/service/gameprovider" +) + +func TestPublicRoomGameListResponseKeepsLegacyProviderFields(t *testing.T) { + resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{ + Items: []gameprovider.RoomGameListItem{ + { + ID: 9527, + GameID: "bs_1046", + GameType: "BAISHUN", + Provider: "BAISHUN", + ProviderGameID: "1046", + Name: "Lucky77Pro", + }, + }, + }) + + if len(resp.Items) != 1 { + t.Fatalf("items = %d, want 1", len(resp.Items)) + } + item := resp.Items[0] + if item.Provider != "BAISHUN" { + t.Fatalf("provider = %q, want BAISHUN", item.Provider) + } + if item.VendorType != "BAISHUN" { + t.Fatalf("vendorType = %q, want BAISHUN", item.VendorType) + } + if item.ProviderGameID != "1046" { + t.Fatalf("providerGameId = %q, want 1046", item.ProviderGameID) + } + if item.VendorGameID != "1046" { + t.Fatalf("vendorGameId = %q, want 1046", item.VendorGameID) + } +} + +func TestPublicLaunchKeepsLegacyBridgeAndProviderFields(t *testing.T) { + launchConfig := map[string]any{"code": "launch-code"} + resp := publicLaunch(&gameprovider.LaunchResponse{ + ID: 9527, + GameSessionID: "bs_room_1", + Provider: "BAISHUN", + GameID: "bs_1046", + ProviderGameID: "1046", + LaunchConfig: launchConfig, + RoomState: gameprovider.RoomStateResponse{ + RoomID: "room-1", + State: "PLAYING", + Provider: "BAISHUN", + CurrentProviderGameID: "1046", + }, + }) + + if resp.Provider != "BAISHUN" { + t.Fatalf("provider = %q, want BAISHUN", resp.Provider) + } + if resp.VendorType != "BAISHUN" { + t.Fatalf("vendorType = %q, want BAISHUN", resp.VendorType) + } + if resp.VendorGameID != "1046" { + t.Fatalf("vendorGameId = %q, want 1046", resp.VendorGameID) + } + bridgeConfig, ok := resp.BridgeConfig.(map[string]any) + if !ok { + t.Fatalf("bridgeConfig type = %T, want map[string]any", resp.BridgeConfig) + } + if got := bridgeConfig["code"]; got != "launch-code" { + t.Fatalf("bridgeConfig[code] = %v, want launch-code", got) + } + if resp.RoomState.Provider != "BAISHUN" { + t.Fatalf("roomState.provider = %q, want BAISHUN", resp.RoomState.Provider) + } + if resp.RoomState.CurrentVendorGameID != "1046" { + t.Fatalf("roomState.currentVendorGameId = %q, want 1046", resp.RoomState.CurrentVendorGameID) + } +} diff --git a/internal/service/baishun/admin.go b/internal/service/baishun/admin.go index 96d808e..649c6ce 100644 --- a/internal/service/baishun/admin.go +++ b/internal/service/baishun/admin.go @@ -202,6 +202,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun tx.Rollback() return nil, err } + showcase := resolveAdminShowcase(req.Showcase, cfg.Showcase, cfg.ID == 0) cfgValues := map[string]any{ "sys_origin": sysOrigin, @@ -212,7 +213,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun "game_code": gameCode, "cover": cover, "amounts": strings.TrimSpace(req.Amounts), - "is_showcase": req.Showcase, + "is_showcase": showcase, "sort": req.Sort, "height": req.Height, "width": req.Width, @@ -238,7 +239,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun GameCode: gameCode, Cover: cover, Amounts: strings.TrimSpace(req.Amounts), - Showcase: req.Showcase, + Showcase: showcase, Sort: req.Sort, Height: req.Height, Width: req.Width, @@ -585,7 +586,7 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, fil Category: "OTHER", GameCode: vendorGameID, Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL), - Showcase: false, + Showcase: true, Sort: int64(catalog.VendorGameID), FullScreen: true, ClientOrigin: "COMMON", @@ -687,6 +688,16 @@ func normalizePageLimit(limit int) int { return limit } +func resolveAdminShowcase(showcase *bool, current bool, isCreate bool) bool { + if showcase != nil { + return *showcase + } + if isCreate { + return true + } + return current +} + func defaultAdminGSP(catalogGSP string, fallback int) string { if strings.TrimSpace(catalogGSP) != "" { return strings.TrimSpace(catalogGSP) diff --git a/internal/service/baishun/admin_test.go b/internal/service/baishun/admin_test.go new file mode 100644 index 0000000..1718896 --- /dev/null +++ b/internal/service/baishun/admin_test.go @@ -0,0 +1,24 @@ +package baishun + +import "testing" + +func TestResolveAdminShowcase(t *testing.T) { + t.Run("create defaults to enabled", func(t *testing.T) { + if got := resolveAdminShowcase(nil, false, true); !got { + t.Fatalf("resolveAdminShowcase(nil, false, true) = %v, want true", got) + } + }) + + t.Run("update keeps current when omitted", func(t *testing.T) { + if got := resolveAdminShowcase(nil, false, false); got { + t.Fatalf("resolveAdminShowcase(nil, false, false) = %v, want false", got) + } + }) + + t.Run("explicit false remains false", func(t *testing.T) { + showcase := false + if got := resolveAdminShowcase(&showcase, true, true); got { + t.Fatalf("resolveAdminShowcase(false, true, true) = %v, want false", got) + } + }) +} diff --git a/internal/service/baishun/launch_fallback_test.go b/internal/service/baishun/launch_fallback_test.go new file mode 100644 index 0000000..b52e57f --- /dev/null +++ b/internal/service/baishun/launch_fallback_test.go @@ -0,0 +1,118 @@ +package baishun + +import ( + "context" + "testing" + + "chatapp3-golang/internal/config" + "chatapp3-golang/internal/integration" + "chatapp3-golang/internal/model" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type stubGateway struct{} + +func (stubGateway) GetUserProfile(context.Context, int64) (integration.UserProfile, error) { + return integration.UserProfile{}, nil +} + +func (stubGateway) MapGoldBalance(context.Context, []int64) (map[int64]int64, error) { + return map[int64]int64{}, nil +} + +func (stubGateway) GetUserLanguage(context.Context, int64) (string, error) { + return "en", nil +} + +func (stubGateway) ExistsGoldEvent(context.Context, string) (bool, error) { + return false, nil +} + +func (stubGateway) ChangeGoldBalance(context.Context, integration.GoldReceiptCommand) error { + return nil +} + +func newTestBaishunService(t *testing.T) (*BaishunService, *gorm.DB) { + t.Helper() + + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if err := db.AutoMigrate( + &model.SysGameListConfig{}, + &model.SysGameListVendorExt{}, + &model.BaishunProviderConfig{}, + &model.BaishunGameCatalog{}, + &model.BaishunLaunchSession{}, + &model.BaishunRoomState{}, + ); err != nil { + t.Fatalf("auto migrate: %v", err) + } + + service := NewBaishunService(config.Config{ + Baishun: config.BaishunConfig{ + PlatformBaseURL: "https://mesh-channels-online.jieyou.shop", + AppID: 7485318192, + AppName: "yumiparty_app", + AppChannel: "yumiparty", + AppKey: "test-app-key", + GSP: 111, + LaunchCodeTTLSeconds: 300, + SSTokenTTLSeconds: 86400, + }, + }, db, nil, stubGateway{}) + return service, db +} + +func TestLaunchGameFallsBackToCatalogWhenConfigMissing(t *testing.T) { + service, db := newTestBaishunService(t) + + if err := db.Create(&model.BaishunGameCatalog{ + ID: 1, + SysOrigin: "LIKEI", + InternalGameID: "bs_1146", + VendorGameID: 1146, + Name: "LordOfOlympus", + Cover: "https://cdn.example.com/cover.png", + PreviewURL: "https://cdn.example.com/preview.png", + DownloadURL: "https://cdn.example.com/game/index.html", + PackageVersion: "1.0.0", + SafeHeight: 24, + GSP: "222", + Status: baishunCatalogEnabled, + }).Error; err != nil { + t.Fatalf("create catalog: %v", err) + } + + resp, err := service.LaunchGame(context.Background(), AuthUser{ + UserID: 1234567, + SysOrigin: "LIKEI", + }, BaishunLaunchAppRequest{ + RoomID: "room-1", + GameID: "bs_1146", + SceneMode: 1, + ClientOrigin: "COMMON", + }, "127.0.0.1") + if err != nil { + t.Fatalf("LaunchGame() error = %v", err) + } + + if resp.GameID != "bs_1146" { + t.Fatalf("gameId = %q, want bs_1146", resp.GameID) + } + if resp.VendorGameID != 1146 { + t.Fatalf("vendorGameId = %d, want 1146", resp.VendorGameID) + } + if resp.Entry.EntryURL != "https://cdn.example.com/game/index.html" { + t.Fatalf("entryUrl = %q, want catalog download url", resp.Entry.EntryURL) + } + if resp.BridgeConfig.GSP != 222 { + t.Fatalf("gsp = %d, want 222", resp.BridgeConfig.GSP) + } + if resp.RoomState.State != baishunRoomStatePlaying { + t.Fatalf("roomState.state = %q, want %q", resp.RoomState.State, baishunRoomStatePlaying) + } +} diff --git a/internal/service/baishun/room.go b/internal/service/baishun/room.go index f276dd6..2034879 100644 --- a/internal/service/baishun/room.go +++ b/internal/service/baishun/room.go @@ -3,6 +3,7 @@ package baishun import ( "chatapp3-golang/internal/model" "context" + "fmt" "net/http" "strconv" "strings" @@ -155,6 +156,33 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri if strings.TrimSpace(row.InternalGameID) != "" { return row, nil } + + var catalog model.BaishunGameCatalog + if err := s.repo.DB.WithContext(ctx). + Where("sys_origin = ? AND internal_game_id = ?", sysOrigin, gameID). + First(&catalog).Error; err == nil { + return gameListRow{ + SysOrigin: sysOrigin, + InternalGameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)), + Name: catalog.Name, + Cover: catalog.Cover, + VendorType: baishunVendorType, + VendorGameID: strconv.Itoa(catalog.VendorGameID), + LaunchMode: baishunLaunchModeRemote, + ExtGSP: catalog.GSP, + CatalogCover: catalog.Cover, + CatalogPreviewURL: catalog.PreviewURL, + CatalogDownloadURL: catalog.DownloadURL, + CatalogPackageVersion: catalog.PackageVersion, + CatalogSafeHeight: catalog.SafeHeight, + CatalogOrientation: catalog.Orientation, + CatalogStatus: catalog.Status, + GameModeRaw: formatAdminGameModeRaw(baishunFixedGameMode), + }, nil + } else if err != gorm.ErrRecordNotFound { + return gameListRow{}, err + } + return gameListRow{}, NewAppError(http.StatusNotFound, "game_not_found", "room game config not found") } diff --git a/internal/service/baishun/types.go b/internal/service/baishun/types.go index 53e320e..2ffed1b 100644 --- a/internal/service/baishun/types.go +++ b/internal/service/baishun/types.go @@ -239,7 +239,7 @@ type SaveAdminBaishunGameRequest struct { GameCode string `json:"gameCode"` Cover string `json:"cover"` Amounts string `json:"amounts"` - Showcase bool `json:"showcase"` + Showcase *bool `json:"showcase"` Sort int64 `json:"sort"` Height float64 `json:"height"` Width float64 `json:"width"`