diff --git a/internal/model/baishun_models.go b/internal/model/baishun_models.go index e53ad71..05e032a 100644 --- a/internal/model/baishun_models.go +++ b/internal/model/baishun_models.go @@ -4,20 +4,23 @@ import "time" // SysGameListConfig 是系统游戏列表主表,描述平台可展示的游戏。 type SysGameListConfig struct { - ID int64 `gorm:"column:id;primaryKey"` // 主键 - SysOrigin string `gorm:"column:sys_origin;size:32"` // 系统标识 - GameOrigin string `gorm:"column:game_origin;size:32"` // 游戏来源 - GameID string `gorm:"column:game_id;size:64"` // 内部游戏 ID - Name string `gorm:"column:name;size:128"` // 游戏名称 - Category string `gorm:"column:category;size:64"` // 分类 - GameCode string `gorm:"column:game_code;size:512"` // 游戏码或入口标识 - Cover string `gorm:"column:cover;size:1024"` // 封面图 - Showcase bool `gorm:"column:is_showcase"` // 是否首页推荐 - Sort int64 `gorm:"column:sort"` // 排序值 - FullScreen bool `gorm:"column:full_screen"` // 是否全屏展示 - ClientOrigin string `gorm:"column:client_origin;size:32"` // 客户端来源 - Regions string `gorm:"column:regions;size:255"` // 可投放地区 - GameMode string `gorm:"column:game_mode;size:255"` // 游戏模式配置 + ID int64 `gorm:"column:id;primaryKey"` // 主键 + SysOrigin string `gorm:"column:sys_origin;size:32"` // 系统标识 + GameOrigin string `gorm:"column:game_origin;size:32"` // 游戏来源 + GameID string `gorm:"column:game_id;size:64"` // 内部游戏 ID + Name string `gorm:"column:name;size:128"` // 游戏名称 + Category string `gorm:"column:category;size:64"` // 分类 + GameCode string `gorm:"column:game_code;size:512"` // 游戏码或入口标识 + Cover string `gorm:"column:cover;size:1024"` // 封面图 + Amounts string `gorm:"column:amounts;size:512"` // 金额配置 + Showcase bool `gorm:"column:is_showcase"` // 是否首页推荐 + Sort int64 `gorm:"column:sort"` // 排序值 + Height float64 `gorm:"column:height"` // 高度 + Width float64 `gorm:"column:width"` // 宽度 + FullScreen bool `gorm:"column:full_screen"` // 是否全屏展示 + ClientOrigin string `gorm:"column:client_origin;size:32"` // 客户端来源 + Regions string `gorm:"column:regions;size:255"` // 可投放地区 + GameMode string `gorm:"column:game_mode;size:255"` // 游戏模式配置 } // TableName 返回系统游戏列表表名。 diff --git a/internal/router/baishun_routes.go b/internal/router/baishun_routes.go index c9c8914..cf6a91a 100644 --- a/internal/router/baishun_routes.go +++ b/internal/router/baishun_routes.go @@ -109,6 +109,73 @@ func registerBaishunRoutes( writeOK(c, resp) }) + consoleGroup := engine.Group("/operate/baishun-game") + consoleGroup.Use(consoleAuthMiddleware(javaClient)) + consoleGroup.GET("/page", func(c *gin.Context) { + cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1"))) + limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20"))) + resp, err := baishunService.PageAdminGames( + c.Request.Context(), + c.Query("sysOrigin"), + c.Query("keyword"), + parseOptionalBool(c.Query("showcase")), + cursor, + limit, + ) + if err != nil { + writeError(c, err) + return + } + writeOK(c, resp) + }) + consoleGroup.POST("/save", func(c *gin.Context) { + var req baishun.SaveAdminBaishunGameRequest + if err := c.ShouldBindJSON(&req); err != nil { + writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) + return + } + resp, err := baishunService.SaveAdminGame(c.Request.Context(), req) + if err != nil { + writeError(c, err) + return + } + writeOK(c, resp) + }) + consoleGroup.DELETE("", func(c *gin.Context) { + id, _ := strconv.ParseInt(strings.TrimSpace(c.Query("id")), 10, 64) + if err := baishunService.DeleteAdminGame(c.Request.Context(), c.Query("sysOrigin"), id); err != nil { + writeError(c, err) + return + } + writeOK(c, gin.H{"deleted": true}) + }) + consoleGroup.POST("/import-catalog", func(c *gin.Context) { + var req baishun.SyncCatalogRequest + if err := c.ShouldBindJSON(&req); err != nil { + writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) + return + } + resp, err := baishunService.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.VendorGameIDs) + if err != nil { + writeError(c, err) + return + } + writeOK(c, resp) + }) + consoleGroup.POST("/sync", func(c *gin.Context) { + var req baishun.SyncCatalogRequest + if err := c.ShouldBindJSON(&req); err != nil { + writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) + return + } + resp, err := baishunService.SyncAdminGames(c.Request.Context(), req) + if err != nil { + writeError(c, err) + return + } + writeOK(c, resp) + }) + callbackGroup := engine.Group("/game/baishun") callbackGroup.POST("/token", func(c *gin.Context) { raw, payload := readRawPayload(c) @@ -268,3 +335,16 @@ func asInt64(value any) int64 { return parsed } } + +func parseOptionalBool(raw string) *bool { + switch strings.ToLower(strings.TrimSpace(raw)) { + case "0", "false": + value := false + return &value + case "1", "true": + value := true + return &value + default: + return nil + } +} diff --git a/internal/service/baishun/admin.go b/internal/service/baishun/admin.go new file mode 100644 index 0000000..4ac6f3a --- /dev/null +++ b/internal/service/baishun/admin.go @@ -0,0 +1,720 @@ +package baishun + +import ( + "chatapp3-golang/internal/model" + "chatapp3-golang/internal/utils" + "context" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "gorm.io/gorm" +) + +const adminTimeLayout = "2006-01-02 15:04:05" + +type adminGameRow struct { + ID int64 + SysOrigin string + InternalGameID string + Name string + Category string + GameCode string + Cover string + Amounts string + Showcase bool + Sort int64 + Height float64 + Width float64 + FullScreen bool + ClientOrigin string + Regions string + GameModeRaw string + CreateTime time.Time + UpdateTime time.Time + VendorGameID string + LaunchMode string + ExtPackageVersion string + ExtPreviewURL string + PackageURL string + ExtOrientation *int + ExtSafeHeight int + ExtGSP string + CurrencyIcon string + CatalogStatus string + CatalogPreviewURL string + CatalogDownloadURL string + CatalogPackage string + CatalogOrientation *int + CatalogSafeHeight int + CatalogGameModeJSON string +} + +// PageAdminGames 返回后台百顺游戏管理分页数据。 +func (s *BaishunService) PageAdminGames( + ctx context.Context, + sysOrigin string, + keyword string, + showcase *bool, + cursor int, + limit int, +) (*AdminBaishunGamePageResponse, error) { + sysOrigin = normalizeAdminSysOrigin(sysOrigin) + cursor = normalizePageCursor(cursor) + limit = normalizePageLimit(limit) + + countQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase) + + var total int64 + if err := countQuery.Count(&total).Error; err != nil { + return nil, err + } + + var rows []adminGameRow + listQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase) + if err := listQuery. + Select(` + cfg.id AS id, + cfg.sys_origin AS sys_origin, + cfg.game_id AS internal_game_id, + cfg.name AS name, + cfg.category AS category, + cfg.game_code AS game_code, + cfg.cover AS cover, + cfg.amounts AS amounts, + cfg.is_showcase AS showcase, + cfg.sort AS sort, + cfg.height AS height, + cfg.width AS width, + cfg.full_screen AS full_screen, + cfg.client_origin AS client_origin, + cfg.regions AS regions, + cfg.game_mode AS game_mode_raw, + cfg.create_time AS create_time, + cfg.update_time AS update_time, + ext.vendor_game_id AS vendor_game_id, + ext.launch_mode AS launch_mode, + 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.gsp AS ext_gsp, + ext.currency_icon AS currency_icon, + cat.status AS catalog_status, + cat.preview_url AS catalog_preview_url, + cat.download_url AS catalog_download_url, + cat.package_version AS catalog_package, + cat.orientation AS catalog_orientation, + cat.safe_height AS catalog_safe_height, + cat.game_mode_json AS catalog_game_mode_json + `). + Order("cfg.is_showcase DESC"). + Order("cfg.sort DESC"). + Order("cfg.update_time DESC"). + Limit(limit). + Offset((cursor - 1) * limit). + Scan(&rows).Error; err != nil { + return nil, err + } + + items := make([]AdminBaishunGameItem, 0, len(rows)) + for _, row := range rows { + items = append(items, row.toAdminItem()) + } + + return &AdminBaishunGamePageResponse{ + Cursor: cursor, + Limit: limit, + Records: items, + Total: total, + }, nil +} + +// SaveAdminGame 保存后台百顺游戏配置。 +func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishunGameRequest) (*AdminBaishunGameItem, error) { + sysOrigin := normalizeAdminSysOrigin(req.SysOrigin) + + catalog, err := s.findCatalogForAdmin(ctx, sysOrigin, req.VendorGameID, req.GameID) + if err != nil { + return nil, err + } + if req.VendorGameID <= 0 { + req.VendorGameID = catalog.VendorGameID + } + if req.VendorGameID <= 0 { + return nil, NewAppError(http.StatusBadRequest, "vendor_game_id_required", "vendorGameId is required") + } + + gameID := strings.TrimSpace(req.GameID) + if gameID == "" { + gameID = defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", req.VendorGameID)) + } + if gameID == "" { + return nil, NewAppError(http.StatusBadRequest, "game_id_required", "gameId is required") + } + + name := defaultIfBlank(strings.TrimSpace(req.Name), defaultIfBlank(catalog.Name, gameID)) + if name == "" { + return nil, NewAppError(http.StatusBadRequest, "name_required", "name is required") + } + category := defaultIfBlank(strings.TrimSpace(req.Category), "OTHER") + cover := defaultIfBlank(strings.TrimSpace(req.Cover), defaultIfBlank(catalog.Cover, catalog.PreviewURL)) + clientOrigin := defaultIfBlank(strings.TrimSpace(req.ClientOrigin), "COMMON") + gameCode := defaultIfBlank(strings.TrimSpace(req.GameCode), strconv.Itoa(req.VendorGameID)) + launchMode := defaultIfBlank(strings.TrimSpace(req.LaunchMode), baishunLaunchModeRemote) + packageVersion := defaultIfBlank(strings.TrimSpace(req.PackageVersion), catalog.PackageVersion) + previewURL := defaultIfBlank(strings.TrimSpace(req.PreviewURL), catalog.PreviewURL) + packageURL := defaultIfBlank(strings.TrimSpace(req.PackageURL), catalog.DownloadURL) + gsp := defaultIfBlank(strings.TrimSpace(req.GSP), defaultAdminGSP(catalog.GSP, s.cfg.Baishun.GSP)) + safeHeight := req.SafeHeight + if safeHeight <= 0 { + safeHeight = catalog.SafeHeight + } + orientation := req.Orientation + if orientation == nil && catalog.Orientation != nil { + value := *catalog.Orientation + orientation = &value + } + gameMode := baishunFixedGameMode + + tx := s.repo.DB.WithContext(ctx).Begin() + if tx.Error != nil { + return nil, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + panic(r) + } + }() + + cfg, err := s.findAdminConfigForSave(tx, sysOrigin, req.ID, gameID, req.VendorGameID) + if err != nil { + tx.Rollback() + return nil, err + } + + cfgValues := map[string]any{ + "sys_origin": sysOrigin, + "game_origin": baishunVendorType, + "game_id": gameID, + "name": name, + "category": category, + "game_code": gameCode, + "cover": cover, + "amounts": strings.TrimSpace(req.Amounts), + "is_showcase": req.Showcase, + "sort": req.Sort, + "height": req.Height, + "width": req.Width, + "full_screen": req.FullScreen, + "client_origin": clientOrigin, + "regions": strings.TrimSpace(req.Regions), + "game_mode": formatAdminGameModeRaw(gameMode), + } + + if cfg.ID == 0 { + nextID, idErr := utils.NextID() + if idErr != nil { + tx.Rollback() + return nil, idErr + } + cfg = model.SysGameListConfig{ + ID: nextID, + SysOrigin: sysOrigin, + GameOrigin: baishunVendorType, + GameID: gameID, + Name: name, + Category: category, + GameCode: gameCode, + Cover: cover, + Amounts: strings.TrimSpace(req.Amounts), + Showcase: req.Showcase, + Sort: req.Sort, + Height: req.Height, + Width: req.Width, + FullScreen: req.FullScreen, + ClientOrigin: clientOrigin, + Regions: strings.TrimSpace(req.Regions), + GameMode: formatAdminGameModeRaw(gameMode), + } + if err := tx.Create(&cfg).Error; err != nil { + tx.Rollback() + return nil, err + } + } else { + if err := tx.Model(&model.SysGameListConfig{}). + Where("id = ?", cfg.ID). + Updates(cfgValues).Error; err != nil { + tx.Rollback() + return nil, err + } + } + + var ext model.SysGameListVendorExt + err = tx.Where("game_list_config_id = ? AND vendor_type = ?", cfg.ID, baishunVendorType).First(&ext).Error + if err != nil && err != gorm.ErrRecordNotFound { + tx.Rollback() + return nil, err + } + + if ext.ID == 0 { + nextID, idErr := utils.NextID() + if idErr != nil { + tx.Rollback() + return nil, idErr + } + ext = model.SysGameListVendorExt{ + ID: nextID, + GameListConfigID: cfg.ID, + SysOrigin: sysOrigin, + VendorType: baishunVendorType, + VendorGameID: strconv.Itoa(req.VendorGameID), + LaunchMode: launchMode, + PackageVersion: packageVersion, + PackageURL: packageURL, + PreviewURL: previewURL, + Orientation: orientation, + SafeHeight: safeHeight, + GSP: gsp, + CurrencyIcon: strings.TrimSpace(req.CurrencyIcon), + Enabled: true, + } + if err := tx.Create(&ext).Error; err != nil { + tx.Rollback() + return nil, err + } + } else { + if err := tx.Model(&model.SysGameListVendorExt{}). + Where("id = ?", ext.ID). + Updates(map[string]any{ + "sys_origin": sysOrigin, + "vendor_game_id": strconv.Itoa(req.VendorGameID), + "launch_mode": launchMode, + "package_version": packageVersion, + "package_url": packageURL, + "preview_url": previewURL, + "orientation": orientation, + "safe_height": safeHeight, + "gsp": gsp, + "currency_icon": strings.TrimSpace(req.CurrencyIcon), + "enabled": true, + "bridge_schema_version": defaultIfBlank(ext.BridgeSchemaVersion, "1.0"), + }).Error; err != nil { + tx.Rollback() + return nil, err + } + } + + if err := tx.Commit().Error; err != nil { + return nil, err + } + return s.getAdminGame(ctx, sysOrigin, cfg.ID) +} + +// DeleteAdminGame 删除后台百顺游戏配置,不影响目录表。 +func (s *BaishunService) DeleteAdminGame(ctx context.Context, sysOrigin string, id int64) error { + if id <= 0 { + return NewAppError(http.StatusBadRequest, "id_required", "id is required") + } + sysOrigin = normalizeAdminSysOrigin(sysOrigin) + + tx := s.repo.DB.WithContext(ctx).Begin() + if tx.Error != nil { + return tx.Error + } + if err := tx.Where("game_list_config_id = ? AND vendor_type = ?", id, baishunVendorType). + Delete(&model.SysGameListVendorExt{}).Error; err != nil { + tx.Rollback() + return err + } + if err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType). + Delete(&model.SysGameListConfig{}).Error; err != nil { + tx.Rollback() + return err + } + return tx.Commit().Error +} + +// ImportCatalogGames 把本地目录中尚未进入后台列表的百顺游戏补齐成可配置记录。 +func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin string, vendorGameIDs []int) (*ImportCatalogResponse, error) { + tx := s.repo.DB.WithContext(ctx).Begin() + if tx.Error != nil { + return nil, tx.Error + } + resp, err := s.importCatalogGamesTx(tx, normalizeAdminSysOrigin(sysOrigin), buildVendorGameIDFilter(vendorGameIDs)) + if err != nil { + tx.Rollback() + return nil, err + } + if err := tx.Commit().Error; err != nil { + return nil, err + } + return resp, nil +} + +// SyncAdminGames 先同步百顺平台目录,再把缺失配置导入后台列表。 +func (s *BaishunService) SyncAdminGames(ctx context.Context, req SyncCatalogRequest) (*SyncAdminCatalogResponse, error) { + syncResp, err := s.SyncCatalog(ctx, req) + if err != nil { + return nil, err + } + importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.VendorGameIDs) + if err != nil { + return nil, err + } + return &SyncAdminCatalogResponse{ + Inserted: syncResp.Inserted, + Updated: syncResp.Updated, + Skipped: syncResp.Skipped, + Existing: importResp.Existing, + Imported: importResp.Imported, + }, nil +} + +func (s *BaishunService) buildAdminGameQuery(ctx context.Context, sysOrigin, keyword string, showcase *bool) *gorm.DB { + query := s.repo.DB.WithContext(ctx). + Table("sys_game_list_config AS cfg"). + Joins("LEFT JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.vendor_type = ?", baishunVendorType). + Joins(` + LEFT JOIN baishun_game_catalog cat + ON cat.sys_origin = cfg.sys_origin + AND ( + CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id + OR cat.internal_game_id = cfg.game_id + ) + `). + Where("cfg.sys_origin = ? AND cfg.game_origin = ?", sysOrigin, baishunVendorType) + if showcase != nil { + query = query.Where("cfg.is_showcase = ?", *showcase) + } + keyword = strings.TrimSpace(keyword) + if keyword != "" { + like := "%" + keyword + "%" + query = query.Where(` + cfg.name LIKE ? + OR cfg.game_id LIKE ? + OR cfg.game_code LIKE ? + OR ext.vendor_game_id LIKE ? + `, like, like, like, like) + } + return query +} + +func (s *BaishunService) getAdminGame(ctx context.Context, sysOrigin string, id int64) (*AdminBaishunGameItem, error) { + var rows []adminGameRow + err := s.buildAdminGameQuery(ctx, sysOrigin, "", nil). + Where("cfg.id = ?", id). + Select(` + cfg.id AS id, + cfg.sys_origin AS sys_origin, + cfg.game_id AS internal_game_id, + cfg.name AS name, + cfg.category AS category, + cfg.game_code AS game_code, + cfg.cover AS cover, + cfg.amounts AS amounts, + cfg.is_showcase AS showcase, + cfg.sort AS sort, + cfg.height AS height, + cfg.width AS width, + cfg.full_screen AS full_screen, + cfg.client_origin AS client_origin, + cfg.regions AS regions, + cfg.game_mode AS game_mode_raw, + cfg.create_time AS create_time, + cfg.update_time AS update_time, + ext.vendor_game_id AS vendor_game_id, + ext.launch_mode AS launch_mode, + 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.gsp AS ext_gsp, + ext.currency_icon AS currency_icon, + cat.status AS catalog_status, + cat.preview_url AS catalog_preview_url, + cat.download_url AS catalog_download_url, + cat.package_version AS catalog_package, + cat.orientation AS catalog_orientation, + cat.safe_height AS catalog_safe_height, + cat.game_mode_json AS catalog_game_mode_json + `). + Limit(1). + Scan(&rows).Error + if err != nil { + return nil, err + } + if len(rows) == 1 { + item := rows[0].toAdminItem() + return &item, nil + } + return nil, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found") +} + +func (s *BaishunService) findCatalogForAdmin(ctx context.Context, sysOrigin string, vendorGameID int, gameID string) (model.BaishunGameCatalog, error) { + query := s.repo.DB.WithContext(ctx).Where("sys_origin = ?", sysOrigin) + switch { + case vendorGameID > 0: + query = query.Where("vendor_game_id = ?", vendorGameID) + case strings.TrimSpace(gameID) != "": + query = query.Where("internal_game_id = ?", strings.TrimSpace(gameID)) + default: + return model.BaishunGameCatalog{}, nil + } + + var catalog model.BaishunGameCatalog + if err := query.First(&catalog).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return model.BaishunGameCatalog{}, nil + } + return model.BaishunGameCatalog{}, err + } + return catalog, nil +} + +func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, id int64, gameID string, vendorGameID int) (model.SysGameListConfig, error) { + var cfg model.SysGameListConfig + if id > 0 { + err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).First(&cfg).Error + if err == gorm.ErrRecordNotFound { + return model.SysGameListConfig{}, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found") + } + return cfg, err + } + + if vendorGameID > 0 { + var ext model.SysGameListVendorExt + err := tx.Where("sys_origin = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, baishunVendorType, strconv.Itoa(vendorGameID)).First(&ext).Error + if err != nil && err != gorm.ErrRecordNotFound { + return model.SysGameListConfig{}, err + } + if ext.GameListConfigID > 0 { + err = tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", ext.GameListConfigID, sysOrigin, baishunVendorType).First(&cfg).Error + if err == nil { + return cfg, nil + } + if err != gorm.ErrRecordNotFound { + return model.SysGameListConfig{}, err + } + } + } + + if strings.TrimSpace(gameID) == "" { + return model.SysGameListConfig{}, nil + } + err := tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, strings.TrimSpace(gameID)).First(&cfg).Error + if err == gorm.ErrRecordNotFound { + return model.SysGameListConfig{}, nil + } + return cfg, err +} + +func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, filter map[int]struct{}) (*ImportCatalogResponse, error) { + query := tx.Where("sys_origin = ? AND status = ?", sysOrigin, baishunCatalogEnabled) + if len(filter) > 0 { + ids := make([]int, 0, len(filter)) + for id := range filter { + ids = append(ids, id) + } + query = query.Where("vendor_game_id IN ?", ids) + } + + var catalogs []model.BaishunGameCatalog + if err := query.Order("vendor_game_id ASC").Find(&catalogs).Error; err != nil { + return nil, err + } + + resp := &ImportCatalogResponse{} + for _, catalog := range catalogs { + vendorGameID := strconv.Itoa(catalog.VendorGameID) + + var ext model.SysGameListVendorExt + err := tx.Where("sys_origin = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, baishunVendorType, vendorGameID).First(&ext).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + if err == nil && ext.GameListConfigID > 0 { + resp.Existing++ + continue + } + + var cfg model.SysGameListConfig + err = tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, catalog.InternalGameID).First(&cfg).Error + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + if err == gorm.ErrRecordNotFound { + nextID, idErr := utils.NextID() + if idErr != nil { + return nil, idErr + } + cfg = model.SysGameListConfig{ + ID: nextID, + SysOrigin: sysOrigin, + GameOrigin: baishunVendorType, + GameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)), + Name: catalog.Name, + Category: "OTHER", + GameCode: vendorGameID, + Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL), + Showcase: false, + Sort: int64(catalog.VendorGameID), + FullScreen: true, + ClientOrigin: "COMMON", + GameMode: formatAdminGameModeRaw(baishunFixedGameMode), + } + if err := tx.Create(&cfg).Error; err != nil { + return nil, err + } + } + + nextID, idErr := utils.NextID() + if idErr != nil { + return nil, idErr + } + newExt := model.SysGameListVendorExt{ + ID: nextID, + GameListConfigID: cfg.ID, + SysOrigin: sysOrigin, + VendorType: baishunVendorType, + VendorGameID: vendorGameID, + LaunchMode: baishunLaunchModeRemote, + PackageVersion: catalog.PackageVersion, + PackageURL: catalog.DownloadURL, + PreviewURL: catalog.PreviewURL, + Orientation: catalog.Orientation, + SafeHeight: catalog.SafeHeight, + GSP: defaultAdminGSP(catalog.GSP, s.cfg.Baishun.GSP), + BridgeSchemaVersion: "1.0", + Enabled: true, + } + if err := tx.Create(&newExt).Error; err != nil { + return nil, err + } + resp.Imported++ + } + return resp, nil +} + +func (r adminGameRow) toAdminItem() AdminBaishunGameItem { + return AdminBaishunGameItem{ + ID: r.ID, + SysOrigin: r.SysOrigin, + GameID: r.InternalGameID, + VendorGameID: parseAdminVendorGameID(r.VendorGameID, r.InternalGameID), + Name: r.Name, + Category: r.Category, + GameCode: r.GameCode, + Cover: r.Cover, + Amounts: r.Amounts, + Showcase: r.Showcase, + Sort: r.Sort, + Height: r.Height, + Width: r.Width, + FullScreen: r.FullScreen, + ClientOrigin: r.ClientOrigin, + Regions: r.Regions, + GameMode: baishunFixedGameMode, + LaunchMode: defaultIfBlank(r.LaunchMode, baishunLaunchModeRemote), + PackageVersion: defaultIfBlank(r.ExtPackageVersion, r.CatalogPackage), + PreviewURL: defaultIfBlank(r.ExtPreviewURL, r.CatalogPreviewURL), + PackageURL: defaultIfBlank(r.PackageURL, r.CatalogDownloadURL), + Orientation: chooseAdminOrientation(r.ExtOrientation, r.CatalogOrientation), + SafeHeight: chooseAdminSafeHeight(r.ExtSafeHeight, r.CatalogSafeHeight), + GSP: r.ExtGSP, + CurrencyIcon: r.CurrencyIcon, + CatalogStatus: r.CatalogStatus, + CreateTime: formatAdminTime(r.CreateTime), + UpdateTime: formatAdminTime(r.UpdateTime), + } +} + +func normalizeAdminSysOrigin(sysOrigin string) string { + sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin)) + if sysOrigin == "" { + return "LIKEI" + } + return sysOrigin +} + +func normalizePageCursor(cursor int) int { + if cursor <= 0 { + return 1 + } + return cursor +} + +func normalizePageLimit(limit int) int { + if limit <= 0 { + return 20 + } + if limit > 200 { + return 200 + } + return limit +} + +func defaultAdminGSP(catalogGSP string, fallback int) string { + if strings.TrimSpace(catalogGSP) != "" { + return strings.TrimSpace(catalogGSP) + } + if fallback > 0 { + return strconv.Itoa(fallback) + } + return "101" +} + +func formatAdminGameModeRaw(gameMode int) string { + return fmt.Sprintf("[%d]", baishunFixedGameMode) +} + +func parseAdminVendorGameID(raw string, gameID string) int { + if parsed, err := strconv.Atoi(strings.TrimSpace(raw)); err == nil { + return parsed + } + if strings.HasPrefix(strings.TrimSpace(gameID), "bs_") { + if parsed, err := strconv.Atoi(strings.TrimPrefix(strings.TrimSpace(gameID), "bs_")); err == nil { + return parsed + } + } + return 0 +} + +func chooseAdminOrientation(extOrientation, catalogOrientation *int) int { + if extOrientation != nil { + return *extOrientation + } + if catalogOrientation != nil { + return *catalogOrientation + } + return 0 +} + +func chooseAdminSafeHeight(extSafeHeight, catalogSafeHeight int) int { + if extSafeHeight > 0 { + return extSafeHeight + } + return catalogSafeHeight +} + +func formatAdminTime(value time.Time) string { + if value.IsZero() { + return "" + } + return value.Format(adminTimeLayout) +} + +func buildVendorGameIDFilter(vendorGameIDs []int) map[int]struct{} { + filter := make(map[int]struct{}, len(vendorGameIDs)) + for _, id := range vendorGameIDs { + if id > 0 { + filter[id] = struct{}{} + } + } + return filter +} diff --git a/internal/service/baishun/game_mode.go b/internal/service/baishun/game_mode.go new file mode 100644 index 0000000..3f307df --- /dev/null +++ b/internal/service/baishun/game_mode.go @@ -0,0 +1,3 @@ +package baishun + +const baishunFixedGameMode = 2 diff --git a/internal/service/baishun/game_mode_test.go b/internal/service/baishun/game_mode_test.go new file mode 100644 index 0000000..40abe15 --- /dev/null +++ b/internal/service/baishun/game_mode_test.go @@ -0,0 +1,32 @@ +package baishun + +import "testing" + +func TestGameListRowGameModeAlwaysFixed(t *testing.T) { + testCases := []string{"", "[1]", "[2]", "[3]", "CHAT_ROOM"} + for _, raw := range testCases { + row := gameListRow{GameModeRaw: raw} + if got := row.gameMode(); got != baishunFixedGameMode { + t.Fatalf("gameMode() = %d, want %d for raw %q", got, baishunFixedGameMode, raw) + } + if got := row.toListItem().GameMode; got != baishunFixedGameMode { + t.Fatalf("toListItem().GameMode = %d, want %d for raw %q", got, baishunFixedGameMode, raw) + } + } +} + +func TestAdminGameModeAlwaysFixed(t *testing.T) { + row := adminGameRow{ + GameModeRaw: "[3]", + CatalogGameModeJSON: "[1]", + } + if got := row.toAdminItem().GameMode; got != baishunFixedGameMode { + t.Fatalf("toAdminItem().GameMode = %d, want %d", got, baishunFixedGameMode) + } + if got := formatAdminGameModeRaw(0); got != "[2]" { + t.Fatalf("formatAdminGameModeRaw(0) = %q, want [2]", got) + } + if got := formatAdminGameModeRaw(99); got != "[2]" { + t.Fatalf("formatAdminGameModeRaw(99) = %q, want [2]", got) + } +} diff --git a/internal/service/baishun/game_row.go b/internal/service/baishun/game_row.go index c6aa6a3..3ee1bd0 100644 --- a/internal/service/baishun/game_row.go +++ b/internal/service/baishun/game_row.go @@ -87,10 +87,7 @@ func (r gameListRow) launchMode() string { // gameMode 返回游戏模式。 func (r gameListRow) gameMode() int { - if parsed := pickFirstInt(r.GameModeRaw, 3); parsed > 0 { - return parsed - } - return 3 + return baishunFixedGameMode } // vendorGameIDInt 返回厂商游戏 ID 的整数值。 diff --git a/internal/service/baishun/room.go b/internal/service/baishun/room.go index 0d23ed1..ab3fdf3 100644 --- a/internal/service/baishun/room.go +++ b/internal/service/baishun/room.go @@ -99,9 +99,6 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), "CHAT_ROOM") { query = query.Where("cfg.category = ?", category) } - if strings.TrimSpace(roomID) != "" { - query = query.Where("cfg.game_mode LIKE ?", "%CHAT_ROOM%") - } if err := query.Order("cfg.sort DESC").Scan(&rows).Error; err != nil { return nil, err } @@ -133,7 +130,7 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c Sort: int64(catalog.VendorGameID), LaunchMode: baishunLaunchModeRemote, FullScreen: true, - GameMode: pickFirstInt(catalog.GameModeJSON, 3), + GameMode: baishunFixedGameMode, SafeHeight: catalog.SafeHeight, Orientation: derefInt(catalog.Orientation), PackageVersion: catalog.PackageVersion, @@ -214,7 +211,7 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri CatalogSafeHeight: catalog.SafeHeight, CatalogOrientation: catalog.Orientation, CatalogStatus: catalog.Status, - GameModeRaw: catalog.GameModeJSON, + GameModeRaw: formatAdminGameModeRaw(baishunFixedGameMode), }, nil } 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 8d6daad..2dcfacc 100644 --- a/internal/service/baishun/types.go +++ b/internal/service/baishun/types.go @@ -181,6 +181,90 @@ type SyncCatalogResponse struct { Skipped int `json:"skipped"` } +// AdminBaishunGameItem 是后台管理页展示的百顺游戏配置行。 +type AdminBaishunGameItem struct { + ID int64 `json:"id"` + SysOrigin string `json:"sysOrigin"` + GameID string `json:"gameId"` + VendorGameID int `json:"vendorGameId"` + Name string `json:"name"` + Category string `json:"category"` + GameCode string `json:"gameCode"` + Cover string `json:"cover"` + Amounts string `json:"amounts"` + Showcase bool `json:"showcase"` + Sort int64 `json:"sort"` + Height float64 `json:"height"` + Width float64 `json:"width"` + FullScreen bool `json:"fullScreen"` + ClientOrigin string `json:"clientOrigin"` + Regions string `json:"regions"` + GameMode int `json:"gameMode"` + LaunchMode string `json:"launchMode"` + PackageVersion string `json:"packageVersion"` + PreviewURL string `json:"previewUrl"` + PackageURL string `json:"packageUrl"` + Orientation int `json:"orientation"` + SafeHeight int `json:"safeHeight"` + GSP string `json:"gsp"` + CurrencyIcon string `json:"currencyIcon"` + CatalogStatus string `json:"catalogStatus"` + CreateTime string `json:"createTime"` + UpdateTime string `json:"updateTime"` +} + +// AdminBaishunGamePageResponse 是后台页分页响应。 +type AdminBaishunGamePageResponse struct { + Cursor int `json:"cursor"` + Limit int `json:"limit"` + Records []AdminBaishunGameItem `json:"records"` + Total int64 `json:"total"` +} + +// SaveAdminBaishunGameRequest 是后台保存百顺游戏配置的入参。 +type SaveAdminBaishunGameRequest struct { + ID int64 `json:"id"` + SysOrigin string `json:"sysOrigin"` + GameID string `json:"gameId"` + VendorGameID int `json:"vendorGameId"` + Name string `json:"name"` + Category string `json:"category"` + GameCode string `json:"gameCode"` + Cover string `json:"cover"` + Amounts string `json:"amounts"` + Showcase bool `json:"showcase"` + Sort int64 `json:"sort"` + Height float64 `json:"height"` + Width float64 `json:"width"` + FullScreen bool `json:"fullScreen"` + ClientOrigin string `json:"clientOrigin"` + Regions string `json:"regions"` + GameMode int `json:"gameMode"` + LaunchMode string `json:"launchMode"` + PackageVersion string `json:"packageVersion"` + PreviewURL string `json:"previewUrl"` + PackageURL string `json:"packageUrl"` + Orientation *int `json:"orientation"` + SafeHeight int `json:"safeHeight"` + GSP string `json:"gsp"` + CurrencyIcon string `json:"currencyIcon"` +} + +// ImportCatalogResponse 描述从本地目录补齐后台配置的结果。 +type ImportCatalogResponse struct { + Existing int `json:"existing"` + Imported int `json:"imported"` +} + +// SyncAdminCatalogResponse 描述同步平台目录并导入后台配置的结果。 +type SyncAdminCatalogResponse struct { + Inserted int `json:"inserted"` + Updated int `json:"updated"` + Skipped int `json:"skipped"` + Existing int `json:"existing"` + Imported int `json:"imported"` +} + // BaishunTokenRequest 是百顺拉取 token/code 回调入参。 type BaishunTokenRequest struct { AppID int64 `json:"app_id"`