1
This commit is contained in:
parent
5e81478c8b
commit
8256fdf7e6
@ -29,6 +29,7 @@ internal/utils
|
||||
### 1. `cmd/api`
|
||||
- 只做启动和依赖装配。
|
||||
- 不承担业务逻辑。
|
||||
- 当前线上只部署 `golang` API 进程,所以这里会同时启动 `registerreward` 的 Redis Stream consumer,确保注册成功事件能被消费并发奖。
|
||||
- 不启动任何 `weekstar` 结算 goroutine。
|
||||
|
||||
### 2. `cmd/consumer`
|
||||
@ -36,6 +37,7 @@ internal/utils
|
||||
- 当前承接:
|
||||
- `registerreward` 的 Redis Stream 消费
|
||||
- `weekstar` 的 RocketMQ 消费
|
||||
- 如果后续单独部署 consumer,`registerreward` 使用同一个 Redis consumer group,重复启动不会重复发奖。
|
||||
|
||||
### 3. `internal/bootstrap`
|
||||
- 统一完成配置加载、存储初始化、依赖探活、Java 网关装配。
|
||||
|
||||
@ -37,6 +37,13 @@ func main() {
|
||||
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)
|
||||
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(
|
||||
baishun.NewAppProvider(baishunService),
|
||||
)
|
||||
@ -76,6 +83,7 @@ func main() {
|
||||
return
|
||||
case <-shutdownSignal.Done():
|
||||
log.Printf("shutdown signal received, draining http traffic")
|
||||
workerCancel()
|
||||
}
|
||||
|
||||
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), app.Config.HTTP.ShutdownTimeout)
|
||||
|
||||
@ -28,25 +28,26 @@ func (SysGameListConfig) TableName() string { return "sys_game_list_config" }
|
||||
|
||||
// SysGameListVendorExt 保存某个厂商在系统游戏表上的扩展信息。
|
||||
type SysGameListVendorExt struct {
|
||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||
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"` // 系统标识
|
||||
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"` // 厂商类型
|
||||
VendorGameID string `gorm:"column:vendor_game_id;size:64"` // 厂商游戏 ID
|
||||
LaunchMode string `gorm:"column:launch_mode;size:32"` // 启动模式
|
||||
PackageVersion string `gorm:"column:package_version;size:32"` // 包版本号
|
||||
PackageURL string `gorm:"column:package_url;size:1024"` // 包下载地址
|
||||
PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址
|
||||
Orientation *int `gorm:"column:orientation"` // 屏幕方向
|
||||
SafeHeight int `gorm:"column:safe_height"` // 安全区域高度
|
||||
GSP string `gorm:"column:gsp;size:64"` // GSP 配置
|
||||
BridgeSchemaVersion string `gorm:"column:bridge_schema_version;size:16"` // Bridge 协议版本
|
||||
CurrencyType *int `gorm:"column:currency_type"` // 货币类型
|
||||
CurrencyIcon string `gorm:"column:currency_icon;size:1024"` // 货币图标
|
||||
ExtraJSON string `gorm:"column:extra_json;type:text"` // 额外扩展配置
|
||||
Enabled bool `gorm:"column:enabled;index:idx_game_vendor_ext_sys_vendor,priority:3"` // 是否启用
|
||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||
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;index:idx_game_vendor_ext_sys_profile_vendor,priority:1"` // 系统标识
|
||||
Profile string `gorm:"column:profile;size:32;default:PROD;index:idx_game_vendor_ext_sys_profile_vendor,priority:2"` // 配置档案:PROD/TEST
|
||||
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"` // 厂商类型
|
||||
VendorGameID string `gorm:"column:vendor_game_id;size:64;index:idx_game_vendor_ext_sys_profile_vendor,priority:4"` // 厂商游戏 ID
|
||||
LaunchMode string `gorm:"column:launch_mode;size:32"` // 启动模式
|
||||
PackageVersion string `gorm:"column:package_version;size:32"` // 包版本号
|
||||
PackageURL string `gorm:"column:package_url;size:1024"` // 包下载地址
|
||||
PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址
|
||||
Orientation *int `gorm:"column:orientation"` // 屏幕方向
|
||||
SafeHeight int `gorm:"column:safe_height"` // 安全区域高度
|
||||
GSP string `gorm:"column:gsp;size:64"` // GSP 配置
|
||||
BridgeSchemaVersion string `gorm:"column:bridge_schema_version;size:16"` // Bridge 协议版本
|
||||
CurrencyType *int `gorm:"column:currency_type"` // 货币类型
|
||||
CurrencyIcon string `gorm:"column:currency_icon;size:1024"` // 货币图标
|
||||
ExtraJSON string `gorm:"column:extra_json;type:text"` // 额外扩展配置
|
||||
Enabled bool `gorm:"column:enabled;index:idx_game_vendor_ext_sys_vendor,priority:3;index:idx_game_vendor_ext_sys_profile_vendor,priority:5"` // 是否启用
|
||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||
}
|
||||
|
||||
// TableName 返回游戏厂商扩展表名。
|
||||
@ -54,18 +55,20 @@ func (SysGameListVendorExt) TableName() string { return "sys_game_list_vendor_ex
|
||||
|
||||
// BaishunProviderConfig 保存每个系统的百顺接入配置。
|
||||
type BaishunProviderConfig struct {
|
||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_provider_sys_origin"` // 系统标识
|
||||
PlatformBaseURL string `gorm:"column:platform_base_url;size:1024"` // 百顺平台基础地址
|
||||
AppID int64 `gorm:"column:app_id;index:idx_baishun_provider_app,priority:1"` // 百顺应用 ID
|
||||
AppName string `gorm:"column:app_name;size:128"` // 百顺应用名
|
||||
AppChannel string `gorm:"column:app_channel;size:64;index:idx_baishun_provider_app,priority:2"` // 百顺渠道
|
||||
AppKey string `gorm:"column:app_key;size:255"` // 百顺密钥
|
||||
GSP int `gorm:"column:gsp"` // 默认 GSP
|
||||
LaunchCodeTTLSeconds int `gorm:"column:launch_code_ttl_seconds"` // 启动码过期秒数
|
||||
SSTokenTTLSeconds int `gorm:"column:ss_token_ttl_seconds"` // ss_token 过期秒数
|
||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_provider_sys_origin_profile,priority:1;index:idx_baishun_provider_active,priority:1"` // 系统标识
|
||||
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_baishun_provider_sys_origin_profile,priority:2"` // 配置档案:PROD/TEST
|
||||
Active bool `gorm:"column:active;index:idx_baishun_provider_active,priority:2"` // 是否当前启用
|
||||
PlatformBaseURL string `gorm:"column:platform_base_url;size:1024"` // 百顺平台基础地址
|
||||
AppID int64 `gorm:"column:app_id;index:idx_baishun_provider_app,priority:1"` // 百顺应用 ID
|
||||
AppName string `gorm:"column:app_name;size:128"` // 百顺应用名
|
||||
AppChannel string `gorm:"column:app_channel;size:64;index:idx_baishun_provider_app,priority:2"` // 百顺渠道
|
||||
AppKey string `gorm:"column:app_key;size:255"` // 百顺密钥
|
||||
GSP int `gorm:"column:gsp"` // 默认 GSP
|
||||
LaunchCodeTTLSeconds int `gorm:"column:launch_code_ttl_seconds"` // 启动码过期秒数
|
||||
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 返回百顺配置表名。
|
||||
@ -73,24 +76,25 @@ func (BaishunProviderConfig) TableName() string { return "baishun_provider_confi
|
||||
|
||||
// BaishunGameCatalog 是百顺平台同步到本地的游戏目录。
|
||||
type BaishunGameCatalog struct {
|
||||
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"` // 系统标识
|
||||
InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_baishun_catalog_internal_game,priority:2"` // 内部游戏 ID
|
||||
VendorGameID int `gorm:"column:vendor_game_id;uniqueIndex:uk_baishun_catalog_sys_vendor_game,priority:2"` // 百顺游戏 ID
|
||||
Name string `gorm:"column:name;size:128"` // 游戏名称
|
||||
Cover string `gorm:"column:cover;size:1024"` // 封面图
|
||||
PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址
|
||||
DownloadURL string `gorm:"column:download_url;size:1024"` // 下载地址
|
||||
PackageVersion string `gorm:"column:package_version;size:32"` // 包版本
|
||||
GameModeJSON string `gorm:"column:game_mode_json;size:64"` // 游戏模式原始 JSON
|
||||
Orientation *int `gorm:"column:orientation"` // 屏幕方向
|
||||
SafeHeight int `gorm:"column:safe_height"` // 安全区域高度
|
||||
VenueLevelJSON string `gorm:"column:venue_level_json;size:64"` // 场馆等级 JSON
|
||||
GSP string `gorm:"column:gsp;size:64"` // GSP 配置
|
||||
RawJSON string `gorm:"column:raw_json;type:longtext"` // 原始目录数据
|
||||
Status string `gorm:"column:status;size:32;index:idx_baishun_catalog_internal_game,priority:3"` // 状态
|
||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||
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"` // 系统标识
|
||||
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
|
||||
InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_baishun_catalog_profile_internal_game,priority:3"` // 内部游戏 ID
|
||||
VendorGameID int `gorm:"column:vendor_game_id;uniqueIndex:uk_baishun_catalog_sys_profile_vendor_game,priority:3"` // 百顺游戏 ID
|
||||
Name string `gorm:"column:name;size:128"` // 游戏名称
|
||||
Cover string `gorm:"column:cover;size:1024"` // 封面图
|
||||
PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址
|
||||
DownloadURL string `gorm:"column:download_url;size:1024"` // 下载地址
|
||||
PackageVersion string `gorm:"column:package_version;size:32"` // 包版本
|
||||
GameModeJSON string `gorm:"column:game_mode_json;size:64"` // 游戏模式原始 JSON
|
||||
Orientation *int `gorm:"column:orientation"` // 屏幕方向
|
||||
SafeHeight int `gorm:"column:safe_height"` // 安全区域高度
|
||||
VenueLevelJSON string `gorm:"column:venue_level_json;size:64"` // 场馆等级 JSON
|
||||
GSP string `gorm:"column:gsp;size:64"` // GSP 配置
|
||||
RawJSON string `gorm:"column:raw_json;type:longtext"` // 原始目录数据
|
||||
Status string `gorm:"column:status;size:32;index:idx_baishun_catalog_profile_internal_game,priority:4"` // 状态
|
||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||
}
|
||||
|
||||
// TableName 返回百顺目录表名。
|
||||
|
||||
@ -96,7 +96,7 @@ func registerBaishunRoutes(
|
||||
consoleGroup := engine.Group("/operate/baishun-game")
|
||||
consoleGroup.Use(consoleAuthMiddleware(javaClient))
|
||||
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 {
|
||||
writeError(c, err)
|
||||
return
|
||||
@ -116,12 +116,26 @@ func registerBaishunRoutes(
|
||||
}
|
||||
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) {
|
||||
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||
resp, err := baishunService.PageAdminGames(
|
||||
c.Request.Context(),
|
||||
c.Query("sysOrigin"),
|
||||
c.Query("profile"),
|
||||
c.Query("keyword"),
|
||||
parseOptionalBool(c.Query("showcase")),
|
||||
cursor,
|
||||
@ -139,6 +153,7 @@ func registerBaishunRoutes(
|
||||
resp, err := baishunService.PageCatalog(
|
||||
c.Request.Context(),
|
||||
c.Query("sysOrigin"),
|
||||
c.Query("profile"),
|
||||
c.Query("keyword"),
|
||||
cursor,
|
||||
limit,
|
||||
@ -164,7 +179,7 @@ func registerBaishunRoutes(
|
||||
})
|
||||
consoleGroup.DELETE("", func(c *gin.Context) {
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(c.Query("id")), 10, 64)
|
||||
if err := baishunService.DeleteAdminGame(c.Request.Context(), c.Query("sysOrigin"), id); err != nil {
|
||||
if err := baishunService.DeleteAdminGame(c.Request.Context(), c.Query("sysOrigin"), c.Query("profile"), id); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
@ -189,7 +204,7 @@ func registerBaishunRoutes(
|
||||
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||
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 {
|
||||
writeError(c, err)
|
||||
return
|
||||
|
||||
@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"chatapp3-golang/internal/service/invite"
|
||||
|
||||
@ -48,6 +49,15 @@ func registerInviteRoutes(engine *gin.Engine, javaClient authGateway, inviteServ
|
||||
}
|
||||
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) {
|
||||
var req invite.BindCodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
@ -18,6 +18,7 @@ const adminTimeLayout = "2006-01-02 15:04:05"
|
||||
type adminGameRow struct {
|
||||
ID int64
|
||||
SysOrigin string
|
||||
Profile string
|
||||
InternalGameID string
|
||||
Name string
|
||||
Category string
|
||||
@ -57,16 +58,21 @@ type adminGameRow struct {
|
||||
func (s *BaishunService) PageAdminGames(
|
||||
ctx context.Context,
|
||||
sysOrigin string,
|
||||
profile string,
|
||||
keyword string,
|
||||
showcase *bool,
|
||||
cursor int,
|
||||
limit int,
|
||||
) (*AdminBaishunGamePageResponse, error) {
|
||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cursor = normalizePageCursor(cursor)
|
||||
limit = normalizePageLimit(limit)
|
||||
|
||||
countQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase)
|
||||
countQuery := s.buildAdminGameQuery(ctx, sysOrigin, profile, keyword, showcase)
|
||||
|
||||
var total int64
|
||||
if err := countQuery.Count(&total).Error; err != nil {
|
||||
@ -74,11 +80,12 @@ func (s *BaishunService) PageAdminGames(
|
||||
}
|
||||
|
||||
var rows []adminGameRow
|
||||
listQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase)
|
||||
listQuery := s.buildAdminGameQuery(ctx, sysOrigin, profile, keyword, showcase)
|
||||
if err := listQuery.
|
||||
Select(`
|
||||
cfg.id AS id,
|
||||
cfg.sys_origin AS sys_origin,
|
||||
ext.profile AS profile,
|
||||
cfg.game_id AS internal_game_id,
|
||||
cfg.name AS name,
|
||||
cfg.category AS category,
|
||||
@ -138,12 +145,16 @@ func (s *BaishunService) PageAdminGames(
|
||||
// SaveAdminGame 保存后台百顺游戏配置。
|
||||
func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishunGameRequest) (*AdminBaishunGameItem, error) {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
@ -262,7 +273,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
||||
}
|
||||
|
||||
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 {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
@ -280,6 +291,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
||||
ID: nextID,
|
||||
GameListConfigID: cfg.ID,
|
||||
SysOrigin: sysOrigin,
|
||||
Profile: profile,
|
||||
VendorType: baishunVendorType,
|
||||
VendorGameID: strconv.Itoa(req.VendorGameID),
|
||||
LaunchMode: launchMode,
|
||||
@ -304,6 +316,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
||||
Where("id = ?", ext.ID).
|
||||
Updates(map[string]any{
|
||||
"sys_origin": sysOrigin,
|
||||
"profile": profile,
|
||||
"vendor_game_id": strconv.Itoa(req.VendorGameID),
|
||||
"launch_mode": launchMode,
|
||||
"package_version": packageVersion,
|
||||
@ -326,40 +339,63 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.getAdminGame(ctx, sysOrigin, cfg.ID)
|
||||
s.clearJavaGameListCache(ctx)
|
||||
return s.getAdminGame(ctx, sysOrigin, profile, cfg.ID)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return NewAppError(http.StatusBadRequest, "id_required", "id is required")
|
||||
}
|
||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx := s.repo.DB.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
if err := tx.Where("game_list_config_id = ? AND vendor_type = ?", id, baishunVendorType).
|
||||
if err := tx.Where("game_list_config_id = ? AND vendor_type = ? AND profile = ?", id, baishunVendorType, profile).
|
||||
Delete(&model.SysGameListVendorExt{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).
|
||||
Delete(&model.SysGameListConfig{}).Error; err != nil {
|
||||
var remaining int64
|
||||
if err := tx.Model(&model.SysGameListVendorExt{}).
|
||||
Where("game_list_config_id = ? AND vendor_type = ?", id, baishunVendorType).
|
||||
Count(&remaining).Error; err != nil {
|
||||
tx.Rollback()
|
||||
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 把本地目录中尚未进入后台列表的百顺游戏补齐成可配置记录。
|
||||
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()
|
||||
if tx.Error != nil {
|
||||
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 {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
@ -367,6 +403,7 @@ func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin strin
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.clearJavaGameListCache(ctx)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@ -376,7 +413,7 @@ func (s *BaishunService) SyncAdminGames(ctx context.Context, req SyncCatalogRequ
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -389,13 +426,14 @@ func (s *BaishunService) SyncAdminGames(ctx context.Context, req SyncCatalogRequ
|
||||
}, 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).
|
||||
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(`
|
||||
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
|
||||
OR cat.internal_game_id = cfg.game_id
|
||||
@ -418,13 +456,14 @@ func (s *BaishunService) buildAdminGameQuery(ctx context.Context, sysOrigin, key
|
||||
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
|
||||
err := s.buildAdminGameQuery(ctx, sysOrigin, "", nil).
|
||||
err := s.buildAdminGameQuery(ctx, sysOrigin, profile, "", nil).
|
||||
Where("cfg.id = ?", id).
|
||||
Select(`
|
||||
cfg.id AS id,
|
||||
cfg.sys_origin AS sys_origin,
|
||||
ext.profile AS profile,
|
||||
cfg.game_id AS internal_game_id,
|
||||
cfg.name AS name,
|
||||
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")
|
||||
}
|
||||
|
||||
func (s *BaishunService) findCatalogForAdmin(ctx context.Context, sysOrigin string, vendorGameID int, gameID string) (model.BaishunGameCatalog, error) {
|
||||
query := s.repo.DB.WithContext(ctx).Where("sys_origin = ?", sysOrigin)
|
||||
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 = ? AND profile = ?", sysOrigin, profile)
|
||||
switch {
|
||||
case vendorGameID > 0:
|
||||
query = query.Where("vendor_game_id = ?", vendorGameID)
|
||||
@ -492,19 +531,31 @@ func (s *BaishunService) findCatalogForAdmin(ctx context.Context, sysOrigin stri
|
||||
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
|
||||
if id > 0 {
|
||||
err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).First(&cfg).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return model.SysGameListConfig{}, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if vendorGameID > 0 {
|
||||
var ext model.SysGameListVendorExt
|
||||
err := tx.Where("sys_origin = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, baishunVendorType, strconv.Itoa(vendorGameID)).First(&ext).Error
|
||||
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 {
|
||||
return model.SysGameListConfig{}, err
|
||||
}
|
||||
@ -522,23 +573,31 @@ func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, i
|
||||
if strings.TrimSpace(gameID) == "" {
|
||||
return model.SysGameListConfig{}, nil
|
||||
}
|
||||
err := tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, strings.TrimSpace(gameID)).First(&cfg).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
err := tx.Table("sys_game_list_config AS cfg").
|
||||
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 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
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
||||
runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
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 {
|
||||
ids := make([]int, 0, len(filter))
|
||||
for id := range filter {
|
||||
@ -558,70 +617,98 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, fil
|
||||
now := time.Now()
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if err == nil && ext.GameListConfigID > 0 {
|
||||
resp.Existing++
|
||||
continue
|
||||
var cfg model.SysGameListConfig
|
||||
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
|
||||
err = tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, catalog.InternalGameID).First(&cfg).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
nextCfgID, idErr := utils.NextID()
|
||||
if idErr != nil {
|
||||
return nil, idErr
|
||||
}
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
nextID, idErr := utils.NextID()
|
||||
if idErr != nil {
|
||||
return nil, idErr
|
||||
}
|
||||
cfg = model.SysGameListConfig{
|
||||
ID: nextID,
|
||||
SysOrigin: sysOrigin,
|
||||
GameOrigin: baishunVendorType,
|
||||
GameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
|
||||
Name: catalog.Name,
|
||||
Category: "OTHER",
|
||||
GameCode: vendorGameID,
|
||||
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
||||
Showcase: true,
|
||||
Sort: int64(catalog.VendorGameID),
|
||||
FullScreen: true,
|
||||
ClientOrigin: "COMMON",
|
||||
GameMode: formatAdminGameModeRaw(baishunFixedGameMode),
|
||||
}
|
||||
if err := tx.Create(&cfg).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := model.SysGameListConfig{
|
||||
ID: nextCfgID,
|
||||
SysOrigin: sysOrigin,
|
||||
GameOrigin: baishunVendorType,
|
||||
GameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
|
||||
Name: catalog.Name,
|
||||
Category: "OTHER",
|
||||
GameCode: vendorGameID,
|
||||
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
||||
Showcase: true,
|
||||
Sort: int64(catalog.VendorGameID),
|
||||
FullScreen: true,
|
||||
ClientOrigin: "COMMON",
|
||||
GameMode: formatAdminGameModeRaw(baishunFixedGameMode),
|
||||
}
|
||||
if err := tx.Create(&cfg).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nextID, idErr := utils.NextID()
|
||||
if idErr != nil {
|
||||
return nil, idErr
|
||||
}
|
||||
newExt := model.SysGameListVendorExt{
|
||||
ID: nextID,
|
||||
GameListConfigID: cfg.ID,
|
||||
SysOrigin: sysOrigin,
|
||||
VendorType: baishunVendorType,
|
||||
VendorGameID: vendorGameID,
|
||||
LaunchMode: baishunLaunchModeRemote,
|
||||
PackageVersion: catalog.PackageVersion,
|
||||
PackageURL: catalog.DownloadURL,
|
||||
PreviewURL: catalog.PreviewURL,
|
||||
Orientation: catalog.Orientation,
|
||||
SafeHeight: catalog.SafeHeight,
|
||||
GSP: defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()),
|
||||
BridgeSchemaVersion: "1.0",
|
||||
ExtraJSON: updateExtExtraJSONWithGameType("", nil, baishunVendorType, false),
|
||||
Enabled: true,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
extraJSON := updateExtExtraJSONWithGameType(ext.ExtraJSON, nil, baishunVendorType, ext.ID != 0)
|
||||
extValues := map[string]any{
|
||||
"game_list_config_id": cfg.ID,
|
||||
"sys_origin": sysOrigin,
|
||||
"profile": profile,
|
||||
"vendor_type": baishunVendorType,
|
||||
"vendor_game_id": vendorGameID,
|
||||
"launch_mode": baishunLaunchModeRemote,
|
||||
"package_version": catalog.PackageVersion,
|
||||
"package_url": catalog.DownloadURL,
|
||||
"preview_url": catalog.PreviewURL,
|
||||
"orientation": catalog.Orientation,
|
||||
"safe_height": catalog.SafeHeight,
|
||||
"gsp": defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()),
|
||||
"bridge_schema_version": "1.0",
|
||||
"extra_json": extraJSON,
|
||||
"enabled": true,
|
||||
"update_time": now,
|
||||
}
|
||||
if err := tx.Create(&newExt).Error; err != nil {
|
||||
return nil, err
|
||||
if ext.ID > 0 {
|
||||
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++
|
||||
}
|
||||
@ -632,6 +719,7 @@ func (r adminGameRow) toAdminItem() AdminBaishunGameItem {
|
||||
return AdminBaishunGameItem{
|
||||
ID: r.ID,
|
||||
SysOrigin: r.SysOrigin,
|
||||
Profile: normalizeBaishunProfile(r.Profile),
|
||||
GameID: r.InternalGameID,
|
||||
GameType: gameTypeFromExtraJSON(r.ExtExtraJSON, baishunVendorType),
|
||||
VendorGameID: parseAdminVendorGameID(r.VendorGameID, r.InternalGameID),
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
package baishun
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chatapp3-golang/internal/model"
|
||||
)
|
||||
|
||||
func TestResolveAdminShowcase(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
|
||||
}
|
||||
|
||||
@ -18,11 +18,12 @@ import (
|
||||
|
||||
// SyncCatalog 从百顺平台拉取游戏目录并同步到本地表。
|
||||
func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
||||
sysOrigin := defaultIfBlank(req.SysOrigin, "LIKEI")
|
||||
if strings.TrimSpace(sysOrigin) == "" {
|
||||
sysOrigin = "LIKEI"
|
||||
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
||||
profile, err := s.requestProfile(ctx, sysOrigin, req.Profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtimeCfg, err := s.requireProviderConfig(ctx, sysOrigin)
|
||||
runtimeCfg, err := s.requireProviderConfigForProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -51,6 +52,7 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
|
||||
record := model.BaishunGameCatalog{
|
||||
ID: id,
|
||||
SysOrigin: sysOrigin,
|
||||
Profile: profile,
|
||||
InternalGameID: fmt.Sprintf("bs_%d", game.GameID),
|
||||
VendorGameID: game.GameID,
|
||||
Name: game.Name,
|
||||
@ -69,7 +71,7 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
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{
|
||||
"internal_game_id", "name", "cover", "preview_url", "download_url",
|
||||
"package_version", "game_mode_json", "orientation", "safe_height",
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
|
||||
type adminCatalogRow struct {
|
||||
VendorGameID int
|
||||
Profile string
|
||||
InternalGameID string
|
||||
Name string
|
||||
Cover string
|
||||
@ -28,24 +29,30 @@ type adminCatalogRow struct {
|
||||
func (s *BaishunService) PageCatalog(
|
||||
ctx context.Context,
|
||||
sysOrigin string,
|
||||
profile string,
|
||||
keyword string,
|
||||
cursor int,
|
||||
limit int,
|
||||
) (*AdminBaishunCatalogPageResponse, error) {
|
||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cursor = normalizePageCursor(cursor)
|
||||
limit = normalizePageLimit(limit)
|
||||
|
||||
countQuery := s.buildAdminCatalogQuery(ctx, sysOrigin, keyword)
|
||||
countQuery := s.buildAdminCatalogQuery(ctx, sysOrigin, profile, keyword)
|
||||
var total int64
|
||||
if err := countQuery.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rows []adminCatalogRow
|
||||
err := s.buildAdminCatalogQuery(ctx, sysOrigin, keyword).
|
||||
err = s.buildAdminCatalogQuery(ctx, sysOrigin, profile, keyword).
|
||||
Select(`
|
||||
cat.vendor_game_id AS vendor_game_id,
|
||||
cat.profile AS profile,
|
||||
cat.internal_game_id AS internal_game_id,
|
||||
cat.name AS name,
|
||||
cat.cover AS cover,
|
||||
@ -56,7 +63,7 @@ func (s *BaishunService) PageCatalog(
|
||||
cat.status AS status,
|
||||
cat.safe_height AS safe_height,
|
||||
cat.orientation AS orientation,
|
||||
ext.game_list_config_id AS added_config_id,
|
||||
cfg.id AS added_config_id,
|
||||
cfg.is_showcase AS showcase,
|
||||
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 {
|
||||
items = append(items, AdminBaishunCatalogItem{
|
||||
VendorGameID: row.VendorGameID,
|
||||
Profile: normalizeBaishunProfile(row.Profile),
|
||||
GameID: row.InternalGameID,
|
||||
Name: row.Name,
|
||||
Cover: row.Cover,
|
||||
@ -100,18 +108,24 @@ func (s *BaishunService) FetchAdminCatalog(ctx context.Context, req SyncCatalogR
|
||||
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).
|
||||
Table("baishun_game_catalog AS cat").
|
||||
Joins(`
|
||||
LEFT JOIN sys_game_list_vendor_ext ext
|
||||
ON ext.sys_origin = cat.sys_origin
|
||||
AND ext.profile = cat.profile
|
||||
AND ext.vendor_type = ?
|
||||
AND ext.vendor_game_id = CAST(cat.vendor_game_id AS CHAR)
|
||||
AND ext.enabled = 1
|
||||
`, baishunVendorType).
|
||||
Joins("LEFT JOIN sys_game_list_config cfg ON cfg.id = ext.game_list_config_id").
|
||||
Where("cat.sys_origin = ?", sysOrigin)
|
||||
Joins(`
|
||||
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)
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
@ -134,3 +148,14 @@ func (s *BaishunService) requireProviderConfig(ctx context.Context, sysOrigin st
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
25
internal/service/baishun/game_list_cache.go
Normal file
25
internal/service/baishun/game_list_cache.go
Normal 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
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ package baishun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"chatapp3-golang/internal/config"
|
||||
@ -37,7 +38,8 @@ func (stubGateway) ChangeGoldBalance(context.Context, integration.GoldReceiptCom
|
||||
func newTestBaishunService(t *testing.T) (*BaishunService, *gorm.DB) {
|
||||
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 {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
@ -73,6 +75,7 @@ func TestLaunchGameFallsBackToCatalogWhenConfigMissing(t *testing.T) {
|
||||
if err := db.Create(&model.BaishunGameCatalog{
|
||||
ID: 1,
|
||||
SysOrigin: "LIKEI",
|
||||
Profile: baishunProfileProd,
|
||||
InternalGameID: "bs_1146",
|
||||
VendorGameID: 1146,
|
||||
Name: "LordOfOlympus",
|
||||
|
||||
61
internal/service/baishun/profile.go
Normal file
61
internal/service/baishun/profile.go
Normal 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)
|
||||
}
|
||||
@ -14,6 +14,7 @@ import (
|
||||
|
||||
type baishunRuntimeConfig struct {
|
||||
SysOrigin string
|
||||
Profile string
|
||||
PlatformBaseURL string
|
||||
AppID int64
|
||||
AppName string
|
||||
@ -52,6 +53,7 @@ func (c baishunRuntimeConfig) ssTokenTTLSeconds() int {
|
||||
func (s *BaishunService) defaultRuntimeConfig(sysOrigin string) baishunRuntimeConfig {
|
||||
return baishunRuntimeConfig{
|
||||
SysOrigin: normalizeAdminSysOrigin(sysOrigin),
|
||||
Profile: baishunProfileProd,
|
||||
PlatformBaseURL: strings.TrimSpace(s.cfg.Baishun.PlatformBaseURL),
|
||||
AppID: s.cfg.Baishun.AppID,
|
||||
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 {
|
||||
base.SysOrigin = normalizeAdminSysOrigin(defaultIfBlank(row.SysOrigin, base.SysOrigin))
|
||||
base.Profile = normalizeBaishunProfile(defaultIfBlank(row.Profile, base.Profile))
|
||||
if 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) {
|
||||
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.Profile = profile
|
||||
|
||||
var row model.BaishunProviderConfig
|
||||
err := s.repo.DB.WithContext(ctx).
|
||||
Where("sys_origin = ?", sysOrigin).
|
||||
err = s.repo.DB.WithContext(ctx).
|
||||
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||
Limit(1).
|
||||
First(&row).Error
|
||||
if err == nil {
|
||||
@ -133,7 +150,11 @@ func (s *BaishunService) resolveRuntimeConfigByApp(ctx context.Context, appID in
|
||||
|
||||
for _, build := range queries {
|
||||
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 {
|
||||
return applyProviderRow(s.defaultRuntimeConfig(row.SysOrigin), row), nil
|
||||
}
|
||||
@ -144,21 +165,33 @@ func (s *BaishunService) resolveRuntimeConfigByApp(ctx context.Context, appID in
|
||||
return base, nil
|
||||
}
|
||||
|
||||
func (s *BaishunService) GetProviderConfig(ctx context.Context, sysOrigin string) (*BaishunProviderConfigItem, error) {
|
||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
||||
func (s *BaishunService) GetProviderConfig(ctx context.Context, sysOrigin string, profile string) (*BaishunProviderConfigItem, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var row model.BaishunProviderConfig
|
||||
_ = s.repo.DB.WithContext(ctx).
|
||||
Where("sys_origin = ?", normalizeAdminSysOrigin(sysOrigin)).
|
||||
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||
Limit(1).
|
||||
First(&row).Error
|
||||
|
||||
return &BaishunProviderConfigItem{
|
||||
ID: row.ID,
|
||||
SysOrigin: runtimeCfg.SysOrigin,
|
||||
Profile: runtimeCfg.Profile,
|
||||
ActiveProfile: activeProfile,
|
||||
Active: runtimeCfg.Profile == activeProfile,
|
||||
PlatformBaseURL: runtimeCfg.PlatformBaseURL,
|
||||
AppID: runtimeCfg.AppID,
|
||||
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) {
|
||||
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
||||
profile, err := validateBaishunProfile(req.Profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
platformBaseURL := strings.TrimSpace(req.PlatformBaseURL)
|
||||
appChannel := strings.TrimSpace(req.AppChannel)
|
||||
appKey := strings.TrimSpace(req.AppKey)
|
||||
@ -196,6 +233,7 @@ func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishun
|
||||
row := model.BaishunProviderConfig{
|
||||
ID: id,
|
||||
SysOrigin: sysOrigin,
|
||||
Profile: profile,
|
||||
PlatformBaseURL: platformBaseURL,
|
||||
AppID: req.AppID,
|
||||
AppName: strings.TrimSpace(req.AppName),
|
||||
@ -216,9 +254,17 @@ func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishun
|
||||
if row.SSTokenTTLSeconds <= 0 {
|
||||
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{
|
||||
Columns: []clause.Column{{Name: "sys_origin"}},
|
||||
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "profile"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"platform_base_url", "app_id", "app_name", "app_channel", "app_key",
|
||||
"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 {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -60,12 +60,18 @@ func (s *BaishunService) GetRoomState(ctx context.Context, user AuthUser, roomID
|
||||
|
||||
// listRoomGames 联表读取房间可启动游戏,并按展示规则排序。
|
||||
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
|
||||
query := s.repo.DB.WithContext(ctx).
|
||||
Table("sys_game_list_config AS cfg").
|
||||
Select(`
|
||||
cfg.id AS config_id,
|
||||
cfg.sys_origin AS sys_origin,
|
||||
ext.profile AS profile,
|
||||
cfg.game_id AS internal_game_id,
|
||||
cfg.name AS name,
|
||||
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.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("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("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 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)
|
||||
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), "CHAT_ROOM") {
|
||||
query = query.Where("cfg.category = ?", category)
|
||||
@ -112,12 +118,18 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
|
||||
|
||||
// findGameRow 按内部游戏 ID 查询游戏聚合信息。
|
||||
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
|
||||
err := s.repo.DB.WithContext(ctx).
|
||||
err = s.repo.DB.WithContext(ctx).
|
||||
Table("sys_game_list_config AS cfg").
|
||||
Select(`
|
||||
cfg.id AS config_id,
|
||||
cfg.sys_origin AS sys_origin,
|
||||
ext.profile AS profile,
|
||||
cfg.game_id AS internal_game_id,
|
||||
cfg.name AS name,
|
||||
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.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("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("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 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).
|
||||
Limit(1).
|
||||
Scan(&row).Error
|
||||
@ -159,10 +171,11 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
|
||||
|
||||
var catalog model.BaishunGameCatalog
|
||||
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 {
|
||||
return gameListRow{
|
||||
SysOrigin: sysOrigin,
|
||||
Profile: profile,
|
||||
InternalGameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
|
||||
Name: catalog.Name,
|
||||
Cover: catalog.Cover,
|
||||
@ -188,12 +201,18 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
|
||||
|
||||
// findGameRowByConfigID 按系统游戏配置主键查询游戏聚合信息。
|
||||
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
|
||||
err := s.repo.DB.WithContext(ctx).
|
||||
err = s.repo.DB.WithContext(ctx).
|
||||
Table("sys_game_list_config AS cfg").
|
||||
Select(`
|
||||
cfg.id AS config_id,
|
||||
cfg.sys_origin AS sys_origin,
|
||||
ext.profile AS profile,
|
||||
cfg.game_id AS internal_game_id,
|
||||
cfg.name AS name,
|
||||
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.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("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("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 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).
|
||||
Limit(1).
|
||||
Scan(&row).Error
|
||||
|
||||
@ -175,6 +175,7 @@ type BaishunLaunchAppResponse struct {
|
||||
// SyncCatalogRequest 是目录同步入参。
|
||||
type SyncCatalogRequest struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
VendorGameIDs []int `json:"vendorGameIds"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
@ -190,6 +191,7 @@ type SyncCatalogResponse struct {
|
||||
type AdminBaishunGameItem struct {
|
||||
ID int64 `json:"id"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
GameID string `json:"gameId"`
|
||||
GameType string `json:"gameType,omitempty"`
|
||||
VendorGameID int `json:"vendorGameId"`
|
||||
@ -232,6 +234,7 @@ type AdminBaishunGamePageResponse struct {
|
||||
type SaveAdminBaishunGameRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
GameID string `json:"gameId"`
|
||||
VendorGameID int `json:"vendorGameId"`
|
||||
Name string `json:"name"`
|
||||
@ -277,6 +280,9 @@ type SyncAdminCatalogResponse struct {
|
||||
type BaishunProviderConfigItem struct {
|
||||
ID int64 `json:"id"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
ActiveProfile string `json:"activeProfile"`
|
||||
Active bool `json:"active"`
|
||||
PlatformBaseURL string `json:"platformBaseUrl"`
|
||||
AppID int64 `json:"appId"`
|
||||
AppName string `json:"appName"`
|
||||
@ -291,6 +297,7 @@ type BaishunProviderConfigItem struct {
|
||||
// SaveBaishunProviderConfigRequest 是后台保存百顺配置的入参。
|
||||
type SaveBaishunProviderConfigRequest struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
PlatformBaseURL string `json:"platformBaseUrl"`
|
||||
AppID int64 `json:"appId"`
|
||||
AppName string `json:"appName"`
|
||||
@ -301,9 +308,16 @@ type SaveBaishunProviderConfigRequest struct {
|
||||
SSTokenTTLSeconds int `json:"ssTokenTtlSeconds"`
|
||||
}
|
||||
|
||||
// ActivateBaishunProviderProfileRequest 是切换百顺正式/测试配置档案的入参。
|
||||
type ActivateBaishunProviderProfileRequest struct {
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
Profile string `json:"profile"`
|
||||
}
|
||||
|
||||
// AdminBaishunCatalogItem 是后台目录页展示的单个百顺目录项。
|
||||
type AdminBaishunCatalogItem struct {
|
||||
VendorGameID int `json:"vendorGameId"`
|
||||
Profile string `json:"profile"`
|
||||
GameID string `json:"gameId"`
|
||||
Name string `json:"name"`
|
||||
Cover string `json:"cover"`
|
||||
@ -410,6 +424,7 @@ type baishunBalanceInfoResponse struct {
|
||||
type gameListRow struct {
|
||||
ConfigID int64
|
||||
SysOrigin string
|
||||
Profile string
|
||||
InternalGameID string
|
||||
Name string
|
||||
Category string
|
||||
|
||||
@ -23,6 +23,7 @@ func (s *InviteService) GetHome(ctx context.Context, user AuthUser) (*HomeRespon
|
||||
now := time.Now()
|
||||
timezone := normalizeTimezone(bundle.Config.Timezone)
|
||||
monthKey := monthKeyAt(now, timezone)
|
||||
periodEndAt, secondsToReset := monthResetInfoAt(now, timezone)
|
||||
monthly, err := s.findMonthlyProgress(ctx, user.SysOrigin, user.UserID, monthKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -53,6 +54,9 @@ func (s *InviteService) GetHome(ctx context.Context, user AuthUser) (*HomeRespon
|
||||
ServiceContact: bundle.Config.ServiceContact,
|
||||
Timezone: timezone,
|
||||
MonthKey: monthKey,
|
||||
ServerTimeMs: now.UnixMilli(),
|
||||
PeriodEndAtMs: periodEndAt.UnixMilli(),
|
||||
SecondsToReset: secondsToReset,
|
||||
ValidUserThreshold: effectiveValidThreshold(inviteeRechargeRules),
|
||||
InviterBindReward: rewardFromRule(inviterBindRule),
|
||||
InviteeBindReward: rewardFromRule(inviteeBindRule),
|
||||
@ -63,7 +67,7 @@ func (s *InviteService) GetHome(ctx context.Context, user AuthUser) (*HomeRespon
|
||||
resp.MonthlyProgress.ValidUserCount = monthly.ValidUserCount
|
||||
resp.MonthlyProgress.TotalRecharge = monthly.TotalRecharge
|
||||
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)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
272
internal/service/invite/invite_test.go
Normal file
272
internal/service/invite/invite_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -36,6 +36,21 @@ func monthKeyAt(t time.Time, timezone string) string {
|
||||
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 判断字符串是否只包含数字。
|
||||
func digitsOnly(value string) bool {
|
||||
for _, ch := range value {
|
||||
|
||||
73
internal/service/invite/rank.go
Normal file
73
internal/service/invite/rank.go
Normal 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
|
||||
}
|
||||
@ -51,7 +51,10 @@ func (s *InviteService) dispatchGold(ctx context.Context, input rewardDispatchIn
|
||||
Remark: input.Remark,
|
||||
})
|
||||
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 {
|
||||
return err
|
||||
@ -87,7 +90,10 @@ func (s *InviteService) dispatchProps(ctx context.Context, input rewardDispatchI
|
||||
SourceGroupID: *input.Reward.RewardGroupID,
|
||||
})
|
||||
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, "")
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ func (s *InviteService) ClaimTask(ctx context.Context, user AuthUser, req TaskCl
|
||||
}
|
||||
progressValue := monthly.TotalRecharge
|
||||
if rule.RuleType == ruleTypeInviterValidCount {
|
||||
progressValue = monthly.ValidUserCount
|
||||
progressValue = monthly.InviteCount
|
||||
}
|
||||
if claimed {
|
||||
return &TaskClaimResponse{
|
||||
|
||||
@ -29,6 +29,8 @@ const (
|
||||
ruleTypeInviteeRecharge = "INVITEE_RECHARGE"
|
||||
ruleTypeInviterValidCount = "INVITER_VALID_COUNT"
|
||||
ruleTypeInviterTotalRecharge = "INVITER_TOTAL_RECHARGE"
|
||||
defaultInviteRankLimit = 50
|
||||
maxInviteRankLimit = 100
|
||||
)
|
||||
|
||||
var errAlreadyProcessed = errors.New("invite recharge already processed")
|
||||
@ -80,6 +82,9 @@ type HomeResponse struct {
|
||||
ServiceContact string `json:"serviceContact"`
|
||||
Timezone string `json:"timezone"`
|
||||
MonthKey string `json:"monthKey"`
|
||||
ServerTimeMs int64 `json:"serverTimeMs"`
|
||||
PeriodEndAtMs int64 `json:"periodEndAtMs"`
|
||||
SecondsToReset int64 `json:"secondsToReset"`
|
||||
ValidUserThreshold int64 `json:"validUserThreshold"`
|
||||
InviterBindReward RewardView `json:"inviterBindReward"`
|
||||
InviteeBindReward RewardView `json:"inviteeBindReward"`
|
||||
@ -95,6 +100,23 @@ type HomeResponse struct {
|
||||
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 是邀请落地页接口返回体。
|
||||
type LandingResponse struct {
|
||||
CampaignEnabled bool `json:"campaignEnabled"`
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type luckyGiftRuntimeAccount struct {
|
||||
URL string
|
||||
AppID string
|
||||
AppChannel 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) {
|
||||
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
|
||||
err := s.repo.DB.WithContext(ctx).
|
||||
Where("sys_origin = ?", normalizeLuckyGiftSysOrigin(sysOrigin)).
|
||||
err = s.repo.DB.WithContext(ctx).
|
||||
Where("sys_origin = ? AND profile = ?", normalizedSysOrigin, profile).
|
||||
Limit(1).
|
||||
First(&row).Error
|
||||
if err == nil {
|
||||
@ -55,8 +64,25 @@ func (s *LuckyGiftService) resolveRuntimeAccount(ctx context.Context, sysOrigin
|
||||
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 {
|
||||
account := luckyGiftRuntimeAccount{
|
||||
URL: strings.TrimSpace(cfg.Baishun.PlatformBaseURL),
|
||||
AppChannel: strings.TrimSpace(cfg.Baishun.AppChannel),
|
||||
AppKey: strings.TrimSpace(cfg.Baishun.AppKey),
|
||||
}
|
||||
@ -67,6 +93,9 @@ func defaultLuckyGiftRuntimeAccount(cfg config.Config) luckyGiftRuntimeAccount {
|
||||
}
|
||||
|
||||
func applyLuckyGiftRuntimeAccount(base luckyGiftRuntimeAccount, row model.BaishunProviderConfig) luckyGiftRuntimeAccount {
|
||||
if strings.TrimSpace(row.PlatformBaseURL) != "" {
|
||||
base.URL = strings.TrimSpace(row.PlatformBaseURL)
|
||||
}
|
||||
if row.AppID > 0 {
|
||||
base.AppID = strconv.FormatInt(row.AppID, 10)
|
||||
}
|
||||
@ -84,6 +113,9 @@ func mergeLuckyGiftProviderConfig(
|
||||
runtime luckyGiftRuntimeAccount,
|
||||
) config.LuckyGiftProviderConfig {
|
||||
merged := base
|
||||
if runtime.URL != "" {
|
||||
merged.URL = runtime.URL
|
||||
}
|
||||
if runtime.AppID != "" {
|
||||
merged.AppID = runtime.AppID
|
||||
}
|
||||
@ -103,3 +135,16 @@ func normalizeLuckyGiftSysOrigin(sysOrigin string) string {
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,12 @@ package luckygift
|
||||
import (
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/model"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestMergeLuckyGiftProviderConfigPrefersRuntimeAccount(t *testing.T) {
|
||||
@ -16,13 +21,14 @@ func TestMergeLuckyGiftProviderConfigPrefersRuntimeAccount(t *testing.T) {
|
||||
}
|
||||
|
||||
merged := mergeLuckyGiftProviderConfig(base, luckyGiftRuntimeAccount{
|
||||
URL: "https://runtime.example.com",
|
||||
AppID: "runtime-app-id",
|
||||
AppChannel: "runtime-app-channel",
|
||||
AppKey: "runtime-app-key",
|
||||
})
|
||||
|
||||
if merged.URL != base.URL {
|
||||
t.Fatalf("expected url to keep base value, got %q", merged.URL)
|
||||
if merged.URL != "https://runtime.example.com" {
|
||||
t.Fatalf("expected runtime url, got %q", merged.URL)
|
||||
}
|
||||
if merged.AppID != "runtime-app-id" {
|
||||
t.Fatalf("expected runtime app id, got %q", merged.AppID)
|
||||
@ -37,15 +43,20 @@ func TestMergeLuckyGiftProviderConfigPrefersRuntimeAccount(t *testing.T) {
|
||||
|
||||
func TestApplyLuckyGiftRuntimeAccountUsesProviderConfigRow(t *testing.T) {
|
||||
account := applyLuckyGiftRuntimeAccount(luckyGiftRuntimeAccount{
|
||||
URL: "https://fallback.example.com",
|
||||
AppID: "fallback-app-id",
|
||||
AppChannel: "fallback-app-channel",
|
||||
AppKey: "fallback-app-key",
|
||||
}, model.BaishunProviderConfig{
|
||||
AppID: 4581045902,
|
||||
AppChannel: "yumiparty",
|
||||
AppKey: "test-key",
|
||||
PlatformBaseURL: "https://provider.example.com",
|
||||
AppID: 4581045902,
|
||||
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" {
|
||||
t.Fatalf("expected row app id, got %q", account.AppID)
|
||||
}
|
||||
@ -60,12 +71,16 @@ func TestApplyLuckyGiftRuntimeAccountUsesProviderConfigRow(t *testing.T) {
|
||||
func TestDefaultLuckyGiftRuntimeAccountUsesBaishunConfig(t *testing.T) {
|
||||
account := defaultLuckyGiftRuntimeAccount(config.Config{
|
||||
Baishun: config.BaishunConfig{
|
||||
AppID: 7485318192,
|
||||
AppChannel: "yumiparty",
|
||||
AppKey: "shared-key",
|
||||
PlatformBaseURL: "https://provider.example.com",
|
||||
AppID: 7485318192,
|
||||
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" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS sys_game_list_vendor_ext (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
game_list_config_id BIGINT NOT NULL,
|
||||
sys_origin VARCHAR(32) NOT NULL,
|
||||
profile VARCHAR(32) NOT NULL DEFAULT 'PROD',
|
||||
vendor_type VARCHAR(32) NOT NULL,
|
||||
vendor_game_id VARCHAR(64) NOT NULL,
|
||||
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,
|
||||
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),
|
||||
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;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS baishun_game_catalog (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
sys_origin VARCHAR(32) NOT NULL,
|
||||
profile VARCHAR(32) NOT NULL DEFAULT 'PROD',
|
||||
internal_game_id VARCHAR(64) NOT NULL,
|
||||
vendor_game_id INT 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',
|
||||
create_time DATETIME NOT NULL DEFAULT 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),
|
||||
KEY idx_baishun_catalog_internal_game (sys_origin, internal_game_id, status)
|
||||
UNIQUE KEY uk_baishun_catalog_sys_profile_vendor_game (sys_origin, profile, vendor_game_id),
|
||||
KEY idx_baishun_catalog_profile_internal_game (sys_origin, profile, internal_game_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS baishun_launch_session (
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS baishun_provider_config (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
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,
|
||||
app_id BIGINT NOT 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,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_baishun_provider_sys_origin (sys_origin),
|
||||
KEY idx_baishun_provider_app (app_id, app_channel)
|
||||
UNIQUE KEY uk_baishun_provider_sys_origin_profile (sys_origin, profile),
|
||||
KEY idx_baishun_provider_app (app_id, app_channel),
|
||||
KEY idx_baishun_provider_active (sys_origin, active, update_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
155
migrations/009_baishun_profile_switch.sql
Normal file
155
migrations/009_baishun_profile_switch.sql
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user