package router import ( "errors" "net/http" "strings" "chatapp3-golang/internal/common" "chatapp3-golang/internal/service/gameprovider" "github.com/gin-gonic/gin" ) // registerGameProviderRoutes 注册统一游戏厂商 app 路由。 func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, registry *gameprovider.Registry, visibility *gameprovider.VisibilityPolicy) { if registry == nil { return } appGroup := engine.Group("/app/game") appGroup.Use(authMiddleware(javaClient)) appGroup.GET("/room/shortcut", func(c *gin.Context) { user, visible, ok := gameListAccess(c, visibility) if !ok { return } if !visible { writeOK(c, []publicRoomGameItem{}) return } resp, err := registry.ListShortcutGames(c.Request.Context(), user, c.Query("roomId")) if err != nil { writeError(c, err) return } writeOK(c, publicRoomGameItems(resp)) }) appGroup.GET("/room/list", func(c *gin.Context) { user, visible, ok := gameListAccess(c, visibility) if !ok { return } if !visible { writeOK(c, publicRoomGameListResponse(&gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}})) return } resp, err := registry.ListRoomGames(c.Request.Context(), user, c.Query("roomId"), c.Query("category")) if err != nil { writeError(c, err) return } writeOK(c, publicRoomGameListResponse(resp)) }) appGroup.GET("/state", func(c *gin.Context) { resp, err := registry.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId")) if err != nil { writeError(c, err) return } writeOK(c, publicRoomState(resp)) }) appGroup.POST("/launch", func(c *gin.Context) { var req gameprovider.LaunchRequest if err := c.ShouldBindJSON(&req); err != nil { writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) return } resp, err := registry.LaunchGame(c.Request.Context(), mustAuthUser(c), req, resolveClientIP(c)) if err != nil { writeError(c, err) return } writeOK(c, publicLaunch(resp)) }) appGroup.POST("/close", func(c *gin.Context) { var req gameprovider.CloseRequest if err := c.ShouldBindJSON(&req); err != nil { writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) return } resp, err := registry.CloseGame(c.Request.Context(), mustAuthUser(c), req) if err != nil { writeError(c, err) return } writeOK(c, publicRoomState(resp)) }) providerGroup := engine.Group("/app/game/providers") providerGroup.Use(authMiddleware(javaClient)) providerGroup.GET("", func(c *gin.Context) { _, visible, ok := gameListAccess(c, visibility) if !ok { return } if !visible { writeOK(c, gameprovider.ProviderListResponse{Items: []gameprovider.ProviderSummary{}}) return } writeOK(c, registry.List()) }) providerGroup.GET("/:provider/room/shortcut", func(c *gin.Context) { user, visible, ok := gameListAccess(c, visibility) if !ok { return } if !visible { writeOK(c, gin.H{"items": []publicRoomGameItem{}}) return } provider, ok := resolveGameProvider(c, registry) if !ok { return } resp, err := provider.ListShortcutGames(c.Request.Context(), user, c.Query("roomId")) if err != nil { writeError(c, err) return } writeOK(c, gin.H{"items": publicRoomGameItems(resp)}) }) providerGroup.GET("/:provider/room/list", func(c *gin.Context) { user, visible, ok := gameListAccess(c, visibility) if !ok { return } if !visible { writeOK(c, publicRoomGameListResponse(&gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}})) return } provider, ok := resolveGameProvider(c, registry) if !ok { return } resp, err := provider.ListRoomGames(c.Request.Context(), user, c.Query("roomId"), c.Query("category")) if err != nil { writeError(c, err) return } writeOK(c, publicRoomGameListResponse(resp)) }) providerGroup.GET("/:provider/state", func(c *gin.Context) { provider, ok := resolveGameProvider(c, registry) if !ok { return } resp, err := provider.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId")) if err != nil { writeError(c, err) return } writeOK(c, publicRoomState(resp)) }) providerGroup.POST("/:provider/launch", func(c *gin.Context) { provider, ok := resolveGameProvider(c, registry) if !ok { return } var req gameprovider.LaunchRequest if err := c.ShouldBindJSON(&req); err != nil { writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) return } resp, err := launchProviderGame(c, registry, provider, req) if err != nil { writeError(c, err) return } writeOK(c, publicLaunch(resp)) }) providerGroup.POST("/:provider/close", func(c *gin.Context) { provider, ok := resolveGameProvider(c, registry) if !ok { return } var req gameprovider.CloseRequest if err := c.ShouldBindJSON(&req); err != nil { writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) return } resp, err := provider.CloseGame(c.Request.Context(), mustAuthUser(c), req) if err != nil { writeError(c, err) return } writeOK(c, publicRoomState(resp)) }) } func gameListAccess(c *gin.Context, visibility *gameprovider.VisibilityPolicy) (common.AuthUser, bool, bool) { user := mustAuthUser(c) // iOS 审核和上架场景需要隐藏游戏入口;这里放在统一列表门禁最前面,避免继续请求 Java 区域/等级接口或 provider SQL。 if isIOSGameListClient(c) { return user, false, true } if gameprovider.IsEmptyGameListUser(user.UserID) { return user, false, true } if visibility == nil { return user, true, true } // 游戏列表只在列表入口做展示门禁,结果里的区域继续向下传给 provider SQL 做 regions 过滤。 result, err := visibility.Evaluate(c.Request.Context(), user) if err != nil { writeError(c, err) return user, false, false } user.RegionID = result.RegionID user.RegionCode = result.RegionCode return user, result.Allow, true } func isIOSGameListClient(c *gin.Context) bool { return strings.EqualFold(strings.TrimSpace(c.GetHeader("Req-Client")), "ios") } func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gameprovider.Provider, bool) { provider, err := registry.Resolve(c.Param("provider")) if err != nil { writeError(c, err) return nil, false } return provider, true } func launchProviderGame(c *gin.Context, registry *gameprovider.Registry, provider gameprovider.Provider, req gameprovider.LaunchRequest) (*gameprovider.LaunchResponse, error) { user := mustAuthUser(c) clientIP := resolveClientIP(c) resp, err := provider.LaunchGame(c.Request.Context(), user, req, clientIP) if err == nil { return resp, nil } if !isGameNotFoundError(err) { return nil, err } // 旧客户端只能把热游列表项伪装成 LEADER 进入已有 WebView;这里再按 gameId 回到真实 provider,避免 LEADER 路由找不到 hg_*。 return registry.LaunchGame(c.Request.Context(), user, req, clientIP) } func isGameNotFoundError(err error) bool { var appErr *common.AppError return errors.As(err, &appErr) && appErr.Code == "game_not_found" } 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 []publicRoomGameItem{} } 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: gameType, Provider: provider, VendorType: 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: 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{}} } return &publicRoomGameList{ Items: publicRoomGameItems(resp.Items), } } func publicRoomState(resp *gameprovider.RoomStateResponse) *publicRoomStateResponse { if resp == nil { return nil } return &publicRoomStateResponse{ RoomID: resp.RoomID, State: resp.State, Provider: appCompatProviderName(resp.Provider), VendorType: appCompatProviderName(resp.Provider), GameSessionID: resp.GameSessionID, CurrentGameID: resp.CurrentGameID, CurrentProviderGameID: resp.CurrentProviderGameID, CurrentVendorGameID: resp.CurrentProviderGameID, CurrentGameName: resp.CurrentGameName, CurrentGameCover: resp.CurrentGameCover, HostUserID: resp.HostUserID, } } func publicLaunch(resp *gameprovider.LaunchResponse) *publicLaunchResponse { if resp == nil { return nil } roomState := publicRoomState(&resp.RoomState) if roomState == nil { roomState = &publicRoomStateResponse{} } return &publicLaunchResponse{ ID: resp.ID, GameSessionID: resp.GameSessionID, Provider: appCompatProviderName(resp.Provider), VendorType: appCompatProviderName(resp.Provider), GameID: resp.GameID, ProviderGameID: resp.ProviderGameID, VendorGameID: resp.ProviderGameID, Entry: resp.Entry, LaunchConfig: resp.LaunchConfig, BridgeConfig: resp.LaunchConfig, RoomState: *roomState, } } func appCompatProviderName(provider string) string { // 客户端历史上只认识 LEADER,不认识内部厂商名 LINGXIAN,所以出参继续保持旧字段值。 if strings.EqualFold(strings.TrimSpace(provider), "LINGXIAN") { return "LEADER" } // 旧客户端没有 HOTGAME 页面;热游先按 LEADER 展示并进入已有 WebView,launch 路由再按 hg_* 分发回真实热游 provider。 switch strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(provider), "_", "")) { case "HOTGAME", "REYOU", "LALU": return "LEADER" } return provider }