This commit is contained in:
hy001 2026-04-24 13:04:06 +08:00
parent 5e81478c8b
commit 8256fdf7e6
27 changed files with 1437 additions and 180 deletions

View File

@ -29,6 +29,7 @@ internal/utils
### 1. `cmd/api` ### 1. `cmd/api`
- 只做启动和依赖装配。 - 只做启动和依赖装配。
- 不承担业务逻辑。 - 不承担业务逻辑。
- 当前线上只部署 `golang` API 进程,所以这里会同时启动 `registerreward` 的 Redis Stream consumer确保注册成功事件能被消费并发奖。
- 不启动任何 `weekstar` 结算 goroutine。 - 不启动任何 `weekstar` 结算 goroutine。
### 2. `cmd/consumer` ### 2. `cmd/consumer`
@ -36,6 +37,7 @@ internal/utils
- 当前承接: - 当前承接:
- `registerreward` 的 Redis Stream 消费 - `registerreward` 的 Redis Stream 消费
- `weekstar` 的 RocketMQ 消费 - `weekstar` 的 RocketMQ 消费
- 如果后续单独部署 consumer`registerreward` 使用同一个 Redis consumer group重复启动不会重复发奖。
### 3. `internal/bootstrap` ### 3. `internal/bootstrap`
- 统一完成配置加载、存储初始化、依赖探活、Java 网关装配。 - 统一完成配置加载、存储初始化、依赖探活、Java 网关装配。

View File

@ -37,6 +37,13 @@ func main() {
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways) registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways) signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways) weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
workerCtx, workerCancel := context.WithCancel(context.Background())
defer workerCancel()
if err := registerRewardService.Start(workerCtx); err != nil {
log.Fatalf("start register reward consumer failed: %v", err)
}
gameProviders := gameprovider.NewRegistry( gameProviders := gameprovider.NewRegistry(
baishun.NewAppProvider(baishunService), baishun.NewAppProvider(baishunService),
) )
@ -76,6 +83,7 @@ func main() {
return return
case <-shutdownSignal.Done(): case <-shutdownSignal.Done():
log.Printf("shutdown signal received, draining http traffic") log.Printf("shutdown signal received, draining http traffic")
workerCancel()
} }
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), app.Config.HTTP.ShutdownTimeout) shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), app.Config.HTTP.ShutdownTimeout)

View File

@ -28,25 +28,26 @@ func (SysGameListConfig) TableName() string { return "sys_game_list_config" }
// SysGameListVendorExt 保存某个厂商在系统游戏表上的扩展信息。 // SysGameListVendorExt 保存某个厂商在系统游戏表上的扩展信息。
type SysGameListVendorExt struct { type SysGameListVendorExt struct {
ID int64 `gorm:"column:id;primaryKey"` // 主键 ID int64 `gorm:"column:id;primaryKey"` // 主键
GameListConfigID int64 `gorm:"column:game_list_config_id;uniqueIndex:uk_game_vendor_ext_cfg_vendor,priority:1"` // 关联系统游戏配置 ID GameListConfigID int64 `gorm:"column:game_list_config_id;uniqueIndex:uk_game_vendor_ext_cfg_vendor,priority:1"` // 关联系统游戏配置 ID
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_game_vendor_ext_sys_vendor,priority:1"` // 系统标识 SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_game_vendor_ext_sys_vendor,priority:1;index:idx_game_vendor_ext_sys_profile_vendor,priority:1"` // 系统标识
VendorType string `gorm:"column:vendor_type;size:32;uniqueIndex:uk_game_vendor_ext_cfg_vendor,priority:2;index:idx_game_vendor_ext_sys_vendor,priority:2"` // 厂商类型 Profile string `gorm:"column:profile;size:32;default:PROD;index:idx_game_vendor_ext_sys_profile_vendor,priority:2"` // 配置档案PROD/TEST
VendorGameID string `gorm:"column:vendor_game_id;size:64"` // 厂商游戏 ID VendorType string `gorm:"column:vendor_type;size:32;uniqueIndex:uk_game_vendor_ext_cfg_vendor,priority:2;index:idx_game_vendor_ext_sys_vendor,priority:2;index:idx_game_vendor_ext_sys_profile_vendor,priority:3"` // 厂商类型
LaunchMode string `gorm:"column:launch_mode;size:32"` // 启动模式 VendorGameID string `gorm:"column:vendor_game_id;size:64;index:idx_game_vendor_ext_sys_profile_vendor,priority:4"` // 厂商游戏 ID
PackageVersion string `gorm:"column:package_version;size:32"` // 包版本号 LaunchMode string `gorm:"column:launch_mode;size:32"` // 启动模式
PackageURL string `gorm:"column:package_url;size:1024"` // 包下载地址 PackageVersion string `gorm:"column:package_version;size:32"` // 包版本号
PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址 PackageURL string `gorm:"column:package_url;size:1024"` // 包下载地址
Orientation *int `gorm:"column:orientation"` // 屏幕方向 PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址
SafeHeight int `gorm:"column:safe_height"` // 安全区域高度 Orientation *int `gorm:"column:orientation"` // 屏幕方向
GSP string `gorm:"column:gsp;size:64"` // GSP 配置 SafeHeight int `gorm:"column:safe_height"` // 安全区域高度
BridgeSchemaVersion string `gorm:"column:bridge_schema_version;size:16"` // Bridge 协议版本 GSP string `gorm:"column:gsp;size:64"` // GSP 配置
CurrencyType *int `gorm:"column:currency_type"` // 货币类型 BridgeSchemaVersion string `gorm:"column:bridge_schema_version;size:16"` // Bridge 协议版本
CurrencyIcon string `gorm:"column:currency_icon;size:1024"` // 货币图标 CurrencyType *int `gorm:"column:currency_type"` // 货币类型
ExtraJSON string `gorm:"column:extra_json;type:text"` // 额外扩展配置 CurrencyIcon string `gorm:"column:currency_icon;size:1024"` // 货币图标
Enabled bool `gorm:"column:enabled;index:idx_game_vendor_ext_sys_vendor,priority:3"` // 是否启用 ExtraJSON string `gorm:"column:extra_json;type:text"` // 额外扩展配置
CreateTime time.Time `gorm:"column:create_time"` // 创建时间 Enabled bool `gorm:"column:enabled;index:idx_game_vendor_ext_sys_vendor,priority:3;index:idx_game_vendor_ext_sys_profile_vendor,priority:5"` // 是否启用
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间 CreateTime time.Time `gorm:"column:create_time"` // 创建时间
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
} }
// TableName 返回游戏厂商扩展表名。 // TableName 返回游戏厂商扩展表名。
@ -54,18 +55,20 @@ func (SysGameListVendorExt) TableName() string { return "sys_game_list_vendor_ex
// BaishunProviderConfig 保存每个系统的百顺接入配置。 // BaishunProviderConfig 保存每个系统的百顺接入配置。
type BaishunProviderConfig struct { type BaishunProviderConfig struct {
ID int64 `gorm:"column:id;primaryKey"` // 主键 ID int64 `gorm:"column:id;primaryKey"` // 主键
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_provider_sys_origin"` // 系统标识 SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_provider_sys_origin_profile,priority:1;index:idx_baishun_provider_active,priority:1"` // 系统标识
PlatformBaseURL string `gorm:"column:platform_base_url;size:1024"` // 百顺平台基础地址 Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_baishun_provider_sys_origin_profile,priority:2"` // 配置档案PROD/TEST
AppID int64 `gorm:"column:app_id;index:idx_baishun_provider_app,priority:1"` // 百顺应用 ID Active bool `gorm:"column:active;index:idx_baishun_provider_active,priority:2"` // 是否当前启用
AppName string `gorm:"column:app_name;size:128"` // 百顺应用名 PlatformBaseURL string `gorm:"column:platform_base_url;size:1024"` // 百顺平台基础地址
AppChannel string `gorm:"column:app_channel;size:64;index:idx_baishun_provider_app,priority:2"` // 百顺渠道 AppID int64 `gorm:"column:app_id;index:idx_baishun_provider_app,priority:1"` // 百顺应用 ID
AppKey string `gorm:"column:app_key;size:255"` // 百顺密钥 AppName string `gorm:"column:app_name;size:128"` // 百顺应用名
GSP int `gorm:"column:gsp"` // 默认 GSP AppChannel string `gorm:"column:app_channel;size:64;index:idx_baishun_provider_app,priority:2"` // 百顺渠道
LaunchCodeTTLSeconds int `gorm:"column:launch_code_ttl_seconds"` // 启动码过期秒数 AppKey string `gorm:"column:app_key;size:255"` // 百顺密钥
SSTokenTTLSeconds int `gorm:"column:ss_token_ttl_seconds"` // ss_token 过期秒数 GSP int `gorm:"column:gsp"` // 默认 GSP
CreateTime time.Time `gorm:"column:create_time"` // 创建时间 LaunchCodeTTLSeconds int `gorm:"column:launch_code_ttl_seconds"` // 启动码过期秒数
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间 SSTokenTTLSeconds int `gorm:"column:ss_token_ttl_seconds"` // ss_token 过期秒数
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
UpdateTime time.Time `gorm:"column:update_time;index:idx_baishun_provider_active,priority:3"` // 更新时间
} }
// TableName 返回百顺配置表名。 // TableName 返回百顺配置表名。
@ -73,24 +76,25 @@ func (BaishunProviderConfig) TableName() string { return "baishun_provider_confi
// BaishunGameCatalog 是百顺平台同步到本地的游戏目录。 // BaishunGameCatalog 是百顺平台同步到本地的游戏目录。
type BaishunGameCatalog struct { type BaishunGameCatalog struct {
ID int64 `gorm:"column:id;primaryKey"` // 主键 ID int64 `gorm:"column:id;primaryKey"` // 主键
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_catalog_sys_vendor_game,priority:1;index:idx_baishun_catalog_internal_game,priority:1"` // 系统标识 SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_catalog_sys_profile_vendor_game,priority:1;index:idx_baishun_catalog_profile_internal_game,priority:1"` // 系统标识
InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_baishun_catalog_internal_game,priority:2"` // 内部游戏 ID Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_baishun_catalog_sys_profile_vendor_game,priority:2;index:idx_baishun_catalog_profile_internal_game,priority:2"` // 配置档案PROD/TEST
VendorGameID int `gorm:"column:vendor_game_id;uniqueIndex:uk_baishun_catalog_sys_vendor_game,priority:2"` // 百顺游戏 ID InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_baishun_catalog_profile_internal_game,priority:3"` // 内部游戏 ID
Name string `gorm:"column:name;size:128"` // 游戏名称 VendorGameID int `gorm:"column:vendor_game_id;uniqueIndex:uk_baishun_catalog_sys_profile_vendor_game,priority:3"` // 百顺游戏 ID
Cover string `gorm:"column:cover;size:1024"` // 封面图 Name string `gorm:"column:name;size:128"` // 游戏名称
PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址 Cover string `gorm:"column:cover;size:1024"` // 封面图
DownloadURL string `gorm:"column:download_url;size:1024"` // 下载地址 PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址
PackageVersion string `gorm:"column:package_version;size:32"` // 包版本 DownloadURL string `gorm:"column:download_url;size:1024"` // 下载地址
GameModeJSON string `gorm:"column:game_mode_json;size:64"` // 游戏模式原始 JSON PackageVersion string `gorm:"column:package_version;size:32"` // 包版本
Orientation *int `gorm:"column:orientation"` // 屏幕方向 GameModeJSON string `gorm:"column:game_mode_json;size:64"` // 游戏模式原始 JSON
SafeHeight int `gorm:"column:safe_height"` // 安全区域高度 Orientation *int `gorm:"column:orientation"` // 屏幕方向
VenueLevelJSON string `gorm:"column:venue_level_json;size:64"` // 场馆等级 JSON SafeHeight int `gorm:"column:safe_height"` // 安全区域高度
GSP string `gorm:"column:gsp;size:64"` // GSP 配置 VenueLevelJSON string `gorm:"column:venue_level_json;size:64"` // 场馆等级 JSON
RawJSON string `gorm:"column:raw_json;type:longtext"` // 原始目录数据 GSP string `gorm:"column:gsp;size:64"` // GSP 配置
Status string `gorm:"column:status;size:32;index:idx_baishun_catalog_internal_game,priority:3"` // 状态 RawJSON string `gorm:"column:raw_json;type:longtext"` // 原始目录数据
CreateTime time.Time `gorm:"column:create_time"` // 创建时间 Status string `gorm:"column:status;size:32;index:idx_baishun_catalog_profile_internal_game,priority:4"` // 状态
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间 CreateTime time.Time `gorm:"column:create_time"` // 创建时间
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
} }
// TableName 返回百顺目录表名。 // TableName 返回百顺目录表名。

View File

@ -96,7 +96,7 @@ func registerBaishunRoutes(
consoleGroup := engine.Group("/operate/baishun-game") consoleGroup := engine.Group("/operate/baishun-game")
consoleGroup.Use(consoleAuthMiddleware(javaClient)) consoleGroup.Use(consoleAuthMiddleware(javaClient))
consoleGroup.GET("/config", func(c *gin.Context) { consoleGroup.GET("/config", func(c *gin.Context) {
resp, err := baishunService.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin")) resp, err := baishunService.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin"), c.Query("profile"))
if err != nil { if err != nil {
writeError(c, err) writeError(c, err)
return return
@ -116,12 +116,26 @@ func registerBaishunRoutes(
} }
writeOK(c, resp) writeOK(c, resp)
}) })
consoleGroup.POST("/config/activate", func(c *gin.Context) {
var req baishun.ActivateBaishunProviderProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
return
}
resp, err := baishunService.ActivateProviderProfile(c.Request.Context(), req)
if err != nil {
writeError(c, err)
return
}
writeOK(c, resp)
})
consoleGroup.GET("/page", func(c *gin.Context) { consoleGroup.GET("/page", func(c *gin.Context) {
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1"))) cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20"))) limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
resp, err := baishunService.PageAdminGames( resp, err := baishunService.PageAdminGames(
c.Request.Context(), c.Request.Context(),
c.Query("sysOrigin"), c.Query("sysOrigin"),
c.Query("profile"),
c.Query("keyword"), c.Query("keyword"),
parseOptionalBool(c.Query("showcase")), parseOptionalBool(c.Query("showcase")),
cursor, cursor,
@ -139,6 +153,7 @@ func registerBaishunRoutes(
resp, err := baishunService.PageCatalog( resp, err := baishunService.PageCatalog(
c.Request.Context(), c.Request.Context(),
c.Query("sysOrigin"), c.Query("sysOrigin"),
c.Query("profile"),
c.Query("keyword"), c.Query("keyword"),
cursor, cursor,
limit, limit,
@ -164,7 +179,7 @@ func registerBaishunRoutes(
}) })
consoleGroup.DELETE("", func(c *gin.Context) { consoleGroup.DELETE("", func(c *gin.Context) {
id, _ := strconv.ParseInt(strings.TrimSpace(c.Query("id")), 10, 64) id, _ := strconv.ParseInt(strings.TrimSpace(c.Query("id")), 10, 64)
if err := baishunService.DeleteAdminGame(c.Request.Context(), c.Query("sysOrigin"), id); err != nil { if err := baishunService.DeleteAdminGame(c.Request.Context(), c.Query("sysOrigin"), c.Query("profile"), id); err != nil {
writeError(c, err) writeError(c, err)
return return
} }
@ -189,7 +204,7 @@ func registerBaishunRoutes(
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
return return
} }
resp, err := baishunService.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.VendorGameIDs) resp, err := baishunService.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs)
if err != nil { if err != nil {
writeError(c, err) writeError(c, err)
return return

View File

@ -2,6 +2,7 @@ package router
import ( import (
"net/http" "net/http"
"strconv"
"chatapp3-golang/internal/service/invite" "chatapp3-golang/internal/service/invite"
@ -48,6 +49,15 @@ func registerInviteRoutes(engine *gin.Engine, javaClient authGateway, inviteServ
} }
writeOK(c, resp) writeOK(c, resp)
}) })
appGroup.GET("/rank", func(c *gin.Context) {
limit, _ := strconv.Atoi(c.Query("limit"))
resp, err := inviteService.GetRank(c.Request.Context(), mustAuthUser(c), limit)
if err != nil {
writeError(c, err)
return
}
writeOK(c, resp)
})
appGroup.POST("/bind-code", func(c *gin.Context) { appGroup.POST("/bind-code", func(c *gin.Context) {
var req invite.BindCodeRequest var req invite.BindCodeRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -18,6 +18,7 @@ const adminTimeLayout = "2006-01-02 15:04:05"
type adminGameRow struct { type adminGameRow struct {
ID int64 ID int64
SysOrigin string SysOrigin string
Profile string
InternalGameID string InternalGameID string
Name string Name string
Category string Category string
@ -57,16 +58,21 @@ type adminGameRow struct {
func (s *BaishunService) PageAdminGames( func (s *BaishunService) PageAdminGames(
ctx context.Context, ctx context.Context,
sysOrigin string, sysOrigin string,
profile string,
keyword string, keyword string,
showcase *bool, showcase *bool,
cursor int, cursor int,
limit int, limit int,
) (*AdminBaishunGamePageResponse, error) { ) (*AdminBaishunGamePageResponse, error) {
sysOrigin = normalizeAdminSysOrigin(sysOrigin) sysOrigin = normalizeAdminSysOrigin(sysOrigin)
profile, err := s.requestProfile(ctx, sysOrigin, profile)
if err != nil {
return nil, err
}
cursor = normalizePageCursor(cursor) cursor = normalizePageCursor(cursor)
limit = normalizePageLimit(limit) limit = normalizePageLimit(limit)
countQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase) countQuery := s.buildAdminGameQuery(ctx, sysOrigin, profile, keyword, showcase)
var total int64 var total int64
if err := countQuery.Count(&total).Error; err != nil { if err := countQuery.Count(&total).Error; err != nil {
@ -74,11 +80,12 @@ func (s *BaishunService) PageAdminGames(
} }
var rows []adminGameRow var rows []adminGameRow
listQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase) listQuery := s.buildAdminGameQuery(ctx, sysOrigin, profile, keyword, showcase)
if err := listQuery. if err := listQuery.
Select(` Select(`
cfg.id AS id, cfg.id AS id,
cfg.sys_origin AS sys_origin, cfg.sys_origin AS sys_origin,
ext.profile AS profile,
cfg.game_id AS internal_game_id, cfg.game_id AS internal_game_id,
cfg.name AS name, cfg.name AS name,
cfg.category AS category, cfg.category AS category,
@ -138,12 +145,16 @@ func (s *BaishunService) PageAdminGames(
// SaveAdminGame 保存后台百顺游戏配置。 // SaveAdminGame 保存后台百顺游戏配置。
func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishunGameRequest) (*AdminBaishunGameItem, error) { func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishunGameRequest) (*AdminBaishunGameItem, error) {
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin) sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin) profile, err := s.requestProfile(ctx, sysOrigin, req.Profile)
if err != nil {
return nil, err
}
runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
catalog, err := s.findCatalogForAdmin(ctx, sysOrigin, req.VendorGameID, req.GameID) catalog, err := s.findCatalogForAdmin(ctx, sysOrigin, profile, req.VendorGameID, req.GameID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -197,7 +208,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
} }
}() }()
cfg, err := s.findAdminConfigForSave(tx, sysOrigin, req.ID, gameID, req.VendorGameID) cfg, err := s.findAdminConfigForSave(tx, sysOrigin, profile, req.ID, gameID, req.VendorGameID)
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
return nil, err return nil, err
@ -262,7 +273,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
} }
var ext model.SysGameListVendorExt var ext model.SysGameListVendorExt
err = tx.Where("game_list_config_id = ? AND vendor_type = ?", cfg.ID, baishunVendorType).First(&ext).Error err = tx.Where("game_list_config_id = ? AND vendor_type = ? AND profile = ?", cfg.ID, baishunVendorType, profile).First(&ext).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
tx.Rollback() tx.Rollback()
return nil, err return nil, err
@ -280,6 +291,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
ID: nextID, ID: nextID,
GameListConfigID: cfg.ID, GameListConfigID: cfg.ID,
SysOrigin: sysOrigin, SysOrigin: sysOrigin,
Profile: profile,
VendorType: baishunVendorType, VendorType: baishunVendorType,
VendorGameID: strconv.Itoa(req.VendorGameID), VendorGameID: strconv.Itoa(req.VendorGameID),
LaunchMode: launchMode, LaunchMode: launchMode,
@ -304,6 +316,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
Where("id = ?", ext.ID). Where("id = ?", ext.ID).
Updates(map[string]any{ Updates(map[string]any{
"sys_origin": sysOrigin, "sys_origin": sysOrigin,
"profile": profile,
"vendor_game_id": strconv.Itoa(req.VendorGameID), "vendor_game_id": strconv.Itoa(req.VendorGameID),
"launch_mode": launchMode, "launch_mode": launchMode,
"package_version": packageVersion, "package_version": packageVersion,
@ -326,40 +339,63 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
if err := tx.Commit().Error; err != nil { if err := tx.Commit().Error; err != nil {
return nil, err return nil, err
} }
return s.getAdminGame(ctx, sysOrigin, cfg.ID) s.clearJavaGameListCache(ctx)
return s.getAdminGame(ctx, sysOrigin, profile, cfg.ID)
} }
// DeleteAdminGame 删除后台百顺游戏配置,不影响目录表。 // DeleteAdminGame 删除后台百顺游戏配置,不影响目录表。
func (s *BaishunService) DeleteAdminGame(ctx context.Context, sysOrigin string, id int64) error { func (s *BaishunService) DeleteAdminGame(ctx context.Context, sysOrigin string, profile string, id int64) error {
if id <= 0 { if id <= 0 {
return NewAppError(http.StatusBadRequest, "id_required", "id is required") return NewAppError(http.StatusBadRequest, "id_required", "id is required")
} }
sysOrigin = normalizeAdminSysOrigin(sysOrigin) sysOrigin = normalizeAdminSysOrigin(sysOrigin)
profile, err := s.requestProfile(ctx, sysOrigin, profile)
if err != nil {
return err
}
tx := s.repo.DB.WithContext(ctx).Begin() tx := s.repo.DB.WithContext(ctx).Begin()
if tx.Error != nil { if tx.Error != nil {
return tx.Error return tx.Error
} }
if err := tx.Where("game_list_config_id = ? AND vendor_type = ?", id, baishunVendorType). if err := tx.Where("game_list_config_id = ? AND vendor_type = ? AND profile = ?", id, baishunVendorType, profile).
Delete(&model.SysGameListVendorExt{}).Error; err != nil { Delete(&model.SysGameListVendorExt{}).Error; err != nil {
tx.Rollback() tx.Rollback()
return err return err
} }
if err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType). var remaining int64
Delete(&model.SysGameListConfig{}).Error; err != nil { if err := tx.Model(&model.SysGameListVendorExt{}).
Where("game_list_config_id = ? AND vendor_type = ?", id, baishunVendorType).
Count(&remaining).Error; err != nil {
tx.Rollback() tx.Rollback()
return err return err
} }
return tx.Commit().Error if remaining == 0 {
if err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).
Delete(&model.SysGameListConfig{}).Error; err != nil {
tx.Rollback()
return err
}
}
if err := tx.Commit().Error; err != nil {
return err
}
s.clearJavaGameListCache(ctx)
return nil
} }
// ImportCatalogGames 把本地目录中尚未进入后台列表的百顺游戏补齐成可配置记录。 // ImportCatalogGames 把本地目录中尚未进入后台列表的百顺游戏补齐成可配置记录。
func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin string, vendorGameIDs []int) (*ImportCatalogResponse, error) { func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin string, profile string, vendorGameIDs []int) (*ImportCatalogResponse, error) {
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
profile, err := s.requestProfile(ctx, sysOrigin, profile)
if err != nil {
return nil, err
}
tx := s.repo.DB.WithContext(ctx).Begin() tx := s.repo.DB.WithContext(ctx).Begin()
if tx.Error != nil { if tx.Error != nil {
return nil, tx.Error return nil, tx.Error
} }
resp, err := s.importCatalogGamesTx(tx, normalizeAdminSysOrigin(sysOrigin), buildVendorGameIDFilter(vendorGameIDs)) resp, err := s.importCatalogGamesTx(tx, sysOrigin, profile, buildVendorGameIDFilter(vendorGameIDs))
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
return nil, err return nil, err
@ -367,6 +403,7 @@ func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin strin
if err := tx.Commit().Error; err != nil { if err := tx.Commit().Error; err != nil {
return nil, err return nil, err
} }
s.clearJavaGameListCache(ctx)
return resp, nil return resp, nil
} }
@ -376,7 +413,7 @@ func (s *BaishunService) SyncAdminGames(ctx context.Context, req SyncCatalogRequ
if err != nil { if err != nil {
return nil, err return nil, err
} }
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.VendorGameIDs) importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.Profile, req.VendorGameIDs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -389,13 +426,14 @@ func (s *BaishunService) SyncAdminGames(ctx context.Context, req SyncCatalogRequ
}, nil }, nil
} }
func (s *BaishunService) buildAdminGameQuery(ctx context.Context, sysOrigin, keyword string, showcase *bool) *gorm.DB { func (s *BaishunService) buildAdminGameQuery(ctx context.Context, sysOrigin, profile, keyword string, showcase *bool) *gorm.DB {
query := s.repo.DB.WithContext(ctx). query := s.repo.DB.WithContext(ctx).
Table("sys_game_list_config AS cfg"). 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("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.vendor_type = ? AND ext.profile = ?", baishunVendorType, profile).
Joins(` Joins(`
LEFT JOIN baishun_game_catalog cat LEFT JOIN baishun_game_catalog cat
ON cat.sys_origin = cfg.sys_origin ON cat.sys_origin = cfg.sys_origin
AND cat.profile = ext.profile
AND ( AND (
CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id
OR cat.internal_game_id = cfg.game_id OR cat.internal_game_id = cfg.game_id
@ -418,13 +456,14 @@ func (s *BaishunService) buildAdminGameQuery(ctx context.Context, sysOrigin, key
return query return query
} }
func (s *BaishunService) getAdminGame(ctx context.Context, sysOrigin string, id int64) (*AdminBaishunGameItem, error) { func (s *BaishunService) getAdminGame(ctx context.Context, sysOrigin string, profile string, id int64) (*AdminBaishunGameItem, error) {
var rows []adminGameRow var rows []adminGameRow
err := s.buildAdminGameQuery(ctx, sysOrigin, "", nil). err := s.buildAdminGameQuery(ctx, sysOrigin, profile, "", nil).
Where("cfg.id = ?", id). Where("cfg.id = ?", id).
Select(` Select(`
cfg.id AS id, cfg.id AS id,
cfg.sys_origin AS sys_origin, cfg.sys_origin AS sys_origin,
ext.profile AS profile,
cfg.game_id AS internal_game_id, cfg.game_id AS internal_game_id,
cfg.name AS name, cfg.name AS name,
cfg.category AS category, cfg.category AS category,
@ -471,8 +510,8 @@ func (s *BaishunService) getAdminGame(ctx context.Context, sysOrigin string, id
return nil, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found") 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) { func (s *BaishunService) findCatalogForAdmin(ctx context.Context, sysOrigin string, profile string, vendorGameID int, gameID string) (model.BaishunGameCatalog, error) {
query := s.repo.DB.WithContext(ctx).Where("sys_origin = ?", sysOrigin) query := s.repo.DB.WithContext(ctx).Where("sys_origin = ? AND profile = ?", sysOrigin, profile)
switch { switch {
case vendorGameID > 0: case vendorGameID > 0:
query = query.Where("vendor_game_id = ?", vendorGameID) query = query.Where("vendor_game_id = ?", vendorGameID)
@ -492,19 +531,31 @@ func (s *BaishunService) findCatalogForAdmin(ctx context.Context, sysOrigin stri
return catalog, nil return catalog, nil
} }
func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, id int64, gameID string, vendorGameID int) (model.SysGameListConfig, error) { func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, profile string, id int64, gameID string, vendorGameID int) (model.SysGameListConfig, error) {
var cfg model.SysGameListConfig var cfg model.SysGameListConfig
if id > 0 { if id > 0 {
err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).First(&cfg).Error err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).First(&cfg).Error
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
return model.SysGameListConfig{}, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found") return model.SysGameListConfig{}, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
} }
if err == nil {
var extCount int64
err = tx.Model(&model.SysGameListVendorExt{}).
Where("game_list_config_id = ? AND vendor_type = ? AND profile = ?", cfg.ID, baishunVendorType, profile).
Count(&extCount).Error
if err != nil {
return model.SysGameListConfig{}, err
}
if extCount == 0 {
return model.SysGameListConfig{}, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
}
}
return cfg, err return cfg, err
} }
if vendorGameID > 0 { if vendorGameID > 0 {
var ext model.SysGameListVendorExt var ext model.SysGameListVendorExt
err := tx.Where("sys_origin = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, baishunVendorType, strconv.Itoa(vendorGameID)).First(&ext).Error err := tx.Where("sys_origin = ? AND profile = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, profile, baishunVendorType, strconv.Itoa(vendorGameID)).First(&ext).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return model.SysGameListConfig{}, err return model.SysGameListConfig{}, err
} }
@ -522,23 +573,31 @@ func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, i
if strings.TrimSpace(gameID) == "" { if strings.TrimSpace(gameID) == "" {
return model.SysGameListConfig{}, nil return model.SysGameListConfig{}, nil
} }
err := tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, strings.TrimSpace(gameID)).First(&cfg).Error err := tx.Table("sys_game_list_config AS cfg").
if err == gorm.ErrRecordNotFound { Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.vendor_type = ? AND ext.profile = ?", baishunVendorType, profile).
Where("cfg.sys_origin = ? AND cfg.game_origin = ? AND cfg.game_id = ?", sysOrigin, baishunVendorType, strings.TrimSpace(gameID)).
Select("cfg.*").
Limit(1).
Scan(&cfg).Error
if err != nil {
return model.SysGameListConfig{}, err
}
if cfg.ID == 0 {
return model.SysGameListConfig{}, nil return model.SysGameListConfig{}, nil
} }
return cfg, err return cfg, nil
} }
func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, filter map[int]struct{}) (*ImportCatalogResponse, error) { func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, profile string, filter map[int]struct{}) (*ImportCatalogResponse, error) {
ctx := tx.Statement.Context ctx := tx.Statement.Context
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin) runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
query := tx.Where("sys_origin = ? AND status = ?", sysOrigin, baishunCatalogEnabled) query := tx.Where("sys_origin = ? AND profile = ? AND status = ?", sysOrigin, profile, baishunCatalogEnabled)
if len(filter) > 0 { if len(filter) > 0 {
ids := make([]int, 0, len(filter)) ids := make([]int, 0, len(filter))
for id := range filter { for id := range filter {
@ -558,70 +617,98 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, fil
now := time.Now() now := time.Now()
var ext model.SysGameListVendorExt var ext model.SysGameListVendorExt
err := tx.Where("sys_origin = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, baishunVendorType, vendorGameID).First(&ext).Error err := tx.Where("sys_origin = ? AND profile = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, profile, baishunVendorType, vendorGameID).First(&ext).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return nil, err return nil, err
} }
if err == nil && ext.GameListConfigID > 0 { if err == nil && ext.GameListConfigID > 0 {
resp.Existing++ var cfg model.SysGameListConfig
continue cfgErr := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", ext.GameListConfigID, sysOrigin, baishunVendorType).First(&cfg).Error
if cfgErr == nil {
resp.Existing++
continue
}
if cfgErr != gorm.ErrRecordNotFound {
return nil, cfgErr
}
} }
var cfg model.SysGameListConfig nextCfgID, idErr := utils.NextID()
err = tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, catalog.InternalGameID).First(&cfg).Error if idErr != nil {
if err != nil && err != gorm.ErrRecordNotFound { return nil, idErr
return nil, err
} }
if err == gorm.ErrRecordNotFound { cfg := model.SysGameListConfig{
nextID, idErr := utils.NextID() ID: nextCfgID,
if idErr != nil { SysOrigin: sysOrigin,
return nil, idErr GameOrigin: baishunVendorType,
} GameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
cfg = model.SysGameListConfig{ Name: catalog.Name,
ID: nextID, Category: "OTHER",
SysOrigin: sysOrigin, GameCode: vendorGameID,
GameOrigin: baishunVendorType, Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
GameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)), Showcase: true,
Name: catalog.Name, Sort: int64(catalog.VendorGameID),
Category: "OTHER", FullScreen: true,
GameCode: vendorGameID, ClientOrigin: "COMMON",
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL), GameMode: formatAdminGameModeRaw(baishunFixedGameMode),
Showcase: true, }
Sort: int64(catalog.VendorGameID), if err := tx.Create(&cfg).Error; err != nil {
FullScreen: true, return nil, err
ClientOrigin: "COMMON",
GameMode: formatAdminGameModeRaw(baishunFixedGameMode),
}
if err := tx.Create(&cfg).Error; err != nil {
return nil, err
}
} }
nextID, idErr := utils.NextID() nextID, idErr := utils.NextID()
if idErr != nil { if idErr != nil {
return nil, idErr return nil, idErr
} }
newExt := model.SysGameListVendorExt{ extraJSON := updateExtExtraJSONWithGameType(ext.ExtraJSON, nil, baishunVendorType, ext.ID != 0)
ID: nextID, extValues := map[string]any{
GameListConfigID: cfg.ID, "game_list_config_id": cfg.ID,
SysOrigin: sysOrigin, "sys_origin": sysOrigin,
VendorType: baishunVendorType, "profile": profile,
VendorGameID: vendorGameID, "vendor_type": baishunVendorType,
LaunchMode: baishunLaunchModeRemote, "vendor_game_id": vendorGameID,
PackageVersion: catalog.PackageVersion, "launch_mode": baishunLaunchModeRemote,
PackageURL: catalog.DownloadURL, "package_version": catalog.PackageVersion,
PreviewURL: catalog.PreviewURL, "package_url": catalog.DownloadURL,
Orientation: catalog.Orientation, "preview_url": catalog.PreviewURL,
SafeHeight: catalog.SafeHeight, "orientation": catalog.Orientation,
GSP: defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()), "safe_height": catalog.SafeHeight,
BridgeSchemaVersion: "1.0", "gsp": defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()),
ExtraJSON: updateExtExtraJSONWithGameType("", nil, baishunVendorType, false), "bridge_schema_version": "1.0",
Enabled: true, "extra_json": extraJSON,
CreateTime: now, "enabled": true,
UpdateTime: now, "update_time": now,
} }
if err := tx.Create(&newExt).Error; err != nil { if ext.ID > 0 {
return nil, err if err := tx.Model(&model.SysGameListVendorExt{}).
Where("id = ?", ext.ID).
Updates(extValues).Error; err != nil {
return nil, err
}
} else {
newExt := model.SysGameListVendorExt{
ID: nextID,
GameListConfigID: cfg.ID,
SysOrigin: sysOrigin,
Profile: profile,
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, runtimeCfg.gsp()),
BridgeSchemaVersion: "1.0",
ExtraJSON: extraJSON,
Enabled: true,
CreateTime: now,
UpdateTime: now,
}
if err := tx.Create(&newExt).Error; err != nil {
return nil, err
}
} }
resp.Imported++ resp.Imported++
} }
@ -632,6 +719,7 @@ func (r adminGameRow) toAdminItem() AdminBaishunGameItem {
return AdminBaishunGameItem{ return AdminBaishunGameItem{
ID: r.ID, ID: r.ID,
SysOrigin: r.SysOrigin, SysOrigin: r.SysOrigin,
Profile: normalizeBaishunProfile(r.Profile),
GameID: r.InternalGameID, GameID: r.InternalGameID,
GameType: gameTypeFromExtraJSON(r.ExtExtraJSON, baishunVendorType), GameType: gameTypeFromExtraJSON(r.ExtExtraJSON, baishunVendorType),
VendorGameID: parseAdminVendorGameID(r.VendorGameID, r.InternalGameID), VendorGameID: parseAdminVendorGameID(r.VendorGameID, r.InternalGameID),

View File

@ -1,6 +1,12 @@
package baishun package baishun
import "testing" import (
"context"
"testing"
"time"
"chatapp3-golang/internal/model"
)
func TestResolveAdminShowcase(t *testing.T) { func TestResolveAdminShowcase(t *testing.T) {
t.Run("create defaults to enabled", func(t *testing.T) { t.Run("create defaults to enabled", func(t *testing.T) {
@ -22,3 +28,192 @@ func TestResolveAdminShowcase(t *testing.T) {
} }
}) })
} }
func TestImportAndListRoomGamesUseActiveProfile(t *testing.T) {
service, db := newTestBaishunService(t)
ctx := context.Background()
now := time.Now()
if err := db.Create(&[]model.BaishunProviderConfig{
{
ID: 1001,
SysOrigin: "LIKEI",
Profile: baishunProfileProd,
Active: true,
PlatformBaseURL: "https://prod.example.com",
AppID: 111,
AppChannel: "prod",
AppKey: "prod-key",
GSP: 101,
CreateTime: now,
UpdateTime: now,
},
{
ID: 1002,
SysOrigin: "LIKEI",
Profile: baishunProfileTest,
Active: false,
PlatformBaseURL: "https://test.example.com",
AppID: 222,
AppChannel: "test",
AppKey: "test-key",
GSP: 102,
CreateTime: now,
UpdateTime: now,
},
}).Error; err != nil {
t.Fatalf("create provider configs: %v", err)
}
if err := db.Create(&[]model.BaishunGameCatalog{
{
ID: 2001,
SysOrigin: "LIKEI",
Profile: baishunProfileProd,
InternalGameID: "bs_9001",
VendorGameID: 9001,
Name: "Prod Game",
Cover: "https://cdn.example.com/prod.png",
DownloadURL: "https://cdn.example.com/prod/index.html",
Status: baishunCatalogEnabled,
CreateTime: now,
UpdateTime: now,
},
{
ID: 2002,
SysOrigin: "LIKEI",
Profile: baishunProfileTest,
InternalGameID: "bs_9001",
VendorGameID: 9001,
Name: "Test Game",
Cover: "https://cdn.example.com/test.png",
DownloadURL: "https://cdn.example.com/test/index.html",
Status: baishunCatalogEnabled,
CreateTime: now,
UpdateTime: now,
},
}).Error; err != nil {
t.Fatalf("create catalogs: %v", err)
}
if resp, err := service.ImportCatalogGames(ctx, "LIKEI", baishunProfileProd, []int{9001}); err != nil {
t.Fatalf("import prod catalog: %v", err)
} else if resp.Imported != 1 {
t.Fatalf("prod imported = %d, want 1", resp.Imported)
}
if resp, err := service.ImportCatalogGames(ctx, "LIKEI", baishunProfileTest, []int{9001}); err != nil {
t.Fatalf("import test catalog: %v", err)
} else if resp.Imported != 1 {
t.Fatalf("test imported = %d, want 1", resp.Imported)
}
prodList, err := service.ListRoomGames(ctx, AuthUser{SysOrigin: "LIKEI"}, "room-1", "")
if err != nil {
t.Fatalf("list prod games: %v", err)
}
if len(prodList.Items) != 1 || prodList.Items[0].Name != "Prod Game" {
t.Fatalf("prod list = %+v, want only Prod Game", prodList.Items)
}
if _, err := service.ActivateProviderProfile(ctx, ActivateBaishunProviderProfileRequest{
SysOrigin: "LIKEI",
Profile: baishunProfileTest,
}); err != nil {
t.Fatalf("activate test profile: %v", err)
}
testList, err := service.ListRoomGames(ctx, AuthUser{SysOrigin: "LIKEI"}, "room-1", "")
if err != nil {
t.Fatalf("list test games: %v", err)
}
if len(testList.Items) != 1 || testList.Items[0].Name != "Test Game" {
t.Fatalf("test list = %+v, want only Test Game", testList.Items)
}
}
func TestCatalogAddedRequiresExistingGameConfigAndImportRepairsDanglingExt(t *testing.T) {
service, db := newTestBaishunService(t)
ctx := context.Background()
now := time.Now()
if err := db.Create(&model.BaishunGameCatalog{
ID: 3001,
SysOrigin: "LIKEI",
Profile: baishunProfileProd,
InternalGameID: "bs_1021",
VendorGameID: 1021,
Name: "LuckyGift",
Cover: "https://cdn.example.com/lucky.png",
DownloadURL: "https://cdn.example.com/lucky/index.html",
Status: baishunCatalogEnabled,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("create catalog: %v", err)
}
if err := db.Create(&model.SysGameListVendorExt{
ID: 4001,
GameListConfigID: 999999,
SysOrigin: "LIKEI",
Profile: baishunProfileProd,
VendorType: baishunVendorType,
VendorGameID: "1021",
Enabled: true,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("create dangling ext: %v", err)
}
if catalogAddedForVendor(t, service, ctx, "LIKEI", baishunProfileProd, 1021) {
t.Fatalf("catalog added before import = true, want false for dangling ext")
}
importResp, err := service.ImportCatalogGames(ctx, "LIKEI", baishunProfileProd, []int{1021})
if err != nil {
t.Fatalf("import catalog with dangling ext: %v", err)
}
if importResp.Imported != 1 || importResp.Existing != 0 {
t.Fatalf("import response = %+v, want imported 1 existing 0", importResp)
}
var ext model.SysGameListVendorExt
if err := db.Where("id = ?", int64(4001)).First(&ext).Error; err != nil {
t.Fatalf("load repaired ext: %v", err)
}
if ext.GameListConfigID == 999999 || ext.GameListConfigID == 0 {
t.Fatalf("ext game_list_config_id = %d, want repaired config id", ext.GameListConfigID)
}
var cfg model.SysGameListConfig
if err := db.Where("id = ? AND sys_origin = ? AND game_origin = ?", ext.GameListConfigID, "LIKEI", baishunVendorType).First(&cfg).Error; err != nil {
t.Fatalf("load repaired config: %v", err)
}
if cfg.Name != "LuckyGift" {
t.Fatalf("repaired config name = %q, want LuckyGift", cfg.Name)
}
if !catalogAddedForVendor(t, service, ctx, "LIKEI", baishunProfileProd, 1021) {
t.Fatalf("catalog added after import = false, want true")
}
}
func catalogAddedForVendor(t *testing.T, service *BaishunService, ctx context.Context, sysOrigin string, profile string, vendorGameID int) bool {
t.Helper()
var rows []struct {
AddedConfigID *int64
}
err := service.buildAdminCatalogQuery(ctx, sysOrigin, profile, "").
Where("cat.vendor_game_id = ?", vendorGameID).
Select("cfg.id AS added_config_id").
Scan(&rows).Error
if err != nil {
t.Fatalf("query catalog added flag: %v", err)
}
if len(rows) != 1 {
t.Fatalf("catalog rows = %d, want 1", len(rows))
}
return rows[0].AddedConfigID != nil && *rows[0].AddedConfigID > 0
}

View File

@ -18,11 +18,12 @@ import (
// SyncCatalog 从百顺平台拉取游戏目录并同步到本地表。 // SyncCatalog 从百顺平台拉取游戏目录并同步到本地表。
func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) { func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
sysOrigin := defaultIfBlank(req.SysOrigin, "LIKEI") sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
if strings.TrimSpace(sysOrigin) == "" { profile, err := s.requestProfile(ctx, sysOrigin, req.Profile)
sysOrigin = "LIKEI" if err != nil {
return nil, err
} }
runtimeCfg, err := s.requireProviderConfig(ctx, sysOrigin) runtimeCfg, err := s.requireProviderConfigForProfile(ctx, sysOrigin, profile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -51,6 +52,7 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
record := model.BaishunGameCatalog{ record := model.BaishunGameCatalog{
ID: id, ID: id,
SysOrigin: sysOrigin, SysOrigin: sysOrigin,
Profile: profile,
InternalGameID: fmt.Sprintf("bs_%d", game.GameID), InternalGameID: fmt.Sprintf("bs_%d", game.GameID),
VendorGameID: game.GameID, VendorGameID: game.GameID,
Name: game.Name, Name: game.Name,
@ -69,7 +71,7 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
UpdateTime: time.Now(), UpdateTime: time.Now(),
} }
err = s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{ err = s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "vendor_game_id"}}, Columns: []clause.Column{{Name: "sys_origin"}, {Name: "profile"}, {Name: "vendor_game_id"}},
DoUpdates: clause.AssignmentColumns([]string{ DoUpdates: clause.AssignmentColumns([]string{
"internal_game_id", "name", "cover", "preview_url", "download_url", "internal_game_id", "name", "cover", "preview_url", "download_url",
"package_version", "game_mode_json", "orientation", "safe_height", "package_version", "game_mode_json", "orientation", "safe_height",

View File

@ -10,6 +10,7 @@ import (
type adminCatalogRow struct { type adminCatalogRow struct {
VendorGameID int VendorGameID int
Profile string
InternalGameID string InternalGameID string
Name string Name string
Cover string Cover string
@ -28,24 +29,30 @@ type adminCatalogRow struct {
func (s *BaishunService) PageCatalog( func (s *BaishunService) PageCatalog(
ctx context.Context, ctx context.Context,
sysOrigin string, sysOrigin string,
profile string,
keyword string, keyword string,
cursor int, cursor int,
limit int, limit int,
) (*AdminBaishunCatalogPageResponse, error) { ) (*AdminBaishunCatalogPageResponse, error) {
sysOrigin = normalizeAdminSysOrigin(sysOrigin) sysOrigin = normalizeAdminSysOrigin(sysOrigin)
profile, err := s.requestProfile(ctx, sysOrigin, profile)
if err != nil {
return nil, err
}
cursor = normalizePageCursor(cursor) cursor = normalizePageCursor(cursor)
limit = normalizePageLimit(limit) limit = normalizePageLimit(limit)
countQuery := s.buildAdminCatalogQuery(ctx, sysOrigin, keyword) countQuery := s.buildAdminCatalogQuery(ctx, sysOrigin, profile, keyword)
var total int64 var total int64
if err := countQuery.Count(&total).Error; err != nil { if err := countQuery.Count(&total).Error; err != nil {
return nil, err return nil, err
} }
var rows []adminCatalogRow var rows []adminCatalogRow
err := s.buildAdminCatalogQuery(ctx, sysOrigin, keyword). err = s.buildAdminCatalogQuery(ctx, sysOrigin, profile, keyword).
Select(` Select(`
cat.vendor_game_id AS vendor_game_id, cat.vendor_game_id AS vendor_game_id,
cat.profile AS profile,
cat.internal_game_id AS internal_game_id, cat.internal_game_id AS internal_game_id,
cat.name AS name, cat.name AS name,
cat.cover AS cover, cat.cover AS cover,
@ -56,7 +63,7 @@ func (s *BaishunService) PageCatalog(
cat.status AS status, cat.status AS status,
cat.safe_height AS safe_height, cat.safe_height AS safe_height,
cat.orientation AS orientation, cat.orientation AS orientation,
ext.game_list_config_id AS added_config_id, cfg.id AS added_config_id,
cfg.is_showcase AS showcase, cfg.is_showcase AS showcase,
DATE_FORMAT(cat.update_time, '%Y-%m-%d %H:%i:%s') AS update_time DATE_FORMAT(cat.update_time, '%Y-%m-%d %H:%i:%s') AS update_time
`). `).
@ -72,6 +79,7 @@ func (s *BaishunService) PageCatalog(
for _, row := range rows { for _, row := range rows {
items = append(items, AdminBaishunCatalogItem{ items = append(items, AdminBaishunCatalogItem{
VendorGameID: row.VendorGameID, VendorGameID: row.VendorGameID,
Profile: normalizeBaishunProfile(row.Profile),
GameID: row.InternalGameID, GameID: row.InternalGameID,
Name: row.Name, Name: row.Name,
Cover: row.Cover, Cover: row.Cover,
@ -100,18 +108,24 @@ func (s *BaishunService) FetchAdminCatalog(ctx context.Context, req SyncCatalogR
return s.SyncCatalog(ctx, req) return s.SyncCatalog(ctx, req)
} }
func (s *BaishunService) buildAdminCatalogQuery(ctx context.Context, sysOrigin string, keyword string) *gorm.DB { func (s *BaishunService) buildAdminCatalogQuery(ctx context.Context, sysOrigin string, profile string, keyword string) *gorm.DB {
query := s.repo.DB.WithContext(ctx). query := s.repo.DB.WithContext(ctx).
Table("baishun_game_catalog AS cat"). Table("baishun_game_catalog AS cat").
Joins(` Joins(`
LEFT JOIN sys_game_list_vendor_ext ext LEFT JOIN sys_game_list_vendor_ext ext
ON ext.sys_origin = cat.sys_origin ON ext.sys_origin = cat.sys_origin
AND ext.profile = cat.profile
AND ext.vendor_type = ? AND ext.vendor_type = ?
AND ext.vendor_game_id = CAST(cat.vendor_game_id AS CHAR) AND ext.vendor_game_id = CAST(cat.vendor_game_id AS CHAR)
AND ext.enabled = 1 AND ext.enabled = 1
`, baishunVendorType). `, baishunVendorType).
Joins("LEFT JOIN sys_game_list_config cfg ON cfg.id = ext.game_list_config_id"). Joins(`
Where("cat.sys_origin = ?", sysOrigin) LEFT JOIN sys_game_list_config cfg
ON cfg.id = ext.game_list_config_id
AND cfg.sys_origin = cat.sys_origin
AND cfg.game_origin = ?
`, baishunVendorType).
Where("cat.sys_origin = ? AND cat.profile = ?", sysOrigin, profile)
keyword = strings.TrimSpace(keyword) keyword = strings.TrimSpace(keyword)
if keyword != "" { if keyword != "" {
like := "%" + keyword + "%" like := "%" + keyword + "%"
@ -134,3 +148,14 @@ func (s *BaishunService) requireProviderConfig(ctx context.Context, sysOrigin st
} }
return runtimeCfg, nil return runtimeCfg, nil
} }
func (s *BaishunService) requireProviderConfigForProfile(ctx context.Context, sysOrigin string, profile string) (baishunRuntimeConfig, error) {
runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
if err != nil {
return baishunRuntimeConfig{}, err
}
if strings.TrimSpace(runtimeCfg.PlatformBaseURL) == "" || runtimeCfg.AppID <= 0 || strings.TrimSpace(runtimeCfg.AppKey) == "" {
return baishunRuntimeConfig{}, NewAppError(http.StatusBadRequest, "baishun_config_missing", "baishun provider config is missing")
}
return runtimeCfg, nil
}

View File

@ -0,0 +1,25 @@
package baishun
import "context"
const javaGameListCachePattern = "GAME_LIST*"
func (s *BaishunService) clearJavaGameListCache(ctx context.Context) {
if s.repo.Redis == nil {
return
}
var cursor uint64
for {
keys, nextCursor, err := s.repo.Redis.Scan(ctx, cursor, javaGameListCachePattern, 100).Result()
if err != nil {
return
}
if len(keys) > 0 {
_ = s.repo.Redis.Del(ctx, keys...).Err()
}
if nextCursor == 0 {
return
}
cursor = nextCursor
}
}

View File

@ -2,6 +2,7 @@ package baishun
import ( import (
"context" "context"
"strings"
"testing" "testing"
"chatapp3-golang/internal/config" "chatapp3-golang/internal/config"
@ -37,7 +38,8 @@ func (stubGateway) ChangeGoldBalance(context.Context, integration.GoldReceiptCom
func newTestBaishunService(t *testing.T) (*BaishunService, *gorm.DB) { func newTestBaishunService(t *testing.T) (*BaishunService, *gorm.DB) {
t.Helper() t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil { if err != nil {
t.Fatalf("open sqlite: %v", err) t.Fatalf("open sqlite: %v", err)
} }
@ -73,6 +75,7 @@ func TestLaunchGameFallsBackToCatalogWhenConfigMissing(t *testing.T) {
if err := db.Create(&model.BaishunGameCatalog{ if err := db.Create(&model.BaishunGameCatalog{
ID: 1, ID: 1,
SysOrigin: "LIKEI", SysOrigin: "LIKEI",
Profile: baishunProfileProd,
InternalGameID: "bs_1146", InternalGameID: "bs_1146",
VendorGameID: 1146, VendorGameID: 1146,
Name: "LordOfOlympus", Name: "LordOfOlympus",

View File

@ -0,0 +1,61 @@
package baishun
import (
"chatapp3-golang/internal/model"
"context"
"net/http"
"strings"
"gorm.io/gorm"
)
const (
baishunProfileProd = "PROD"
baishunProfileTest = "TEST"
)
func normalizeBaishunProfile(profile string) string {
switch strings.ToUpper(strings.TrimSpace(profile)) {
case "", "PROD", "PRODUCT", "PRODUCTION", "ONLINE", "OFFICIAL", "FORMAL":
return baishunProfileProd
case "TEST", "TESTING", "SANDBOX", "DEV":
return baishunProfileTest
default:
return strings.ToUpper(strings.TrimSpace(profile))
}
}
func validateBaishunProfile(profile string) (string, error) {
profile = normalizeBaishunProfile(profile)
switch profile {
case baishunProfileProd, baishunProfileTest:
return profile, nil
default:
return "", NewAppError(http.StatusBadRequest, "profile_invalid", "profile must be PROD or TEST")
}
}
func (s *BaishunService) activeProfile(ctx context.Context, sysOrigin string) (string, error) {
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
var row model.BaishunProviderConfig
err := s.repo.DB.WithContext(ctx).
Where("sys_origin = ? AND active = ?", sysOrigin, true).
Order("update_time DESC").
Limit(1).
First(&row).Error
if err == nil {
return normalizeBaishunProfile(row.Profile), nil
}
if err == gorm.ErrRecordNotFound {
return baishunProfileProd, nil
}
return "", err
}
func (s *BaishunService) requestProfile(ctx context.Context, sysOrigin string, profile string) (string, error) {
if strings.TrimSpace(profile) != "" {
return validateBaishunProfile(profile)
}
return s.activeProfile(ctx, sysOrigin)
}

View File

@ -14,6 +14,7 @@ import (
type baishunRuntimeConfig struct { type baishunRuntimeConfig struct {
SysOrigin string SysOrigin string
Profile string
PlatformBaseURL string PlatformBaseURL string
AppID int64 AppID int64
AppName string AppName string
@ -52,6 +53,7 @@ func (c baishunRuntimeConfig) ssTokenTTLSeconds() int {
func (s *BaishunService) defaultRuntimeConfig(sysOrigin string) baishunRuntimeConfig { func (s *BaishunService) defaultRuntimeConfig(sysOrigin string) baishunRuntimeConfig {
return baishunRuntimeConfig{ return baishunRuntimeConfig{
SysOrigin: normalizeAdminSysOrigin(sysOrigin), SysOrigin: normalizeAdminSysOrigin(sysOrigin),
Profile: baishunProfileProd,
PlatformBaseURL: strings.TrimSpace(s.cfg.Baishun.PlatformBaseURL), PlatformBaseURL: strings.TrimSpace(s.cfg.Baishun.PlatformBaseURL),
AppID: s.cfg.Baishun.AppID, AppID: s.cfg.Baishun.AppID,
AppName: strings.TrimSpace(s.cfg.Baishun.AppName), AppName: strings.TrimSpace(s.cfg.Baishun.AppName),
@ -65,6 +67,7 @@ func (s *BaishunService) defaultRuntimeConfig(sysOrigin string) baishunRuntimeCo
func applyProviderRow(base baishunRuntimeConfig, row model.BaishunProviderConfig) baishunRuntimeConfig { func applyProviderRow(base baishunRuntimeConfig, row model.BaishunProviderConfig) baishunRuntimeConfig {
base.SysOrigin = normalizeAdminSysOrigin(defaultIfBlank(row.SysOrigin, base.SysOrigin)) base.SysOrigin = normalizeAdminSysOrigin(defaultIfBlank(row.SysOrigin, base.SysOrigin))
base.Profile = normalizeBaishunProfile(defaultIfBlank(row.Profile, base.Profile))
if strings.TrimSpace(row.PlatformBaseURL) != "" { if strings.TrimSpace(row.PlatformBaseURL) != "" {
base.PlatformBaseURL = strings.TrimSpace(row.PlatformBaseURL) base.PlatformBaseURL = strings.TrimSpace(row.PlatformBaseURL)
} }
@ -94,11 +97,25 @@ func applyProviderRow(base baishunRuntimeConfig, row model.BaishunProviderConfig
func (s *BaishunService) resolveRuntimeConfig(ctx context.Context, sysOrigin string) (baishunRuntimeConfig, error) { func (s *BaishunService) resolveRuntimeConfig(ctx context.Context, sysOrigin string) (baishunRuntimeConfig, error) {
sysOrigin = normalizeAdminSysOrigin(sysOrigin) sysOrigin = normalizeAdminSysOrigin(sysOrigin)
profile, err := s.activeProfile(ctx, sysOrigin)
if err != nil {
return baishunRuntimeConfig{}, err
}
return s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
}
func (s *BaishunService) resolveRuntimeConfigForProfile(ctx context.Context, sysOrigin string, profile string) (baishunRuntimeConfig, error) {
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
profile, err := validateBaishunProfile(profile)
if err != nil {
return baishunRuntimeConfig{}, err
}
base := s.defaultRuntimeConfig(sysOrigin) base := s.defaultRuntimeConfig(sysOrigin)
base.Profile = profile
var row model.BaishunProviderConfig var row model.BaishunProviderConfig
err := s.repo.DB.WithContext(ctx). err = s.repo.DB.WithContext(ctx).
Where("sys_origin = ?", sysOrigin). Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
Limit(1). Limit(1).
First(&row).Error First(&row).Error
if err == nil { if err == nil {
@ -133,7 +150,11 @@ func (s *BaishunService) resolveRuntimeConfigByApp(ctx context.Context, appID in
for _, build := range queries { for _, build := range queries {
var row model.BaishunProviderConfig var row model.BaishunProviderConfig
err := build(s.repo.DB.WithContext(ctx)).Limit(1).First(&row).Error err := build(s.repo.DB.WithContext(ctx)).
Order("active DESC").
Order("update_time DESC").
Limit(1).
First(&row).Error
if err == nil { if err == nil {
return applyProviderRow(s.defaultRuntimeConfig(row.SysOrigin), row), nil return applyProviderRow(s.defaultRuntimeConfig(row.SysOrigin), row), nil
} }
@ -144,21 +165,33 @@ func (s *BaishunService) resolveRuntimeConfigByApp(ctx context.Context, appID in
return base, nil return base, nil
} }
func (s *BaishunService) GetProviderConfig(ctx context.Context, sysOrigin string) (*BaishunProviderConfigItem, error) { func (s *BaishunService) GetProviderConfig(ctx context.Context, sysOrigin string, profile string) (*BaishunProviderConfigItem, error) {
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin) sysOrigin = normalizeAdminSysOrigin(sysOrigin)
activeProfile, err := s.activeProfile(ctx, sysOrigin)
if err != nil {
return nil, err
}
profile, err = s.requestProfile(ctx, sysOrigin, profile)
if err != nil {
return nil, err
}
runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var row model.BaishunProviderConfig var row model.BaishunProviderConfig
_ = s.repo.DB.WithContext(ctx). _ = s.repo.DB.WithContext(ctx).
Where("sys_origin = ?", normalizeAdminSysOrigin(sysOrigin)). Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
Limit(1). Limit(1).
First(&row).Error First(&row).Error
return &BaishunProviderConfigItem{ return &BaishunProviderConfigItem{
ID: row.ID, ID: row.ID,
SysOrigin: runtimeCfg.SysOrigin, SysOrigin: runtimeCfg.SysOrigin,
Profile: runtimeCfg.Profile,
ActiveProfile: activeProfile,
Active: runtimeCfg.Profile == activeProfile,
PlatformBaseURL: runtimeCfg.PlatformBaseURL, PlatformBaseURL: runtimeCfg.PlatformBaseURL,
AppID: runtimeCfg.AppID, AppID: runtimeCfg.AppID,
AppName: runtimeCfg.AppName, AppName: runtimeCfg.AppName,
@ -173,6 +206,10 @@ func (s *BaishunService) GetProviderConfig(ctx context.Context, sysOrigin string
func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishunProviderConfigRequest) (*BaishunProviderConfigItem, error) { func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishunProviderConfigRequest) (*BaishunProviderConfigItem, error) {
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin) sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
profile, err := validateBaishunProfile(req.Profile)
if err != nil {
return nil, err
}
platformBaseURL := strings.TrimSpace(req.PlatformBaseURL) platformBaseURL := strings.TrimSpace(req.PlatformBaseURL)
appChannel := strings.TrimSpace(req.AppChannel) appChannel := strings.TrimSpace(req.AppChannel)
appKey := strings.TrimSpace(req.AppKey) appKey := strings.TrimSpace(req.AppKey)
@ -196,6 +233,7 @@ func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishun
row := model.BaishunProviderConfig{ row := model.BaishunProviderConfig{
ID: id, ID: id,
SysOrigin: sysOrigin, SysOrigin: sysOrigin,
Profile: profile,
PlatformBaseURL: platformBaseURL, PlatformBaseURL: platformBaseURL,
AppID: req.AppID, AppID: req.AppID,
AppName: strings.TrimSpace(req.AppName), AppName: strings.TrimSpace(req.AppName),
@ -216,9 +254,17 @@ func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishun
if row.SSTokenTTLSeconds <= 0 { if row.SSTokenTTLSeconds <= 0 {
row.SSTokenTTLSeconds = 86400 row.SSTokenTTLSeconds = 86400
} }
var activeCount int64
if err := s.repo.DB.WithContext(ctx).
Model(&model.BaishunProviderConfig{}).
Where("sys_origin = ? AND active = ?", sysOrigin, true).
Count(&activeCount).Error; err != nil {
return nil, err
}
row.Active = activeCount == 0
if err := s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{ if err := s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "sys_origin"}}, Columns: []clause.Column{{Name: "sys_origin"}, {Name: "profile"}},
DoUpdates: clause.AssignmentColumns([]string{ DoUpdates: clause.AssignmentColumns([]string{
"platform_base_url", "app_id", "app_name", "app_channel", "app_key", "platform_base_url", "app_id", "app_name", "app_channel", "app_key",
"gsp", "launch_code_ttl_seconds", "ss_token_ttl_seconds", "update_time", "gsp", "launch_code_ttl_seconds", "ss_token_ttl_seconds", "update_time",
@ -226,5 +272,49 @@ func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishun
}).Create(&row).Error; err != nil { }).Create(&row).Error; err != nil {
return nil, err return nil, err
} }
return s.GetProviderConfig(ctx, sysOrigin) if row.Active {
s.clearJavaGameListCache(ctx)
}
return s.GetProviderConfig(ctx, sysOrigin, profile)
}
func (s *BaishunService) ActivateProviderProfile(ctx context.Context, req ActivateBaishunProviderProfileRequest) (*BaishunProviderConfigItem, error) {
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
profile, err := validateBaishunProfile(req.Profile)
if err != nil {
return nil, err
}
var row model.BaishunProviderConfig
if err := s.repo.DB.WithContext(ctx).
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
Limit(1).
First(&row).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, NewAppError(http.StatusBadRequest, "profile_config_missing", "profile config is missing")
}
return nil, err
}
now := time.Now()
if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.BaishunProviderConfig{}).
Where("sys_origin = ?", sysOrigin).
Updates(map[string]any{
"active": false,
"update_time": now,
}).Error; err != nil {
return err
}
return tx.Model(&model.BaishunProviderConfig{}).
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
Updates(map[string]any{
"active": true,
"update_time": now,
}).Error
}); err != nil {
return nil, err
}
s.clearJavaGameListCache(ctx)
return s.GetProviderConfig(ctx, sysOrigin, profile)
} }

View File

@ -60,12 +60,18 @@ func (s *BaishunService) GetRoomState(ctx context.Context, user AuthUser, roomID
// listRoomGames 联表读取房间可启动游戏,并按展示规则排序。 // listRoomGames 联表读取房间可启动游戏,并按展示规则排序。
func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, category string) ([]RoomGameListItem, error) { func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, category string) ([]RoomGameListItem, error) {
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
profile, err := s.activeProfile(ctx, sysOrigin)
if err != nil {
return nil, err
}
var rows []gameListRow var rows []gameListRow
query := s.repo.DB.WithContext(ctx). query := s.repo.DB.WithContext(ctx).
Table("sys_game_list_config AS cfg"). Table("sys_game_list_config AS cfg").
Select(` Select(`
cfg.id AS config_id, cfg.id AS config_id,
cfg.sys_origin AS sys_origin, cfg.sys_origin AS sys_origin,
ext.profile AS profile,
cfg.game_id AS internal_game_id, cfg.game_id AS internal_game_id,
cfg.name AS name, cfg.name AS name,
cfg.category AS category, cfg.category AS category,
@ -93,8 +99,8 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
cat.safe_height AS catalog_safe_height, cat.safe_height AS catalog_safe_height,
cat.status AS catalog_status cat.status AS catalog_status
`). `).
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1"). Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1 AND ext.profile = ?", profile).
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"). Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, baishunVendorType) Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, baishunVendorType)
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), "CHAT_ROOM") { if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), "CHAT_ROOM") {
query = query.Where("cfg.category = ?", category) query = query.Where("cfg.category = ?", category)
@ -112,12 +118,18 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
// findGameRow 按内部游戏 ID 查询游戏聚合信息。 // findGameRow 按内部游戏 ID 查询游戏聚合信息。
func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID string) (gameListRow, error) { func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID string) (gameListRow, error) {
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
profile, err := s.activeProfile(ctx, sysOrigin)
if err != nil {
return gameListRow{}, err
}
var row gameListRow var row gameListRow
err := s.repo.DB.WithContext(ctx). err = s.repo.DB.WithContext(ctx).
Table("sys_game_list_config AS cfg"). Table("sys_game_list_config AS cfg").
Select(` Select(`
cfg.id AS config_id, cfg.id AS config_id,
cfg.sys_origin AS sys_origin, cfg.sys_origin AS sys_origin,
ext.profile AS profile,
cfg.game_id AS internal_game_id, cfg.game_id AS internal_game_id,
cfg.name AS name, cfg.name AS name,
cfg.category AS category, cfg.category AS category,
@ -145,8 +157,8 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
cat.safe_height AS catalog_safe_height, cat.safe_height AS catalog_safe_height,
cat.status AS catalog_status cat.status AS catalog_status
`). `).
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1"). Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1 AND ext.profile = ?", profile).
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"). Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
Where("cfg.sys_origin = ? AND cfg.game_id = ? AND ext.vendor_type = ?", sysOrigin, gameID, baishunVendorType). Where("cfg.sys_origin = ? AND cfg.game_id = ? AND ext.vendor_type = ?", sysOrigin, gameID, baishunVendorType).
Limit(1). Limit(1).
Scan(&row).Error Scan(&row).Error
@ -159,10 +171,11 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
var catalog model.BaishunGameCatalog var catalog model.BaishunGameCatalog
if err := s.repo.DB.WithContext(ctx). if err := s.repo.DB.WithContext(ctx).
Where("sys_origin = ? AND internal_game_id = ?", sysOrigin, gameID). Where("sys_origin = ? AND profile = ? AND internal_game_id = ?", sysOrigin, profile, gameID).
First(&catalog).Error; err == nil { First(&catalog).Error; err == nil {
return gameListRow{ return gameListRow{
SysOrigin: sysOrigin, SysOrigin: sysOrigin,
Profile: profile,
InternalGameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)), InternalGameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
Name: catalog.Name, Name: catalog.Name,
Cover: catalog.Cover, Cover: catalog.Cover,
@ -188,12 +201,18 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
// findGameRowByConfigID 按系统游戏配置主键查询游戏聚合信息。 // findGameRowByConfigID 按系统游戏配置主键查询游戏聚合信息。
func (s *BaishunService) findGameRowByConfigID(ctx context.Context, sysOrigin string, configID int64) (gameListRow, error) { func (s *BaishunService) findGameRowByConfigID(ctx context.Context, sysOrigin string, configID int64) (gameListRow, error) {
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
profile, err := s.activeProfile(ctx, sysOrigin)
if err != nil {
return gameListRow{}, err
}
var row gameListRow var row gameListRow
err := s.repo.DB.WithContext(ctx). err = s.repo.DB.WithContext(ctx).
Table("sys_game_list_config AS cfg"). Table("sys_game_list_config AS cfg").
Select(` Select(`
cfg.id AS config_id, cfg.id AS config_id,
cfg.sys_origin AS sys_origin, cfg.sys_origin AS sys_origin,
ext.profile AS profile,
cfg.game_id AS internal_game_id, cfg.game_id AS internal_game_id,
cfg.name AS name, cfg.name AS name,
cfg.category AS category, cfg.category AS category,
@ -221,8 +240,8 @@ func (s *BaishunService) findGameRowByConfigID(ctx context.Context, sysOrigin st
cat.safe_height AS catalog_safe_height, cat.safe_height AS catalog_safe_height,
cat.status AS catalog_status cat.status AS catalog_status
`). `).
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1"). Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1 AND ext.profile = ?", profile).
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"). Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
Where("cfg.sys_origin = ? AND cfg.id = ? AND ext.vendor_type = ?", sysOrigin, configID, baishunVendorType). Where("cfg.sys_origin = ? AND cfg.id = ? AND ext.vendor_type = ?", sysOrigin, configID, baishunVendorType).
Limit(1). Limit(1).
Scan(&row).Error Scan(&row).Error

View File

@ -175,6 +175,7 @@ type BaishunLaunchAppResponse struct {
// SyncCatalogRequest 是目录同步入参。 // SyncCatalogRequest 是目录同步入参。
type SyncCatalogRequest struct { type SyncCatalogRequest struct {
SysOrigin string `json:"sysOrigin"` SysOrigin string `json:"sysOrigin"`
Profile string `json:"profile"`
VendorGameIDs []int `json:"vendorGameIds"` VendorGameIDs []int `json:"vendorGameIds"`
Force bool `json:"force"` Force bool `json:"force"`
} }
@ -190,6 +191,7 @@ type SyncCatalogResponse struct {
type AdminBaishunGameItem struct { type AdminBaishunGameItem struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SysOrigin string `json:"sysOrigin"` SysOrigin string `json:"sysOrigin"`
Profile string `json:"profile"`
GameID string `json:"gameId"` GameID string `json:"gameId"`
GameType string `json:"gameType,omitempty"` GameType string `json:"gameType,omitempty"`
VendorGameID int `json:"vendorGameId"` VendorGameID int `json:"vendorGameId"`
@ -232,6 +234,7 @@ type AdminBaishunGamePageResponse struct {
type SaveAdminBaishunGameRequest struct { type SaveAdminBaishunGameRequest struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SysOrigin string `json:"sysOrigin"` SysOrigin string `json:"sysOrigin"`
Profile string `json:"profile"`
GameID string `json:"gameId"` GameID string `json:"gameId"`
VendorGameID int `json:"vendorGameId"` VendorGameID int `json:"vendorGameId"`
Name string `json:"name"` Name string `json:"name"`
@ -277,6 +280,9 @@ type SyncAdminCatalogResponse struct {
type BaishunProviderConfigItem struct { type BaishunProviderConfigItem struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SysOrigin string `json:"sysOrigin"` SysOrigin string `json:"sysOrigin"`
Profile string `json:"profile"`
ActiveProfile string `json:"activeProfile"`
Active bool `json:"active"`
PlatformBaseURL string `json:"platformBaseUrl"` PlatformBaseURL string `json:"platformBaseUrl"`
AppID int64 `json:"appId"` AppID int64 `json:"appId"`
AppName string `json:"appName"` AppName string `json:"appName"`
@ -291,6 +297,7 @@ type BaishunProviderConfigItem struct {
// SaveBaishunProviderConfigRequest 是后台保存百顺配置的入参。 // SaveBaishunProviderConfigRequest 是后台保存百顺配置的入参。
type SaveBaishunProviderConfigRequest struct { type SaveBaishunProviderConfigRequest struct {
SysOrigin string `json:"sysOrigin"` SysOrigin string `json:"sysOrigin"`
Profile string `json:"profile"`
PlatformBaseURL string `json:"platformBaseUrl"` PlatformBaseURL string `json:"platformBaseUrl"`
AppID int64 `json:"appId"` AppID int64 `json:"appId"`
AppName string `json:"appName"` AppName string `json:"appName"`
@ -301,9 +308,16 @@ type SaveBaishunProviderConfigRequest struct {
SSTokenTTLSeconds int `json:"ssTokenTtlSeconds"` SSTokenTTLSeconds int `json:"ssTokenTtlSeconds"`
} }
// ActivateBaishunProviderProfileRequest 是切换百顺正式/测试配置档案的入参。
type ActivateBaishunProviderProfileRequest struct {
SysOrigin string `json:"sysOrigin"`
Profile string `json:"profile"`
}
// AdminBaishunCatalogItem 是后台目录页展示的单个百顺目录项。 // AdminBaishunCatalogItem 是后台目录页展示的单个百顺目录项。
type AdminBaishunCatalogItem struct { type AdminBaishunCatalogItem struct {
VendorGameID int `json:"vendorGameId"` VendorGameID int `json:"vendorGameId"`
Profile string `json:"profile"`
GameID string `json:"gameId"` GameID string `json:"gameId"`
Name string `json:"name"` Name string `json:"name"`
Cover string `json:"cover"` Cover string `json:"cover"`
@ -410,6 +424,7 @@ type baishunBalanceInfoResponse struct {
type gameListRow struct { type gameListRow struct {
ConfigID int64 ConfigID int64
SysOrigin string SysOrigin string
Profile string
InternalGameID string InternalGameID string
Name string Name string
Category string Category string

View File

@ -23,6 +23,7 @@ func (s *InviteService) GetHome(ctx context.Context, user AuthUser) (*HomeRespon
now := time.Now() now := time.Now()
timezone := normalizeTimezone(bundle.Config.Timezone) timezone := normalizeTimezone(bundle.Config.Timezone)
monthKey := monthKeyAt(now, timezone) monthKey := monthKeyAt(now, timezone)
periodEndAt, secondsToReset := monthResetInfoAt(now, timezone)
monthly, err := s.findMonthlyProgress(ctx, user.SysOrigin, user.UserID, monthKey) monthly, err := s.findMonthlyProgress(ctx, user.SysOrigin, user.UserID, monthKey)
if err != nil { if err != nil {
return nil, err return nil, err
@ -53,6 +54,9 @@ func (s *InviteService) GetHome(ctx context.Context, user AuthUser) (*HomeRespon
ServiceContact: bundle.Config.ServiceContact, ServiceContact: bundle.Config.ServiceContact,
Timezone: timezone, Timezone: timezone,
MonthKey: monthKey, MonthKey: monthKey,
ServerTimeMs: now.UnixMilli(),
PeriodEndAtMs: periodEndAt.UnixMilli(),
SecondsToReset: secondsToReset,
ValidUserThreshold: effectiveValidThreshold(inviteeRechargeRules), ValidUserThreshold: effectiveValidThreshold(inviteeRechargeRules),
InviterBindReward: rewardFromRule(inviterBindRule), InviterBindReward: rewardFromRule(inviterBindRule),
InviteeBindReward: rewardFromRule(inviteeBindRule), InviteeBindReward: rewardFromRule(inviteeBindRule),
@ -63,7 +67,7 @@ func (s *InviteService) GetHome(ctx context.Context, user AuthUser) (*HomeRespon
resp.MonthlyProgress.ValidUserCount = monthly.ValidUserCount resp.MonthlyProgress.ValidUserCount = monthly.ValidUserCount
resp.MonthlyProgress.TotalRecharge = monthly.TotalRecharge resp.MonthlyProgress.TotalRecharge = monthly.TotalRecharge
resp.MonthlyProgress.RewardGoldCoins = monthly.RewardGoldCoins resp.MonthlyProgress.RewardGoldCoins = monthly.RewardGoldCoins
resp.ValidCountTasks = buildTaskViews(validCountRules, monthly.ValidUserCount, claims) resp.ValidCountTasks = buildTaskViews(validCountRules, monthly.InviteCount, claims)
resp.TotalRechargeTasks = buildTaskViews(totalRechargeRules, monthly.TotalRecharge, claims) resp.TotalRechargeTasks = buildTaskViews(totalRechargeRules, monthly.TotalRecharge, claims)
return resp, nil return resp, nil
} }

View File

@ -0,0 +1,272 @@
package invite
import (
"chatapp3-golang/internal/config"
"chatapp3-golang/internal/integration"
"chatapp3-golang/internal/model"
"context"
"errors"
"strings"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type inviteTestGateway struct {
grantGoldErr error
grantPropsErr error
grantGoldCalls int
}
func (g *inviteTestGateway) EnsureInviteCode(context.Context, string) (integration.InviteCode, error) {
return integration.InviteCode{InviteCode: "INV-TEST"}, nil
}
func (g *inviteTestGateway) GetDeviceFingerprint(context.Context, int64) (string, error) {
return "", nil
}
func (g *inviteTestGateway) GrantGold(context.Context, integration.GrantGoldRequest) error {
g.grantGoldCalls++
return g.grantGoldErr
}
func (g *inviteTestGateway) GrantProps(context.Context, integration.GrantPropsRequest) error {
return g.grantPropsErr
}
func newTestInviteService(t *testing.T, gateway *inviteTestGateway) (*InviteService, *gorm.DB, func()) {
t.Helper()
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(
&model.InviteCodeMapping{},
&model.UserBaseInfo{},
&model.InviteCampaignConfig{},
&model.InviteCampaignRewardRule{},
&model.InviteCampaignRelation{},
&model.InviteCampaignInviteeProgress{},
&model.InviteCampaignMonthlyProgress{},
&model.InviteCampaignTaskClaim{},
&model.InviteCampaignRewardLog{},
&model.InviteCampaignRechargeEvent{},
); err != nil {
t.Fatalf("auto migrate: %v", err)
}
mr, err := miniredis.Run()
if err != nil {
t.Fatalf("miniredis run: %v", err)
}
redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()})
if gateway == nil {
gateway = &inviteTestGateway{}
}
service := NewInviteService(config.Config{
Invite: config.InviteConfig{PublicBaseURL: "https://invite.example.com"},
}, db, redisClient, gateway)
return service, db, func() {
redisClient.Close()
mr.Close()
}
}
func seedInviteCampaign(t *testing.T, db *gorm.DB) {
t.Helper()
now := time.Now()
if err := db.Create(&model.InviteCampaignConfig{
ID: 1,
SysOrigin: "LIKEI",
Enabled: true,
Timezone: "Asia/Shanghai",
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed config: %v", err)
}
}
func TestMonthResetInfoAtReturnsNextMonthStart(t *testing.T) {
now := time.Date(2026, 4, 30, 23, 59, 30, 0, time.FixedZone("CST", 8*3600))
periodEnd, seconds := monthResetInfoAt(now, "Asia/Shanghai")
if got := periodEnd.In(time.FixedZone("CST", 8*3600)).Format(time.RFC3339); got != "2026-05-01T00:00:00+08:00" {
t.Fatalf("periodEnd = %s, want 2026-05-01T00:00:00+08:00", got)
}
if seconds != 30 {
t.Fatalf("seconds = %d, want 30", seconds)
}
}
func TestGetHomeUsesInviteCountForInviteCountTasks(t *testing.T) {
service, db, cleanup := newTestInviteService(t, nil)
defer cleanup()
seedInviteCampaign(t, db)
now := time.Now()
monthKey := monthKeyAt(now, "Asia/Shanghai")
if err := db.Create(&model.InviteCodeMapping{UserID: 1001, InviteCode: "INV1001", Account: "u1001"}).Error; err != nil {
t.Fatalf("seed invite code: %v", err)
}
if err := db.Create(&model.InviteCampaignRewardRule{
ID: 2001,
SysOrigin: "LIKEI",
RuleType: ruleTypeInviterValidCount,
ThresholdValue: 5,
GoldAmount: 100,
Enabled: true,
Sort: 1,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed rule: %v", err)
}
if err := db.Create(&model.InviteCampaignMonthlyProgress{
ID: 3001,
SysOrigin: "LIKEI",
InviterUserID: 1001,
MonthKey: monthKey,
InviteCount: 7,
ValidUserCount: 0,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed monthly progress: %v", err)
}
resp, err := service.GetHome(context.Background(), AuthUser{UserID: 1001, SysOrigin: "LIKEI"})
if err != nil {
t.Fatalf("GetHome() error = %v", err)
}
if len(resp.ValidCountTasks) != 1 {
t.Fatalf("ValidCountTasks length = %d, want 1", len(resp.ValidCountTasks))
}
task := resp.ValidCountTasks[0]
if task.Progress != 7 || !task.Achieved {
t.Fatalf("task progress = %d achieved = %v, want progress 7 achieved true", task.Progress, task.Achieved)
}
if resp.SecondsToReset <= 0 || resp.PeriodEndAtMs <= resp.ServerTimeMs {
t.Fatalf("invalid countdown fields: server=%d end=%d seconds=%d", resp.ServerTimeMs, resp.PeriodEndAtMs, resp.SecondsToReset)
}
}
func TestClaimTaskUsesInviteCountForInviteCountTasks(t *testing.T) {
gateway := &inviteTestGateway{}
service, db, cleanup := newTestInviteService(t, gateway)
defer cleanup()
seedInviteCampaign(t, db)
now := time.Now()
monthKey := monthKeyAt(now, "Asia/Shanghai")
if err := db.Create(&model.InviteCampaignRewardRule{
ID: 2002,
SysOrigin: "LIKEI",
RuleType: ruleTypeInviterValidCount,
ThresholdValue: 5,
GoldAmount: 100,
Enabled: true,
Sort: 1,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed rule: %v", err)
}
if err := db.Create(&model.InviteCampaignMonthlyProgress{
ID: 3002,
SysOrigin: "LIKEI",
InviterUserID: 1002,
MonthKey: monthKey,
InviteCount: 5,
ValidUserCount: 0,
CreateTime: now,
UpdateTime: now,
}).Error; err != nil {
t.Fatalf("seed monthly progress: %v", err)
}
resp, err := service.ClaimTask(context.Background(), AuthUser{UserID: 1002, SysOrigin: "LIKEI"}, TaskClaimRequest{RuleID: 2002})
if err != nil {
t.Fatalf("ClaimTask() error = %v", err)
}
if !resp.Claimed || resp.Progress != 5 {
t.Fatalf("claim response = %+v, want claimed progress 5", resp)
}
if gateway.grantGoldCalls != 1 {
t.Fatalf("grantGoldCalls = %d, want 1", gateway.grantGoldCalls)
}
}
func TestGetRankUsesLifetimeInviteAndRewardStats(t *testing.T) {
service, db, cleanup := newTestInviteService(t, nil)
defer cleanup()
now := time.Now()
rows := []any{
&model.InviteCampaignRelation{ID: 1, SysOrigin: "LIKEI", InviterUserID: 20, InviteeUserID: 201, EligibleStatus: relationStatusEligible, BindTime: now, CreateTime: now, UpdateTime: now},
&model.InviteCampaignRelation{ID: 2, SysOrigin: "LIKEI", InviterUserID: 20, InviteeUserID: 202, EligibleStatus: relationStatusEligible, BindTime: now, CreateTime: now, UpdateTime: now},
&model.InviteCampaignRelation{ID: 3, SysOrigin: "LIKEI", InviterUserID: 10, InviteeUserID: 101, EligibleStatus: relationStatusEligible, BindTime: now, CreateTime: now, UpdateTime: now},
&model.InviteCampaignRelation{ID: 4, SysOrigin: "LIKEI", InviterUserID: 10, InviteeUserID: 102, EligibleStatus: relationStatusBlocked, BindTime: now, CreateTime: now, UpdateTime: now},
&model.UserBaseInfo{ID: 20, Account: "user20", UserNickname: "Mina", UserAvatar: "avatar-20", OriginSys: "LIKEI", CreateTime: now},
&model.UserBaseInfo{ID: 10, Account: "user10", UserNickname: "Lina", UserAvatar: "avatar-10", OriginSys: "LIKEI", CreateTime: now},
&model.InviteCampaignRewardLog{ID: 11, SysOrigin: "LIKEI", InviterUserID: refInt64(20), TargetUserID: 20, BusinessKey: "r20-1", GoldAmount: 100, Status: rewardLogStatusSuccess, CreateTime: now, UpdateTime: now},
&model.InviteCampaignRewardLog{ID: 12, SysOrigin: "LIKEI", InviterUserID: refInt64(20), TargetUserID: 20, BusinessKey: "r20-2", GoldAmount: 200, Status: rewardLogStatusSuccess, CreateTime: now, UpdateTime: now},
&model.InviteCampaignRewardLog{ID: 13, SysOrigin: "LIKEI", InviterUserID: refInt64(10), TargetUserID: 10, BusinessKey: "r10-1", GoldAmount: 900, Status: rewardLogStatusSuccess, CreateTime: now, UpdateTime: now},
&model.InviteCampaignRewardLog{ID: 14, SysOrigin: "LIKEI", InviterUserID: refInt64(20), TargetUserID: 201, BusinessKey: "invitee-reward", GoldAmount: 999, Status: rewardLogStatusSuccess, CreateTime: now, UpdateTime: now},
&model.InviteCampaignRewardLog{ID: 15, SysOrigin: "LIKEI", InviterUserID: refInt64(20), TargetUserID: 20, BusinessKey: "failed-reward", GoldAmount: 999, Status: rewardLogStatusFailed, CreateTime: now, UpdateTime: now},
}
for _, row := range rows {
if err := db.Create(row).Error; err != nil {
t.Fatalf("seed row %T: %v", row, err)
}
}
resp, err := service.GetRank(context.Background(), AuthUser{SysOrigin: "LIKEI"}, 10)
if err != nil {
t.Fatalf("GetRank() error = %v", err)
}
if len(resp.Items) != 2 {
t.Fatalf("rank length = %d, want 2", len(resp.Items))
}
if got := resp.Items[0]; got.Rank != 1 || got.UserID != 20 || got.Nickname != "Mina" || got.InviteCount != 2 || got.RewardGoldCoins != 300 {
t.Fatalf("top rank = %+v, want user 20 count 2 reward 300", got)
}
if got := resp.Items[1]; got.Rank != 2 || got.UserID != 10 || got.InviteCount != 1 || got.RewardGoldCoins != 900 {
t.Fatalf("second rank = %+v, want user 10 count 1 reward 900", got)
}
}
func TestDispatchGoldReturnsGrantError(t *testing.T) {
grantErr := errors.New("wallet unavailable")
service, db, cleanup := newTestInviteService(t, &inviteTestGateway{grantGoldErr: grantErr})
defer cleanup()
_, err := service.dispatchReward(context.Background(), rewardDispatchInput{
SysOrigin: "LIKEI",
TargetUserID: 1001,
Reward: RewardView{GoldAmount: 100},
Scene: ruleTypeInviterValidCount,
BusinessKey: "claim:1001:202604:1",
GoldOrigin: "INVITE_USER_REWARDS",
Remark: "test reward",
})
if !errors.Is(err, grantErr) {
t.Fatalf("dispatchReward() error = %v, want %v", err, grantErr)
}
var log model.InviteCampaignRewardLog
if err := db.Where("business_key = ?", "claim:1001:202604:1:gold").First(&log).Error; err != nil {
t.Fatalf("load reward log: %v", err)
}
if log.Status != rewardLogStatusFailed {
t.Fatalf("reward log status = %s, want %s", log.Status, rewardLogStatusFailed)
}
}

View File

@ -36,6 +36,21 @@ func monthKeyAt(t time.Time, timezone string) string {
return t.In(location).Format("200601") return t.In(location).Format("200601")
} }
// monthResetInfoAt 返回指定时间所在活动月的结束时间和剩余秒数。
func monthResetInfoAt(t time.Time, timezone string) (time.Time, int64) {
location, err := time.LoadLocation(normalizeTimezone(timezone))
if err != nil {
location = time.FixedZone(defaultTimezone, 3*3600)
}
localTime := t.In(location)
periodEnd := time.Date(localTime.Year(), localTime.Month()+1, 1, 0, 0, 0, 0, location)
seconds := int64(periodEnd.Sub(localTime).Seconds())
if seconds < 0 {
seconds = 0
}
return periodEnd, seconds
}
// digitsOnly 判断字符串是否只包含数字。 // digitsOnly 判断字符串是否只包含数字。
func digitsOnly(value string) bool { func digitsOnly(value string) bool {
for _, ch := range value { for _, ch := range value {

View File

@ -0,0 +1,73 @@
package invite
import (
"context"
"strings"
)
type inviteRankRow struct {
UserID int64
Nickname string
Avatar string
InviteCount int64
RewardGoldCoins int64
}
// GetRank 返回累计邀请排行榜。排行榜不按月重置。
func (s *InviteService) GetRank(ctx context.Context, user AuthUser, limit int) (*InviteRankResponse, error) {
limit = normalizeInviteRankLimit(limit)
rows := make([]inviteRankRow, 0, limit)
if err := s.repo.DB.WithContext(ctx).Raw(`
SELECT
invite_rank.user_id AS user_id,
COALESCE(NULLIF(user_info.user_nickname, ''), invite_code.account, '') AS nickname,
COALESCE(user_info.user_avatar, '') AS avatar,
invite_rank.invite_count AS invite_count,
COALESCE(reward_rank.reward_gold_coins, 0) AS reward_gold_coins
FROM (
SELECT inviter_user_id AS user_id, COUNT(1) AS invite_count
FROM invite_campaign_relation
WHERE sys_origin = ? AND eligible_status = ?
GROUP BY inviter_user_id
) invite_rank
LEFT JOIN (
SELECT target_user_id AS user_id, COALESCE(SUM(gold_amount), 0) AS reward_gold_coins
FROM invite_campaign_reward_log
WHERE sys_origin = ? AND status = ? AND target_user_id = inviter_user_id
GROUP BY target_user_id
) reward_rank ON reward_rank.user_id = invite_rank.user_id
LEFT JOIN user_base_info user_info ON user_info.id = invite_rank.user_id
LEFT JOIN invite_code_mapping invite_code ON invite_code.user_id = invite_rank.user_id
ORDER BY invite_rank.invite_count DESC, reward_gold_coins DESC, invite_rank.user_id ASC
LIMIT ?
`, user.SysOrigin, relationStatusEligible, user.SysOrigin, rewardLogStatusSuccess, limit).Scan(&rows).Error; err != nil {
return nil, err
}
items := make([]InviteRankItem, 0, len(rows))
for index, row := range rows {
items = append(items, InviteRankItem{
Rank: index + 1,
UserID: row.UserID,
Nickname: strings.TrimSpace(row.Nickname),
Avatar: strings.TrimSpace(row.Avatar),
InviteCount: row.InviteCount,
RewardGoldCoins: row.RewardGoldCoins,
})
}
return &InviteRankResponse{
SysOrigin: user.SysOrigin,
Limit: limit,
Items: items,
}, nil
}
func normalizeInviteRankLimit(limit int) int {
if limit <= 0 {
return defaultInviteRankLimit
}
if limit > maxInviteRankLimit {
return maxInviteRankLimit
}
return limit
}

View File

@ -51,7 +51,10 @@ func (s *InviteService) dispatchGold(ctx context.Context, input rewardDispatchIn
Remark: input.Remark, Remark: input.Remark,
}) })
if err != nil { if err != nil {
return s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusFailed, err.Error()) if logErr := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusFailed, err.Error()); logErr != nil {
return logErr
}
return err
} }
if err := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusSuccess, ""); err != nil { if err := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusSuccess, ""); err != nil {
return err return err
@ -87,7 +90,10 @@ func (s *InviteService) dispatchProps(ctx context.Context, input rewardDispatchI
SourceGroupID: *input.Reward.RewardGroupID, SourceGroupID: *input.Reward.RewardGroupID,
}) })
if err != nil { if err != nil {
return s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusFailed, err.Error()) if logErr := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusFailed, err.Error()); logErr != nil {
return logErr
}
return err
} }
return s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusSuccess, "") return s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusSuccess, "")
} }

View File

@ -40,7 +40,7 @@ func (s *InviteService) ClaimTask(ctx context.Context, user AuthUser, req TaskCl
} }
progressValue := monthly.TotalRecharge progressValue := monthly.TotalRecharge
if rule.RuleType == ruleTypeInviterValidCount { if rule.RuleType == ruleTypeInviterValidCount {
progressValue = monthly.ValidUserCount progressValue = monthly.InviteCount
} }
if claimed { if claimed {
return &TaskClaimResponse{ return &TaskClaimResponse{

View File

@ -29,6 +29,8 @@ const (
ruleTypeInviteeRecharge = "INVITEE_RECHARGE" ruleTypeInviteeRecharge = "INVITEE_RECHARGE"
ruleTypeInviterValidCount = "INVITER_VALID_COUNT" ruleTypeInviterValidCount = "INVITER_VALID_COUNT"
ruleTypeInviterTotalRecharge = "INVITER_TOTAL_RECHARGE" ruleTypeInviterTotalRecharge = "INVITER_TOTAL_RECHARGE"
defaultInviteRankLimit = 50
maxInviteRankLimit = 100
) )
var errAlreadyProcessed = errors.New("invite recharge already processed") var errAlreadyProcessed = errors.New("invite recharge already processed")
@ -80,6 +82,9 @@ type HomeResponse struct {
ServiceContact string `json:"serviceContact"` ServiceContact string `json:"serviceContact"`
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
MonthKey string `json:"monthKey"` MonthKey string `json:"monthKey"`
ServerTimeMs int64 `json:"serverTimeMs"`
PeriodEndAtMs int64 `json:"periodEndAtMs"`
SecondsToReset int64 `json:"secondsToReset"`
ValidUserThreshold int64 `json:"validUserThreshold"` ValidUserThreshold int64 `json:"validUserThreshold"`
InviterBindReward RewardView `json:"inviterBindReward"` InviterBindReward RewardView `json:"inviterBindReward"`
InviteeBindReward RewardView `json:"inviteeBindReward"` InviteeBindReward RewardView `json:"inviteeBindReward"`
@ -95,6 +100,23 @@ type HomeResponse struct {
Stats InviteStats `json:"stats"` Stats InviteStats `json:"stats"`
} }
// InviteRankItem 是邀请排行榜单行展示数据。
type InviteRankItem struct {
Rank int `json:"rank"`
UserID int64 `json:"userId"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
InviteCount int64 `json:"inviteCount"`
RewardGoldCoins int64 `json:"rewardGoldCoins"`
}
// InviteRankResponse 是邀请排行榜接口返回体。
type InviteRankResponse struct {
SysOrigin string `json:"sysOrigin"`
Limit int `json:"limit"`
Items []InviteRankItem `json:"items"`
}
// LandingResponse 是邀请落地页接口返回体。 // LandingResponse 是邀请落地页接口返回体。
type LandingResponse struct { type LandingResponse struct {
CampaignEnabled bool `json:"campaignEnabled"` CampaignEnabled bool `json:"campaignEnabled"`

View File

@ -12,6 +12,7 @@ import (
) )
type luckyGiftRuntimeAccount struct { type luckyGiftRuntimeAccount struct {
URL string
AppID string AppID string
AppChannel string AppChannel string
AppKey string AppKey string
@ -40,10 +41,18 @@ func (s *LuckyGiftService) resolveProviderConfig(ctx context.Context, req LuckyG
func (s *LuckyGiftService) resolveRuntimeAccount(ctx context.Context, sysOrigin string) (luckyGiftRuntimeAccount, error) { func (s *LuckyGiftService) resolveRuntimeAccount(ctx context.Context, sysOrigin string) (luckyGiftRuntimeAccount, error) {
account := defaultLuckyGiftRuntimeAccount(s.cfg) account := defaultLuckyGiftRuntimeAccount(s.cfg)
if s.repo.DB == nil {
return account, nil
}
normalizedSysOrigin := normalizeLuckyGiftSysOrigin(sysOrigin)
profile, err := s.resolveActiveBaishunProfile(ctx, normalizedSysOrigin)
if err != nil {
return luckyGiftRuntimeAccount{}, err
}
var row model.BaishunProviderConfig var row model.BaishunProviderConfig
err := s.repo.DB.WithContext(ctx). err = s.repo.DB.WithContext(ctx).
Where("sys_origin = ?", normalizeLuckyGiftSysOrigin(sysOrigin)). Where("sys_origin = ? AND profile = ?", normalizedSysOrigin, profile).
Limit(1). Limit(1).
First(&row).Error First(&row).Error
if err == nil { if err == nil {
@ -55,8 +64,25 @@ func (s *LuckyGiftService) resolveRuntimeAccount(ctx context.Context, sysOrigin
return luckyGiftRuntimeAccount{}, err return luckyGiftRuntimeAccount{}, err
} }
func (s *LuckyGiftService) resolveActiveBaishunProfile(ctx context.Context, sysOrigin string) (string, error) {
var row model.BaishunProviderConfig
err := s.repo.DB.WithContext(ctx).
Where("sys_origin = ? AND active = ?", normalizeLuckyGiftSysOrigin(sysOrigin), true).
Order("update_time DESC").
Limit(1).
First(&row).Error
if err == nil {
return normalizeLuckyGiftBaishunProfile(row.Profile), nil
}
if err == gorm.ErrRecordNotFound {
return luckyGiftBaishunProfileProd, nil
}
return "", err
}
func defaultLuckyGiftRuntimeAccount(cfg config.Config) luckyGiftRuntimeAccount { func defaultLuckyGiftRuntimeAccount(cfg config.Config) luckyGiftRuntimeAccount {
account := luckyGiftRuntimeAccount{ account := luckyGiftRuntimeAccount{
URL: strings.TrimSpace(cfg.Baishun.PlatformBaseURL),
AppChannel: strings.TrimSpace(cfg.Baishun.AppChannel), AppChannel: strings.TrimSpace(cfg.Baishun.AppChannel),
AppKey: strings.TrimSpace(cfg.Baishun.AppKey), AppKey: strings.TrimSpace(cfg.Baishun.AppKey),
} }
@ -67,6 +93,9 @@ func defaultLuckyGiftRuntimeAccount(cfg config.Config) luckyGiftRuntimeAccount {
} }
func applyLuckyGiftRuntimeAccount(base luckyGiftRuntimeAccount, row model.BaishunProviderConfig) luckyGiftRuntimeAccount { func applyLuckyGiftRuntimeAccount(base luckyGiftRuntimeAccount, row model.BaishunProviderConfig) luckyGiftRuntimeAccount {
if strings.TrimSpace(row.PlatformBaseURL) != "" {
base.URL = strings.TrimSpace(row.PlatformBaseURL)
}
if row.AppID > 0 { if row.AppID > 0 {
base.AppID = strconv.FormatInt(row.AppID, 10) base.AppID = strconv.FormatInt(row.AppID, 10)
} }
@ -84,6 +113,9 @@ func mergeLuckyGiftProviderConfig(
runtime luckyGiftRuntimeAccount, runtime luckyGiftRuntimeAccount,
) config.LuckyGiftProviderConfig { ) config.LuckyGiftProviderConfig {
merged := base merged := base
if runtime.URL != "" {
merged.URL = runtime.URL
}
if runtime.AppID != "" { if runtime.AppID != "" {
merged.AppID = runtime.AppID merged.AppID = runtime.AppID
} }
@ -103,3 +135,16 @@ func normalizeLuckyGiftSysOrigin(sysOrigin string) string {
} }
return sysOrigin return sysOrigin
} }
const luckyGiftBaishunProfileProd = "PROD"
func normalizeLuckyGiftBaishunProfile(profile string) string {
switch strings.ToUpper(strings.TrimSpace(profile)) {
case "", "PROD", "PRODUCT", "PRODUCTION", "ONLINE", "OFFICIAL", "FORMAL":
return luckyGiftBaishunProfileProd
case "TEST", "TESTING", "SANDBOX", "DEV":
return "TEST"
default:
return strings.ToUpper(strings.TrimSpace(profile))
}
}

View File

@ -3,7 +3,12 @@ package luckygift
import ( import (
"chatapp3-golang/internal/config" "chatapp3-golang/internal/config"
"chatapp3-golang/internal/model" "chatapp3-golang/internal/model"
"context"
"testing" "testing"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
) )
func TestMergeLuckyGiftProviderConfigPrefersRuntimeAccount(t *testing.T) { func TestMergeLuckyGiftProviderConfigPrefersRuntimeAccount(t *testing.T) {
@ -16,13 +21,14 @@ func TestMergeLuckyGiftProviderConfigPrefersRuntimeAccount(t *testing.T) {
} }
merged := mergeLuckyGiftProviderConfig(base, luckyGiftRuntimeAccount{ merged := mergeLuckyGiftProviderConfig(base, luckyGiftRuntimeAccount{
URL: "https://runtime.example.com",
AppID: "runtime-app-id", AppID: "runtime-app-id",
AppChannel: "runtime-app-channel", AppChannel: "runtime-app-channel",
AppKey: "runtime-app-key", AppKey: "runtime-app-key",
}) })
if merged.URL != base.URL { if merged.URL != "https://runtime.example.com" {
t.Fatalf("expected url to keep base value, got %q", merged.URL) t.Fatalf("expected runtime url, got %q", merged.URL)
} }
if merged.AppID != "runtime-app-id" { if merged.AppID != "runtime-app-id" {
t.Fatalf("expected runtime app id, got %q", merged.AppID) t.Fatalf("expected runtime app id, got %q", merged.AppID)
@ -37,15 +43,20 @@ func TestMergeLuckyGiftProviderConfigPrefersRuntimeAccount(t *testing.T) {
func TestApplyLuckyGiftRuntimeAccountUsesProviderConfigRow(t *testing.T) { func TestApplyLuckyGiftRuntimeAccountUsesProviderConfigRow(t *testing.T) {
account := applyLuckyGiftRuntimeAccount(luckyGiftRuntimeAccount{ account := applyLuckyGiftRuntimeAccount(luckyGiftRuntimeAccount{
URL: "https://fallback.example.com",
AppID: "fallback-app-id", AppID: "fallback-app-id",
AppChannel: "fallback-app-channel", AppChannel: "fallback-app-channel",
AppKey: "fallback-app-key", AppKey: "fallback-app-key",
}, model.BaishunProviderConfig{ }, model.BaishunProviderConfig{
AppID: 4581045902, PlatformBaseURL: "https://provider.example.com",
AppChannel: "yumiparty", AppID: 4581045902,
AppKey: "test-key", AppChannel: "yumiparty",
AppKey: "test-key",
}) })
if account.URL != "https://provider.example.com" {
t.Fatalf("expected row url, got %q", account.URL)
}
if account.AppID != "4581045902" { if account.AppID != "4581045902" {
t.Fatalf("expected row app id, got %q", account.AppID) t.Fatalf("expected row app id, got %q", account.AppID)
} }
@ -60,12 +71,16 @@ func TestApplyLuckyGiftRuntimeAccountUsesProviderConfigRow(t *testing.T) {
func TestDefaultLuckyGiftRuntimeAccountUsesBaishunConfig(t *testing.T) { func TestDefaultLuckyGiftRuntimeAccountUsesBaishunConfig(t *testing.T) {
account := defaultLuckyGiftRuntimeAccount(config.Config{ account := defaultLuckyGiftRuntimeAccount(config.Config{
Baishun: config.BaishunConfig{ Baishun: config.BaishunConfig{
AppID: 7485318192, PlatformBaseURL: "https://provider.example.com",
AppChannel: "yumiparty", AppID: 7485318192,
AppKey: "shared-key", AppChannel: "yumiparty",
AppKey: "shared-key",
}, },
}) })
if account.URL != "https://provider.example.com" {
t.Fatalf("expected baishun platform base url, got %q", account.URL)
}
if account.AppID != "7485318192" { if account.AppID != "7485318192" {
t.Fatalf("expected baishun app id, got %q", account.AppID) t.Fatalf("expected baishun app id, got %q", account.AppID)
} }
@ -76,3 +91,85 @@ func TestDefaultLuckyGiftRuntimeAccountUsesBaishunConfig(t *testing.T) {
t.Fatalf("expected baishun app key, got %q", account.AppKey) t.Fatalf("expected baishun app key, got %q", account.AppKey)
} }
} }
func TestResolveProviderConfigUsesActiveBaishunProfile(t *testing.T) {
service, db := newTestLuckyGiftService(t, config.Config{
Baishun: config.BaishunConfig{
PlatformBaseURL: "https://fallback.example.com",
AppID: 111,
AppChannel: "fallback",
AppKey: "fallback-key",
},
LuckyGift: config.LuckyGiftConfig{
Providers: []config.LuckyGiftProviderConfig{
{
StandardID: 99,
URL: "https://legacy.example.com/lucky_gift/start_game",
AppID: "legacy",
AppChannel: "legacy",
AppKey: "legacy-key",
},
},
},
})
now := time.Now()
if err := db.Create(&[]model.BaishunProviderConfig{
{
ID: 1001,
SysOrigin: "LIKEI",
Profile: "PROD",
Active: false,
PlatformBaseURL: "https://prod.example.com",
AppID: 222,
AppChannel: "prod",
AppKey: "prod-key",
CreateTime: now,
UpdateTime: now,
},
{
ID: 1002,
SysOrigin: "LIKEI",
Profile: "TEST",
Active: true,
PlatformBaseURL: "https://test.example.com",
AppID: 333,
AppChannel: "test",
AppKey: "test-key",
CreateTime: now,
UpdateTime: now.Add(time.Second),
},
}).Error; err != nil {
t.Fatalf("create provider configs: %v", err)
}
resolved, err := service.resolveProviderConfig(context.Background(), LuckyGiftDrawRequest{
SysOrigin: "LIKEI",
StandardID: 99,
})
if err != nil {
t.Fatalf("resolve provider config: %v", err)
}
endpoint, err := resolveLuckyGiftProviderEndpoint(resolved.URL)
if err != nil {
t.Fatalf("resolve lucky gift endpoint: %v", err)
}
if endpoint != "https://test.example.com/lucky_gift/start_game" {
t.Fatalf("endpoint = %q, want active test endpoint", endpoint)
}
if resolved.AppID != "333" || resolved.AppChannel != "test" || resolved.AppKey != "test-key" {
t.Fatalf("resolved account = %+v, want active test account", resolved)
}
}
func newTestLuckyGiftService(t *testing.T, cfg config.Config) (*LuckyGiftService, *gorm.DB) {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&model.BaishunProviderConfig{}); err != nil {
t.Fatalf("auto migrate: %v", err)
}
return NewLuckyGiftService(cfg, db, nil, nil), db
}

View File

@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS sys_game_list_vendor_ext (
id BIGINT NOT NULL PRIMARY KEY, id BIGINT NOT NULL PRIMARY KEY,
game_list_config_id BIGINT NOT NULL, game_list_config_id BIGINT NOT NULL,
sys_origin VARCHAR(32) NOT NULL, sys_origin VARCHAR(32) NOT NULL,
profile VARCHAR(32) NOT NULL DEFAULT 'PROD',
vendor_type VARCHAR(32) NOT NULL, vendor_type VARCHAR(32) NOT NULL,
vendor_game_id VARCHAR(64) NOT NULL, vendor_game_id VARCHAR(64) NOT NULL,
launch_mode VARCHAR(32) NOT NULL DEFAULT 'H5_REMOTE', launch_mode VARCHAR(32) NOT NULL DEFAULT 'H5_REMOTE',
@ -19,12 +20,14 @@ CREATE TABLE IF NOT EXISTS sys_game_list_vendor_ext (
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_game_vendor_ext_cfg_vendor (game_list_config_id, vendor_type), UNIQUE KEY uk_game_vendor_ext_cfg_vendor (game_list_config_id, vendor_type),
KEY idx_game_vendor_ext_sys_vendor (sys_origin, vendor_type, enabled) KEY idx_game_vendor_ext_sys_vendor (sys_origin, vendor_type, enabled),
KEY idx_game_vendor_ext_sys_profile_vendor (sys_origin, profile, vendor_type, vendor_game_id, enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS baishun_game_catalog ( CREATE TABLE IF NOT EXISTS baishun_game_catalog (
id BIGINT NOT NULL PRIMARY KEY, id BIGINT NOT NULL PRIMARY KEY,
sys_origin VARCHAR(32) NOT NULL, sys_origin VARCHAR(32) NOT NULL,
profile VARCHAR(32) NOT NULL DEFAULT 'PROD',
internal_game_id VARCHAR(64) NOT NULL, internal_game_id VARCHAR(64) NOT NULL,
vendor_game_id INT NOT NULL, vendor_game_id INT NOT NULL,
name VARCHAR(128) NOT NULL, name VARCHAR(128) NOT NULL,
@ -41,8 +44,8 @@ CREATE TABLE IF NOT EXISTS baishun_game_catalog (
status VARCHAR(32) NOT NULL DEFAULT 'ENABLED', status VARCHAR(32) NOT NULL DEFAULT 'ENABLED',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_baishun_catalog_sys_vendor_game (sys_origin, vendor_game_id), UNIQUE KEY uk_baishun_catalog_sys_profile_vendor_game (sys_origin, profile, vendor_game_id),
KEY idx_baishun_catalog_internal_game (sys_origin, internal_game_id, status) KEY idx_baishun_catalog_profile_internal_game (sys_origin, profile, internal_game_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS baishun_launch_session ( CREATE TABLE IF NOT EXISTS baishun_launch_session (

View File

@ -1,6 +1,8 @@
CREATE TABLE IF NOT EXISTS baishun_provider_config ( CREATE TABLE IF NOT EXISTS baishun_provider_config (
id BIGINT NOT NULL PRIMARY KEY, id BIGINT NOT NULL PRIMARY KEY,
sys_origin VARCHAR(32) NOT NULL, sys_origin VARCHAR(32) NOT NULL,
profile VARCHAR(32) NOT NULL DEFAULT 'PROD',
active TINYINT(1) NOT NULL DEFAULT 1,
platform_base_url VARCHAR(1024) NOT NULL, platform_base_url VARCHAR(1024) NOT NULL,
app_id BIGINT NOT NULL, app_id BIGINT NOT NULL,
app_name VARCHAR(128) DEFAULT NULL, app_name VARCHAR(128) DEFAULT NULL,
@ -11,6 +13,7 @@ CREATE TABLE IF NOT EXISTS baishun_provider_config (
ss_token_ttl_seconds INT NOT NULL DEFAULT 86400, ss_token_ttl_seconds INT NOT NULL DEFAULT 86400,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_baishun_provider_sys_origin (sys_origin), UNIQUE KEY uk_baishun_provider_sys_origin_profile (sys_origin, profile),
KEY idx_baishun_provider_app (app_id, app_channel) KEY idx_baishun_provider_app (app_id, app_channel),
KEY idx_baishun_provider_active (sys_origin, active, update_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,155 @@
SET @current_schema := DATABASE();
SET @add_provider_profile_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = @current_schema
AND table_name = 'baishun_provider_config'
AND column_name = 'profile'
),
'ALTER TABLE `baishun_provider_config` ADD COLUMN `profile` varchar(32) NOT NULL DEFAULT ''PROD'' AFTER `sys_origin`',
'SELECT 1'
);
PREPARE add_provider_profile_stmt FROM @add_provider_profile_sql;
EXECUTE add_provider_profile_stmt;
DEALLOCATE PREPARE add_provider_profile_stmt;
SET @add_provider_active_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = @current_schema
AND table_name = 'baishun_provider_config'
AND column_name = 'active'
),
'ALTER TABLE `baishun_provider_config` ADD COLUMN `active` tinyint(1) NOT NULL DEFAULT 1 AFTER `profile`',
'SELECT 1'
);
PREPARE add_provider_active_stmt FROM @add_provider_active_sql;
EXECUTE add_provider_active_stmt;
DEALLOCATE PREPARE add_provider_active_stmt;
SET @drop_provider_old_unique_sql := IF(
EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = @current_schema
AND table_name = 'baishun_provider_config'
AND index_name = 'uk_baishun_provider_sys_origin'
),
'ALTER TABLE `baishun_provider_config` DROP INDEX `uk_baishun_provider_sys_origin`',
'SELECT 1'
);
PREPARE drop_provider_old_unique_stmt FROM @drop_provider_old_unique_sql;
EXECUTE drop_provider_old_unique_stmt;
DEALLOCATE PREPARE drop_provider_old_unique_stmt;
SET @add_provider_profile_unique_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = @current_schema
AND table_name = 'baishun_provider_config'
AND index_name = 'uk_baishun_provider_sys_origin_profile'
),
'ALTER TABLE `baishun_provider_config` ADD UNIQUE KEY `uk_baishun_provider_sys_origin_profile` (`sys_origin`, `profile`)',
'SELECT 1'
);
PREPARE add_provider_profile_unique_stmt FROM @add_provider_profile_unique_sql;
EXECUTE add_provider_profile_unique_stmt;
DEALLOCATE PREPARE add_provider_profile_unique_stmt;
SET @add_provider_active_index_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = @current_schema
AND table_name = 'baishun_provider_config'
AND index_name = 'idx_baishun_provider_active'
),
'ALTER TABLE `baishun_provider_config` ADD KEY `idx_baishun_provider_active` (`sys_origin`, `active`, `update_time`)',
'SELECT 1'
);
PREPARE add_provider_active_index_stmt FROM @add_provider_active_index_sql;
EXECUTE add_provider_active_index_stmt;
DEALLOCATE PREPARE add_provider_active_index_stmt;
SET @add_catalog_profile_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = @current_schema
AND table_name = 'baishun_game_catalog'
AND column_name = 'profile'
),
'ALTER TABLE `baishun_game_catalog` ADD COLUMN `profile` varchar(32) NOT NULL DEFAULT ''PROD'' AFTER `sys_origin`',
'SELECT 1'
);
PREPARE add_catalog_profile_stmt FROM @add_catalog_profile_sql;
EXECUTE add_catalog_profile_stmt;
DEALLOCATE PREPARE add_catalog_profile_stmt;
SET @drop_catalog_old_unique_sql := IF(
EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = @current_schema
AND table_name = 'baishun_game_catalog'
AND index_name = 'uk_baishun_catalog_sys_vendor_game'
),
'ALTER TABLE `baishun_game_catalog` DROP INDEX `uk_baishun_catalog_sys_vendor_game`',
'SELECT 1'
);
PREPARE drop_catalog_old_unique_stmt FROM @drop_catalog_old_unique_sql;
EXECUTE drop_catalog_old_unique_stmt;
DEALLOCATE PREPARE drop_catalog_old_unique_stmt;
SET @add_catalog_profile_unique_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = @current_schema
AND table_name = 'baishun_game_catalog'
AND index_name = 'uk_baishun_catalog_sys_profile_vendor_game'
),
'ALTER TABLE `baishun_game_catalog` ADD UNIQUE KEY `uk_baishun_catalog_sys_profile_vendor_game` (`sys_origin`, `profile`, `vendor_game_id`)',
'SELECT 1'
);
PREPARE add_catalog_profile_unique_stmt FROM @add_catalog_profile_unique_sql;
EXECUTE add_catalog_profile_unique_stmt;
DEALLOCATE PREPARE add_catalog_profile_unique_stmt;
SET @add_catalog_profile_index_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = @current_schema
AND table_name = 'baishun_game_catalog'
AND index_name = 'idx_baishun_catalog_profile_internal_game'
),
'ALTER TABLE `baishun_game_catalog` ADD KEY `idx_baishun_catalog_profile_internal_game` (`sys_origin`, `profile`, `internal_game_id`, `status`)',
'SELECT 1'
);
PREPARE add_catalog_profile_index_stmt FROM @add_catalog_profile_index_sql;
EXECUTE add_catalog_profile_index_stmt;
DEALLOCATE PREPARE add_catalog_profile_index_stmt;
SET @add_vendor_ext_profile_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = @current_schema
AND table_name = 'sys_game_list_vendor_ext'
AND column_name = 'profile'
),
'ALTER TABLE `sys_game_list_vendor_ext` ADD COLUMN `profile` varchar(32) NOT NULL DEFAULT ''PROD'' AFTER `sys_origin`',
'SELECT 1'
);
PREPARE add_vendor_ext_profile_stmt FROM @add_vendor_ext_profile_sql;
EXECUTE add_vendor_ext_profile_stmt;
DEALLOCATE PREPARE add_vendor_ext_profile_stmt;
SET @add_vendor_ext_profile_index_sql := IF(
NOT EXISTS (
SELECT 1 FROM information_schema.statistics
WHERE table_schema = @current_schema
AND table_name = 'sys_game_list_vendor_ext'
AND index_name = 'idx_game_vendor_ext_sys_profile_vendor'
),
'ALTER TABLE `sys_game_list_vendor_ext` ADD KEY `idx_game_vendor_ext_sys_profile_vendor` (`sys_origin`, `profile`, `vendor_type`, `vendor_game_id`, `enabled`)',
'SELECT 1'
);
PREPARE add_vendor_ext_profile_index_stmt FROM @add_vendor_ext_profile_index_sql;
EXECUTE add_vendor_ext_profile_index_stmt;
DEALLOCATE PREPARE add_vendor_ext_profile_index_stmt;