Compare commits
No commits in common. "main" and "bug-fix" have entirely different histories.
@ -21,7 +21,6 @@ import (
|
|||||||
"chatapp3-golang/internal/service/gameopen"
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
"chatapp3-golang/internal/service/hostcenter"
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
"chatapp3-golang/internal/service/hotgame"
|
|
||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
"chatapp3-golang/internal/service/lingxian"
|
"chatapp3-golang/internal/service/lingxian"
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
@ -58,7 +57,6 @@ func main() {
|
|||||||
baishunService := baishun.NewBaishunService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
baishunService := baishun.NewBaishunService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
binanceRechargeService := binancerecharge.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
binanceRechargeService := binancerecharge.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
lingxianService := lingxian.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
lingxianService := lingxian.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
||||||
hotgameService := hotgame.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
|
||||||
gameOpenService := gameopen.NewGameOpenService(app.Config, app.Repository.DB, &app.Gateways)
|
gameOpenService := gameopen.NewGameOpenService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
luckyGiftService := luckygift.NewLuckyGiftService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet)
|
luckyGiftService := luckygift.NewLuckyGiftService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet)
|
||||||
rechargeAgencyService := rechargeagency.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
rechargeAgencyService := rechargeagency.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
@ -117,9 +115,7 @@ func main() {
|
|||||||
gameProviders := gameprovider.NewRegistry(
|
gameProviders := gameprovider.NewRegistry(
|
||||||
baishun.NewAppProvider(baishunService),
|
baishun.NewAppProvider(baishunService),
|
||||||
lingxian.NewAppProvider(lingxianService),
|
lingxian.NewAppProvider(lingxianService),
|
||||||
hotgame.NewAppProvider(hotgameService),
|
|
||||||
)
|
)
|
||||||
gameVisibility := gameprovider.NewVisibilityPolicy(&app.Gateways)
|
|
||||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||||
AppPopup: appPopupService,
|
AppPopup: appPopupService,
|
||||||
ErrorLog: errorLogService,
|
ErrorLog: errorLogService,
|
||||||
@ -130,12 +126,10 @@ func main() {
|
|||||||
Lingxian: lingxianService,
|
Lingxian: lingxianService,
|
||||||
GameOpen: gameOpenService,
|
GameOpen: gameOpenService,
|
||||||
GameProviders: gameProviders,
|
GameProviders: gameProviders,
|
||||||
GameVisibility: gameVisibility,
|
|
||||||
LuckyGift: luckyGiftService,
|
LuckyGift: luckyGiftService,
|
||||||
ManagerCenter: managerCenterService,
|
ManagerCenter: managerCenterService,
|
||||||
PropsStore: propsStoreService,
|
PropsStore: propsStoreService,
|
||||||
HostCenter: hostCenterService,
|
HostCenter: hostCenterService,
|
||||||
Hotgame: hotgameService,
|
|
||||||
RechargeAgency: rechargeAgencyService,
|
RechargeAgency: rechargeAgencyService,
|
||||||
RechargeReward: rechargeRewardService,
|
RechargeReward: rechargeRewardService,
|
||||||
RegionIMGroup: regionIMGroupService,
|
RegionIMGroup: regionIMGroupService,
|
||||||
|
|||||||
@ -6,6 +6,4 @@ type AuthUser struct {
|
|||||||
SysOrigin string
|
SysOrigin string
|
||||||
Token string
|
Token string
|
||||||
Authorization string
|
Authorization string
|
||||||
RegionID string
|
|
||||||
RegionCode string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,6 @@ type Config struct {
|
|||||||
TencentIM TencentIMConfig
|
TencentIM TencentIMConfig
|
||||||
Baishun BaishunConfig
|
Baishun BaishunConfig
|
||||||
Lingxian LingxianConfig
|
Lingxian LingxianConfig
|
||||||
Hotgame HotgameConfig
|
|
||||||
GameOpen GameOpenConfig
|
GameOpen GameOpenConfig
|
||||||
LuckyGift LuckyGiftConfig
|
LuckyGift LuckyGiftConfig
|
||||||
BinanceRecharge BinanceRechargeConfig
|
BinanceRecharge BinanceRechargeConfig
|
||||||
@ -269,12 +268,6 @@ type LingxianConfig struct {
|
|||||||
AppKey string
|
AppKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// HotgameConfig 保存热游模块的环境兜底配置。
|
|
||||||
type HotgameConfig struct {
|
|
||||||
AppKey string
|
|
||||||
CallbackBaseURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GameOpenConfig 保存统一三方游戏回调配置。
|
// GameOpenConfig 保存统一三方游戏回调配置。
|
||||||
type GameOpenConfig struct {
|
type GameOpenConfig struct {
|
||||||
PublicBaseURL string
|
PublicBaseURL string
|
||||||
@ -439,7 +432,7 @@ func Load() Config {
|
|||||||
SecretID: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_ID", "TASK_CENTER_ARCHIVE_COS_SECRET_ID", "LIKEI_TENCENT_COS_SECRET_ID"}, ""),
|
SecretID: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_ID", "TASK_CENTER_ARCHIVE_COS_SECRET_ID", "LIKEI_TENCENT_COS_SECRET_ID"}, ""),
|
||||||
SecretKey: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "LIKEI_TENCENT_COS_SECRET_KEY"}, ""),
|
SecretKey: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "LIKEI_TENCENT_COS_SECRET_KEY"}, ""),
|
||||||
Prefix: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_PREFIX", "TASK_CENTER_ARCHIVE_COS_PREFIX"}, "task-center-events/v1"),
|
Prefix: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_PREFIX", "TASK_CENTER_ARCHIVE_COS_PREFIX"}, "task-center-events/v1"),
|
||||||
BatchSize: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_BATCH_SIZE", "TASK_CENTER_ARCHIVE_BATCH_SIZE"}, 500),
|
BatchSize: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_BATCH_SIZE", "TASK_CENTER_ARCHIVE_BATCH_SIZE"}, 10000),
|
||||||
FlushSeconds: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_FLUSH_SECONDS", "TASK_CENTER_ARCHIVE_FLUSH_SECONDS"}, 60),
|
FlushSeconds: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_FLUSH_SECONDS", "TASK_CENTER_ARCHIVE_FLUSH_SECONDS"}, 60),
|
||||||
Workers: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_WORKERS", "TASK_CENTER_ARCHIVE_WORKERS"}, 2),
|
Workers: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_WORKERS", "TASK_CENTER_ARCHIVE_WORKERS"}, 2),
|
||||||
},
|
},
|
||||||
@ -624,10 +617,6 @@ func Load() Config {
|
|||||||
GameListURL: getEnvAny([]string{"CHATAPP_LINGXIAN_GAME_LIST_URL", "LINGXIAN_GAME_LIST_URL"}, "https://sg-test.leadercc.com/yumichat_games/test_game_list.json"),
|
GameListURL: getEnvAny([]string{"CHATAPP_LINGXIAN_GAME_LIST_URL", "LINGXIAN_GAME_LIST_URL"}, "https://sg-test.leadercc.com/yumichat_games/test_game_list.json"),
|
||||||
AppKey: getEnvAny([]string{"CHATAPP_LINGXIAN_APP_KEY", "LINGXIAN_APP_KEY", "GAME_HKYS_SIGN_KEY", "LIKEI_GAME_HKYS_SIGN_KEY"}, ""),
|
AppKey: getEnvAny([]string{"CHATAPP_LINGXIAN_APP_KEY", "LINGXIAN_APP_KEY", "GAME_HKYS_SIGN_KEY", "LIKEI_GAME_HKYS_SIGN_KEY"}, ""),
|
||||||
},
|
},
|
||||||
Hotgame: HotgameConfig{
|
|
||||||
AppKey: getEnvAny([]string{"CHATAPP_HOTGAME_APP_KEY", "HOTGAME_APP_KEY", "CHATAPP_REYOU_APP_KEY", "REYOU_APP_KEY"}, ""),
|
|
||||||
CallbackBaseURL: getEnvAny([]string{"CHATAPP_HOTGAME_CALLBACK_BASE_URL", "HOTGAME_CALLBACK_BASE_URL", "CHATAPP_REYOU_CALLBACK_BASE_URL", "REYOU_CALLBACK_BASE_URL"}, ""),
|
|
||||||
},
|
|
||||||
GameOpen: GameOpenConfig{
|
GameOpen: GameOpenConfig{
|
||||||
PublicBaseURL: getEnvAny([]string{"CHATAPP_GAME_OPEN_PUBLIC_BASE_URL", "GAME_OPEN_PUBLIC_BASE_URL"}, ""),
|
PublicBaseURL: getEnvAny([]string{"CHATAPP_GAME_OPEN_PUBLIC_BASE_URL", "GAME_OPEN_PUBLIC_BASE_URL"}, ""),
|
||||||
AppKey: getEnvAny([]string{"CHATAPP_GAME_OPEN_APP_KEY", "GAME_OPEN_APP_KEY"}, "game-open-test-key"),
|
AppKey: getEnvAny([]string{"CHATAPP_GAME_OPEN_APP_KEY", "GAME_OPEN_APP_KEY"}, "game-open-test-key"),
|
||||||
|
|||||||
@ -146,11 +146,6 @@ func (g *Gateways) GetUserRegion(ctx context.Context, userID int64, authorizatio
|
|||||||
return g.Profile.GetUserRegion(ctx, userID, authorization)
|
return g.Profile.GetUserRegion(ctx, userID, authorization)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserLevel 透传到用户等级网关。
|
|
||||||
func (g *Gateways) GetUserLevel(ctx context.Context, sysOrigin string, userID int64) (UserLevel, error) {
|
|
||||||
return g.Profile.GetUserLevel(ctx, sysOrigin, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveRegionCodeByCountryCode 透传到 Java 区域配置网关。
|
// ResolveRegionCodeByCountryCode 透传到 Java 区域配置网关。
|
||||||
func (g *Gateways) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
func (g *Gateways) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
||||||
return g.Profile.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
|
return g.Profile.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
|
||||||
@ -405,11 +400,6 @@ func (g *ProfileGateway) GetUserRegion(ctx context.Context, userID int64, author
|
|||||||
return g.client.GetUserRegion(ctx, userID, authorization)
|
return g.client.GetUserRegion(ctx, userID, authorization)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserLevel 查询用户财富/魅力等级。
|
|
||||||
func (g *ProfileGateway) GetUserLevel(ctx context.Context, sysOrigin string, userID int64) (UserLevel, error) {
|
|
||||||
return g.client.GetUserLevel(ctx, sysOrigin, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveRegionCodeByCountryCode 根据 Java 区域配置把国家码解析成区域编码。
|
// ResolveRegionCodeByCountryCode 根据 Java 区域配置把国家码解析成区域编码。
|
||||||
func (g *ProfileGateway) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
func (g *ProfileGateway) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
||||||
return g.client.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
|
return g.client.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
|
||||||
|
|||||||
@ -59,12 +59,6 @@ type UserRegion struct {
|
|||||||
RegionCode string `json:"regionCode"`
|
RegionCode string `json:"regionCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserLevel struct {
|
|
||||||
UserID Int64Value `json:"userId"`
|
|
||||||
WealthLevel int `json:"wealthLevel"`
|
|
||||||
CharmLevel int `json:"charmLevel"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegionConfig struct {
|
type RegionConfig struct {
|
||||||
RegionCode string `json:"regionCode"`
|
RegionCode string `json:"regionCode"`
|
||||||
SysOrigin string `json:"sysOrigin"`
|
SysOrigin string `json:"sysOrigin"`
|
||||||
@ -1050,21 +1044,6 @@ func (c *Client) GetUserRegion(ctx context.Context, userID int64, authorization
|
|||||||
return resp.Body, nil
|
return resp.Body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetUserLevel(ctx context.Context, sysOrigin string, userID int64) (UserLevel, error) {
|
|
||||||
if userID <= 0 {
|
|
||||||
return UserLevel{}, fmt.Errorf("userId is required")
|
|
||||||
}
|
|
||||||
values := url.Values{}
|
|
||||||
values.Set("sysOrigin", strings.TrimSpace(sysOrigin))
|
|
||||||
values.Set("userId", strconv.FormatInt(userID, 10))
|
|
||||||
endpoint := c.cfg.Java.OtherBaseURL + "/user-level/client/getLevelByUserId?" + values.Encode()
|
|
||||||
var resp resultResponse[UserLevel]
|
|
||||||
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
|
||||||
return UserLevel{}, err
|
|
||||||
}
|
|
||||||
return resp.Body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
func (c *Client) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
||||||
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
|
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
|
||||||
if countryCode == "" {
|
if countryCode == "" {
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// HotgameProviderConfig 保存每个系统、每个环境的热游回调验签配置。
|
|
||||||
type HotgameProviderConfig struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_hotgame_provider_sys_origin_profile,priority:1;index:idx_hotgame_provider_active,priority:1"` // 系统标识
|
|
||||||
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_hotgame_provider_sys_origin_profile,priority:2"` // 配置环境:PROD/TEST
|
|
||||||
Active bool `gorm:"column:active;index:idx_hotgame_provider_active,priority:2"` // app 当前启用环境
|
|
||||||
AppKey string `gorm:"column:app_key;size:255"` // 热游回调 MD5 key
|
|
||||||
CallbackBaseURL string `gorm:"column:callback_base_url;size:1024"` // 提供给热游配置的回调域名
|
|
||||||
TestUID string `gorm:"column:test_uid;size:64"` // 联调用 UID 覆盖
|
|
||||||
TestToken string `gorm:"column:test_token;size:1024"` // 联调用 token 覆盖
|
|
||||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time;index:idx_hotgame_provider_active,priority:3"` // 更新时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 返回热游接入配置表名。
|
|
||||||
func (HotgameProviderConfig) TableName() string { return "hotgame_provider_config" }
|
|
||||||
|
|
||||||
// HotgameGameCatalog 保存从热游对接表导入的游戏目录。
|
|
||||||
type HotgameGameCatalog struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_hotgame_catalog_sys_profile_vendor_game,priority:1;index:idx_hotgame_catalog_profile_internal_game,priority:1"` // 系统标识
|
|
||||||
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_hotgame_catalog_sys_profile_vendor_game,priority:2;index:idx_hotgame_catalog_profile_internal_game,priority:2"` // 配置环境:PROD/TEST
|
|
||||||
InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_hotgame_catalog_profile_internal_game,priority:3"` // app 内部游戏 ID
|
|
||||||
VendorGameID string `gorm:"column:vendor_game_id;size:64;uniqueIndex:uk_hotgame_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"` // 版本号
|
|
||||||
RawJSON string `gorm:"column:raw_json;type:longtext"` // 原始目录 JSON
|
|
||||||
Status string `gorm:"column:status;size:32;index:idx_hotgame_catalog_profile_internal_game,priority:4"` // 目录状态
|
|
||||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 返回热游目录表名。
|
|
||||||
func (HotgameGameCatalog) TableName() string { return "hotgame_game_catalog" }
|
|
||||||
@ -116,19 +116,19 @@ func (TaskCenterEventDedup) TableName() string { return "task_center_event_dedup
|
|||||||
|
|
||||||
// TaskCenterEventArchiveOutbox 保存待归档到 COS 的原始事件。
|
// TaskCenterEventArchiveOutbox 保存待归档到 COS 的原始事件。
|
||||||
type TaskCenterEventArchiveOutbox struct {
|
type TaskCenterEventArchiveOutbox struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey;index:idx_task_center_archive_claim_create,priority:3;index:idx_task_center_archive_claim_stale,priority:3"`
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_archive_event,priority:1"`
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_archive_event,priority:1"`
|
||||||
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_task_center_archive_event,priority:2"`
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_task_center_archive_event,priority:2"`
|
||||||
EventType string `gorm:"column:event_type;size:64"`
|
EventType string `gorm:"column:event_type;size:64"`
|
||||||
UserID int64 `gorm:"column:user_id"`
|
UserID int64 `gorm:"column:user_id"`
|
||||||
OccurredAt time.Time `gorm:"column:occurred_at"`
|
OccurredAt time.Time `gorm:"column:occurred_at"`
|
||||||
RawJSON string `gorm:"column:raw_json;type:text"`
|
RawJSON string `gorm:"column:raw_json;type:text"`
|
||||||
Status string `gorm:"column:status;size:32;index:idx_task_center_archive_status_time,priority:1;index:idx_task_center_archive_claim_create,priority:1;index:idx_task_center_archive_claim_stale,priority:1"`
|
Status string `gorm:"column:status;size:32;index:idx_task_center_archive_status_time,priority:1"`
|
||||||
RetryCount int `gorm:"column:retry_count"`
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
COSKey string `gorm:"column:cos_key;size:512"`
|
COSKey string `gorm:"column:cos_key;size:512"`
|
||||||
FailureReason string `gorm:"column:failure_reason;size:512"`
|
FailureReason string `gorm:"column:failure_reason;size:512"`
|
||||||
CreateTime time.Time `gorm:"column:create_time;index:idx_task_center_archive_status_time,priority:2;index:idx_task_center_archive_claim_create,priority:2"`
|
CreateTime time.Time `gorm:"column:create_time;index:idx_task_center_archive_status_time,priority:2"`
|
||||||
UpdateTime time.Time `gorm:"column:update_time;index:idx_task_center_archive_claim_stale,priority:2"`
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
ArchivedAt *time.Time `gorm:"column:archived_at"`
|
ArchivedAt *time.Time `gorm:"column:archived_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,8 +46,6 @@ func registerGameOpenRoutes(engine *gin.Engine, cfg config.Config, service *game
|
|||||||
c.JSON(http.StatusOK, service.HandleSupplement(c.Request.Context(), req, string(raw)))
|
c.JSON(http.StatusOK, service.HandleSupplement(c.Request.Context(), req, string(raw)))
|
||||||
})
|
})
|
||||||
|
|
||||||
registerHotgameCallbackRoutes(engine, service)
|
|
||||||
|
|
||||||
internalGroup := engine.Group("/internal/game/open")
|
internalGroup := engine.Group("/internal/game/open")
|
||||||
internalGroup.Use(internalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
internalGroup.Use(internalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
||||||
internalGroup.GET("/info", func(c *gin.Context) {
|
internalGroup.GET("/info", func(c *gin.Context) {
|
||||||
@ -60,34 +58,6 @@ func registerGameOpenRoutes(engine *gin.Engine, cfg config.Config, service *game
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerHotgameCallbackRoutes(engine *gin.Engine, service *gameopen.GameOpenService) {
|
|
||||||
handleUserInfo := func(c *gin.Context) {
|
|
||||||
raw, _ := c.GetRawData()
|
|
||||||
var req gameopen.HotgameGetUserInfoRequest
|
|
||||||
if err := json.Unmarshal(raw, &req); err != nil {
|
|
||||||
c.JSON(http.StatusOK, gameopen.CallbackResponse{ErrorCode: 4001, ErrorMsg: err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, service.HandleHotgameGetUserInfo(c.Request.Context(), req, string(raw)))
|
|
||||||
}
|
|
||||||
handleUpdateBalance := func(c *gin.Context) {
|
|
||||||
raw, _ := c.GetRawData()
|
|
||||||
var req gameopen.HotgameUpdateBalanceRequest
|
|
||||||
if err := json.Unmarshal(raw, &req); err != nil {
|
|
||||||
c.JSON(http.StatusOK, gameopen.CallbackResponse{ErrorCode: 4001, ErrorMsg: err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, service.HandleHotgameUpdateBalance(c.Request.Context(), req, string(raw)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 热游文档要求平台直接提供 /getUserInfo 和 /updateBalance,同时保留命名空间路径便于网关灰度。
|
|
||||||
engine.POST("/getUserInfo", handleUserInfo)
|
|
||||||
engine.POST("/updateBalance", handleUpdateBalance)
|
|
||||||
hotgameGroup := engine.Group("/game/hotgame")
|
|
||||||
hotgameGroup.POST("/getUserInfo", handleUserInfo)
|
|
||||||
hotgameGroup.POST("/updateBalance", handleUpdateBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolvePublicBaseURL(c *gin.Context, cfg config.Config) string {
|
func resolvePublicBaseURL(c *gin.Context, cfg config.Config) string {
|
||||||
if value := strings.TrimRight(strings.TrimSpace(cfg.GameOpen.PublicBaseURL), "/"); value != "" {
|
if value := strings.TrimRight(strings.TrimSpace(cfg.GameOpen.PublicBaseURL), "/"); value != "" {
|
||||||
return value
|
return value
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -12,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// registerGameProviderRoutes 注册统一游戏厂商 app 路由。
|
// registerGameProviderRoutes 注册统一游戏厂商 app 路由。
|
||||||
func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, registry *gameprovider.Registry, visibility *gameprovider.VisibilityPolicy) {
|
func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, registry *gameprovider.Registry) {
|
||||||
if registry == nil {
|
if registry == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -20,15 +19,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
appGroup := engine.Group("/app/game")
|
appGroup := engine.Group("/app/game")
|
||||||
appGroup.Use(authMiddleware(javaClient))
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
appGroup.GET("/room/shortcut", func(c *gin.Context) {
|
appGroup.GET("/room/shortcut", func(c *gin.Context) {
|
||||||
user, visible, ok := gameListAccess(c, visibility)
|
resp, err := registry.ListShortcutGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
writeOK(c, []publicRoomGameItem{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := registry.ListShortcutGames(c.Request.Context(), user, c.Query("roomId"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
@ -36,15 +27,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
writeOK(c, publicRoomGameItems(resp))
|
writeOK(c, publicRoomGameItems(resp))
|
||||||
})
|
})
|
||||||
appGroup.GET("/room/list", func(c *gin.Context) {
|
appGroup.GET("/room/list", func(c *gin.Context) {
|
||||||
user, visible, ok := gameListAccess(c, visibility)
|
resp, err := registry.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category"))
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
writeOK(c, publicRoomGameListResponse(&gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := registry.ListRoomGames(c.Request.Context(), user, c.Query("roomId"), c.Query("category"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
@ -89,30 +72,14 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
providerGroup := engine.Group("/app/game/providers")
|
providerGroup := engine.Group("/app/game/providers")
|
||||||
providerGroup.Use(authMiddleware(javaClient))
|
providerGroup.Use(authMiddleware(javaClient))
|
||||||
providerGroup.GET("", func(c *gin.Context) {
|
providerGroup.GET("", func(c *gin.Context) {
|
||||||
_, visible, ok := gameListAccess(c, visibility)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
writeOK(c, gameprovider.ProviderListResponse{Items: []gameprovider.ProviderSummary{}})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, registry.List())
|
writeOK(c, registry.List())
|
||||||
})
|
})
|
||||||
providerGroup.GET("/:provider/room/shortcut", func(c *gin.Context) {
|
providerGroup.GET("/:provider/room/shortcut", func(c *gin.Context) {
|
||||||
user, visible, ok := gameListAccess(c, visibility)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
writeOK(c, gin.H{"items": []publicRoomGameItem{}})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
provider, ok := resolveGameProvider(c, registry)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := provider.ListShortcutGames(c.Request.Context(), user, c.Query("roomId"))
|
resp, err := provider.ListShortcutGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
@ -120,19 +87,11 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
writeOK(c, gin.H{"items": publicRoomGameItems(resp)})
|
writeOK(c, gin.H{"items": publicRoomGameItems(resp)})
|
||||||
})
|
})
|
||||||
providerGroup.GET("/:provider/room/list", func(c *gin.Context) {
|
providerGroup.GET("/:provider/room/list", func(c *gin.Context) {
|
||||||
user, visible, ok := gameListAccess(c, visibility)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
writeOK(c, publicRoomGameListResponse(&gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
provider, ok := resolveGameProvider(c, registry)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := provider.ListRoomGames(c.Request.Context(), user, c.Query("roomId"), c.Query("category"))
|
resp, err := provider.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
@ -161,7 +120,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := launchProviderGame(c, registry, provider, req)
|
resp, err := provider.LaunchGame(c.Request.Context(), mustAuthUser(c), req, resolveClientIP(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
@ -187,33 +146,6 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func gameListAccess(c *gin.Context, visibility *gameprovider.VisibilityPolicy) (common.AuthUser, bool, bool) {
|
|
||||||
user := mustAuthUser(c)
|
|
||||||
// iOS 审核和上架场景需要隐藏游戏入口;这里放在统一列表门禁最前面,避免继续请求 Java 区域/等级接口或 provider SQL。
|
|
||||||
if isIOSGameListClient(c) {
|
|
||||||
return user, false, true
|
|
||||||
}
|
|
||||||
if gameprovider.IsEmptyGameListUser(user.UserID) {
|
|
||||||
return user, false, true
|
|
||||||
}
|
|
||||||
if visibility == nil {
|
|
||||||
return user, true, true
|
|
||||||
}
|
|
||||||
// 游戏列表只在列表入口做展示门禁,结果里的区域继续向下传给 provider SQL 做 regions 过滤。
|
|
||||||
result, err := visibility.Evaluate(c.Request.Context(), user)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return user, false, false
|
|
||||||
}
|
|
||||||
user.RegionID = result.RegionID
|
|
||||||
user.RegionCode = result.RegionCode
|
|
||||||
return user, result.Allow, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func isIOSGameListClient(c *gin.Context) bool {
|
|
||||||
return strings.EqualFold(strings.TrimSpace(c.GetHeader("Req-Client")), "ios")
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gameprovider.Provider, bool) {
|
func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gameprovider.Provider, bool) {
|
||||||
provider, err := registry.Resolve(c.Param("provider"))
|
provider, err := registry.Resolve(c.Param("provider"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -223,25 +155,6 @@ func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gamep
|
|||||||
return provider, true
|
return provider, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func launchProviderGame(c *gin.Context, registry *gameprovider.Registry, provider gameprovider.Provider, req gameprovider.LaunchRequest) (*gameprovider.LaunchResponse, error) {
|
|
||||||
user := mustAuthUser(c)
|
|
||||||
clientIP := resolveClientIP(c)
|
|
||||||
resp, err := provider.LaunchGame(c.Request.Context(), user, req, clientIP)
|
|
||||||
if err == nil {
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
if !isGameNotFoundError(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 旧客户端只能把热游列表项伪装成 LEADER 进入已有 WebView;这里再按 gameId 回到真实 provider,避免 LEADER 路由找不到 hg_*。
|
|
||||||
return registry.LaunchGame(c.Request.Context(), user, req, clientIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isGameNotFoundError(err error) bool {
|
|
||||||
var appErr *common.AppError
|
|
||||||
return errors.As(err, &appErr) && appErr.Code == "game_not_found"
|
|
||||||
}
|
|
||||||
|
|
||||||
type publicRoomGameItem struct {
|
type publicRoomGameItem struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
GameID string `json:"gameId"`
|
GameID string `json:"gameId"`
|
||||||
@ -398,14 +311,8 @@ func publicLaunch(resp *gameprovider.LaunchResponse) *publicLaunchResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func appCompatProviderName(provider string) string {
|
func appCompatProviderName(provider string) string {
|
||||||
// 客户端历史上只认识 LEADER,不认识内部厂商名 LINGXIAN,所以出参继续保持旧字段值。
|
|
||||||
if strings.EqualFold(strings.TrimSpace(provider), "LINGXIAN") {
|
if strings.EqualFold(strings.TrimSpace(provider), "LINGXIAN") {
|
||||||
return "LEADER"
|
return "LEADER"
|
||||||
}
|
}
|
||||||
// 旧客户端没有 HOTGAME 页面;热游先按 LEADER 展示并进入已有 WebView,launch 路由再按 hg_* 分发回真实热游 provider。
|
|
||||||
switch strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(provider), "_", "")) {
|
|
||||||
case "HOTGAME", "REYOU", "LALU":
|
|
||||||
return "LEADER"
|
|
||||||
}
|
|
||||||
return provider
|
return provider
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,9 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPublicRoomGameListResponseKeepsLegacyProviderFields(t *testing.T) {
|
func TestPublicRoomGameListResponseKeepsLegacyProviderFields(t *testing.T) {
|
||||||
@ -83,41 +74,6 @@ func TestPublicRoomGameListResponseMapsLingxianToLeaderForOldClients(t *testing.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPublicRoomGameListResponseMapsHotgameAliasesToLeaderForOldClients(t *testing.T) {
|
|
||||||
for _, alias := range []string{"HOTGAME", "HOT_GAME", "REYOU", "LALU"} {
|
|
||||||
resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{
|
|
||||||
Items: []gameprovider.RoomGameListItem{
|
|
||||||
{
|
|
||||||
ID: 9530,
|
|
||||||
GameID: "hg_1",
|
|
||||||
GameType: alias,
|
|
||||||
Provider: alias,
|
|
||||||
ProviderGameID: "1",
|
|
||||||
Name: "Hotgame",
|
|
||||||
LaunchParams: map[string]any{"gameType": alias, "screenMode": "seven"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(resp.Items) != 1 {
|
|
||||||
t.Fatalf("%s items = %d, want 1", alias, len(resp.Items))
|
|
||||||
}
|
|
||||||
item := resp.Items[0]
|
|
||||||
if item.Provider != "LEADER" {
|
|
||||||
t.Fatalf("%s provider = %q, want LEADER", alias, item.Provider)
|
|
||||||
}
|
|
||||||
if item.GameType != "LEADER" {
|
|
||||||
t.Fatalf("%s gameType = %q, want LEADER", alias, item.GameType)
|
|
||||||
}
|
|
||||||
if item.VendorType != "LEADER" {
|
|
||||||
t.Fatalf("%s vendorType = %q, want LEADER", alias, item.VendorType)
|
|
||||||
}
|
|
||||||
if got := item.LaunchParams["gameType"]; got != "LEADER" {
|
|
||||||
t.Fatalf("%s launchParams.gameType = %v, want LEADER", alias, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPublicLaunchKeepsLegacyBridgeAndProviderFields(t *testing.T) {
|
func TestPublicLaunchKeepsLegacyBridgeAndProviderFields(t *testing.T) {
|
||||||
launchConfig := map[string]any{"code": "launch-code"}
|
launchConfig := map[string]any{"code": "launch-code"}
|
||||||
resp := publicLaunch(&gameprovider.LaunchResponse{
|
resp := publicLaunch(&gameprovider.LaunchResponse{
|
||||||
@ -185,325 +141,3 @@ func TestPublicLaunchMapsLingxianToLeaderForOldClients(t *testing.T) {
|
|||||||
t.Fatalf("roomState.provider = %q, want LEADER", resp.RoomState.Provider)
|
t.Fatalf("roomState.provider = %q, want LEADER", resp.RoomState.Provider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGameProviderRoutesReturnRoomListForTurkeyRegionUser(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
provider := &routeStubProvider{}
|
|
||||||
engine := gin.New()
|
|
||||||
registerGameProviderRoutes(
|
|
||||||
engine,
|
|
||||||
routeStubAuthGateway{},
|
|
||||||
gameprovider.NewRegistry(provider),
|
|
||||||
gameprovider.NewVisibilityPolicy(
|
|
||||||
routeStubVisibilityGateway{
|
|
||||||
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/app/game/room/list?roomId=1", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
||||||
}
|
|
||||||
var payload struct {
|
|
||||||
Body struct {
|
|
||||||
Items []publicRoomGameItem `json:"items"`
|
|
||||||
} `json:"body"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
|
||||||
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
|
||||||
}
|
|
||||||
if len(payload.Body.Items) != 1 {
|
|
||||||
t.Fatalf("items = %d, want 1", len(payload.Body.Items))
|
|
||||||
}
|
|
||||||
if provider.listRoomCalls != 1 {
|
|
||||||
t.Fatalf("provider list calls = %d, want 1", provider.listRoomCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGameProviderRoutesReturnEmptyRoomListForIOSClient(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
provider := &routeStubProvider{}
|
|
||||||
engine := gin.New()
|
|
||||||
registerGameProviderRoutes(
|
|
||||||
engine,
|
|
||||||
routeStubAuthGateway{},
|
|
||||||
gameprovider.NewRegistry(provider),
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/app/game/room/list?roomId=1", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token")
|
|
||||||
req.Header.Set("Req-Client", "ios")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
||||||
}
|
|
||||||
var payload struct {
|
|
||||||
Body struct {
|
|
||||||
Items []publicRoomGameItem `json:"items"`
|
|
||||||
} `json:"body"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
|
||||||
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
|
||||||
}
|
|
||||||
if len(payload.Body.Items) != 0 {
|
|
||||||
t.Fatalf("items = %d, want 0", len(payload.Body.Items))
|
|
||||||
}
|
|
||||||
if provider.listRoomCalls != 0 {
|
|
||||||
t.Fatalf("provider list calls = %d, want 0", provider.listRoomCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGameProviderRoutesReturnEmptyShortcutForIOSClient(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
provider := &routeStubProvider{}
|
|
||||||
engine := gin.New()
|
|
||||||
registerGameProviderRoutes(
|
|
||||||
engine,
|
|
||||||
routeStubAuthGateway{},
|
|
||||||
gameprovider.NewRegistry(provider),
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/app/game/room/shortcut?roomId=1", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token")
|
|
||||||
req.Header.Set("Req-Client", " iOS ")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
||||||
}
|
|
||||||
var payload struct {
|
|
||||||
Body []publicRoomGameItem `json:"body"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
|
||||||
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
|
||||||
}
|
|
||||||
if len(payload.Body) != 0 {
|
|
||||||
t.Fatalf("items = %d, want 0", len(payload.Body))
|
|
||||||
}
|
|
||||||
if provider.shortcutCalls != 0 {
|
|
||||||
t.Fatalf("provider shortcut calls = %d, want 0", provider.shortcutCalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGameProviderRoutesReturnEmptyProvidersForIOSClient(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
engine := gin.New()
|
|
||||||
registerGameProviderRoutes(
|
|
||||||
engine,
|
|
||||||
routeStubAuthGateway{},
|
|
||||||
gameprovider.NewRegistry(&routeStubProvider{}),
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/app/game/providers", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token")
|
|
||||||
req.Header.Set("Req-Client", "IOS")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
||||||
}
|
|
||||||
var payload struct {
|
|
||||||
Body struct {
|
|
||||||
Items []gameprovider.ProviderSummary `json:"items"`
|
|
||||||
} `json:"body"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
|
||||||
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
|
||||||
}
|
|
||||||
if len(payload.Body.Items) != 0 {
|
|
||||||
t.Fatalf("providers = %d, want 0", len(payload.Body.Items))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGameProviderRoutesReturnProvidersWhenVisibilityPolicyAllows(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
engine := gin.New()
|
|
||||||
registerGameProviderRoutes(
|
|
||||||
engine,
|
|
||||||
routeStubAuthGateway{},
|
|
||||||
gameprovider.NewRegistry(&routeStubProvider{}),
|
|
||||||
gameprovider.NewVisibilityPolicy(
|
|
||||||
routeStubVisibilityGateway{
|
|
||||||
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/app/game/providers", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token")
|
|
||||||
req.Header.Set("X-Real-IP", "88.255.1.1")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
||||||
}
|
|
||||||
var payload struct {
|
|
||||||
Body struct {
|
|
||||||
Items []gameprovider.ProviderSummary `json:"items"`
|
|
||||||
} `json:"body"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
|
||||||
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
|
||||||
}
|
|
||||||
if len(payload.Body.Items) != 1 {
|
|
||||||
t.Fatalf("providers = %d, want 1", len(payload.Body.Items))
|
|
||||||
}
|
|
||||||
if payload.Body.Items[0].Key != "TEST" {
|
|
||||||
t.Fatalf("provider key = %q, want TEST", payload.Body.Items[0].Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProviderLaunchFallsBackToRegistryWhenLegacyProviderCannotFindGame(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
engine := gin.New()
|
|
||||||
registerGameProviderRoutes(
|
|
||||||
engine,
|
|
||||||
routeStubAuthGateway{},
|
|
||||||
gameprovider.NewRegistry(
|
|
||||||
&routeLaunchStubProvider{key: "LINGXIAN"},
|
|
||||||
&routeLaunchStubProvider{key: "HOTGAME", matchGameID: "hg_1"},
|
|
||||||
),
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/app/game/providers/LEADER/launch", strings.NewReader(`{"roomId":"room-1","gameId":"hg_1"}`))
|
|
||||||
req.Header.Set("Authorization", "Bearer token")
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
||||||
}
|
|
||||||
var payload struct {
|
|
||||||
Body publicLaunchResponse `json:"body"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
|
||||||
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
|
||||||
}
|
|
||||||
if payload.Body.GameID != "hg_1" {
|
|
||||||
t.Fatalf("gameId = %q, want hg_1", payload.Body.GameID)
|
|
||||||
}
|
|
||||||
if payload.Body.Provider != "LEADER" {
|
|
||||||
t.Fatalf("provider = %q, want LEADER for old-client compat", payload.Body.Provider)
|
|
||||||
}
|
|
||||||
if payload.Body.GameSessionID != "hg-session" {
|
|
||||||
t.Fatalf("gameSessionId = %q, want hg-session", payload.Body.GameSessionID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type routeStubAuthGateway struct{}
|
|
||||||
|
|
||||||
func (routeStubAuthGateway) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
|
||||||
return integration.UserCredential{UserID: integration.Int64Value(1001), SysOrigin: "LIKEI"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (routeStubAuthGateway) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) {
|
|
||||||
return integration.ConsoleAccount{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type routeStubVisibilityGateway struct {
|
|
||||||
region integration.UserRegion
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s routeStubVisibilityGateway) GetUserRegion(context.Context, int64, string) (integration.UserRegion, error) {
|
|
||||||
return s.region, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type routeStubProvider struct {
|
|
||||||
listRoomCalls int
|
|
||||||
shortcutCalls int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeStubProvider) Key() string { return "TEST" }
|
|
||||||
|
|
||||||
func (p *routeStubProvider) DisplayName() string { return "Test Provider" }
|
|
||||||
|
|
||||||
func (p *routeStubProvider) SupportsLaunch(context.Context, gameprovider.AuthUser, gameprovider.LaunchRequest) (bool, error) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeStubProvider) ListShortcutGames(context.Context, gameprovider.AuthUser, string) ([]gameprovider.RoomGameListItem, error) {
|
|
||||||
p.shortcutCalls++
|
|
||||||
return []gameprovider.RoomGameListItem{{ID: 1, GameID: "test_1", Provider: "TEST", ProviderGameID: "1", Name: "Test Game"}}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeStubProvider) ListRoomGames(context.Context, gameprovider.AuthUser, string, string) (*gameprovider.RoomGameListResponse, error) {
|
|
||||||
p.listRoomCalls++
|
|
||||||
return &gameprovider.RoomGameListResponse{
|
|
||||||
Items: []gameprovider.RoomGameListItem{{ID: 1, GameID: "test_1", Provider: "TEST", ProviderGameID: "1", Name: "Test Game"}},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeStubProvider) GetRoomState(context.Context, gameprovider.AuthUser, string) (*gameprovider.RoomStateResponse, error) {
|
|
||||||
return &gameprovider.RoomStateResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeStubProvider) LaunchGame(context.Context, gameprovider.AuthUser, gameprovider.LaunchRequest, string) (*gameprovider.LaunchResponse, error) {
|
|
||||||
return nil, common.NewAppError(http.StatusBadRequest, "unsupported", "unsupported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeStubProvider) CloseGame(context.Context, gameprovider.AuthUser, gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) {
|
|
||||||
return &gameprovider.RoomStateResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type routeLaunchStubProvider struct {
|
|
||||||
key string
|
|
||||||
matchGameID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeLaunchStubProvider) Key() string { return p.key }
|
|
||||||
|
|
||||||
func (p *routeLaunchStubProvider) DisplayName() string { return p.key }
|
|
||||||
|
|
||||||
func (p *routeLaunchStubProvider) SupportsLaunch(_ context.Context, _ gameprovider.AuthUser, req gameprovider.LaunchRequest) (bool, error) {
|
|
||||||
return p.matchGameID != "" && req.GameID == p.matchGameID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeLaunchStubProvider) ListShortcutGames(context.Context, gameprovider.AuthUser, string) ([]gameprovider.RoomGameListItem, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeLaunchStubProvider) ListRoomGames(context.Context, gameprovider.AuthUser, string, string) (*gameprovider.RoomGameListResponse, error) {
|
|
||||||
return &gameprovider.RoomGameListResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeLaunchStubProvider) GetRoomState(context.Context, gameprovider.AuthUser, string) (*gameprovider.RoomStateResponse, error) {
|
|
||||||
return &gameprovider.RoomStateResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeLaunchStubProvider) LaunchGame(_ context.Context, _ gameprovider.AuthUser, req gameprovider.LaunchRequest, _ string) (*gameprovider.LaunchResponse, error) {
|
|
||||||
if p.matchGameID == "" || req.GameID != p.matchGameID {
|
|
||||||
return nil, common.NewAppError(http.StatusNotFound, "game_not_found", "game not found")
|
|
||||||
}
|
|
||||||
return &gameprovider.LaunchResponse{
|
|
||||||
GameSessionID: "hg-session",
|
|
||||||
Provider: p.key,
|
|
||||||
GameID: req.GameID,
|
|
||||||
ProviderGameID: "1",
|
|
||||||
Entry: gameprovider.LaunchEntry{LaunchMode: "H5_REMOTE"},
|
|
||||||
RoomState: gameprovider.RoomStateResponse{
|
|
||||||
RoomID: req.RoomID,
|
|
||||||
State: "PLAYING",
|
|
||||||
Provider: p.key,
|
|
||||||
GameSessionID: "hg-session",
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *routeLaunchStubProvider) CloseGame(context.Context, gameprovider.AuthUser, gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) {
|
|
||||||
return &gameprovider.RoomStateResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,115 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"chatapp3-golang/internal/service/hotgame"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// registerHotgameRoutes 注册热游后台管理类 Go 接口。
|
|
||||||
func registerHotgameRoutes(engine *gin.Engine, javaClient authGateway, service *hotgame.Service) {
|
|
||||||
if service == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
consoleGroup := engine.Group("/operate/hotgame-game")
|
|
||||||
consoleGroup.Use(consoleAuthMiddleware(javaClient))
|
|
||||||
consoleGroup.GET("/config", func(c *gin.Context) {
|
|
||||||
resp, err := service.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin"), c.Query("profile"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.POST("/config", func(c *gin.Context) {
|
|
||||||
var req hotgame.SaveProviderConfigRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := service.SaveProviderConfig(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.POST("/config/activate", func(c *gin.Context) {
|
|
||||||
var req hotgame.ActivateProviderProfileRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := service.ActivateProviderProfile(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.GET("/catalog/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 := service.PageCatalog(
|
|
||||||
c.Request.Context(),
|
|
||||||
c.Query("sysOrigin"),
|
|
||||||
c.Query("profile"),
|
|
||||||
c.Query("keyword"),
|
|
||||||
cursor,
|
|
||||||
limit,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.POST("/catalog/fetch", func(c *gin.Context) {
|
|
||||||
var req hotgame.SyncCatalogRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := service.FetchAdminCatalog(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.POST("/import-catalog", func(c *gin.Context) {
|
|
||||||
var req hotgame.SyncCatalogRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defaultShowcase := true
|
|
||||||
if req.DefaultShowcase != nil {
|
|
||||||
defaultShowcase = *req.DefaultShowcase
|
|
||||||
}
|
|
||||||
resp, err := service.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs, defaultShowcase)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.POST("/sync", func(c *gin.Context) {
|
|
||||||
var req hotgame.SyncCatalogRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := service.SyncAdminGames(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -37,15 +37,6 @@ func registerManagerCenterRoutes(engine *gin.Engine, javaClient authGateway, ser
|
|||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
|
|
||||||
group.GET("/countries", func(c *gin.Context) {
|
|
||||||
resp, err := service.ListCountries(c.Request.Context(), mustAuthUser(c))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
|
|
||||||
group.GET("/props", func(c *gin.Context) {
|
group.GET("/props", func(c *gin.Context) {
|
||||||
resp, err := service.ListProps(c.Request.Context(), mustAuthUser(c), c.Query("type"))
|
resp, err := service.ListProps(c.Request.Context(), mustAuthUser(c), c.Query("type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -119,18 +110,4 @@ func registerManagerCenterRoutes(engine *gin.Engine, javaClient authGateway, ser
|
|||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
|
|
||||||
group.POST("/users/country", func(c *gin.Context) {
|
|
||||||
var req managercenter.ChangeUserCountryRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, managercenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := service.ChangeUserCountry(c.Request.Context(), mustAuthUser(c), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,9 +112,6 @@ func trimBearer(authorization string) string {
|
|||||||
|
|
||||||
// resolveClientIP 解析请求来源 IP。
|
// resolveClientIP 解析请求来源 IP。
|
||||||
func resolveClientIP(c *gin.Context) string {
|
func resolveClientIP(c *gin.Context) string {
|
||||||
if realIP := strings.TrimSpace(c.GetHeader("X-Real-IP")); realIP != "" {
|
|
||||||
return realIP
|
|
||||||
}
|
|
||||||
if forwarded := strings.TrimSpace(c.GetHeader("X-Forwarded-For")); forwarded != "" {
|
if forwarded := strings.TrimSpace(c.GetHeader("X-Forwarded-For")); forwarded != "" {
|
||||||
parts := strings.Split(forwarded, ",")
|
parts := strings.Split(forwarded, ",")
|
||||||
if len(parts) > 0 {
|
if len(parts) > 0 {
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import (
|
|||||||
"chatapp3-golang/internal/service/gameopen"
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
"chatapp3-golang/internal/service/hostcenter"
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
"chatapp3-golang/internal/service/hotgame"
|
|
||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
"chatapp3-golang/internal/service/lingxian"
|
"chatapp3-golang/internal/service/lingxian"
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
@ -49,12 +48,10 @@ type Services struct {
|
|||||||
Lingxian *lingxian.Service
|
Lingxian *lingxian.Service
|
||||||
GameOpen *gameopen.GameOpenService
|
GameOpen *gameopen.GameOpenService
|
||||||
GameProviders *gameprovider.Registry
|
GameProviders *gameprovider.Registry
|
||||||
GameVisibility *gameprovider.VisibilityPolicy
|
|
||||||
LuckyGift *luckygift.LuckyGiftService
|
LuckyGift *luckygift.LuckyGiftService
|
||||||
ManagerCenter *managercenter.Service
|
ManagerCenter *managercenter.Service
|
||||||
PropsStore *propsstore.Service
|
PropsStore *propsstore.Service
|
||||||
HostCenter *hostcenter.Service
|
HostCenter *hostcenter.Service
|
||||||
Hotgame *hotgame.Service
|
|
||||||
RechargeAgency *rechargeagency.Service
|
RechargeAgency *rechargeagency.Service
|
||||||
RechargeReward *rechargereward.Service
|
RechargeReward *rechargereward.Service
|
||||||
RegionIMGroup *regionimgroup.Service
|
RegionIMGroup *regionimgroup.Service
|
||||||
@ -103,10 +100,9 @@ func NewRouter(
|
|||||||
registerRoomTurnoverRewardRoutes(engine, javaClient, services.RoomTurnoverReward)
|
registerRoomTurnoverRewardRoutes(engine, javaClient, services.RoomTurnoverReward)
|
||||||
registerHostCenterRoutes(engine, services.HostCenter)
|
registerHostCenterRoutes(engine, services.HostCenter)
|
||||||
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
||||||
registerGameProviderRoutes(engine, javaClient, services.GameProviders, services.GameVisibility)
|
registerGameProviderRoutes(engine, javaClient, services.GameProviders)
|
||||||
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
||||||
registerLingxianRoutes(engine, javaClient, services.Lingxian)
|
registerLingxianRoutes(engine, javaClient, services.Lingxian)
|
||||||
registerHotgameRoutes(engine, javaClient, services.Hotgame)
|
|
||||||
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
||||||
registerManagerCenterRoutes(engine, javaClient, services.ManagerCenter)
|
registerManagerCenterRoutes(engine, javaClient, services.ManagerCenter)
|
||||||
registerPropsStoreRoutes(engine, javaClient, services.PropsStore)
|
registerPropsStoreRoutes(engine, javaClient, services.PropsStore)
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
// ListShortcutGames 返回房间里的快捷游戏列表,数量上限为 5 个。
|
// ListShortcutGames 返回房间里的快捷游戏列表,数量上限为 5 个。
|
||||||
func (s *BaishunService) ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error) {
|
func (s *BaishunService) ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error) {
|
||||||
items, err := s.listRoomGames(ctx, user.SysOrigin, user.RegionID, roomID, "")
|
items, err := s.listRoomGames(ctx, user.SysOrigin, roomID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ func (s *BaishunService) ListShortcutGames(ctx context.Context, user AuthUser, r
|
|||||||
|
|
||||||
// ListRoomGames 返回房间可启动的完整游戏列表。
|
// ListRoomGames 返回房间可启动的完整游戏列表。
|
||||||
func (s *BaishunService) ListRoomGames(ctx context.Context, user AuthUser, roomID string, category string) (*RoomGameListResponse, error) {
|
func (s *BaishunService) ListRoomGames(ctx context.Context, user AuthUser, roomID string, category string) (*RoomGameListResponse, error) {
|
||||||
items, err := s.listRoomGames(ctx, user.SysOrigin, user.RegionID, roomID, category)
|
items, err := s.listRoomGames(ctx, user.SysOrigin, roomID, category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ func (s *BaishunService) GetRoomState(ctx context.Context, user AuthUser, roomID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listRoomGames 联表读取房间可启动游戏,并按展示规则排序。
|
// listRoomGames 联表读取房间可启动游戏,并按展示规则排序。
|
||||||
func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, regionID, roomID, category string) ([]RoomGameListItem, error) {
|
func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, category string) ([]RoomGameListItem, error) {
|
||||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
profile, err := s.activeProfile(ctx, sysOrigin)
|
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -102,10 +102,6 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, regionID,
|
|||||||
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("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").
|
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
||||||
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, baishunVendorType)
|
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, baishunVendorType)
|
||||||
if strings.TrimSpace(regionID) != "" {
|
|
||||||
// regions 为空表示全区域;有值时按用户区域 ID 精确匹配,避免土耳其用户看到其他区域游戏。
|
|
||||||
query = query.Where("(COALESCE(TRIM(cfg.regions), '') = '' OR FIND_IN_SET(?, REPLACE(COALESCE(cfg.regions, ''), ' ', '')) > 0)", strings.TrimSpace(regionID))
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), "CHAT_ROOM") {
|
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), "CHAT_ROOM") {
|
||||||
query = query.Where("cfg.category = ?", category)
|
query = query.Where("cfg.category = ?", category)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,387 +0,0 @@
|
|||||||
package gameopen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/service/taskcenter"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
hotgameVendorType = "HOTGAME"
|
|
||||||
hotgameCodeTokenInvalid = 1001
|
|
||||||
hotgameCodeInsufficientBalance = 2001
|
|
||||||
hotgameCodeDuplicateOrder = 3001
|
|
||||||
hotgameCodeBadRequest = 4001
|
|
||||||
hotgameCodeSignatureError = 4002
|
|
||||||
hotgameCodeServerError = 5000
|
|
||||||
)
|
|
||||||
|
|
||||||
// HotgameGetUserInfoRequest 是热游 /getUserInfo 回调入参。
|
|
||||||
type HotgameGetUserInfoRequest struct {
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
UID string `json:"uid"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Sign string `json:"sign"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HotgameUpdateBalanceRequest 是热游 /updateBalance 回调入参;roundId 文档是 int,所以这里兼容数字和字符串。
|
|
||||||
type HotgameUpdateBalanceRequest struct {
|
|
||||||
OrderID string `json:"orderId"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
RoundID any `json:"roundId"`
|
|
||||||
UID string `json:"uid"`
|
|
||||||
Coin int64 `json:"coin"`
|
|
||||||
Type int `json:"type"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Sign string `json:"sign"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleHotgameGetUserInfo 按热游协议查询玩家资料和余额。
|
|
||||||
func (s *GameOpenService) HandleHotgameGetUserInfo(ctx context.Context, req HotgameGetUserInfoRequest, rawJSON string) CallbackResponse {
|
|
||||||
if err := s.validateHotgameGetUserInfoRequest(req); err != nil {
|
|
||||||
resp := failResponse(hotgameCodeBadRequest, err.Error())
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, "", req.toQueryUserRequest(), nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
appKey := s.resolveCallbackAppKeyForVendor(ctx, hotgameVendorType, req.GameID, req.Token)
|
|
||||||
if strings.TrimSpace(appKey) == "" {
|
|
||||||
resp := failResponse(hotgameCodeBadRequest, "app key is blank")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, "", req.toQueryUserRequest(), nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
if !verifySign(req.Sign, buildHotgameGetUserInfoSign(req, appKey)) {
|
|
||||||
resp := failResponse(hotgameCodeSignatureError, "signature error")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, "", req.toQueryUserRequest(), nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Token = normalizeCallbackToken(req.Token)
|
|
||||||
credential, err := s.java.AuthenticateToken(ctx, req.Token)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(hotgameCodeTokenInvalid, "token invalid")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, "", req.toQueryUserRequest(), nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
resolved, err := s.resolveUserWithToken(ctx, credential, req.UID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(hotgameCodeTokenInvalid, "uid token mismatch")
|
|
||||||
userID := int64(credential.UserID)
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, credential.SysOrigin, req.toQueryUserRequest(), &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
userID := resolved.userID
|
|
||||||
|
|
||||||
profile := resolved.profile
|
|
||||||
if int64(profile.ID) == 0 {
|
|
||||||
profile, err = s.java.GetUserProfile(ctx, userID)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(hotgameCodeServerError, "profile load failed")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, credential.SysOrigin, req.toQueryUserRequest(), &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
balance, err := s.readUserBalance(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(hotgameCodeServerError, "balance load failed")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, credential.SysOrigin, req.toQueryUserRequest(), &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := successResponse(map[string]any{
|
|
||||||
"uid": req.UID,
|
|
||||||
"nickname": profile.UserNickname,
|
|
||||||
"avatar": profile.UserAvatar,
|
|
||||||
"coin": balance,
|
|
||||||
"vipLevel": 0,
|
|
||||||
})
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-get-user-info", rawJSON, resp, credential.SysOrigin, req.toQueryUserRequest(), &userID, gameOpenLogStatusSuccess)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleHotgameUpdateBalance 按热游协议处理实时扣加币,并用 orderId 做钱包事件幂等。
|
|
||||||
func (s *GameOpenService) HandleHotgameUpdateBalance(ctx context.Context, req HotgameUpdateBalanceRequest, rawJSON string) CallbackResponse {
|
|
||||||
updateReq := req.toUpdateCoinRequest()
|
|
||||||
if err := s.validateHotgameUpdateBalanceRequest(req); err != nil {
|
|
||||||
resp := failResponse(hotgameCodeBadRequest, err.Error())
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, "", updateReq, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
appKey := s.resolveCallbackAppKeyForVendor(ctx, hotgameVendorType, req.GameID, req.Token)
|
|
||||||
if strings.TrimSpace(appKey) == "" {
|
|
||||||
resp := failResponse(hotgameCodeBadRequest, "app key is blank")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, "", updateReq, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
if !verifySign(req.Sign, buildHotgameUpdateBalanceSign(req, appKey)) {
|
|
||||||
resp := failResponse(hotgameCodeSignatureError, "signature error")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, "", updateReq, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Token = normalizeCallbackToken(req.Token)
|
|
||||||
updateReq.Token = req.Token
|
|
||||||
credential, err := s.java.AuthenticateToken(ctx, req.Token)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(hotgameCodeTokenInvalid, "token invalid")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, "", updateReq, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
resolved, err := s.resolveUserWithToken(ctx, credential, req.UID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(hotgameCodeTokenInvalid, "uid token mismatch")
|
|
||||||
userID := int64(credential.UserID)
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
userID := resolved.userID
|
|
||||||
|
|
||||||
eventID := "GAME_HOTGAME:" + strings.TrimSpace(req.OrderID)
|
|
||||||
exists, err := s.java.ExistsGoldEvent(ctx, eventID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(hotgameCodeServerError, "event check failed")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
resp := failResponse(hotgameCodeDuplicateOrder, "duplicate orderId")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{
|
|
||||||
ReceiptType: receiptTypeFromType(req.Type),
|
|
||||||
UserID: userID,
|
|
||||||
SysOrigin: credential.SysOrigin,
|
|
||||||
EventID: eventID,
|
|
||||||
Remark: fmt.Sprintf("hotgame=%s type=%d round=%s", req.GameID, req.Type, updateReq.RoundID),
|
|
||||||
Amount: integration.NewPennyAmountPayloadFromDollar(req.Coin),
|
|
||||||
CloseDelayAsset: false,
|
|
||||||
OpUserType: "APP",
|
|
||||||
CustomizeOrigin: "GAME_HOTGAME_" + strings.TrimSpace(req.GameID),
|
|
||||||
CustomizeOriginDesc: "HOTGAME[" + strings.TrimSpace(req.GameID) + "]",
|
|
||||||
}); err != nil {
|
|
||||||
// 热游文档要求扣款失败必须返回失败码;支出失败统一归到余额不足类,防止游戏继续开奖。
|
|
||||||
code := hotgameCodeServerError
|
|
||||||
if req.Type == 1 || looksLikeInsufficientBalance(err) {
|
|
||||||
code = hotgameCodeInsufficientBalance
|
|
||||||
}
|
|
||||||
resp := failResponse(code, err.Error())
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
balance, err := s.readUserBalance(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(hotgameCodeServerError, "balance load failed")
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := successResponse(map[string]any{"coin": balance, "responseId": eventID})
|
|
||||||
s.saveCallbackLog(ctx, "hotgame-update-balance", rawJSON, resp, credential.SysOrigin, updateReq, &userID, gameOpenLogStatusSuccess)
|
|
||||||
s.reportHotgameConsumeTaskEvent(ctx, credential.SysOrigin, userID, updateReq)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (req HotgameGetUserInfoRequest) toQueryUserRequest() QueryUserRequest {
|
|
||||||
return QueryUserRequest{
|
|
||||||
GameID: req.GameID,
|
|
||||||
UID: req.UID,
|
|
||||||
Token: req.Token,
|
|
||||||
Sign: req.Sign,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (req HotgameUpdateBalanceRequest) toUpdateCoinRequest() UpdateCoinRequest {
|
|
||||||
return UpdateCoinRequest{
|
|
||||||
OrderID: req.OrderID,
|
|
||||||
GameID: req.GameID,
|
|
||||||
RoundID: hotgameRoundIDString(req.RoundID),
|
|
||||||
UID: req.UID,
|
|
||||||
Coin: req.Coin,
|
|
||||||
Type: req.Type,
|
|
||||||
Token: req.Token,
|
|
||||||
Sign: req.Sign,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) validateHotgameGetUserInfoRequest(req HotgameGetUserInfoRequest) error {
|
|
||||||
switch {
|
|
||||||
case strings.TrimSpace(req.GameID) == "":
|
|
||||||
return newBadRequest("gameId is required")
|
|
||||||
case strings.TrimSpace(req.UID) == "":
|
|
||||||
return newBadRequest("uid is required")
|
|
||||||
case strings.TrimSpace(req.Token) == "":
|
|
||||||
return newBadRequest("token is required")
|
|
||||||
case strings.TrimSpace(req.Sign) == "":
|
|
||||||
return newBadRequest("sign is required")
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) validateHotgameUpdateBalanceRequest(req HotgameUpdateBalanceRequest) error {
|
|
||||||
switch {
|
|
||||||
case strings.TrimSpace(req.OrderID) == "":
|
|
||||||
return newBadRequest("orderId is required")
|
|
||||||
case strings.TrimSpace(req.GameID) == "":
|
|
||||||
return newBadRequest("gameId is required")
|
|
||||||
case strings.TrimSpace(hotgameRoundIDString(req.RoundID)) == "":
|
|
||||||
return newBadRequest("roundId is required")
|
|
||||||
case strings.TrimSpace(req.UID) == "":
|
|
||||||
return newBadRequest("uid is required")
|
|
||||||
case req.Coin <= 0:
|
|
||||||
return newBadRequest("coin is required")
|
|
||||||
case req.Type != 1 && req.Type != 2:
|
|
||||||
return newBadRequest("type must be 1 or 2")
|
|
||||||
case strings.TrimSpace(req.Token) == "":
|
|
||||||
return newBadRequest("token is required")
|
|
||||||
case strings.TrimSpace(req.Sign) == "":
|
|
||||||
return newBadRequest("sign is required")
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildHotgameGetUserInfoSign(req HotgameGetUserInfoRequest, key string) string {
|
|
||||||
return md5Hex(req.GameID + req.UID + req.Token + key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildHotgameUpdateBalanceSign(req HotgameUpdateBalanceRequest, key string) string {
|
|
||||||
return md5Hex(
|
|
||||||
req.OrderID +
|
|
||||||
req.GameID +
|
|
||||||
hotgameRoundIDString(req.RoundID) +
|
|
||||||
req.UID +
|
|
||||||
fmt.Sprintf("%d", req.Coin) +
|
|
||||||
fmt.Sprintf("%d", req.Type) +
|
|
||||||
req.Token +
|
|
||||||
key,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hotgameRoundIDString(value any) string {
|
|
||||||
switch typed := value.(type) {
|
|
||||||
case nil:
|
|
||||||
return ""
|
|
||||||
case string:
|
|
||||||
return strings.TrimSpace(typed)
|
|
||||||
case json.Number:
|
|
||||||
return strings.TrimSpace(typed.String())
|
|
||||||
case float64:
|
|
||||||
if typed == float64(int64(typed)) {
|
|
||||||
return fmt.Sprintf("%d", int64(typed))
|
|
||||||
}
|
|
||||||
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", typed), "0"), ".")
|
|
||||||
default:
|
|
||||||
return strings.TrimSpace(fmt.Sprint(typed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) resolveCallbackAppKeyForVendor(ctx context.Context, vendorType string, gameID, token string) string {
|
|
||||||
if s != nil && s.repo.DB != nil {
|
|
||||||
if sysOrigin := callbackTokenSysOrigin(token); sysOrigin != "" {
|
|
||||||
if appKey := s.resolveActiveProviderAppKey(ctx, vendorType, sysOrigin); appKey != "" {
|
|
||||||
return appKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if appKey := s.resolveProviderAppKeyByGame(ctx, vendorType, gameID); appKey != "" {
|
|
||||||
return appKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(s.cfg.GameOpen.AppKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) resolveProviderAppKeyByGame(ctx context.Context, vendorType string, gameID string) string {
|
|
||||||
gameID = strings.TrimSpace(gameID)
|
|
||||||
if gameID == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
switch strings.ToUpper(strings.TrimSpace(vendorType)) {
|
|
||||||
case "LINGXIAN":
|
|
||||||
return s.resolveLingxianAppKeyByGame(ctx, gameID)
|
|
||||||
case hotgameVendorType:
|
|
||||||
var row struct {
|
|
||||||
AppKey string
|
|
||||||
}
|
|
||||||
err := s.repo.DB.WithContext(ctx).
|
|
||||||
Table("hotgame_provider_config AS cfg").
|
|
||||||
Select("cfg.app_key AS app_key").
|
|
||||||
Joins("JOIN sys_game_list_vendor_ext ext ON ext.sys_origin = cfg.sys_origin AND ext.profile = cfg.profile AND ext.vendor_type = ? AND ext.enabled = ?", hotgameVendorType, true).
|
|
||||||
Where("cfg.active = ? AND ext.vendor_game_id = ?", true, gameID).
|
|
||||||
Order("cfg.update_time DESC").
|
|
||||||
Limit(1).
|
|
||||||
Scan(&row).Error
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(row.AppKey)
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) resolveActiveProviderAppKey(ctx context.Context, vendorType string, sysOrigin string) string {
|
|
||||||
switch strings.ToUpper(strings.TrimSpace(vendorType)) {
|
|
||||||
case "LINGXIAN":
|
|
||||||
return s.resolveActiveLingxianAppKey(ctx, sysOrigin)
|
|
||||||
case hotgameVendorType:
|
|
||||||
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
|
||||||
if sysOrigin == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
var row model.HotgameProviderConfig
|
|
||||||
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 ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(row.AppKey)
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func looksLikeInsufficientBalance(err error) bool {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
message := strings.ToLower(err.Error())
|
|
||||||
return strings.Contains(message, "insufficient") ||
|
|
||||||
strings.Contains(message, "balance not made") ||
|
|
||||||
strings.Contains(message, "余额不足")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) reportHotgameConsumeTaskEvent(ctx context.Context, sysOrigin string, userID int64, req UpdateCoinRequest) {
|
|
||||||
if s.taskReporter == nil || receiptTypeFromType(req.Type) != "EXPENDITURE" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
deltaValue := req.Coin
|
|
||||||
if deltaValue < 0 {
|
|
||||||
deltaValue = -deltaValue
|
|
||||||
}
|
|
||||||
if deltaValue <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
payload := map[string]any{
|
|
||||||
"provider": hotgameVendorType,
|
|
||||||
"orderId": strings.TrimSpace(req.OrderID),
|
|
||||||
"gameId": strings.TrimSpace(req.GameID),
|
|
||||||
"roundId": strings.TrimSpace(req.RoundID),
|
|
||||||
}
|
|
||||||
if err := s.taskReporter.ReportSimpleEvent(ctx, sysOrigin, "GAME_HOTGAME_CONSUME:"+strings.TrimSpace(req.OrderID), taskcenter.EventTypeGameConsumeGold, userID, deltaValue, payload); err != nil {
|
|
||||||
log.Printf("report hotgame task event failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -219,7 +219,20 @@ func (s *GameOpenService) GetIntegrationInfo(ctx context.Context, publicBaseURL
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *GameOpenService) resolveCallbackAppKey(ctx context.Context, gameID, token string) string {
|
func (s *GameOpenService) resolveCallbackAppKey(ctx context.Context, gameID, token string) string {
|
||||||
return s.resolveCallbackAppKeyForVendor(ctx, "LINGXIAN", gameID, token)
|
if s != nil && s.repo.DB != nil {
|
||||||
|
if sysOrigin := callbackTokenSysOrigin(token); sysOrigin != "" {
|
||||||
|
if appKey := s.resolveActiveLingxianAppKey(ctx, sysOrigin); appKey != "" {
|
||||||
|
return appKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if appKey := s.resolveLingxianAppKeyByGame(ctx, gameID); appKey != "" {
|
||||||
|
return appKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(s.cfg.GameOpen.AppKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GameOpenService) resolveLingxianAppKeyByGame(ctx context.Context, gameID string) string {
|
func (s *GameOpenService) resolveLingxianAppKeyByGame(ctx context.Context, gameID string) string {
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"chatapp3-golang/internal/integration"
|
"chatapp3-golang/internal/integration"
|
||||||
"chatapp3-golang/internal/model"
|
"chatapp3-golang/internal/model"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -21,7 +20,6 @@ type stubGateway struct {
|
|||||||
accountMap map[string]integration.UserProfile
|
accountMap map[string]integration.UserProfile
|
||||||
balance int64
|
balance int64
|
||||||
exists bool
|
exists bool
|
||||||
changeErr error
|
|
||||||
cmd integration.GoldReceiptCommand
|
cmd integration.GoldReceiptCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +72,7 @@ func (s *stubGateway) ExistsGoldEvent(context.Context, string) (bool, error) {
|
|||||||
|
|
||||||
func (s *stubGateway) ChangeGoldBalance(_ context.Context, cmd integration.GoldReceiptCommand) error {
|
func (s *stubGateway) ChangeGoldBalance(_ context.Context, cmd integration.GoldReceiptCommand) error {
|
||||||
s.cmd = cmd
|
s.cmd = cmd
|
||||||
return s.changeErr
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleQueryUser(t *testing.T) {
|
func TestHandleQueryUser(t *testing.T) {
|
||||||
@ -353,97 +351,6 @@ func TestCallbacksUseLingxianProviderConfigAppKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHotgameGetUserInfoUsesHotgameSign(t *testing.T) {
|
|
||||||
gateway := &stubGateway{
|
|
||||||
credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"},
|
|
||||||
profile: integration.UserProfile{
|
|
||||||
ID: 1234567,
|
|
||||||
Account: "1234567",
|
|
||||||
UserNickname: "tester",
|
|
||||||
UserAvatar: "https://example.com/avatar.png",
|
|
||||||
},
|
|
||||||
balance: 88,
|
|
||||||
}
|
|
||||||
service := NewGameOpenService(config.Config{
|
|
||||||
GameOpen: config.GameOpenConfig{AppKey: "hotgame-key"},
|
|
||||||
}, nil, gateway)
|
|
||||||
req := HotgameGetUserInfoRequest{
|
|
||||||
GameID: "1",
|
|
||||||
UID: "1234567",
|
|
||||||
Token: "token",
|
|
||||||
}
|
|
||||||
req.Sign = buildHotgameGetUserInfoSign(req, "hotgame-key")
|
|
||||||
|
|
||||||
resp := service.HandleHotgameGetUserInfo(context.Background(), req, "{}")
|
|
||||||
if resp.ErrorCode != 0 {
|
|
||||||
t.Fatalf("HandleHotgameGetUserInfo() error = %+v", resp)
|
|
||||||
}
|
|
||||||
data, ok := resp.Data.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("HandleHotgameGetUserInfo() data type = %T", resp.Data)
|
|
||||||
}
|
|
||||||
if got := data["vipLevel"]; got != 0 {
|
|
||||||
t.Fatalf("HandleHotgameGetUserInfo() vipLevel = %v, want 0", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHotgameUpdateBalanceAcceptsNumericRoundIDAndDuplicateReturns3001(t *testing.T) {
|
|
||||||
gateway := &stubGateway{
|
|
||||||
credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"},
|
|
||||||
profile: integration.UserProfile{ID: 1234567, Account: "1234567"},
|
|
||||||
balance: 99,
|
|
||||||
exists: true,
|
|
||||||
}
|
|
||||||
service := NewGameOpenService(config.Config{
|
|
||||||
GameOpen: config.GameOpenConfig{AppKey: "hotgame-key"},
|
|
||||||
}, nil, gateway)
|
|
||||||
req := HotgameUpdateBalanceRequest{
|
|
||||||
OrderID: "order-1",
|
|
||||||
GameID: "1",
|
|
||||||
RoundID: float64(3199),
|
|
||||||
UID: "1234567",
|
|
||||||
Coin: 15,
|
|
||||||
Type: 1,
|
|
||||||
Token: "token",
|
|
||||||
}
|
|
||||||
req.Sign = buildHotgameUpdateBalanceSign(req, "hotgame-key")
|
|
||||||
|
|
||||||
resp := service.HandleHotgameUpdateBalance(context.Background(), req, "{}")
|
|
||||||
if resp.ErrorCode != hotgameCodeDuplicateOrder {
|
|
||||||
t.Fatalf("HandleHotgameUpdateBalance() errorCode = %d, want %d", resp.ErrorCode, hotgameCodeDuplicateOrder)
|
|
||||||
}
|
|
||||||
if gateway.cmd.EventID != "" {
|
|
||||||
t.Fatalf("HandleHotgameUpdateBalance() should not change duplicate order, got %q", gateway.cmd.EventID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHotgameUpdateBalanceDeductFailureReturns2001(t *testing.T) {
|
|
||||||
gateway := &stubGateway{
|
|
||||||
credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"},
|
|
||||||
profile: integration.UserProfile{ID: 1234567, Account: "1234567"},
|
|
||||||
balance: 99,
|
|
||||||
changeErr: errors.New("insufficient_balance"),
|
|
||||||
}
|
|
||||||
service := NewGameOpenService(config.Config{
|
|
||||||
GameOpen: config.GameOpenConfig{AppKey: "hotgame-key"},
|
|
||||||
}, nil, gateway)
|
|
||||||
req := HotgameUpdateBalanceRequest{
|
|
||||||
OrderID: "order-1",
|
|
||||||
GameID: "1",
|
|
||||||
RoundID: float64(3199),
|
|
||||||
UID: "1234567",
|
|
||||||
Coin: 15,
|
|
||||||
Type: 1,
|
|
||||||
Token: "token",
|
|
||||||
}
|
|
||||||
req.Sign = buildHotgameUpdateBalanceSign(req, "hotgame-key")
|
|
||||||
|
|
||||||
resp := service.HandleHotgameUpdateBalance(context.Background(), req, "{}")
|
|
||||||
if resp.ErrorCode != hotgameCodeInsufficientBalance {
|
|
||||||
t.Fatalf("HandleHotgameUpdateBalance() errorCode = %d, want %d", resp.ErrorCode, hotgameCodeInsufficientBalance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGameOpenTestDB(t *testing.T) *gorm.DB {
|
func newGameOpenTestDB(t *testing.T) *gorm.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||||
@ -452,7 +359,6 @@ func newGameOpenTestDB(t *testing.T) *gorm.DB {
|
|||||||
}
|
}
|
||||||
if err := db.AutoMigrate(
|
if err := db.AutoMigrate(
|
||||||
&model.LingxianProviderConfig{},
|
&model.LingxianProviderConfig{},
|
||||||
&model.HotgameProviderConfig{},
|
|
||||||
&model.SysGameListVendorExt{},
|
&model.SysGameListVendorExt{},
|
||||||
&model.GameOpenCallbackLog{},
|
&model.GameOpenCallbackLog{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|||||||
@ -14,10 +14,6 @@ type Registry struct {
|
|||||||
ordered []Provider
|
ordered []Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
var emptyGameListUserIDs = map[int64]struct{}{
|
|
||||||
2061394478798794754: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRegistry 创建厂商注册表。
|
// NewRegistry 创建厂商注册表。
|
||||||
func NewRegistry(providers ...Provider) *Registry {
|
func NewRegistry(providers ...Provider) *Registry {
|
||||||
r := &Registry{
|
r := &Registry{
|
||||||
@ -79,9 +75,6 @@ func (r *Registry) List() ProviderListResponse {
|
|||||||
|
|
||||||
// ListShortcutGames 返回聚合后的快捷游戏列表。
|
// ListShortcutGames 返回聚合后的快捷游戏列表。
|
||||||
func (r *Registry) ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error) {
|
func (r *Registry) ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error) {
|
||||||
if IsEmptyGameListUser(user.UserID) {
|
|
||||||
return []RoomGameListItem{}, nil
|
|
||||||
}
|
|
||||||
if r == nil || len(r.ordered) == 0 {
|
if r == nil || len(r.ordered) == 0 {
|
||||||
return []RoomGameListItem{}, nil
|
return []RoomGameListItem{}, nil
|
||||||
}
|
}
|
||||||
@ -103,9 +96,6 @@ func (r *Registry) ListShortcutGames(ctx context.Context, user AuthUser, roomID
|
|||||||
|
|
||||||
// ListRoomGames 返回聚合后的房间游戏列表。
|
// ListRoomGames 返回聚合后的房间游戏列表。
|
||||||
func (r *Registry) ListRoomGames(ctx context.Context, user AuthUser, roomID, category string) (*RoomGameListResponse, error) {
|
func (r *Registry) ListRoomGames(ctx context.Context, user AuthUser, roomID, category string) (*RoomGameListResponse, error) {
|
||||||
if IsEmptyGameListUser(user.UserID) {
|
|
||||||
return &RoomGameListResponse{Items: []RoomGameListItem{}}, nil
|
|
||||||
}
|
|
||||||
if r == nil || len(r.ordered) == 0 {
|
if r == nil || len(r.ordered) == 0 {
|
||||||
return &RoomGameListResponse{Items: []RoomGameListItem{}}, nil
|
return &RoomGameListResponse{Items: []RoomGameListItem{}}, nil
|
||||||
}
|
}
|
||||||
@ -124,11 +114,6 @@ func (r *Registry) ListRoomGames(ctx context.Context, user AuthUser, roomID, cat
|
|||||||
return &RoomGameListResponse{Items: items}, nil
|
return &RoomGameListResponse{Items: items}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsEmptyGameListUser(userID int64) bool {
|
|
||||||
_, ok := emptyGameListUserIDs[userID]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveByLaunchRequest 根据启动请求解析内部对应的厂商。
|
// ResolveByLaunchRequest 根据启动请求解析内部对应的厂商。
|
||||||
func (r *Registry) ResolveByLaunchRequest(ctx context.Context, user AuthUser, req LaunchRequest) (Provider, error) {
|
func (r *Registry) ResolveByLaunchRequest(ctx context.Context, user AuthUser, req LaunchRequest) (Provider, error) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
@ -251,8 +236,6 @@ func normalizeProviderAlias(key string) string {
|
|||||||
switch normalizeKey(key) {
|
switch normalizeKey(key) {
|
||||||
case "LEADER":
|
case "LEADER":
|
||||||
return "LINGXIAN"
|
return "LINGXIAN"
|
||||||
case "HOTGAME", "HOT_GAME", "REYOU", "LALU":
|
|
||||||
return "HOTGAME"
|
|
||||||
default:
|
default:
|
||||||
return normalizeKey(key)
|
return normalizeKey(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,20 +80,6 @@ func TestRegistryResolveMapsLeaderAliasToLingxian(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegistryResolveMapsHotgameAliases(t *testing.T) {
|
|
||||||
registry := NewRegistry(&stubProvider{key: "HOTGAME", name: "热游"})
|
|
||||||
|
|
||||||
for _, alias := range []string{"HOTGAME", "hot_game", "REYOU", "lalu"} {
|
|
||||||
provider, err := registry.Resolve(alias)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Resolve(%q) error = %v", alias, err)
|
|
||||||
}
|
|
||||||
if provider.Key() != "HOTGAME" {
|
|
||||||
t.Fatalf("Resolve(%q) key = %q, want HOTGAME", alias, provider.Key())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegistryListKeepsRegistrationOrder(t *testing.T) {
|
func TestRegistryListKeepsRegistrationOrder(t *testing.T) {
|
||||||
registry := NewRegistry(
|
registry := NewRegistry(
|
||||||
&stubProvider{key: "BAISHUN", name: "百顺"},
|
&stubProvider{key: "BAISHUN", name: "百顺"},
|
||||||
@ -139,47 +125,6 @@ func TestRegistryListRoomGamesAggregatesAndSorts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegistryListRoomGamesReturnsEmptyForHardcodedTestUser(t *testing.T) {
|
|
||||||
registry := NewRegistry(&stubProvider{
|
|
||||||
key: "BAISHUN",
|
|
||||||
name: "百顺",
|
|
||||||
room: []RoomGameListItem{{GameID: "a", Sort: 30}},
|
|
||||||
})
|
|
||||||
|
|
||||||
resp, err := registry.ListRoomGames(
|
|
||||||
context.Background(),
|
|
||||||
AuthUser{UserID: 2061394478798794754},
|
|
||||||
"room-1",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListRoomGames() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(resp.Items) != 0 {
|
|
||||||
t.Fatalf("ListRoomGames() items = %d, want 0", len(resp.Items))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegistryListShortcutGamesReturnsEmptyForHardcodedTestUser(t *testing.T) {
|
|
||||||
registry := NewRegistry(&stubProvider{
|
|
||||||
key: "BAISHUN",
|
|
||||||
name: "百顺",
|
|
||||||
shortcut: []RoomGameListItem{{GameID: "a", Sort: 30}},
|
|
||||||
})
|
|
||||||
|
|
||||||
items, err := registry.ListShortcutGames(
|
|
||||||
context.Background(),
|
|
||||||
AuthUser{UserID: 2061394478798794754},
|
|
||||||
"room-1",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListShortcutGames() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(items) != 0 {
|
|
||||||
t.Fatalf("ListShortcutGames() items = %d, want 0", len(items))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegistryLaunchGameResolvesProviderByGameID(t *testing.T) {
|
func TestRegistryLaunchGameResolvesProviderByGameID(t *testing.T) {
|
||||||
baishunProvider := &stubProvider{
|
baishunProvider := &stubProvider{
|
||||||
key: "BAISHUN",
|
key: "BAISHUN",
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
package gameprovider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
)
|
|
||||||
|
|
||||||
const gameListTargetSysOrigin = "LIKEI"
|
|
||||||
|
|
||||||
// VisibilityGateway 聚合游戏列表门禁需要的 Java 用户区域能力。
|
|
||||||
type VisibilityGateway interface {
|
|
||||||
GetUserRegion(ctx context.Context, userID int64, authorization string) (integration.UserRegion, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisibilityResult 返回门禁结果,并把已经查到的区域带回路由继续参与 SQL 过滤。
|
|
||||||
type VisibilityResult struct {
|
|
||||||
Allow bool
|
|
||||||
RegionID string
|
|
||||||
RegionCode string
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisibilityPolicy 判断当前用户是否可以看到游戏列表。
|
|
||||||
type VisibilityPolicy struct {
|
|
||||||
java VisibilityGateway
|
|
||||||
targetSys string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewVisibilityPolicy 创建 LIKEI 游戏列表门禁:土耳其财富等级限制已下线,现在对所有区域直接展示,
|
|
||||||
// 保留 Evaluate 是因为路由仍依赖它查询用户区域给 provider SQL 做 regions 过滤。
|
|
||||||
func NewVisibilityPolicy(java VisibilityGateway) *VisibilityPolicy {
|
|
||||||
return &VisibilityPolicy{
|
|
||||||
java: java,
|
|
||||||
targetSys: gameListTargetSysOrigin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate 给 LIKEI 用户补齐区域信息并放行;非 LIKEI 用户沿用自带区域直接展示。
|
|
||||||
func (p *VisibilityPolicy) Evaluate(ctx context.Context, user AuthUser) (VisibilityResult, error) {
|
|
||||||
if p == nil || !strings.EqualFold(strings.TrimSpace(user.SysOrigin), p.targetSys) {
|
|
||||||
return VisibilityResult{Allow: true, RegionID: user.RegionID, RegionCode: user.RegionCode}, nil
|
|
||||||
}
|
|
||||||
if user.UserID <= 0 || p.java == nil {
|
|
||||||
return VisibilityResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
region, err := p.java.GetUserRegion(ctx, user.UserID, user.Authorization)
|
|
||||||
if err != nil {
|
|
||||||
return VisibilityResult{}, err
|
|
||||||
}
|
|
||||||
return VisibilityResult{
|
|
||||||
Allow: true,
|
|
||||||
RegionID: strings.TrimSpace(region.RegionID),
|
|
||||||
RegionCode: strings.TrimSpace(region.RegionCode),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
package gameprovider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
)
|
|
||||||
|
|
||||||
type stubVisibilityGateway struct {
|
|
||||||
region integration.UserRegion
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s stubVisibilityGateway) GetUserRegion(context.Context, int64, string) (integration.UserRegion, error) {
|
|
||||||
return s.region, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const account1007500UserID int64 = 2054163533368717314
|
|
||||||
|
|
||||||
func TestVisibilityPolicyAllowsAllRegions(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
region integration.UserRegion
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "turkey region is visible without wealth gate",
|
|
||||||
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "south asia region is visible",
|
|
||||||
region: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
policy := NewVisibilityPolicy(stubVisibilityGateway{region: tt.region})
|
|
||||||
|
|
||||||
result, err := policy.Evaluate(context.Background(), AuthUser{UserID: account1007500UserID, SysOrigin: "LIKEI"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Evaluate() error = %v", err)
|
|
||||||
}
|
|
||||||
if !result.Allow {
|
|
||||||
t.Fatalf("Evaluate() allow = false, want true")
|
|
||||||
}
|
|
||||||
if result.RegionID != tt.region.RegionID || result.RegionCode != tt.region.RegionCode {
|
|
||||||
t.Fatalf("Evaluate() region = %#v, want %#v", result, tt.region)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVisibilityPolicySkipsOtherSysOrigin(t *testing.T) {
|
|
||||||
policy := NewVisibilityPolicy(stubVisibilityGateway{
|
|
||||||
region: integration.UserRegion{RegionID: "2046067036517363714", RegionCode: "AR"},
|
|
||||||
})
|
|
||||||
|
|
||||||
result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 1001, SysOrigin: "ASWAT", RegionID: "r-1", RegionCode: "SA"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Evaluate() error = %v", err)
|
|
||||||
}
|
|
||||||
if !result.Allow {
|
|
||||||
t.Fatalf("Evaluate() allow = false, want true")
|
|
||||||
}
|
|
||||||
if result.RegionID != "r-1" || result.RegionCode != "SA" {
|
|
||||||
t.Fatalf("Evaluate() region = %#v, want user-provided region", result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVisibilityPolicyDeniesInvalidUser(t *testing.T) {
|
|
||||||
policy := NewVisibilityPolicy(stubVisibilityGateway{})
|
|
||||||
|
|
||||||
result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 0, SysOrigin: "LIKEI"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Evaluate() error = %v", err)
|
|
||||||
}
|
|
||||||
if result.Allow {
|
|
||||||
t.Fatalf("Evaluate() allow = true, want false for invalid user")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,315 +0,0 @@
|
|||||||
package hotgame
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Key 返回统一厂商标识,和后台游戏来源 HOTGAME 保持一致。
|
|
||||||
func (p *AppProvider) Key() string {
|
|
||||||
return vendorType
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisplayName 返回后台和调试接口展示名。
|
|
||||||
func (p *AppProvider) DisplayName() string {
|
|
||||||
return displayName
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportsLaunch 判断统一启动请求是否命中热游配置。
|
|
||||||
func (p *AppProvider) SupportsLaunch(ctx context.Context, user gameprovider.AuthUser, req gameprovider.LaunchRequest) (bool, error) {
|
|
||||||
_, err := p.service.findGameRow(ctx, user.SysOrigin, req)
|
|
||||||
if err == nil {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
var appErr *common.AppError
|
|
||||||
if errors.As(err, &appErr) && appErr.Code == "game_not_found" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListShortcutGames 返回统一快捷列表,热游沿用完整列表前 5 个。
|
|
||||||
func (p *AppProvider) ListShortcutGames(ctx context.Context, user gameprovider.AuthUser, roomID string) ([]gameprovider.RoomGameListItem, error) {
|
|
||||||
resp, err := p.ListRoomGames(ctx, user, roomID, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(resp.Items) > 5 {
|
|
||||||
resp.Items = resp.Items[:5]
|
|
||||||
}
|
|
||||||
return resp.Items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListRoomGames 返回用户当前区域可见的热游房间游戏列表。
|
|
||||||
func (p *AppProvider) ListRoomGames(ctx context.Context, user gameprovider.AuthUser, roomID, category string) (*gameprovider.RoomGameListResponse, error) {
|
|
||||||
items, err := p.service.listRoomGames(ctx, user.SysOrigin, user.RegionID, category)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &gameprovider.RoomGameListResponse{Items: items}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoomState 热游 H5 不需要 Go 持久化房间状态,统一入口只返回空闲态。
|
|
||||||
func (p *AppProvider) GetRoomState(ctx context.Context, user gameprovider.AuthUser, roomID string) (*gameprovider.RoomStateResponse, error) {
|
|
||||||
return &gameprovider.RoomStateResponse{RoomID: roomID, State: "IDLE", Provider: vendorType}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LaunchGame 生成热游 H5 URL,uid/token/lang 会被替换进热游提供的启动链接。
|
|
||||||
func (p *AppProvider) LaunchGame(ctx context.Context, user gameprovider.AuthUser, req gameprovider.LaunchRequest, clientIP string) (*gameprovider.LaunchResponse, error) {
|
|
||||||
row, err := p.service.findGameRow(ctx, user.SysOrigin, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
runtimeCfg, err := p.service.resolveRuntimeConfigForProfile(ctx, normalizeSysOrigin(user.SysOrigin), row.Profile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
entryURL := buildLaunchURL(row.entryURL(), runtimeCfg, user, req)
|
|
||||||
sessionID := fmt.Sprintf("hg_%d_%s", user.UserID, row.VendorGameID)
|
|
||||||
return &gameprovider.LaunchResponse{
|
|
||||||
ID: row.ConfigID,
|
|
||||||
GameSessionID: sessionID,
|
|
||||||
Provider: vendorType,
|
|
||||||
GameID: row.InternalGameID,
|
|
||||||
ProviderGameID: row.VendorGameID,
|
|
||||||
Entry: gameprovider.LaunchEntry{
|
|
||||||
LaunchMode: defaultIfBlank(row.LaunchMode, launchModeH5),
|
|
||||||
EntryURL: entryURL,
|
|
||||||
PreviewURL: row.previewURL(),
|
|
||||||
DownloadURL: row.entryURL(),
|
|
||||||
PackageVersion: row.packageVersion(),
|
|
||||||
Orientation: row.orientation(),
|
|
||||||
SafeHeight: row.safeHeight(),
|
|
||||||
},
|
|
||||||
LaunchConfig: map[string]any{
|
|
||||||
"uid": launchUID(runtimeCfg, user),
|
|
||||||
"token": launchToken(runtimeCfg, user),
|
|
||||||
"lang": launchLang(req),
|
|
||||||
"gameId": row.VendorGameID,
|
|
||||||
},
|
|
||||||
RoomState: gameprovider.RoomStateResponse{
|
|
||||||
RoomID: req.RoomID,
|
|
||||||
State: "PLAYING",
|
|
||||||
Provider: vendorType,
|
|
||||||
GameSessionID: sessionID,
|
|
||||||
CurrentGameID: row.InternalGameID,
|
|
||||||
CurrentProviderGameID: row.VendorGameID,
|
|
||||||
CurrentGameName: row.name(),
|
|
||||||
CurrentGameCover: row.cover(),
|
|
||||||
HostUserID: user.UserID,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseGame 热游关闭只影响客户端 WebView,Go 侧不保留会话状态。
|
|
||||||
func (p *AppProvider) CloseGame(ctx context.Context, user gameprovider.AuthUser, req gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) {
|
|
||||||
return &gameprovider.RoomStateResponse{RoomID: req.RoomID, State: "IDLE", Provider: vendorType}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) listRoomGames(ctx context.Context, sysOrigin, regionID, category string) ([]gameprovider.RoomGameListItem, error) {
|
|
||||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
||||||
profile, err := s.activeProfile(ctx, sysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var rows []gameRow
|
|
||||||
query := s.baseGameQuery(ctx, sysOrigin, profile).
|
|
||||||
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, vendorType)
|
|
||||||
if strings.TrimSpace(regionID) != "" {
|
|
||||||
// 统一游戏列表按运营配置的区域 ID 过滤;空 regions 表示所有区域都可见。
|
|
||||||
query = query.Where("(COALESCE(TRIM(cfg.regions), '') = '' OR FIND_IN_SET(?, REPLACE(COALESCE(cfg.regions, ''), ' ', '')) > 0)", strings.TrimSpace(regionID))
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), roomCategory) {
|
|
||||||
query = query.Where("cfg.category = ?", category)
|
|
||||||
}
|
|
||||||
if err := query.Order("cfg.sort DESC").Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items := make([]gameprovider.RoomGameListItem, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
items = append(items, row.toListItem())
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) findGameRow(ctx context.Context, sysOrigin string, req gameprovider.LaunchRequest) (gameRow, error) {
|
|
||||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
||||||
profile, err := s.activeProfile(ctx, sysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return gameRow{}, err
|
|
||||||
}
|
|
||||||
var row gameRow
|
|
||||||
query := s.baseGameQuery(ctx, sysOrigin, profile).
|
|
||||||
Where("cfg.sys_origin = ? AND ext.vendor_type = ?", sysOrigin, vendorType)
|
|
||||||
if req.ID > 0 {
|
|
||||||
query = query.Where("cfg.id = ?", req.ID)
|
|
||||||
} else {
|
|
||||||
query = query.Where("cfg.game_id = ? OR ext.vendor_game_id = ?", strings.TrimSpace(req.GameID), strings.TrimSpace(req.GameID))
|
|
||||||
}
|
|
||||||
if err := query.Limit(1).Scan(&row).Error; err != nil {
|
|
||||||
return gameRow{}, err
|
|
||||||
}
|
|
||||||
if row.ConfigID > 0 {
|
|
||||||
return row, nil
|
|
||||||
}
|
|
||||||
return gameRow{}, common.NewAppError(http.StatusNotFound, "game_not_found", "hotgame game config not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) baseGameQuery(ctx context.Context, sysOrigin, profile string) *gorm.DB {
|
|
||||||
return s.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,
|
|
||||||
cfg.cover AS cover,
|
|
||||||
cfg.sort AS sort,
|
|
||||||
cfg.full_screen AS full_screen,
|
|
||||||
ext.vendor_game_id AS vendor_game_id,
|
|
||||||
ext.launch_mode AS launch_mode,
|
|
||||||
ext.package_version AS ext_package_version,
|
|
||||||
ext.preview_url AS ext_preview_url,
|
|
||||||
ext.package_url AS package_url,
|
|
||||||
ext.orientation AS ext_orientation,
|
|
||||||
ext.safe_height AS ext_safe_height,
|
|
||||||
ext.extra_json AS ext_extra_json,
|
|
||||||
cat.name AS catalog_name,
|
|
||||||
cat.cover AS catalog_cover,
|
|
||||||
cat.preview_url AS catalog_preview_url,
|
|
||||||
cat.download_url AS catalog_download_url,
|
|
||||||
cat.package_version AS catalog_package_version,
|
|
||||||
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 AND ext.profile = ?", profile).
|
|
||||||
Joins("LEFT JOIN hotgame_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND cat.vendor_game_id = ext.vendor_game_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r gameRow) toListItem() gameprovider.RoomGameListItem {
|
|
||||||
return gameprovider.RoomGameListItem{
|
|
||||||
ID: r.ConfigID,
|
|
||||||
GameID: r.InternalGameID,
|
|
||||||
GameType: vendorType,
|
|
||||||
Provider: vendorType,
|
|
||||||
ProviderGameID: r.VendorGameID,
|
|
||||||
Name: r.name(),
|
|
||||||
Cover: r.cover(),
|
|
||||||
Category: r.Category,
|
|
||||||
Sort: r.Sort,
|
|
||||||
LaunchMode: defaultIfBlank(r.LaunchMode, launchModeH5),
|
|
||||||
FullScreen: r.FullScreen,
|
|
||||||
GameMode: 3,
|
|
||||||
SafeHeight: r.safeHeight(),
|
|
||||||
Orientation: r.orientation(),
|
|
||||||
PackageVersion: r.packageVersion(),
|
|
||||||
Status: defaultIfBlank(r.CatalogStatus, catalogEnabled),
|
|
||||||
LaunchParams: map[string]any{
|
|
||||||
"gameType": vendorType,
|
|
||||||
"lang": defaultLaunchLang,
|
|
||||||
"screenMode": defaultIfBlank(catalogRawMode(r.ExtExtraJSON), "seven"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r gameRow) name() string {
|
|
||||||
return defaultIfBlank(r.Name, r.CatalogName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r gameRow) cover() string {
|
|
||||||
// 热游的 preview_url 是带用户占位符的 H5 启动地址,不是图片资源;封面为空时直接返回空值,避免客户端把启动页当图片加载。
|
|
||||||
return defaultIfBlank(r.Cover, r.CatalogCover)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r gameRow) previewURL() string {
|
|
||||||
return defaultIfBlank(r.ExtPreviewURL, r.CatalogPreviewURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r gameRow) entryURL() string {
|
|
||||||
return defaultIfBlank(r.PackageURL, defaultIfBlank(r.CatalogDownloadURL, r.ExtPreviewURL))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r gameRow) packageVersion() string {
|
|
||||||
return defaultIfBlank(r.ExtPackageVersion, r.CatalogPackageVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r gameRow) orientation() int {
|
|
||||||
if r.ExtOrientation != nil {
|
|
||||||
return *r.ExtOrientation
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r gameRow) safeHeight() int {
|
|
||||||
return r.ExtSafeHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildLaunchURL(entry string, cfg runtimeConfig, user gameprovider.AuthUser, req gameprovider.LaunchRequest) string {
|
|
||||||
entry = strings.TrimSpace(entry)
|
|
||||||
uid := launchUID(cfg, user)
|
|
||||||
token := launchToken(cfg, user)
|
|
||||||
lang := launchLang(req)
|
|
||||||
replaced := strings.NewReplacer(
|
|
||||||
"{uid}", url.QueryEscape(uid),
|
|
||||||
"{token}", url.QueryEscape(token),
|
|
||||||
"{lang}", url.QueryEscape(lang),
|
|
||||||
).Replace(entry)
|
|
||||||
if replaced != entry {
|
|
||||||
return replaced
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := url.Parse(entry)
|
|
||||||
if err != nil {
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
query := parsed.Query()
|
|
||||||
query.Set("uid", uid)
|
|
||||||
query.Set("token", token)
|
|
||||||
query.Set("lang", lang)
|
|
||||||
parsed.RawQuery = query.Encode()
|
|
||||||
return parsed.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchUID(cfg runtimeConfig, user gameprovider.AuthUser) string {
|
|
||||||
return defaultIfBlank(cfg.TestUID, strconv.FormatInt(user.UserID, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchToken(cfg runtimeConfig, user gameprovider.AuthUser) string {
|
|
||||||
return defaultIfBlank(cfg.TestToken, user.Token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchLang(req gameprovider.LaunchRequest) string {
|
|
||||||
for _, key := range []string{"lang", "language", "locale"} {
|
|
||||||
if req.Params == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if value := strings.TrimSpace(fmt.Sprint(req.Params[key])); value != "" && value != "<nil>" {
|
|
||||||
return normalizeLaunchLang(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultLaunchLang
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeLaunchLang(value string) string {
|
|
||||||
value = strings.ToLower(strings.TrimSpace(value))
|
|
||||||
if strings.Contains(value, "-") {
|
|
||||||
value = strings.SplitN(value, "-", 2)[0]
|
|
||||||
}
|
|
||||||
switch value {
|
|
||||||
case "en", "ar", "id", "tr", "ur", "vi":
|
|
||||||
return value
|
|
||||||
default:
|
|
||||||
return defaultLaunchLang
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,446 +0,0 @@
|
|||||||
package hotgame
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/utils"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultCatalogGames = []defaultCatalogGame{
|
|
||||||
{
|
|
||||||
GameID: "1",
|
|
||||||
Name: "幸运77(Lucky77)",
|
|
||||||
TestURL: "https://hk.hotgame.win/Seven7/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxLCJtYWNoQXJlYSI6Mn0%3D&uid={uid}&token={token}&lang={lang}",
|
|
||||||
ProdURL: "https://cdn-res.hotgame.win/Seven7/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxLCJtYWNoQXJlYSI6Mn0%3D&uid={uid}&token={token}&lang={lang}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
GameID: "13",
|
|
||||||
Name: "海盗捕鱼(PirateFishing)",
|
|
||||||
TestURL: "https://hk.hotgame.win/PirateFishing/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxMywibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
ProdURL: "https://cdn-res.hotgame.win/PirateFishing/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxMywibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
GameID: "19",
|
|
||||||
Name: "FortuneSlot (宝石solt)",
|
|
||||||
TestURL: "https://hk.hotgame.win/FortuneSlot/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxOSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
ProdURL: "https://cdn-res.hotgame.win/FortuneSlot/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxOSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
GameID: "15",
|
|
||||||
Name: "美味摩天轮(Delicious)",
|
|
||||||
TestURL: "https://hk.hotgame.win/Wheel_Delicious/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
ProdURL: "https://cdn-res.hotgame.win/Wheel_Delicious/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
GameID: "20",
|
|
||||||
Name: "水果派对(FruitParty)",
|
|
||||||
TestURL: "https://hk.hotgame.win/FruitParty2/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoyMCwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
ProdURL: "https://cdn-res.hotgame.win/FruitParty2/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoyMCwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
GameID: "16",
|
|
||||||
Name: "太空摩天轮(GreedyStar)",
|
|
||||||
TestURL: "https://hk.hotgame.win/Wheel_OuterSpace/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNiwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
ProdURL: "https://cdn-res.hotgame.win/Wheel_OuterSpace/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNiwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncCatalog 把表格里确认过的热游可用游戏同步到本地目录表。
|
|
||||||
func (s *Service) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
|
||||||
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
|
||||||
profile, err := s.requestProfile(ctx, sysOrigin, req.Profile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := s.requireRuntimeConfigForProfile(ctx, sysOrigin, profile); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
filter := buildVendorGameIDFilter(req.VendorGameIDs)
|
|
||||||
resp := &SyncCatalogResponse{}
|
|
||||||
for _, game := range defaultCatalogGames {
|
|
||||||
vendorGameID := strings.TrimSpace(game.GameID)
|
|
||||||
if vendorGameID == "" {
|
|
||||||
resp.Skipped++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(filter) > 0 {
|
|
||||||
if _, ok := filter[vendorGameID]; !ok {
|
|
||||||
resp.Skipped++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
id, err := utils.NextID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
launchURL := hotgameLaunchURLForProfile(game, profile)
|
|
||||||
record := model.HotgameGameCatalog{
|
|
||||||
ID: id,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
Profile: profile,
|
|
||||||
InternalGameID: internalGameID(vendorGameID),
|
|
||||||
VendorGameID: vendorGameID,
|
|
||||||
Name: game.Name,
|
|
||||||
Cover: "",
|
|
||||||
PreviewURL: launchURL,
|
|
||||||
DownloadURL: launchURL,
|
|
||||||
PackageVersion: "2026",
|
|
||||||
RawJSON: utils.MustJSONString(game, ""),
|
|
||||||
Status: catalogEnabled,
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
}
|
|
||||||
if err := s.db.WithContext(ctx).Clauses(clause.OnConflict{
|
|
||||||
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", "raw_json", "status", "update_time",
|
|
||||||
}),
|
|
||||||
}).Create(&record).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if req.Force {
|
|
||||||
resp.Updated++
|
|
||||||
} else {
|
|
||||||
resp.Inserted++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PageCatalog 返回热游目录,并标记每个目录游戏是否已经进入统一游戏列表。
|
|
||||||
func (s *Service) PageCatalog(ctx context.Context, sysOrigin, profile, keyword string, cursor, limit int) (*CatalogPageResponse, error) {
|
|
||||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
||||||
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cursor = normalizePageCursor(cursor)
|
|
||||||
limit = normalizePageLimit(limit)
|
|
||||||
|
|
||||||
countQuery := s.buildCatalogQuery(ctx, sysOrigin, profile, keyword)
|
|
||||||
var total int64
|
|
||||||
if err := countQuery.Count(&total).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type row struct {
|
|
||||||
VendorGameID string
|
|
||||||
Profile string
|
|
||||||
InternalGameID string
|
|
||||||
Name string
|
|
||||||
Cover string
|
|
||||||
PreviewURL string
|
|
||||||
DownloadURL string
|
|
||||||
PackageVersion string
|
|
||||||
Status string
|
|
||||||
AddedConfigID *int64
|
|
||||||
Showcase *bool
|
|
||||||
UpdateTime string
|
|
||||||
}
|
|
||||||
var rows []row
|
|
||||||
if err := s.buildCatalogQuery(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,
|
|
||||||
cat.preview_url AS preview_url,
|
|
||||||
cat.download_url AS download_url,
|
|
||||||
cat.package_version AS package_version,
|
|
||||||
cat.status AS status,
|
|
||||||
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
|
|
||||||
`).
|
|
||||||
Order("CAST(cat.vendor_game_id AS UNSIGNED) ASC").
|
|
||||||
Limit(limit).
|
|
||||||
Offset((cursor - 1) * limit).
|
|
||||||
Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
items := make([]CatalogItem, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
items = append(items, CatalogItem{
|
|
||||||
VendorGameID: row.VendorGameID,
|
|
||||||
Profile: normalizeProfile(row.Profile),
|
|
||||||
GameID: row.InternalGameID,
|
|
||||||
Name: row.Name,
|
|
||||||
Cover: row.Cover,
|
|
||||||
PreviewURL: row.PreviewURL,
|
|
||||||
DownloadURL: row.DownloadURL,
|
|
||||||
PackageVersion: row.PackageVersion,
|
|
||||||
Status: row.Status,
|
|
||||||
Added: row.AddedConfigID != nil && *row.AddedConfigID > 0,
|
|
||||||
Showcase: row.Showcase != nil && *row.Showcase,
|
|
||||||
UpdateTime: row.UpdateTime,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return &CatalogPageResponse{Cursor: cursor, Limit: limit, Records: items, Total: total}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) buildCatalogQuery(ctx context.Context, sysOrigin, profile, keyword string) *gorm.DB {
|
|
||||||
query := s.db.WithContext(ctx).
|
|
||||||
Table("hotgame_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 = cat.vendor_game_id
|
|
||||||
AND ext.enabled = 1
|
|
||||||
`, vendorType).
|
|
||||||
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 = ?
|
|
||||||
`, vendorType).
|
|
||||||
Where("cat.sys_origin = ? AND cat.profile = ?", sysOrigin, profile)
|
|
||||||
keyword = strings.TrimSpace(keyword)
|
|
||||||
if keyword != "" {
|
|
||||||
like := "%" + keyword + "%"
|
|
||||||
query = query.Where("cat.name LIKE ? OR cat.internal_game_id LIKE ? OR cat.vendor_game_id LIKE ?", like, like, like)
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchAdminCatalog 供后台按钮调用,热游没有远程目录接口,因此读取内置对接表目录。
|
|
||||||
func (s *Service) FetchAdminCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
|
||||||
return s.SyncCatalog(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportCatalogGames 把热游目录导入统一游戏列表,app 侧列表随后会通过统一 provider 返回。
|
|
||||||
func (s *Service) ImportCatalogGames(ctx context.Context, sysOrigin, profile string, vendorGameIDs []string, defaultShowcase ...bool) (*ImportCatalogResponse, error) {
|
|
||||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
||||||
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tx := s.db.WithContext(ctx).Begin()
|
|
||||||
if tx.Error != nil {
|
|
||||||
return nil, tx.Error
|
|
||||||
}
|
|
||||||
resp, err := s.importCatalogGamesTx(tx, sysOrigin, profile, buildVendorGameIDFilter(vendorGameIDs), resolveImportDefaultShowcase(defaultShowcase...))
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.clearJavaGameListCache(ctx)
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncAdminGames 一次完成目录同步和统一游戏列表导入,便于后台首次配置。
|
|
||||||
func (s *Service) SyncAdminGames(ctx context.Context, req SyncCatalogRequest) (*SyncAdminCatalogResponse, error) {
|
|
||||||
syncResp, err := s.SyncCatalog(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.Profile, req.VendorGameIDs, resolveBoolPtrDefault(req.DefaultShowcase, true))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &SyncAdminCatalogResponse{
|
|
||||||
Inserted: syncResp.Inserted,
|
|
||||||
Updated: syncResp.Updated,
|
|
||||||
Skipped: syncResp.Skipped,
|
|
||||||
Existing: importResp.Existing,
|
|
||||||
Imported: importResp.Imported,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) importCatalogGamesTx(tx *gorm.DB, sysOrigin, profile string, filter map[string]struct{}, defaultShowcase bool) (*ImportCatalogResponse, error) {
|
|
||||||
query := tx.Where("sys_origin = ? AND profile = ? AND status = ?", sysOrigin, profile, catalogEnabled)
|
|
||||||
if len(filter) > 0 {
|
|
||||||
ids := make([]string, 0, len(filter))
|
|
||||||
for id := range filter {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
sort.Strings(ids)
|
|
||||||
query = query.Where("vendor_game_id IN ?", ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
var catalogs []model.HotgameGameCatalog
|
|
||||||
if err := query.Order("CAST(vendor_game_id AS UNSIGNED) ASC").Find(&catalogs).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &ImportCatalogResponse{}
|
|
||||||
for _, catalog := range catalogs {
|
|
||||||
vendorGameID := strings.TrimSpace(catalog.VendorGameID)
|
|
||||||
if vendorGameID == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
fullScreen := false
|
|
||||||
var ext model.SysGameListVendorExt
|
|
||||||
err := tx.Where("sys_origin = ? AND profile = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, profile, vendorType, vendorGameID).First(&ext).Error
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err == nil && ext.GameListConfigID > 0 {
|
|
||||||
if err := s.refreshExistingGame(tx, ext.GameListConfigID, catalog, fullScreen); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Existing++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
nextCfgID, idErr := utils.NextID()
|
|
||||||
if idErr != nil {
|
|
||||||
return nil, idErr
|
|
||||||
}
|
|
||||||
cfg := model.SysGameListConfig{
|
|
||||||
ID: nextCfgID,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
GameOrigin: vendorType,
|
|
||||||
GameID: defaultIfBlank(catalog.InternalGameID, vendorGameID),
|
|
||||||
Name: catalog.Name,
|
|
||||||
Category: roomCategory,
|
|
||||||
GameCode: vendorGameID,
|
|
||||||
// 热游启动地址包含 uid/token/lang 占位符且整体很长,不能在缺少封面时回填到 cover;封面为空时交给客户端展示默认占位,真正启动地址只放在扩展表里。
|
|
||||||
Cover: hotgameCatalogCover(catalog),
|
|
||||||
Showcase: defaultShowcase,
|
|
||||||
Sort: parseSort(vendorGameID),
|
|
||||||
FullScreen: fullScreen,
|
|
||||||
ClientOrigin: "COMMON",
|
|
||||||
GameMode: "[3]",
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
Profile: profile,
|
|
||||||
VendorType: vendorType,
|
|
||||||
VendorGameID: vendorGameID,
|
|
||||||
LaunchMode: launchModeH5,
|
|
||||||
PackageVersion: catalog.PackageVersion,
|
|
||||||
PackageURL: catalog.DownloadURL,
|
|
||||||
PreviewURL: catalog.PreviewURL,
|
|
||||||
BridgeSchemaVersion: "1.0",
|
|
||||||
ExtraJSON: `{"launchParams":{"gameType":"HOTGAME","screenMode":"seven"}}`,
|
|
||||||
Enabled: true,
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&newExt).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Imported++
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) refreshExistingGame(tx *gorm.DB, configID int64, catalog model.HotgameGameCatalog, fullScreen bool) error {
|
|
||||||
// 目录链接来自热游正式表格,重新同步时要同时刷新主表展示信息和扩展表启动入口,避免旧 URL 继续下发给 app。
|
|
||||||
if err := tx.Model(&model.SysGameListConfig{}).
|
|
||||||
Where("id = ? AND game_origin = ?", configID, vendorType).
|
|
||||||
Updates(map[string]any{
|
|
||||||
"name": catalog.Name,
|
|
||||||
// 这里和新导入保持一致,只刷新真实封面,避免把 H5 启动链接写入封面列导致 MySQL 长度错误。
|
|
||||||
"cover": hotgameCatalogCover(catalog),
|
|
||||||
"full_screen": fullScreen,
|
|
||||||
}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Model(&model.SysGameListVendorExt{}).
|
|
||||||
Where("game_list_config_id = ? AND vendor_type = ?", configID, vendorType).
|
|
||||||
Updates(map[string]any{
|
|
||||||
"package_version": catalog.PackageVersion,
|
|
||||||
"package_url": catalog.DownloadURL,
|
|
||||||
"preview_url": catalog.PreviewURL,
|
|
||||||
"extra_json": `{"launchParams":{"gameType":"HOTGAME","screenMode":"seven"}}`,
|
|
||||||
"update_time": time.Now(),
|
|
||||||
}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func hotgameLaunchURLForProfile(game defaultCatalogGame, profile string) string {
|
|
||||||
if normalizeProfile(profile) == profileTest {
|
|
||||||
return strings.TrimSpace(game.TestURL)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(game.ProdURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hotgameCatalogCover(catalog model.HotgameGameCatalog) string {
|
|
||||||
return strings.TrimSpace(catalog.Cover)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveImportDefaultShowcase(values ...bool) bool {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return values[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveBoolPtrDefault(value *bool, fallback bool) bool {
|
|
||||||
if value == nil {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return *value
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildVendorGameIDFilter(vendorGameIDs []string) map[string]struct{} {
|
|
||||||
filter := make(map[string]struct{}, len(vendorGameIDs))
|
|
||||||
for _, id := range vendorGameIDs {
|
|
||||||
id = strings.TrimSpace(id)
|
|
||||||
if id != "" {
|
|
||||||
filter[id] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filter
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePageCursor(cursor int) int {
|
|
||||||
if cursor <= 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePageLimit(limit int) int {
|
|
||||||
if limit <= 0 {
|
|
||||||
return 20
|
|
||||||
}
|
|
||||||
if limit > 200 {
|
|
||||||
return 200
|
|
||||||
}
|
|
||||||
return limit
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSort(value string) int64 {
|
|
||||||
var sortValue int64
|
|
||||||
_, _ = fmt.Sscanf(strings.TrimSpace(value), "%d", &sortValue)
|
|
||||||
return sortValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func catalogRawMode(rawJSON string) string {
|
|
||||||
var raw struct {
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
}
|
|
||||||
_ = json.Unmarshal([]byte(strings.TrimSpace(rawJSON)), &raw)
|
|
||||||
return strings.TrimSpace(raw.Mode)
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
package hotgame
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/config"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSyncImportAndListRoomGames(t *testing.T) {
|
|
||||||
db := newHotgameTestDB(t)
|
|
||||||
service := NewService(config.Config{}, db, nil)
|
|
||||||
|
|
||||||
resp, err := service.SyncCatalog(context.Background(), SyncCatalogRequest{
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
Profile: "PROD",
|
|
||||||
VendorGameIDs: []string{"1"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SyncCatalog() error = %v", err)
|
|
||||||
}
|
|
||||||
if resp.Inserted != 1 {
|
|
||||||
t.Fatalf("SyncCatalog() inserted = %d, want 1", resp.Inserted)
|
|
||||||
}
|
|
||||||
|
|
||||||
importResp, err := service.ImportCatalogGames(context.Background(), "LIKEI", "PROD", []string{"1"}, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ImportCatalogGames() error = %v", err)
|
|
||||||
}
|
|
||||||
if importResp.Imported != 1 {
|
|
||||||
t.Fatalf("ImportCatalogGames() imported = %d, want 1", importResp.Imported)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg model.SysGameListConfig
|
|
||||||
if err := db.Where("game_origin = ? AND game_id = ?", vendorType, "hg_1").First(&cfg).Error; err != nil {
|
|
||||||
t.Fatalf("load imported game config: %v", err)
|
|
||||||
}
|
|
||||||
if cfg.Cover != "" {
|
|
||||||
t.Fatalf("imported cover = %q, want blank because launch url must stay out of cover", cfg.Cover)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ext model.SysGameListVendorExt
|
|
||||||
if err := db.Where("game_list_config_id = ? AND vendor_type = ?", cfg.ID, vendorType).First(&ext).Error; err != nil {
|
|
||||||
t.Fatalf("load imported vendor ext: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(ext.PreviewURL, "uid={uid}") || !strings.Contains(ext.PackageURL, "token={token}") {
|
|
||||||
t.Fatalf("vendor ext lost launch url placeholders: preview=%q package=%q", ext.PreviewURL, ext.PackageURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
provider := NewAppProvider(service)
|
|
||||||
list, err := provider.ListRoomGames(context.Background(), gameprovider.AuthUser{UserID: 1234567, SysOrigin: "LIKEI"}, "room-1", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListRoomGames() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(list.Items) != 1 {
|
|
||||||
t.Fatalf("ListRoomGames() items = %d, want 1", len(list.Items))
|
|
||||||
}
|
|
||||||
item := list.Items[0]
|
|
||||||
if item.Provider != vendorType || item.ProviderGameID != "1" || item.GameID != "hg_1" {
|
|
||||||
t.Fatalf("ListRoomGames() item = %+v", item)
|
|
||||||
}
|
|
||||||
if item.Cover != "" {
|
|
||||||
t.Fatalf("ListRoomGames() cover = %q, want blank because hotgame preview url is not an image", item.Cover)
|
|
||||||
}
|
|
||||||
if item.LaunchParams["screenMode"] != "seven" {
|
|
||||||
t.Fatalf("ListRoomGames() screenMode = %v, want seven", item.LaunchParams["screenMode"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLaunchGameReplacesHotgameURLParams(t *testing.T) {
|
|
||||||
db := newHotgameTestDB(t)
|
|
||||||
service := NewService(config.Config{}, db, nil)
|
|
||||||
if _, err := service.SyncAdminGames(context.Background(), SyncCatalogRequest{
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
Profile: "PROD",
|
|
||||||
VendorGameIDs: []string{"1"},
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("SyncAdminGames() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
provider := NewAppProvider(service)
|
|
||||||
resp, err := provider.LaunchGame(
|
|
||||||
context.Background(),
|
|
||||||
gameprovider.AuthUser{UserID: 1234567, SysOrigin: "LIKEI", Token: "token=="},
|
|
||||||
gameprovider.LaunchRequest{RoomID: "room-1", GameID: "hg_1", Params: map[string]any{"lang": "tr-TR"}},
|
|
||||||
"127.0.0.1",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("LaunchGame() error = %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(resp.Entry.EntryURL, "uid=1234567") {
|
|
||||||
t.Fatalf("LaunchGame() url missing uid: %s", resp.Entry.EntryURL)
|
|
||||||
}
|
|
||||||
if !strings.Contains(resp.Entry.EntryURL, "token=token%3D%3D") {
|
|
||||||
t.Fatalf("LaunchGame() url missing escaped token: %s", resp.Entry.EntryURL)
|
|
||||||
}
|
|
||||||
if !strings.Contains(resp.Entry.EntryURL, "lang=tr") {
|
|
||||||
t.Fatalf("LaunchGame() url missing lang: %s", resp.Entry.EntryURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHotgameTestDB(t *testing.T) *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.HotgameProviderConfig{},
|
|
||||||
&model.HotgameGameCatalog{},
|
|
||||||
&model.SysGameListConfig{},
|
|
||||||
&model.SysGameListVendorExt{},
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("migrate sqlite: %v", err)
|
|
||||||
}
|
|
||||||
if err := db.Create(&model.HotgameProviderConfig{
|
|
||||||
ID: 1,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
Profile: "PROD",
|
|
||||||
Active: true,
|
|
||||||
AppKey: "hotgame-key",
|
|
||||||
CreateTime: time.Now(),
|
|
||||||
UpdateTime: time.Now(),
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create provider config: %v", err)
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
package hotgame
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/utils"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Service) defaultRuntimeConfig(sysOrigin string) runtimeConfig {
|
|
||||||
return runtimeConfig{
|
|
||||||
SysOrigin: normalizeSysOrigin(sysOrigin),
|
|
||||||
Profile: profileProd,
|
|
||||||
AppKey: strings.TrimSpace(s.cfg.Hotgame.AppKey),
|
|
||||||
CallbackBaseURL: strings.TrimSpace(s.cfg.Hotgame.CallbackBaseURL),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) activeProfile(ctx context.Context, sysOrigin string) (string, error) {
|
|
||||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
||||||
var row model.HotgameProviderConfig
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Where("sys_origin = ? AND active = ?", sysOrigin, true).
|
|
||||||
Order("update_time DESC").
|
|
||||||
Limit(1).
|
|
||||||
First(&row).Error
|
|
||||||
if err == nil {
|
|
||||||
return normalizeProfile(row.Profile), nil
|
|
||||||
}
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
return profileProd, nil
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) requestProfile(ctx context.Context, sysOrigin, profile string) (string, error) {
|
|
||||||
if strings.TrimSpace(profile) != "" {
|
|
||||||
return normalizeProfile(profile), nil
|
|
||||||
}
|
|
||||||
return s.activeProfile(ctx, sysOrigin)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) resolveRuntimeConfigForProfile(ctx context.Context, sysOrigin, profile string) (runtimeConfig, error) {
|
|
||||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
|
||||||
profile = normalizeProfile(profile)
|
|
||||||
base := s.defaultRuntimeConfig(sysOrigin)
|
|
||||||
base.Profile = profile
|
|
||||||
|
|
||||||
var row model.HotgameProviderConfig
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
|
||||||
Limit(1).
|
|
||||||
First(&row).Error
|
|
||||||
if err == nil {
|
|
||||||
return configToRuntime(base, row), nil
|
|
||||||
}
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
return base, nil
|
|
||||||
}
|
|
||||||
return runtimeConfig{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) requireRuntimeConfigForProfile(ctx context.Context, sysOrigin, profile string) (runtimeConfig, error) {
|
|
||||||
cfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
|
||||||
if err != nil {
|
|
||||||
return runtimeConfig{}, err
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(cfg.AppKey) == "" {
|
|
||||||
return runtimeConfig{}, common.NewAppError(http.StatusBadRequest, "hotgame_config_missing", "hotgame appKey is required")
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProviderConfig 读取某个系统、某个环境的热游配置,并附带当前启用环境。
|
|
||||||
func (s *Service) GetProviderConfig(ctx context.Context, sysOrigin, profile string) (*ProviderConfigItem, error) {
|
|
||||||
sysOrigin = normalizeSysOrigin(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.HotgameProviderConfig
|
|
||||||
_ = s.db.WithContext(ctx).
|
|
||||||
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
|
||||||
Limit(1).
|
|
||||||
First(&row).Error
|
|
||||||
|
|
||||||
return &ProviderConfigItem{
|
|
||||||
ID: row.ID,
|
|
||||||
SysOrigin: runtimeCfg.SysOrigin,
|
|
||||||
Profile: runtimeCfg.Profile,
|
|
||||||
ActiveProfile: activeProfile,
|
|
||||||
Active: runtimeCfg.Profile == activeProfile,
|
|
||||||
AppKey: runtimeCfg.AppKey,
|
|
||||||
CallbackBaseURL: runtimeCfg.CallbackBaseURL,
|
|
||||||
TestUID: runtimeCfg.TestUID,
|
|
||||||
TestToken: runtimeCfg.TestToken,
|
|
||||||
UpdateTime: formatTime(row.UpdateTime),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveProviderConfig 保存热游配置;首个配置自动设为当前启用环境。
|
|
||||||
func (s *Service) SaveProviderConfig(ctx context.Context, req SaveProviderConfigRequest) (*ProviderConfigItem, error) {
|
|
||||||
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
|
||||||
profile := normalizeProfile(req.Profile)
|
|
||||||
appKey := strings.TrimSpace(req.AppKey)
|
|
||||||
if appKey == "" {
|
|
||||||
return nil, common.NewAppError(http.StatusBadRequest, "app_key_required", "appKey is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := utils.NextID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
row := model.HotgameProviderConfig{
|
|
||||||
ID: id,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
Profile: profile,
|
|
||||||
AppKey: appKey,
|
|
||||||
CallbackBaseURL: strings.TrimSpace(req.CallbackBaseURL),
|
|
||||||
TestUID: strings.TrimSpace(req.TestUID),
|
|
||||||
TestToken: strings.TrimSpace(req.TestToken),
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
var activeCount int64
|
|
||||||
if err := s.db.WithContext(ctx).
|
|
||||||
Model(&model.HotgameProviderConfig{}).
|
|
||||||
Where("sys_origin = ? AND active = ?", sysOrigin, true).
|
|
||||||
Count(&activeCount).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
row.Active = activeCount == 0
|
|
||||||
|
|
||||||
if err := s.db.WithContext(ctx).Clauses(clause.OnConflict{
|
|
||||||
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "profile"}},
|
|
||||||
DoUpdates: clause.AssignmentColumns([]string{
|
|
||||||
"app_key", "callback_base_url", "test_uid", "test_token", "update_time",
|
|
||||||
}),
|
|
||||||
}).Create(&row).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if row.Active {
|
|
||||||
s.clearJavaGameListCache(ctx)
|
|
||||||
}
|
|
||||||
return s.GetProviderConfig(ctx, sysOrigin, profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActivateProviderProfile 切换 app 侧读取的热游环境,并清理 Java 老游戏列表缓存。
|
|
||||||
func (s *Service) ActivateProviderProfile(ctx context.Context, req ActivateProviderProfileRequest) (*ProviderConfigItem, error) {
|
|
||||||
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
|
||||||
profile := normalizeProfile(req.Profile)
|
|
||||||
var row model.HotgameProviderConfig
|
|
||||||
if err := s.db.WithContext(ctx).
|
|
||||||
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
|
||||||
Limit(1).
|
|
||||||
First(&row).Error; err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
return nil, common.NewAppError(http.StatusBadRequest, "profile_config_missing", "profile config is missing")
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := tx.Model(&model.HotgameProviderConfig{}).
|
|
||||||
Where("sys_origin = ?", sysOrigin).
|
|
||||||
Updates(map[string]any{"active": false, "update_time": now}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Model(&model.HotgameProviderConfig{}).
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) clearJavaGameListCache(ctx context.Context) {
|
|
||||||
if s.cache == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = s.cache.Del(ctx, "GAME_LIST").Err()
|
|
||||||
}
|
|
||||||
@ -1,235 +0,0 @@
|
|||||||
package hotgame
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/config"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
vendorType = "HOTGAME"
|
|
||||||
displayName = "热游"
|
|
||||||
profileProd = "PROD"
|
|
||||||
profileTest = "TEST"
|
|
||||||
launchModeH5 = "H5_REMOTE"
|
|
||||||
catalogEnabled = "ENABLED"
|
|
||||||
roomCategory = "CHAT_ROOM"
|
|
||||||
defaultLaunchLang = "en"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dbHandle interface {
|
|
||||||
WithContext(ctx context.Context) *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service 负责热游后台配置、目录导入、app 列表和启动 URL 生成。
|
|
||||||
type Service struct {
|
|
||||||
cfg config.Config
|
|
||||||
db dbHandle
|
|
||||||
cache redis.Cmdable
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewService 创建热游服务;db 为空时路由仍可安全跳过。
|
|
||||||
func NewService(cfg config.Config, db dbHandle, cache redis.Cmdable) *Service {
|
|
||||||
return &Service{cfg: cfg, db: db, cache: cache}
|
|
||||||
}
|
|
||||||
|
|
||||||
// runtimeConfig 是某个系统、某个环境下实际用于启动和回调验签的配置快照。
|
|
||||||
type runtimeConfig struct {
|
|
||||||
SysOrigin string
|
|
||||||
Profile string
|
|
||||||
AppKey string
|
|
||||||
CallbackBaseURL string
|
|
||||||
TestUID string
|
|
||||||
TestToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProviderConfigItem 是后台热游配置页读取到的配置。
|
|
||||||
type ProviderConfigItem struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
Profile string `json:"profile"`
|
|
||||||
ActiveProfile string `json:"activeProfile"`
|
|
||||||
Active bool `json:"active"`
|
|
||||||
AppKey string `json:"appKey"`
|
|
||||||
CallbackBaseURL string `json:"callbackBaseUrl"`
|
|
||||||
TestUID string `json:"testUid"`
|
|
||||||
TestToken string `json:"testToken"`
|
|
||||||
UpdateTime string `json:"updateTime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveProviderConfigRequest 是后台保存热游接入配置的入参。
|
|
||||||
type SaveProviderConfigRequest struct {
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
Profile string `json:"profile"`
|
|
||||||
AppKey string `json:"appKey"`
|
|
||||||
CallbackBaseURL string `json:"callbackBaseUrl"`
|
|
||||||
TestUID string `json:"testUid"`
|
|
||||||
TestToken string `json:"testToken"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActivateProviderProfileRequest 是后台切换当前启用热游环境的入参。
|
|
||||||
type ActivateProviderProfileRequest struct {
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
Profile string `json:"profile"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncCatalogRequest 是后台同步或导入热游目录的入参。
|
|
||||||
type SyncCatalogRequest struct {
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
Profile string `json:"profile"`
|
|
||||||
VendorGameIDs []string `json:"vendorGameIds"`
|
|
||||||
Force bool `json:"force"`
|
|
||||||
DefaultShowcase *bool `json:"defaultShowcase"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncCatalogResponse 是热游目录同步结果。
|
|
||||||
type SyncCatalogResponse struct {
|
|
||||||
Inserted int `json:"inserted"`
|
|
||||||
Updated int `json:"updated"`
|
|
||||||
Skipped int `json:"skipped"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportCatalogResponse 是热游目录导入统一游戏列表的结果。
|
|
||||||
type ImportCatalogResponse struct {
|
|
||||||
Existing int `json:"existing"`
|
|
||||||
Imported int `json:"imported"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncAdminCatalogResponse 是“同步目录并导入游戏”的合并结果。
|
|
||||||
type SyncAdminCatalogResponse struct {
|
|
||||||
Inserted int `json:"inserted"`
|
|
||||||
Updated int `json:"updated"`
|
|
||||||
Skipped int `json:"skipped"`
|
|
||||||
Existing int `json:"existing"`
|
|
||||||
Imported int `json:"imported"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CatalogItem 是后台热游目录表格行。
|
|
||||||
type CatalogItem struct {
|
|
||||||
VendorGameID string `json:"vendorGameId"`
|
|
||||||
Profile string `json:"profile"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Cover string `json:"cover"`
|
|
||||||
PreviewURL string `json:"previewUrl"`
|
|
||||||
DownloadURL string `json:"downloadUrl"`
|
|
||||||
PackageVersion string `json:"packageVersion"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Added bool `json:"added"`
|
|
||||||
Showcase bool `json:"showcase"`
|
|
||||||
UpdateTime string `json:"updateTime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CatalogPageResponse 是热游目录分页响应。
|
|
||||||
type CatalogPageResponse struct {
|
|
||||||
Cursor int `json:"cursor"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Records []CatalogItem `json:"records"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type defaultCatalogGame struct {
|
|
||||||
GameID string
|
|
||||||
Name string
|
|
||||||
TestURL string
|
|
||||||
ProdURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
type gameRow struct {
|
|
||||||
ConfigID int64
|
|
||||||
SysOrigin string
|
|
||||||
Profile string
|
|
||||||
InternalGameID string
|
|
||||||
Name string
|
|
||||||
Category string
|
|
||||||
Cover string
|
|
||||||
Sort int64
|
|
||||||
FullScreen bool
|
|
||||||
VendorGameID string
|
|
||||||
LaunchMode string
|
|
||||||
ExtPackageVersion string
|
|
||||||
ExtPreviewURL string
|
|
||||||
PackageURL string
|
|
||||||
ExtOrientation *int
|
|
||||||
ExtSafeHeight int
|
|
||||||
ExtExtraJSON string
|
|
||||||
CatalogName string
|
|
||||||
CatalogCover string
|
|
||||||
CatalogPreviewURL string
|
|
||||||
CatalogDownloadURL string
|
|
||||||
CatalogPackageVersion string
|
|
||||||
CatalogStatus string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ gameprovider.Provider = (*AppProvider)(nil)
|
|
||||||
|
|
||||||
// AppProvider 把热游服务适配到统一游戏厂商注册表。
|
|
||||||
type AppProvider struct {
|
|
||||||
service *Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAppProvider 创建热游 app 侧 provider。
|
|
||||||
func NewAppProvider(service *Service) *AppProvider {
|
|
||||||
if service == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &AppProvider{service: service}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeSysOrigin(sysOrigin string) string {
|
|
||||||
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
|
||||||
if sysOrigin == "" {
|
|
||||||
return "LIKEI"
|
|
||||||
}
|
|
||||||
return sysOrigin
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeProfile(profile string) string {
|
|
||||||
switch strings.ToUpper(strings.TrimSpace(profile)) {
|
|
||||||
case "", "PROD", "PRODUCT", "PRODUCTION", "ONLINE", "OFFICIAL", "FORMAL":
|
|
||||||
return profileProd
|
|
||||||
case "TEST", "TESTING", "SANDBOX", "DEV":
|
|
||||||
return profileTest
|
|
||||||
default:
|
|
||||||
return strings.ToUpper(strings.TrimSpace(profile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultIfBlank(value, fallback string) string {
|
|
||||||
if strings.TrimSpace(value) != "" {
|
|
||||||
return strings.TrimSpace(value)
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatTime(t time.Time) string {
|
|
||||||
if t.IsZero() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return t.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
||||||
func configToRuntime(base runtimeConfig, row model.HotgameProviderConfig) runtimeConfig {
|
|
||||||
base.SysOrigin = normalizeSysOrigin(defaultIfBlank(row.SysOrigin, base.SysOrigin))
|
|
||||||
base.Profile = normalizeProfile(defaultIfBlank(row.Profile, base.Profile))
|
|
||||||
base.AppKey = defaultIfBlank(row.AppKey, base.AppKey)
|
|
||||||
base.CallbackBaseURL = defaultIfBlank(row.CallbackBaseURL, base.CallbackBaseURL)
|
|
||||||
base.TestUID = strings.TrimSpace(row.TestUID)
|
|
||||||
base.TestToken = strings.TrimSpace(row.TestToken)
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
func internalGameID(vendorGameID string) string {
|
|
||||||
vendorGameID = strings.TrimSpace(vendorGameID)
|
|
||||||
if vendorGameID == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "hg_" + vendorGameID
|
|
||||||
}
|
|
||||||
@ -47,7 +47,7 @@ func (p *AppProvider) ListShortcutGames(ctx context.Context, user gameprovider.A
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *AppProvider) ListRoomGames(ctx context.Context, user gameprovider.AuthUser, roomID, category string) (*gameprovider.RoomGameListResponse, error) {
|
func (p *AppProvider) ListRoomGames(ctx context.Context, user gameprovider.AuthUser, roomID, category string) (*gameprovider.RoomGameListResponse, error) {
|
||||||
items, err := p.service.listRoomGames(ctx, user.SysOrigin, user.RegionID, category)
|
items, err := p.service.listRoomGames(ctx, user.SysOrigin, category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -116,7 +116,7 @@ func (p *AppProvider) CloseGame(ctx context.Context, user gameprovider.AuthUser,
|
|||||||
return &gameprovider.RoomStateResponse{RoomID: req.RoomID, State: "IDLE", Provider: vendorType}, nil
|
return &gameprovider.RoomStateResponse{RoomID: req.RoomID, State: "IDLE", Provider: vendorType}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) listRoomGames(ctx context.Context, sysOrigin, regionID, category string) ([]gameprovider.RoomGameListItem, error) {
|
func (s *Service) listRoomGames(ctx context.Context, sysOrigin, category string) ([]gameprovider.RoomGameListItem, error) {
|
||||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||||
profile, err := s.activeProfile(ctx, sysOrigin)
|
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -125,10 +125,6 @@ func (s *Service) listRoomGames(ctx context.Context, sysOrigin, regionID, catego
|
|||||||
var rows []gameRow
|
var rows []gameRow
|
||||||
query := s.baseGameQuery(ctx, sysOrigin, profile).
|
query := s.baseGameQuery(ctx, sysOrigin, profile).
|
||||||
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, vendorType)
|
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, vendorType)
|
||||||
if strings.TrimSpace(regionID) != "" {
|
|
||||||
// regions 为空表示全区域;有值时按用户区域 ID 精确匹配,避免土耳其用户看到其他区域游戏。
|
|
||||||
query = query.Where("(COALESCE(TRIM(cfg.regions), '') = '' OR FIND_IN_SET(?, REPLACE(COALESCE(cfg.regions, ''), ' ', '')) > 0)", strings.TrimSpace(regionID))
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), roomCategory) {
|
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), roomCategory) {
|
||||||
query = query.Where("cfg.category = ?", category)
|
query = query.Where("cfg.category = ?", category)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,7 +124,7 @@ func (s *Service) ListBDLeaders(ctx context.Context, user AuthUser) (*BDLeaderLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AddBDLeader(ctx context.Context, user AuthUser, req AddBDLeaderRequest) (*AddBDLeaderResponse, error) {
|
func (s *Service) AddBDLeader(ctx context.Context, user AuthUser, req AddBDLeaderRequest) (*AddBDLeaderResponse, error) {
|
||||||
manager, err := s.requireManagerFeature(ctx, user, managerFeatureAddBDLeader)
|
manager, err := s.requireManagerInfo(ctx, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/integration"
|
"chatapp3-golang/internal/integration"
|
||||||
"chatapp3-golang/internal/model"
|
"chatapp3-golang/internal/model"
|
||||||
@ -15,18 +14,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type teamManagerInfoRow struct {
|
type teamManagerInfoRow struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
UserID int64 `gorm:"column:user_id"`
|
UserID int64 `gorm:"column:user_id"`
|
||||||
Origin string `gorm:"column:sys_origin"`
|
Origin string `gorm:"column:sys_origin"`
|
||||||
Contact string `gorm:"column:contact"`
|
Contact string `gorm:"column:contact"`
|
||||||
RegionID string `gorm:"column:region_id"`
|
RegionID string `gorm:"column:region_id"`
|
||||||
CanAddBDLeader *bool `gorm:"column:can_add_bd_leader"`
|
CreateUser int64 `gorm:"column:create_user"`
|
||||||
CanGiftProps *bool `gorm:"column:can_gift_props"`
|
UpdateUser int64 `gorm:"column:update_user"`
|
||||||
CanBanUser *bool `gorm:"column:can_ban_user"`
|
|
||||||
CanUnbanUser *bool `gorm:"column:can_unban_user"`
|
|
||||||
CanChangeCountry *bool `gorm:"column:can_change_country"`
|
|
||||||
CreateUser int64 `gorm:"column:create_user"`
|
|
||||||
UpdateUser int64 `gorm:"column:update_user"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (teamManagerInfoRow) TableName() string {
|
func (teamManagerInfoRow) TableName() string {
|
||||||
@ -34,38 +28,21 @@ func (teamManagerInfoRow) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type userBaseInfoRow struct {
|
type userBaseInfoRow struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
Account string `gorm:"column:account"`
|
Account string `gorm:"column:account"`
|
||||||
UserAvatar string `gorm:"column:user_avatar"`
|
UserAvatar string `gorm:"column:user_avatar"`
|
||||||
UserNickname string `gorm:"column:user_nickname"`
|
UserNickname string `gorm:"column:user_nickname"`
|
||||||
CountryID int64 `gorm:"column:country_id"`
|
CountryCode string `gorm:"column:country_code"`
|
||||||
CountryCode string `gorm:"column:country_code"`
|
CountryName string `gorm:"column:country_name"`
|
||||||
CountryName string `gorm:"column:country_name"`
|
OriginSys string `gorm:"column:origin_sys"`
|
||||||
OriginSys string `gorm:"column:origin_sys"`
|
AccountStatus string `gorm:"column:account_status"`
|
||||||
AccountStatus string `gorm:"column:account_status"`
|
IsDel bool `gorm:"column:is_del"`
|
||||||
IsDel bool `gorm:"column:is_del"`
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
|
||||||
UpdateUser int64 `gorm:"column:update_user"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (userBaseInfoRow) TableName() string {
|
func (userBaseInfoRow) TableName() string {
|
||||||
return "user_base_info"
|
return "user_base_info"
|
||||||
}
|
}
|
||||||
|
|
||||||
type sysCountryCodeRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
AlphaTwo string `gorm:"column:alpha_two"`
|
|
||||||
CountryName string `gorm:"column:country_name"`
|
|
||||||
NationalFlag string `gorm:"column:national_flag"`
|
|
||||||
PhonePrefix int `gorm:"column:phone_prefix"`
|
|
||||||
Open bool `gorm:"column:is_open"`
|
|
||||||
Sort int `gorm:"column:sort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sysCountryCodeRow) TableName() string {
|
|
||||||
return "sys_country_code"
|
|
||||||
}
|
|
||||||
|
|
||||||
type propsSourceRecordRow struct {
|
type propsSourceRecordRow struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
Type string `gorm:"column:type"`
|
Type string `gorm:"column:type"`
|
||||||
@ -96,8 +73,7 @@ func (propsNobleVIPAbilityRow) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetProfile(ctx context.Context, user AuthUser) (*ProfileResponse, error) {
|
func (s *Service) GetProfile(ctx context.Context, user AuthUser) (*ProfileResponse, error) {
|
||||||
manager, err := s.requireManagerInfo(ctx, user)
|
if err := s.ensureManager(ctx, user); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +81,7 @@ func (s *Service) GetProfile(ctx context.Context, user AuthUser) (*ProfileRespon
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ProfileResponse{Manager: view, Features: managerFeaturesFromRow(manager)}, nil
|
return &ProfileResponse{Manager: view}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SearchUser(ctx context.Context, user AuthUser, account string) (*UserSearchResponse, error) {
|
func (s *Service) SearchUser(ctx context.Context, user AuthUser, account string) (*UserSearchResponse, error) {
|
||||||
@ -125,34 +101,6 @@ func (s *Service) SearchUser(ctx context.Context, user AuthUser, account string)
|
|||||||
return &UserSearchResponse{User: view}, nil
|
return &UserSearchResponse{User: view}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCountries 给经理中心返回当前开放国家,H5 只展示可绑定目标,避免经理手输无效国家码。
|
|
||||||
func (s *Service) ListCountries(ctx context.Context, user AuthUser) (*CountryListResponse, error) {
|
|
||||||
if err := s.ensureManager(ctx, user); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if s.db == nil {
|
|
||||||
return nil, serviceUnavailable("database_unavailable", "database is unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []sysCountryCodeRow
|
|
||||||
if err := s.db.WithContext(ctx).
|
|
||||||
Table("sys_country_code").
|
|
||||||
Select("id, alpha_two, country_name, national_flag, phone_prefix, is_open, sort").
|
|
||||||
Where("is_open = ?", true).
|
|
||||||
Order("sort ASC, country_name ASC, id ASC").
|
|
||||||
Find(&rows).Error; err != nil {
|
|
||||||
return nil, serverError("country_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
records := make([]CountryView, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
if view, ok := countryRowToView(row); ok {
|
|
||||||
records = append(records, view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &CountryListResponse{Records: records}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListProps(ctx context.Context, user AuthUser, propsType string) (*PropsListResponse, error) {
|
func (s *Service) ListProps(ctx context.Context, user AuthUser, propsType string) (*PropsListResponse, error) {
|
||||||
if err := s.ensureManager(ctx, user); err != nil {
|
if err := s.ensureManager(ctx, user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -200,7 +148,7 @@ func (s *Service) ListProps(ctx context.Context, user AuthUser, propsType string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendProps(ctx context.Context, user AuthUser, req SendPropsRequest) (*SendPropsResponse, error) {
|
func (s *Service) SendProps(ctx context.Context, user AuthUser, req SendPropsRequest) (*SendPropsResponse, error) {
|
||||||
if _, err := s.requireManagerFeature(ctx, user, managerFeatureGiftProps); err != nil {
|
if err := s.ensureManager(ctx, user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if req.AcceptUserID.Int64() <= 0 {
|
if req.AcceptUserID.Int64() <= 0 {
|
||||||
@ -295,7 +243,7 @@ func (s *Service) SendProps(ctx context.Context, user AuthUser, req SendPropsReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) BanUser(ctx context.Context, user AuthUser, req BanUserRequest) (*BanUserResponse, error) {
|
func (s *Service) BanUser(ctx context.Context, user AuthUser, req BanUserRequest) (*BanUserResponse, error) {
|
||||||
if _, err := s.requireManagerFeature(ctx, user, managerFeatureBanUser); err != nil {
|
if err := s.ensureManager(ctx, user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if req.UserID.Int64() <= 0 {
|
if req.UserID.Int64() <= 0 {
|
||||||
@ -342,7 +290,7 @@ func (s *Service) BanUser(ctx context.Context, user AuthUser, req BanUserRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UnbanUser(ctx context.Context, user AuthUser, req UnbanUserRequest) (*UnbanUserResponse, error) {
|
func (s *Service) UnbanUser(ctx context.Context, user AuthUser, req UnbanUserRequest) (*UnbanUserResponse, error) {
|
||||||
if _, err := s.requireManagerFeature(ctx, user, managerFeatureUnbanUser); err != nil {
|
if err := s.ensureManager(ctx, user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if req.UserID.Int64() <= 0 {
|
if req.UserID.Int64() <= 0 {
|
||||||
@ -379,116 +327,11 @@ func (s *Service) UnbanUser(ctx context.Context, user AuthUser, req UnbanUserReq
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeUserCountry 只允许 admin 开启改国家权限的经理移动普通用户国家;目标用户已有业务身份时拒绝,避免跨国家后团队、币商和主播归属数据不一致。
|
|
||||||
func (s *Service) ChangeUserCountry(ctx context.Context, user AuthUser, req ChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
|
|
||||||
if _, err := s.requireManagerFeature(ctx, user, managerFeatureChangeCountry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if req.UserID.Int64() <= 0 {
|
|
||||||
return nil, badRequest("user_required", "userId is required")
|
|
||||||
}
|
|
||||||
if req.CountryID.Int64() <= 0 {
|
|
||||||
return nil, badRequest("country_required", "countryId is required")
|
|
||||||
}
|
|
||||||
if s.db == nil {
|
|
||||||
return nil, serviceUnavailable("database_unavailable", "database is unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := s.loadUserRowByID(ctx, req.UserID.Int64())
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, notFound("user_not_found", "user not found")
|
|
||||||
}
|
|
||||||
return nil, serverError("user_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
country, err := s.loadOpenCountryByID(ctx, req.CountryID.Int64())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := s.ensureUserCountryMovable(ctx, target.ID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只改国家三元组,不触碰昵称、头像等资料审核字段;清理 Java 资料缓存后 App/H5 会重新读到新国家。
|
|
||||||
updates := map[string]any{
|
|
||||||
"country_id": country.ID,
|
|
||||||
"country_code": strings.ToUpper(strings.TrimSpace(country.AlphaTwo)),
|
|
||||||
"country_name": strings.TrimSpace(country.CountryName),
|
|
||||||
"update_time": time.Now(),
|
|
||||||
"update_user": user.UserID,
|
|
||||||
}
|
|
||||||
if err := s.db.WithContext(ctx).
|
|
||||||
Table("user_base_info").
|
|
||||||
Where("id = ? AND is_del = ?", target.ID, false).
|
|
||||||
Updates(updates).Error; err != nil {
|
|
||||||
return nil, serverError("user_country_update_failed", err.Error())
|
|
||||||
}
|
|
||||||
if s.java != nil {
|
|
||||||
if err := s.java.RemoveUserProfileCacheAll(ctx, target.ID); err != nil {
|
|
||||||
return nil, serverError("remove_profile_cache_failed", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target.CountryID = country.ID
|
|
||||||
target.CountryCode = strings.ToUpper(strings.TrimSpace(country.AlphaTwo))
|
|
||||||
target.CountryName = strings.TrimSpace(country.CountryName)
|
|
||||||
view, err := s.userRowToView(ctx, target)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ChangeUserCountryResponse{Success: true, User: view}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ensureManager(ctx context.Context, user AuthUser) error {
|
func (s *Service) ensureManager(ctx context.Context, user AuthUser) error {
|
||||||
_, err := s.requireManagerInfo(ctx, user)
|
_, err := s.requireManagerInfo(ctx, user)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// requireManagerFeature 先确认调用人仍然是团队经理,再检查 admin 给这个经理打开的单项 H5 操作权限。
|
|
||||||
// 这样前端隐藏按钮和后端写操作使用同一张 team_manager_base_info 表做判断,H5 被绕过时也会被服务层拦截。
|
|
||||||
func (s *Service) requireManagerFeature(ctx context.Context, user AuthUser, feature managerFeature) (teamManagerInfoRow, error) {
|
|
||||||
manager, err := s.requireManagerInfo(ctx, user)
|
|
||||||
if err != nil {
|
|
||||||
return teamManagerInfoRow{}, err
|
|
||||||
}
|
|
||||||
features := managerFeaturesFromRow(manager)
|
|
||||||
if !features.enabled(feature) {
|
|
||||||
return teamManagerInfoRow{}, forbidden("manager_feature_disabled", "manager feature is disabled")
|
|
||||||
}
|
|
||||||
return manager, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func managerFeaturesFromRow(row teamManagerInfoRow) ManagerFeatures {
|
|
||||||
return ManagerFeatures{
|
|
||||||
AddBDLeader: defaultEnabled(row.CanAddBDLeader),
|
|
||||||
GiftProps: defaultEnabled(row.CanGiftProps),
|
|
||||||
BanUser: defaultEnabled(row.CanBanUser),
|
|
||||||
UnbanUser: defaultEnabled(row.CanUnbanUser),
|
|
||||||
ChangeCountry: defaultEnabled(row.CanChangeCountry),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultEnabled(value *bool) bool {
|
|
||||||
return value == nil || *value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (features ManagerFeatures) enabled(feature managerFeature) bool {
|
|
||||||
switch feature {
|
|
||||||
case managerFeatureAddBDLeader:
|
|
||||||
return features.AddBDLeader
|
|
||||||
case managerFeatureGiftProps:
|
|
||||||
return features.GiftProps
|
|
||||||
case managerFeatureBanUser:
|
|
||||||
return features.BanUser
|
|
||||||
case managerFeatureUnbanUser:
|
|
||||||
return features.UnbanUser
|
|
||||||
case managerFeatureChangeCountry:
|
|
||||||
return features.ChangeCountry
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) findUserView(ctx context.Context, account string) (UserView, error) {
|
func (s *Service) findUserView(ctx context.Context, account string) (UserView, error) {
|
||||||
if userID, err := strconv.ParseInt(account, 10, 64); err == nil && userID > 0 {
|
if userID, err := strconv.ParseInt(account, 10, 64); err == nil && userID > 0 {
|
||||||
if row, err := s.loadUserRowByID(ctx, userID); err == nil {
|
if row, err := s.loadUserRowByID(ctx, userID); err == nil {
|
||||||
@ -554,65 +397,6 @@ func (s *Service) loadUserRowByAccount(ctx context.Context, account string) (use
|
|||||||
return row, err
|
return row, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) loadOpenCountryByID(ctx context.Context, countryID int64) (sysCountryCodeRow, error) {
|
|
||||||
var row sysCountryCodeRow
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Table("sys_country_code").
|
|
||||||
Select("id, alpha_two, country_name, national_flag, phone_prefix, is_open, sort").
|
|
||||||
Where("id = ? AND is_open = ?", countryID, true).
|
|
||||||
First(&row).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return sysCountryCodeRow{}, notFound("country_not_found", "country not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return sysCountryCodeRow{}, serverError("country_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(row.AlphaTwo) == "" {
|
|
||||||
return sysCountryCodeRow{}, badRequest("country_code_required", "country code is required")
|
|
||||||
}
|
|
||||||
return row, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureUserCountryMovable 串行检查所有会绑定归属关系的身份;任一命中都返回同一个业务错误,前端显示统一提示。
|
|
||||||
func (s *Service) ensureUserCountryMovable(ctx context.Context, userID int64) error {
|
|
||||||
locked, err := s.userHasLockedHistoryIdentity(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if locked {
|
|
||||||
return badRequest("user_identity_locked", "user has host, agency, BD, BD leader or coin seller identity and cannot be moved")
|
|
||||||
}
|
|
||||||
locked, err = s.userIsBDLeader(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if locked {
|
|
||||||
return badRequest("user_identity_locked", "user has host, agency, BD, BD leader or coin seller identity and cannot be moved")
|
|
||||||
}
|
|
||||||
if s.java == nil {
|
|
||||||
return serviceUnavailable("java_gateway_unavailable", "java gateway is unavailable")
|
|
||||||
}
|
|
||||||
locked, err = s.java.ExistsFreightSeller(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return serverError("coin_seller_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
if locked {
|
|
||||||
return badRequest("user_identity_locked", "user has host, agency, BD, BD leader or coin seller identity and cannot be moved")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) userHasLockedHistoryIdentity(ctx context.Context, userID int64) (bool, error) {
|
|
||||||
var count int64
|
|
||||||
if err := s.db.WithContext(ctx).
|
|
||||||
Table("user_history_identity").
|
|
||||||
Where("user_id = ? AND (is_host = ? OR is_agent = ? OR is_bd = ?)", userID, true, true, true).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return false, serverError("history_identity_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
return count > 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) userRowToView(ctx context.Context, row userBaseInfoRow) (UserView, error) {
|
func (s *Service) userRowToView(ctx context.Context, row userBaseInfoRow) (UserView, error) {
|
||||||
hasRecharge, canBan := s.userRechargeEligibility(ctx, row.ID)
|
hasRecharge, canBan := s.userRechargeEligibility(ctx, row.ID)
|
||||||
canBan = canBan && !isUnbannableAccountStatus(row.AccountStatus)
|
canBan = canBan && !isUnbannableAccountStatus(row.AccountStatus)
|
||||||
@ -621,7 +405,6 @@ func (s *Service) userRowToView(ctx context.Context, row userBaseInfoRow) (UserV
|
|||||||
Account: row.Account,
|
Account: row.Account,
|
||||||
UserAvatar: row.UserAvatar,
|
UserAvatar: row.UserAvatar,
|
||||||
UserNickname: row.UserNickname,
|
UserNickname: row.UserNickname,
|
||||||
CountryID: ID(row.CountryID),
|
|
||||||
CountryCode: row.CountryCode,
|
CountryCode: row.CountryCode,
|
||||||
CountryName: row.CountryName,
|
CountryName: row.CountryName,
|
||||||
OriginSys: row.OriginSys,
|
OriginSys: row.OriginSys,
|
||||||
@ -639,7 +422,6 @@ func (s *Service) javaProfileToView(ctx context.Context, profile integration.Use
|
|||||||
Account: profile.Account,
|
Account: profile.Account,
|
||||||
UserAvatar: profile.UserAvatar,
|
UserAvatar: profile.UserAvatar,
|
||||||
UserNickname: profile.UserNickname,
|
UserNickname: profile.UserNickname,
|
||||||
CountryID: ID(profile.CountryID),
|
|
||||||
CountryCode: profile.CountryCode,
|
CountryCode: profile.CountryCode,
|
||||||
CountryName: profile.CountryName,
|
CountryName: profile.CountryName,
|
||||||
OriginSys: profile.OriginSys,
|
OriginSys: profile.OriginSys,
|
||||||
@ -665,9 +447,6 @@ func overlayJavaProfile(view *UserView, profile integration.UserProfile) {
|
|||||||
if profile.CountryName != "" {
|
if profile.CountryName != "" {
|
||||||
view.CountryName = profile.CountryName
|
view.CountryName = profile.CountryName
|
||||||
}
|
}
|
||||||
if int64(profile.CountryID) > 0 {
|
|
||||||
view.CountryID = ID(profile.CountryID)
|
|
||||||
}
|
|
||||||
if profile.OriginSys != "" {
|
if profile.OriginSys != "" {
|
||||||
view.OriginSys = profile.OriginSys
|
view.OriginSys = profile.OriginSys
|
||||||
}
|
}
|
||||||
@ -679,21 +458,6 @@ func overlayJavaProfile(view *UserView, profile integration.UserProfile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func countryRowToView(row sysCountryCodeRow) (CountryView, bool) {
|
|
||||||
code := strings.ToUpper(strings.TrimSpace(row.AlphaTwo))
|
|
||||||
name := strings.TrimSpace(row.CountryName)
|
|
||||||
if row.ID <= 0 || code == "" || name == "" {
|
|
||||||
return CountryView{}, false
|
|
||||||
}
|
|
||||||
return CountryView{
|
|
||||||
ID: ID(row.ID),
|
|
||||||
CountryCode: code,
|
|
||||||
CountryName: name,
|
|
||||||
NationalFlag: strings.TrimSpace(row.NationalFlag),
|
|
||||||
PhonePrefix: row.PhonePrefix,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) userHasRecharge(ctx context.Context, userID int64) (bool, error) {
|
func (s *Service) userHasRecharge(ctx context.Context, userID int64) (bool, error) {
|
||||||
if userID <= 0 {
|
if userID <= 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -759,7 +523,7 @@ func (s *Service) loadGiftableNobleVIPAbility(ctx context.Context, propsID int64
|
|||||||
return propsNobleVIPAbilityRow{}, serverError("vip_ability_query_failed", err.Error())
|
return propsNobleVIPAbilityRow{}, serverError("vip_ability_query_failed", err.Error())
|
||||||
}
|
}
|
||||||
if row.VIPLevel > maxGiftableVIPLevel {
|
if row.VIPLevel > maxGiftableVIPLevel {
|
||||||
return propsNobleVIPAbilityRow{}, badRequest("vip_not_giftable", "only noble VIP level 1-5 can be gifted")
|
return propsNobleVIPAbilityRow{}, badRequest("vip_not_giftable", "only noble VIP level 1-3 can be gifted")
|
||||||
}
|
}
|
||||||
return row, nil
|
return row, nil
|
||||||
}
|
}
|
||||||
@ -857,7 +621,7 @@ func propsRowToView(row propsSourceRecordRow, vipLevels map[int64]int) (PropsVie
|
|||||||
if row.Type == propsTypeNobleVIP {
|
if row.Type == propsTypeNobleVIP {
|
||||||
var exists bool
|
var exists bool
|
||||||
level, exists = vipLevels[row.ID]
|
level, exists = vipLevels[row.ID]
|
||||||
if !exists || level > maxGiftableVIPLevel {
|
if !exists || level > 3 {
|
||||||
return PropsView{}, false
|
return PropsView{}, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,6 @@ import (
|
|||||||
type fakeManagerGateway struct {
|
type fakeManagerGateway struct {
|
||||||
recharges map[int64][]integration.UserTotalRecharge
|
recharges map[int64][]integration.UserTotalRecharge
|
||||||
rechargeErr error
|
rechargeErr error
|
||||||
freightSellers map[int64]bool
|
|
||||||
freightSellerErr error
|
|
||||||
abilities map[int64]integration.PropsNobleVIPAbility
|
abilities map[int64]integration.PropsNobleVIPAbility
|
||||||
maxAbility integration.PropsNobleVIPAbility
|
maxAbility integration.PropsNobleVIPAbility
|
||||||
giveRequests []integration.GivePropsBackpackRequest
|
giveRequests []integration.GivePropsBackpackRequest
|
||||||
@ -54,13 +52,6 @@ func (f *fakeManagerGateway) MapUserTotalRecharge(_ context.Context, userIDs []i
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeManagerGateway) ExistsFreightSeller(_ context.Context, userID int64) (bool, error) {
|
|
||||||
if f.freightSellerErr != nil {
|
|
||||||
return false, f.freightSellerErr
|
|
||||||
}
|
|
||||||
return f.freightSellers[userID], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) GivePropsBackpack(_ context.Context, req integration.GivePropsBackpackRequest) error {
|
func (f *fakeManagerGateway) GivePropsBackpack(_ context.Context, req integration.GivePropsBackpackRequest) error {
|
||||||
f.giveRequests = append(f.giveRequests, req)
|
f.giveRequests = append(f.giveRequests, req)
|
||||||
return nil
|
return nil
|
||||||
@ -107,14 +98,10 @@ func TestListPropsRequiresAdminFreeAndFiltersVIPLevels(t *testing.T) {
|
|||||||
propsSourceRecordRow{ID: 102, Type: propsTypeRide, Name: "Paid ride", AdminFree: false},
|
propsSourceRecordRow{ID: 102, Type: propsTypeRide, Name: "Paid ride", AdminFree: false},
|
||||||
propsSourceRecordRow{ID: 201, Type: propsTypeNobleVIP, Name: "VIP 3", AdminFree: true},
|
propsSourceRecordRow{ID: 201, Type: propsTypeNobleVIP, Name: "VIP 3", AdminFree: true},
|
||||||
propsSourceRecordRow{ID: 204, Type: propsTypeNobleVIP, Name: "VIP 4", AdminFree: true},
|
propsSourceRecordRow{ID: 204, Type: propsTypeNobleVIP, Name: "VIP 4", AdminFree: true},
|
||||||
propsSourceRecordRow{ID: 205, Type: propsTypeNobleVIP, Name: "VIP 5", AdminFree: true},
|
|
||||||
propsSourceRecordRow{ID: 206, Type: propsTypeNobleVIP, Name: "VIP 6", AdminFree: true},
|
|
||||||
)
|
)
|
||||||
seedAbilities(t, service.db.(*gorm.DB),
|
seedAbilities(t, service.db.(*gorm.DB),
|
||||||
propsNobleVIPAbilityRow{ID: 201, VIPLevel: 3},
|
propsNobleVIPAbilityRow{ID: 201, VIPLevel: 3},
|
||||||
propsNobleVIPAbilityRow{ID: 204, VIPLevel: 4},
|
propsNobleVIPAbilityRow{ID: 204, VIPLevel: 4},
|
||||||
propsNobleVIPAbilityRow{ID: 205, VIPLevel: 5},
|
|
||||||
propsNobleVIPAbilityRow{ID: 206, VIPLevel: 6},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
resp, err := service.ListProps(context.Background(), AuthUser{UserID: 1}, "")
|
resp, err := service.ListProps(context.Background(), AuthUser{UserID: 1}, "")
|
||||||
@ -133,16 +120,10 @@ func TestListPropsRequiresAdminFreeAndFiltersVIPLevels(t *testing.T) {
|
|||||||
t.Fatalf("records = %+v, non-admin-free props must be hidden", resp.Records)
|
t.Fatalf("records = %+v, non-admin-free props must be hidden", resp.Records)
|
||||||
}
|
}
|
||||||
if item, ok := got[201]; !ok || item.Days != defaultVIPGiftDays || item.VIPLevel != 3 {
|
if item, ok := got[201]; !ok || item.Days != defaultVIPGiftDays || item.VIPLevel != 3 {
|
||||||
t.Fatalf("vip 201 = %+v, want level 3 with default VIP days", item)
|
t.Fatalf("vip 201 = %+v, want level 3 with 3 days", item)
|
||||||
}
|
}
|
||||||
if item, ok := got[204]; !ok || item.VIPLevel != 4 {
|
if _, ok := got[204]; ok {
|
||||||
t.Fatalf("vip 204 = %+v, want level 4 visible", item)
|
t.Fatalf("records = %+v, VIP level 4 must be hidden", resp.Records)
|
||||||
}
|
|
||||||
if item, ok := got[205]; !ok || item.VIPLevel != 5 {
|
|
||||||
t.Fatalf("vip 205 = %+v, want level 5 visible", item)
|
|
||||||
}
|
|
||||||
if _, ok := got[206]; ok {
|
|
||||||
t.Fatalf("records = %+v, VIP level 6 must be hidden", resp.Records)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,32 +136,18 @@ func TestListPropsReturnsVIPLevelConfigs(t *testing.T) {
|
|||||||
model.VipLevelConfig{ID: 302, SysOrigin: "LIKEI", Level: 2, LevelCode: "VIP2", DisplayName: "VIP 2", Enabled: false, DurationDays: 30},
|
model.VipLevelConfig{ID: 302, SysOrigin: "LIKEI", Level: 2, LevelCode: "VIP2", DisplayName: "VIP 2", Enabled: false, DurationDays: 30},
|
||||||
model.VipLevelConfig{ID: 303, SysOrigin: "ATYOU", Level: 1, LevelCode: "VIP1", DisplayName: "ATYOU VIP 1", Enabled: true, DurationDays: 30},
|
model.VipLevelConfig{ID: 303, SysOrigin: "ATYOU", Level: 1, LevelCode: "VIP1", DisplayName: "ATYOU VIP 1", Enabled: true, DurationDays: 30},
|
||||||
model.VipLevelConfig{ID: 304, SysOrigin: "LIKEI", Level: 4, LevelCode: "VIP4", DisplayName: "VIP 4", Enabled: true, DurationDays: 30},
|
model.VipLevelConfig{ID: 304, SysOrigin: "LIKEI", Level: 4, LevelCode: "VIP4", DisplayName: "VIP 4", Enabled: true, DurationDays: 30},
|
||||||
model.VipLevelConfig{ID: 305, SysOrigin: "LIKEI", Level: 5, LevelCode: "VIP5", DisplayName: "VIP 5", Enabled: true, DurationDays: 30},
|
|
||||||
model.VipLevelConfig{ID: 306, SysOrigin: "LIKEI", Level: 6, LevelCode: "VIP6", DisplayName: "VIP 6", Enabled: true, DurationDays: 30},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
resp, err := service.ListProps(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, propsTypeNobleVIP)
|
resp, err := service.ListProps(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, propsTypeNobleVIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ListProps() error = %v", err)
|
t.Fatalf("ListProps() error = %v", err)
|
||||||
}
|
}
|
||||||
if len(resp.Records) != 3 {
|
if len(resp.Records) != 1 {
|
||||||
t.Fatalf("records = %+v, want enabled LIKEI vip configs up to level 5", resp.Records)
|
t.Fatalf("records = %+v, want only one enabled LIKEI vip config", resp.Records)
|
||||||
}
|
}
|
||||||
got := map[int64]PropsView{}
|
got := resp.Records[0]
|
||||||
for _, item := range resp.Records {
|
if got.ID.Int64() != 301 || got.Type != propsTypeNobleVIP || got.VIPLevel != 1 || got.Days != 30 {
|
||||||
got[item.ID.Int64()] = item
|
t.Fatalf("vip props = %+v, want config 301 level 1", got)
|
||||||
}
|
|
||||||
if item := got[301]; item.Type != propsTypeNobleVIP || item.VIPLevel != 1 || item.Days != 30 {
|
|
||||||
t.Fatalf("vip props 301 = %+v, want config 301 level 1", item)
|
|
||||||
}
|
|
||||||
if item := got[304]; item.Type != propsTypeNobleVIP || item.VIPLevel != 4 || item.Days != 30 {
|
|
||||||
t.Fatalf("vip props 304 = %+v, want config 304 level 4", item)
|
|
||||||
}
|
|
||||||
if item := got[305]; item.Type != propsTypeNobleVIP || item.VIPLevel != 5 || item.Days != 30 {
|
|
||||||
t.Fatalf("vip props 305 = %+v, want config 305 level 5", item)
|
|
||||||
}
|
|
||||||
if _, ok := got[306]; ok {
|
|
||||||
t.Fatalf("records = %+v, VIP level 6 must be hidden", resp.Records)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,27 +167,6 @@ func TestGetProfileToleratesRechargeQueryFailure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetProfileReturnsManagerFeatures(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManagerWithFeatures(t, db, 1, ManagerFeatures{
|
|
||||||
AddBDLeader: true,
|
|
||||||
GiftProps: false,
|
|
||||||
BanUser: true,
|
|
||||||
UnbanUser: false,
|
|
||||||
ChangeCountry: true,
|
|
||||||
})
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 1, Account: "1"})
|
|
||||||
|
|
||||||
resp, err := service.GetProfile(context.Background(), AuthUser{UserID: 1})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetProfile() error = %v", err)
|
|
||||||
}
|
|
||||||
if !resp.Features.AddBDLeader || resp.Features.GiftProps || !resp.Features.BanUser || resp.Features.UnbanUser || !resp.Features.ChangeCountry {
|
|
||||||
t.Fatalf("features = %+v, want row switches returned", resp.Features)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddBDLeaderCreatesLeaderAndListEntry(t *testing.T) {
|
func TestAddBDLeaderCreatesLeaderAndListEntry(t *testing.T) {
|
||||||
service, _ := newManagerCenterTestService(t)
|
service, _ := newManagerCenterTestService(t)
|
||||||
db := service.db.(*gorm.DB)
|
db := service.db.(*gorm.DB)
|
||||||
@ -288,24 +234,6 @@ func TestAddBDLeaderRejectsTargetFromOtherOrigin(t *testing.T) {
|
|||||||
assertAppErrorCode(t, err, "bd_leader_origin_mismatch")
|
assertAppErrorCode(t, err, "bd_leader_origin_mismatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddBDLeaderRejectsDisabledFeature(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManagerWithFeatures(t, db, 1, ManagerFeatures{GiftProps: true, BanUser: true, UnbanUser: true})
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2002", OriginSys: "LIKEI"})
|
|
||||||
|
|
||||||
_, err := service.AddBDLeader(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, AddBDLeaderRequest{UserID: ID(2)})
|
|
||||||
assertAppErrorCode(t, err, "manager_feature_disabled")
|
|
||||||
|
|
||||||
var count int64
|
|
||||||
if err := db.Model(&bdLeaderRow{}).Where("user_id = ?", int64(2)).Count(&count).Error; err != nil {
|
|
||||||
t.Fatalf("count bd leaders: %v", err)
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
t.Fatalf("bd leader count = %d, want 0", count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendPropsRejectsNonAdminFree(t *testing.T) {
|
func TestSendPropsRejectsNonAdminFree(t *testing.T) {
|
||||||
service, fake := newManagerCenterTestService(t)
|
service, fake := newManagerCenterTestService(t)
|
||||||
db := service.db.(*gorm.DB)
|
db := service.db.(*gorm.DB)
|
||||||
@ -366,23 +294,6 @@ func TestSendNobleVIPGivesAccessoriesAndSwitchesMax(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendPropsRejectsDisabledFeature(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManagerWithFeatures(t, db, 1, ManagerFeatures{AddBDLeader: true, BanUser: true, UnbanUser: true})
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"})
|
|
||||||
seedProps(t, db, propsSourceRecordRow{ID: 101, Type: propsTypeRide, Name: "Giftable ride", AdminFree: true})
|
|
||||||
|
|
||||||
_, err := service.SendProps(context.Background(), AuthUser{UserID: 1}, SendPropsRequest{
|
|
||||||
AcceptUserID: ID(2),
|
|
||||||
PropsID: ID(101),
|
|
||||||
})
|
|
||||||
assertAppErrorCode(t, err, "manager_feature_disabled")
|
|
||||||
if len(fake.giveRequests) != 0 {
|
|
||||||
t.Fatalf("give requests = %+v, want none", fake.giveRequests)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendBadgeActivatesBadgeBackpack(t *testing.T) {
|
func TestSendBadgeActivatesBadgeBackpack(t *testing.T) {
|
||||||
service, fake := newManagerCenterTestService(t)
|
service, fake := newManagerCenterTestService(t)
|
||||||
db := service.db.(*gorm.DB)
|
db := service.db.(*gorm.DB)
|
||||||
@ -422,12 +333,12 @@ func TestSendVIPUsesVIPLevelConfig(t *testing.T) {
|
|||||||
seedVIPConfigs(t, db, model.VipLevelConfig{
|
seedVIPConfigs(t, db, model.VipLevelConfig{
|
||||||
ID: 301,
|
ID: 301,
|
||||||
SysOrigin: "LIKEI",
|
SysOrigin: "LIKEI",
|
||||||
Level: 5,
|
Level: 3,
|
||||||
LevelCode: "VIP5",
|
LevelCode: "VIP3",
|
||||||
DisplayName: "VIP 5",
|
DisplayName: "VIP 3",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
DurationDays: 30,
|
DurationDays: 30,
|
||||||
PriceGold: 5000,
|
PriceGold: 3000,
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err := service.SendProps(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, SendPropsRequest{
|
resp, err := service.SendProps(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, SendPropsRequest{
|
||||||
@ -437,7 +348,7 @@ func TestSendVIPUsesVIPLevelConfig(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SendProps() error = %v", err)
|
t.Fatalf("SendProps() error = %v", err)
|
||||||
}
|
}
|
||||||
if !resp.Success || resp.Type != propsTypeNobleVIP || resp.VIPLevel != 5 || resp.Days != 30 {
|
if !resp.Success || resp.Type != propsTypeNobleVIP || resp.VIPLevel != 3 || resp.Days != 30 {
|
||||||
t.Fatalf("resp = %+v, want vip gift success", resp)
|
t.Fatalf("resp = %+v, want vip gift success", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,8 +356,8 @@ func TestSendVIPUsesVIPLevelConfig(t *testing.T) {
|
|||||||
if err := db.Where("sys_origin = ? AND user_id = ?", "LIKEI", int64(2)).First(&state).Error; err != nil {
|
if err := db.Where("sys_origin = ? AND user_id = ?", "LIKEI", int64(2)).First(&state).Error; err != nil {
|
||||||
t.Fatalf("load state: %v", err)
|
t.Fatalf("load state: %v", err)
|
||||||
}
|
}
|
||||||
if state.Level != 5 || state.Status != vipStatusActive {
|
if state.Level != 3 || state.Status != vipStatusActive {
|
||||||
t.Fatalf("state = %+v, want vip5 active", state)
|
t.Fatalf("state = %+v, want vip3 active", state)
|
||||||
}
|
}
|
||||||
var orders int64
|
var orders int64
|
||||||
if err := db.Model(&model.VipOrderRecord{}).Where("user_id = ? AND order_type = ?", int64(2), vipOrderTypeGMGift).Count(&orders).Error; err != nil {
|
if err := db.Model(&model.VipOrderRecord{}).Where("user_id = ? AND order_type = ?", int64(2), vipOrderTypeGMGift).Count(&orders).Error; err != nil {
|
||||||
@ -456,7 +367,7 @@ func TestSendVIPUsesVIPLevelConfig(t *testing.T) {
|
|||||||
t.Fatalf("orders = %d, want 1", orders)
|
t.Fatalf("orders = %d, want 1", orders)
|
||||||
}
|
}
|
||||||
var records int64
|
var records int64
|
||||||
if err := db.Model(&gmVIPGiftRecordRow{}).Where("user_id = ? AND vip_level = ?", int64(2), 5).Count(&records).Error; err != nil {
|
if err := db.Model(&gmVIPGiftRecordRow{}).Where("user_id = ? AND vip_level = ?", int64(2), 3).Count(&records).Error; err != nil {
|
||||||
t.Fatalf("count gift records: %v", err)
|
t.Fatalf("count gift records: %v", err)
|
||||||
}
|
}
|
||||||
if records != 1 {
|
if records != 1 {
|
||||||
@ -464,48 +375,6 @@ func TestSendVIPUsesVIPLevelConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVIPGiftCleanupExpiresOldVIPResourcesOnly(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
if err := db.AutoMigrate(&managerVIPCleanupPropsRow{}, &managerVIPCleanupBadgeRow{}); err != nil {
|
|
||||||
t.Fatalf("auto migrate cleanup tables: %v", err)
|
|
||||||
}
|
|
||||||
now := time.Date(2026, 5, 30, 10, 0, 0, 0, time.UTC)
|
|
||||||
future := now.AddDate(0, 0, 20)
|
|
||||||
userID := int64(2)
|
|
||||||
|
|
||||||
seedManagerVIPCleanupProps(t, db,
|
|
||||||
managerVIPCleanupPropsRow{ID: 1, UserID: userID, PropsID: 101, Type: propsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true},
|
|
||||||
managerVIPCleanupPropsRow{ID: 2, UserID: userID, PropsID: 201, Type: propsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true},
|
|
||||||
managerVIPCleanupPropsRow{ID: 3, UserID: userID, PropsID: 999, Type: propsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true},
|
|
||||||
managerVIPCleanupPropsRow{ID: 4, UserID: userID, PropsID: 103, Type: propsTypeDataCard, ExpireTime: future, UseProps: true, AllowGive: true},
|
|
||||||
)
|
|
||||||
seedManagerVIPCleanupBadges(t, db,
|
|
||||||
managerVIPCleanupBadgeRow{ID: 1, UserID: userID, BadgeID: 100, ExpireTime: future, UseProps: true},
|
|
||||||
managerVIPCleanupBadgeRow{ID: 2, UserID: userID, BadgeID: 200, ExpireTime: future, UseProps: true},
|
|
||||||
)
|
|
||||||
|
|
||||||
target := vipGiftResourcesFromConfig(model.VipLevelConfig{
|
|
||||||
BadgeResourceID: 200,
|
|
||||||
AvatarFrameResourceID: 201,
|
|
||||||
})
|
|
||||||
all := append(vipGiftResourcesFromConfig(model.VipLevelConfig{
|
|
||||||
BadgeResourceID: 100,
|
|
||||||
AvatarFrameResourceID: 101,
|
|
||||||
BackgroundCardResourceID: 103,
|
|
||||||
}), target...)
|
|
||||||
if err := service.cleanupSupersededVIPGiftResources(context.Background(), userID, target, all, now); err != nil {
|
|
||||||
t.Fatalf("cleanupSupersededVIPGiftResources() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertManagerVIPCleanupProps(t, db, 101, now, false, false)
|
|
||||||
assertManagerVIPCleanupProps(t, db, 103, now, false, false)
|
|
||||||
assertManagerVIPCleanupProps(t, db, 201, future, true, true)
|
|
||||||
assertManagerVIPCleanupProps(t, db, 999, future, true, true)
|
|
||||||
assertManagerVIPCleanupBadge(t, db, 100, now, false)
|
|
||||||
assertManagerVIPCleanupBadge(t, db, 200, future, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendVIPRejectsLevelAboveGiftLimit(t *testing.T) {
|
func TestSendVIPRejectsLevelAboveGiftLimit(t *testing.T) {
|
||||||
service, _ := newManagerCenterTestService(t)
|
service, _ := newManagerCenterTestService(t)
|
||||||
db := service.db.(*gorm.DB)
|
db := service.db.(*gorm.DB)
|
||||||
@ -517,9 +386,9 @@ func TestSendVIPRejectsLevelAboveGiftLimit(t *testing.T) {
|
|||||||
seedVIPConfigs(t, db, model.VipLevelConfig{
|
seedVIPConfigs(t, db, model.VipLevelConfig{
|
||||||
ID: 304,
|
ID: 304,
|
||||||
SysOrigin: "LIKEI",
|
SysOrigin: "LIKEI",
|
||||||
Level: 6,
|
Level: 4,
|
||||||
LevelCode: "VIP6",
|
LevelCode: "VIP4",
|
||||||
DisplayName: "VIP 6",
|
DisplayName: "VIP 4",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
DurationDays: 30,
|
DurationDays: 30,
|
||||||
})
|
})
|
||||||
@ -531,90 +400,6 @@ func TestSendVIPRejectsLevelAboveGiftLimit(t *testing.T) {
|
|||||||
assertAppErrorCode(t, err, "vip_not_giftable")
|
assertAppErrorCode(t, err, "vip_not_giftable")
|
||||||
}
|
}
|
||||||
|
|
||||||
type managerVIPCleanupPropsRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
UserID int64 `gorm:"column:user_id"`
|
|
||||||
PropsID int64 `gorm:"column:props_id"`
|
|
||||||
Type string `gorm:"column:type"`
|
|
||||||
ExpireTime time.Time `gorm:"column:expire_time"`
|
|
||||||
UseProps bool `gorm:"column:is_use_props"`
|
|
||||||
AllowGive bool `gorm:"column:allow_give"`
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (managerVIPCleanupPropsRow) TableName() string {
|
|
||||||
return "user_props_backpack"
|
|
||||||
}
|
|
||||||
|
|
||||||
type managerVIPCleanupBadgeRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
UserID int64 `gorm:"column:user_id"`
|
|
||||||
BadgeID int64 `gorm:"column:badge_id"`
|
|
||||||
ExpireType string `gorm:"column:expire_type"`
|
|
||||||
ExpireTime time.Time `gorm:"column:expire_time"`
|
|
||||||
UseProps bool `gorm:"column:is_use_props"`
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (managerVIPCleanupBadgeRow) TableName() string {
|
|
||||||
return "user_badge_backpack"
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedManagerVIPCleanupProps(t *testing.T, db *gorm.DB, rows ...managerVIPCleanupPropsRow) {
|
|
||||||
t.Helper()
|
|
||||||
for _, row := range rows {
|
|
||||||
if err := db.Create(&row).Error; err != nil {
|
|
||||||
t.Fatalf("seed manager cleanup props: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedManagerVIPCleanupBadges(t *testing.T, db *gorm.DB, rows ...managerVIPCleanupBadgeRow) {
|
|
||||||
t.Helper()
|
|
||||||
for _, row := range rows {
|
|
||||||
if err := db.Create(&row).Error; err != nil {
|
|
||||||
t.Fatalf("seed manager cleanup badge: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertManagerVIPCleanupProps(t *testing.T, db *gorm.DB, propsID int64, expireTime time.Time, useProps bool, allowGive bool) {
|
|
||||||
t.Helper()
|
|
||||||
var row managerVIPCleanupPropsRow
|
|
||||||
if err := db.Where("props_id = ?", propsID).First(&row).Error; err != nil {
|
|
||||||
t.Fatalf("load manager cleanup props %d: %v", propsID, err)
|
|
||||||
}
|
|
||||||
if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps || row.AllowGive != allowGive {
|
|
||||||
t.Fatalf("props %d = expire:%s use:%v allow:%v, want expire:%s use:%v allow:%v",
|
|
||||||
propsID, row.ExpireTime, row.UseProps, row.AllowGive, expireTime, useProps, allowGive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertManagerVIPCleanupBadge(t *testing.T, db *gorm.DB, badgeID int64, expireTime time.Time, useProps bool) {
|
|
||||||
t.Helper()
|
|
||||||
var row managerVIPCleanupBadgeRow
|
|
||||||
if err := db.Where("badge_id = ?", badgeID).First(&row).Error; err != nil {
|
|
||||||
t.Fatalf("load manager cleanup badge %d: %v", badgeID, err)
|
|
||||||
}
|
|
||||||
if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps {
|
|
||||||
t.Fatalf("badge %d = expire:%s use:%v, want expire:%s use:%v",
|
|
||||||
badgeID, row.ExpireTime, row.UseProps, expireTime, useProps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBanUserRejectsDisabledFeature(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManagerWithFeatures(t, db, 1, ManagerFeatures{AddBDLeader: true, GiftProps: true, UnbanUser: true})
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"})
|
|
||||||
|
|
||||||
_, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{UserID: ID(2)})
|
|
||||||
assertAppErrorCode(t, err, "manager_feature_disabled")
|
|
||||||
if len(fake.punishRequests) != 0 {
|
|
||||||
t.Fatalf("punish requests = %+v, want none", fake.punishRequests)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBanUserRejectsRechargedUser(t *testing.T) {
|
func TestBanUserRejectsRechargedUser(t *testing.T) {
|
||||||
service, fake := newManagerCenterTestService(t)
|
service, fake := newManagerCenterTestService(t)
|
||||||
db := service.db.(*gorm.DB)
|
db := service.db.(*gorm.DB)
|
||||||
@ -694,19 +479,6 @@ func TestBanUserWithoutRechargeCallsPunishment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnbanUserRejectsDisabledFeature(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManagerWithFeatures(t, db, 1, ManagerFeatures{AddBDLeader: true, GiftProps: true, BanUser: true})
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", AccountStatus: accountStatusArchive})
|
|
||||||
|
|
||||||
_, err := service.UnbanUser(context.Background(), AuthUser{UserID: 1}, UnbanUserRequest{UserID: ID(2)})
|
|
||||||
assertAppErrorCode(t, err, "manager_feature_disabled")
|
|
||||||
if len(fake.unblockRequests) != 0 {
|
|
||||||
t.Fatalf("unblock requests = %+v, want none", fake.unblockRequests)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnbanUserRejectsNormalUser(t *testing.T) {
|
func TestUnbanUserRejectsNormalUser(t *testing.T) {
|
||||||
service, fake := newManagerCenterTestService(t)
|
service, fake := newManagerCenterTestService(t)
|
||||||
db := service.db.(*gorm.DB)
|
db := service.db.(*gorm.DB)
|
||||||
@ -766,117 +538,6 @@ func TestUnbanUserAllowsFrozenUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListCountriesReturnsOpenCountries(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedCountries(t, db,
|
|
||||||
sysCountryCodeRow{ID: 10, AlphaTwo: "SA", CountryName: "Saudi Arabia", Open: true, Sort: 2},
|
|
||||||
sysCountryCodeRow{ID: 11, AlphaTwo: "AE", CountryName: "United Arab Emirates", Open: true, Sort: 1},
|
|
||||||
sysCountryCodeRow{ID: 12, AlphaTwo: "XX", CountryName: "Closed", Open: false, Sort: 3},
|
|
||||||
)
|
|
||||||
|
|
||||||
resp, err := service.ListCountries(context.Background(), AuthUser{UserID: 1})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListCountries() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(resp.Records) != 2 {
|
|
||||||
t.Fatalf("countries = %+v, want two open rows", resp.Records)
|
|
||||||
}
|
|
||||||
if resp.Records[0].CountryCode != "AE" || resp.Records[1].CountryCode != "SA" {
|
|
||||||
t.Fatalf("countries = %+v, want sort order AE then SA", resp.Records)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeUserCountryUpdatesCountryAndClearsCache(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", CountryID: 10, CountryCode: "SA", CountryName: "Saudi Arabia"})
|
|
||||||
seedCountries(t, db, sysCountryCodeRow{ID: 11, AlphaTwo: "AE", CountryName: "United Arab Emirates", Open: true})
|
|
||||||
|
|
||||||
resp, err := service.ChangeUserCountry(context.Background(), AuthUser{UserID: 1}, ChangeUserCountryRequest{
|
|
||||||
UserID: ID(2),
|
|
||||||
CountryID: ID(11),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ChangeUserCountry() error = %v", err)
|
|
||||||
}
|
|
||||||
if !resp.Success || resp.User.CountryCode != "AE" || resp.User.CountryName != "United Arab Emirates" {
|
|
||||||
t.Fatalf("resp = %+v, want changed country", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
var row userBaseInfoRow
|
|
||||||
if err := db.Where("id = ?", int64(2)).First(&row).Error; err != nil {
|
|
||||||
t.Fatalf("load user: %v", err)
|
|
||||||
}
|
|
||||||
if row.CountryID != 11 || row.CountryCode != "AE" || row.CountryName != "United Arab Emirates" || row.UpdateUser != 1 {
|
|
||||||
t.Fatalf("user row = %+v, want AE country and manager updater", row)
|
|
||||||
}
|
|
||||||
if !equalInt64Slice(fake.cacheCleared, []int64{2}) {
|
|
||||||
t.Fatalf("cacheCleared = %+v, want [2]", fake.cacheCleared)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeUserCountryRejectsDisabledFeature(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManagerWithFeatures(t, db, 1, ManagerFeatures{AddBDLeader: true, GiftProps: true, BanUser: true, UnbanUser: true})
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", CountryID: 10, CountryCode: "SA", CountryName: "Saudi Arabia"})
|
|
||||||
seedCountries(t, db, sysCountryCodeRow{ID: 11, AlphaTwo: "AE", CountryName: "United Arab Emirates", Open: true})
|
|
||||||
|
|
||||||
_, err := service.ChangeUserCountry(context.Background(), AuthUser{UserID: 1}, ChangeUserCountryRequest{UserID: ID(2), CountryID: ID(11)})
|
|
||||||
assertAppErrorCode(t, err, "manager_feature_disabled")
|
|
||||||
|
|
||||||
var row userBaseInfoRow
|
|
||||||
if err := db.Where("id = ?", int64(2)).First(&row).Error; err != nil {
|
|
||||||
t.Fatalf("load user: %v", err)
|
|
||||||
}
|
|
||||||
if row.CountryID != 10 || row.CountryCode != "SA" {
|
|
||||||
t.Fatalf("user row = %+v, want unchanged country when feature disabled", row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeUserCountryRejectsHistoryIdentity(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", CountryID: 10, CountryCode: "SA"})
|
|
||||||
seedCountries(t, db, sysCountryCodeRow{ID: 11, AlphaTwo: "AE", CountryName: "United Arab Emirates", Open: true})
|
|
||||||
if err := db.Create(&userHistoryIdentityRow{UserID: 2, IsHost: true}).Error; err != nil {
|
|
||||||
t.Fatalf("seed identity: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := service.ChangeUserCountry(context.Background(), AuthUser{UserID: 1}, ChangeUserCountryRequest{UserID: ID(2), CountryID: ID(11)})
|
|
||||||
assertAppErrorCode(t, err, "user_identity_locked")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeUserCountryRejectsBDLeader(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", CountryID: 10, CountryCode: "SA"})
|
|
||||||
seedCountries(t, db, sysCountryCodeRow{ID: 11, AlphaTwo: "AE", CountryName: "United Arab Emirates", Open: true})
|
|
||||||
if err := db.Create(&bdLeaderRow{ID: 20, UserID: 2, Origin: "LIKEI", RegionID: "SA"}).Error; err != nil {
|
|
||||||
t.Fatalf("seed bd leader: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := service.ChangeUserCountry(context.Background(), AuthUser{UserID: 1}, ChangeUserCountryRequest{UserID: ID(2), CountryID: ID(11)})
|
|
||||||
assertAppErrorCode(t, err, "user_identity_locked")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeUserCountryRejectsCoinSeller(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", CountryID: 10, CountryCode: "SA"})
|
|
||||||
seedCountries(t, db, sysCountryCodeRow{ID: 11, AlphaTwo: "AE", CountryName: "United Arab Emirates", Open: true})
|
|
||||||
fake.freightSellers[2] = true
|
|
||||||
|
|
||||||
_, err := service.ChangeUserCountry(context.Background(), AuthUser{UserID: 1}, ChangeUserCountryRequest{UserID: ID(2), CountryID: ID(11)})
|
|
||||||
assertAppErrorCode(t, err, "user_identity_locked")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newManagerCenterTestService(t *testing.T) (*Service, *fakeManagerGateway) {
|
func newManagerCenterTestService(t *testing.T) (*Service, *fakeManagerGateway) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||||
@ -886,7 +547,6 @@ func newManagerCenterTestService(t *testing.T) (*Service, *fakeManagerGateway) {
|
|||||||
if err := db.AutoMigrate(
|
if err := db.AutoMigrate(
|
||||||
&teamManagerInfoRow{},
|
&teamManagerInfoRow{},
|
||||||
&userBaseInfoRow{},
|
&userBaseInfoRow{},
|
||||||
&sysCountryCodeRow{},
|
|
||||||
&bdLeaderRow{},
|
&bdLeaderRow{},
|
||||||
&businessDevelopmentBaseInfoRow{},
|
&businessDevelopmentBaseInfoRow{},
|
||||||
&userHistoryIdentityRow{},
|
&userHistoryIdentityRow{},
|
||||||
@ -901,45 +561,19 @@ func newManagerCenterTestService(t *testing.T) (*Service, *fakeManagerGateway) {
|
|||||||
t.Fatalf("auto migrate: %v", err)
|
t.Fatalf("auto migrate: %v", err)
|
||||||
}
|
}
|
||||||
fake := &fakeManagerGateway{
|
fake := &fakeManagerGateway{
|
||||||
recharges: map[int64][]integration.UserTotalRecharge{},
|
recharges: map[int64][]integration.UserTotalRecharge{},
|
||||||
freightSellers: map[int64]bool{},
|
abilities: map[int64]integration.PropsNobleVIPAbility{},
|
||||||
abilities: map[int64]integration.PropsNobleVIPAbility{},
|
|
||||||
}
|
}
|
||||||
return NewService(db, fake), fake
|
return NewService(db, fake), fake
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedManager(t *testing.T, db *gorm.DB, userID int64) {
|
func seedManager(t *testing.T, db *gorm.DB, userID int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
seedManagerWithFeatures(t, db, userID, ManagerFeatures{
|
if err := db.Create(&teamManagerInfoRow{ID: userID, UserID: userID, Origin: "LIKEI", RegionID: "SA"}).Error; err != nil {
|
||||||
AddBDLeader: true,
|
|
||||||
GiftProps: true,
|
|
||||||
BanUser: true,
|
|
||||||
UnbanUser: true,
|
|
||||||
ChangeCountry: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedManagerWithFeatures(t *testing.T, db *gorm.DB, userID int64, features ManagerFeatures) {
|
|
||||||
t.Helper()
|
|
||||||
if err := db.Create(&teamManagerInfoRow{
|
|
||||||
ID: userID,
|
|
||||||
UserID: userID,
|
|
||||||
Origin: "LIKEI",
|
|
||||||
RegionID: "SA",
|
|
||||||
CanAddBDLeader: boolPtr(features.AddBDLeader),
|
|
||||||
CanGiftProps: boolPtr(features.GiftProps),
|
|
||||||
CanBanUser: boolPtr(features.BanUser),
|
|
||||||
CanUnbanUser: boolPtr(features.UnbanUser),
|
|
||||||
CanChangeCountry: boolPtr(features.ChangeCountry),
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("seed manager: %v", err)
|
t.Fatalf("seed manager: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func boolPtr(value bool) *bool {
|
|
||||||
return &value
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedUsers(t *testing.T, db *gorm.DB, rows ...userBaseInfoRow) {
|
func seedUsers(t *testing.T, db *gorm.DB, rows ...userBaseInfoRow) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
@ -952,15 +586,6 @@ func seedUsers(t *testing.T, db *gorm.DB, rows ...userBaseInfoRow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedCountries(t *testing.T, db *gorm.DB, rows ...sysCountryCodeRow) {
|
|
||||||
t.Helper()
|
|
||||||
for _, row := range rows {
|
|
||||||
if err := db.Create(&row).Error; err != nil {
|
|
||||||
t.Fatalf("seed country: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedProps(t *testing.T, db *gorm.DB, rows ...propsSourceRecordRow) {
|
func seedProps(t *testing.T, db *gorm.DB, rows ...propsSourceRecordRow) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
|
|||||||
@ -28,10 +28,9 @@ const (
|
|||||||
vipGiftOrigin = "VIP_PURCHASE"
|
vipGiftOrigin = "VIP_PURCHASE"
|
||||||
vipGiftOriginDesc = "VIP purchase"
|
vipGiftOriginDesc = "VIP purchase"
|
||||||
|
|
||||||
defaultGiftDays = 7
|
defaultGiftDays = 7
|
||||||
defaultVIPGiftDays = 30
|
defaultVIPGiftDays = 30
|
||||||
// 经理中心 VIP 赠送按当前运营范围放开 VIP1-VIP5;列表和发放校验共用该上限,避免展示可选但提交失败。
|
maxGiftableVIPLevel = 3
|
||||||
maxGiftableVIPLevel = 5
|
|
||||||
|
|
||||||
accountStatusNormal = "NORMAL"
|
accountStatusNormal = "NORMAL"
|
||||||
accountStatusFreeze = "FREEZE"
|
accountStatusFreeze = "FREEZE"
|
||||||
@ -39,16 +38,6 @@ const (
|
|||||||
accountStatusArchiveDevice = "ARCHIVE_DEVICE"
|
accountStatusArchiveDevice = "ARCHIVE_DEVICE"
|
||||||
)
|
)
|
||||||
|
|
||||||
type managerFeature string
|
|
||||||
|
|
||||||
const (
|
|
||||||
managerFeatureAddBDLeader managerFeature = "add_bd_leader"
|
|
||||||
managerFeatureGiftProps managerFeature = "gift_props"
|
|
||||||
managerFeatureBanUser managerFeature = "ban_user"
|
|
||||||
managerFeatureUnbanUser managerFeature = "unban_user"
|
|
||||||
managerFeatureChangeCountry managerFeature = "change_country"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppError = common.AppError
|
type AppError = common.AppError
|
||||||
type AuthUser = common.AuthUser
|
type AuthUser = common.AuthUser
|
||||||
|
|
||||||
@ -62,7 +51,6 @@ type managerGateway interface {
|
|||||||
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
||||||
GetUserProfileByAccountOne(ctx context.Context, account string) (integration.UserProfile, error)
|
GetUserProfileByAccountOne(ctx context.Context, account string) (integration.UserProfile, error)
|
||||||
MapUserTotalRecharge(ctx context.Context, userIDs []int64) (map[int64][]integration.UserTotalRecharge, error)
|
MapUserTotalRecharge(ctx context.Context, userIDs []int64) (map[int64][]integration.UserTotalRecharge, error)
|
||||||
ExistsFreightSeller(ctx context.Context, userID int64) (bool, error)
|
|
||||||
GivePropsBackpack(ctx context.Context, req integration.GivePropsBackpackRequest) error
|
GivePropsBackpack(ctx context.Context, req integration.GivePropsBackpackRequest) error
|
||||||
GetNobleVIPAbility(ctx context.Context, sourceID int64) (integration.PropsNobleVIPAbility, error)
|
GetNobleVIPAbility(ctx context.Context, sourceID int64) (integration.PropsNobleVIPAbility, error)
|
||||||
GetUserMaxNobleVIPAbility(ctx context.Context, userID int64) (integration.PropsNobleVIPAbility, error)
|
GetUserMaxNobleVIPAbility(ctx context.Context, userID int64) (integration.PropsNobleVIPAbility, error)
|
||||||
@ -118,40 +106,18 @@ func (id *ID) UnmarshalJSON(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProfileResponse struct {
|
type ProfileResponse struct {
|
||||||
Manager UserView `json:"manager"`
|
Manager UserView `json:"manager"`
|
||||||
Features ManagerFeatures `json:"features"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ManagerFeatures struct {
|
|
||||||
AddBDLeader bool `json:"addBdLeader"`
|
|
||||||
GiftProps bool `json:"giftProps"`
|
|
||||||
BanUser bool `json:"banUser"`
|
|
||||||
UnbanUser bool `json:"unbanUser"`
|
|
||||||
ChangeCountry bool `json:"changeCountry"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSearchResponse struct {
|
type UserSearchResponse struct {
|
||||||
User UserView `json:"user"`
|
User UserView `json:"user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CountryListResponse struct {
|
|
||||||
Records []CountryView `json:"records"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CountryView struct {
|
|
||||||
ID ID `json:"id"`
|
|
||||||
CountryCode string `json:"countryCode"`
|
|
||||||
CountryName string `json:"countryName"`
|
|
||||||
NationalFlag string `json:"nationalFlag,omitempty"`
|
|
||||||
PhonePrefix int `json:"phonePrefix,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserView struct {
|
type UserView struct {
|
||||||
UserID ID `json:"userId"`
|
UserID ID `json:"userId"`
|
||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
UserAvatar string `json:"userAvatar"`
|
UserAvatar string `json:"userAvatar"`
|
||||||
UserNickname string `json:"userNickname"`
|
UserNickname string `json:"userNickname"`
|
||||||
CountryID ID `json:"countryId,omitempty"`
|
|
||||||
CountryCode string `json:"countryCode,omitempty"`
|
CountryCode string `json:"countryCode,omitempty"`
|
||||||
CountryName string `json:"countryName,omitempty"`
|
CountryName string `json:"countryName,omitempty"`
|
||||||
OriginSys string `json:"originSys,omitempty"`
|
OriginSys string `json:"originSys,omitempty"`
|
||||||
@ -213,16 +179,6 @@ type UnbanUserResponse struct {
|
|||||||
AccountStatus string `json:"accountStatus"`
|
AccountStatus string `json:"accountStatus"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangeUserCountryRequest struct {
|
|
||||||
UserID ID `json:"userId"`
|
|
||||||
CountryID ID `json:"countryId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChangeUserCountryResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
User UserView `json:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BDLeaderListResponse struct {
|
type BDLeaderListResponse struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Records []BDLeaderView `json:"records"`
|
Records []BDLeaderView `json:"records"`
|
||||||
|
|||||||
@ -171,7 +171,6 @@ func (s *Service) sendVIPGift(ctx context.Context, manager AuthUser, target user
|
|||||||
CreateTime: now,
|
CreateTime: now,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousResources := vipGiftResourcesFromState(state)
|
|
||||||
applyVIPGiftConfigToState(state, config, startAt, expireAt, now)
|
applyVIPGiftConfigToState(state, config, startAt, expireAt, now)
|
||||||
if err := tx.Save(state).Error; err != nil {
|
if err := tx.Save(state).Error; err != nil {
|
||||||
return nil, serverError("vip_state_save_failed", err.Error())
|
return nil, serverError("vip_state_save_failed", err.Error())
|
||||||
@ -210,7 +209,7 @@ func (s *Service) sendVIPGift(ctx context.Context, manager AuthUser, target user
|
|||||||
}
|
}
|
||||||
committed = true
|
committed = true
|
||||||
|
|
||||||
if err := s.grantVIPGiftResources(ctx, sysOrigin, target.ID, manager.UserID, config, expireAt, previousResources, now); err != nil {
|
if err := s.grantVIPGiftResources(ctx, target.ID, manager.UserID, config, expireAt, now); err != nil {
|
||||||
return nil, serverError("vip_resource_grant_failed", err.Error())
|
return nil, serverError("vip_resource_grant_failed", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,8 +301,11 @@ func applyVIPGiftConfigToState(state *model.UserVipState, config model.VipLevelC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) grantVIPGiftResources(ctx context.Context, sysOrigin string, userID int64, managerID int64, config model.VipLevelConfig, expireAt time.Time, additionalOldResources []vipGiftResource, now time.Time) error {
|
func (s *Service) grantVIPGiftResources(ctx context.Context, userID int64, managerID int64, config model.VipLevelConfig, expireAt time.Time, now time.Time) error {
|
||||||
resources := vipGiftResourcesFromConfig(config)
|
resources := vipGiftResourcesFromConfig(config)
|
||||||
|
if len(resources) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
days := config.DurationDays
|
days := config.DurationDays
|
||||||
if days <= 0 {
|
if days <= 0 {
|
||||||
days = defaultVIPGiftDays
|
days = defaultVIPGiftDays
|
||||||
@ -335,14 +337,6 @@ func (s *Service) grantVIPGiftResources(ctx context.Context, sysOrigin string, u
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allResources, err := s.vipGiftResourcesFromSysOrigin(ctx, sysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
allResources = append(allResources, additionalOldResources...)
|
|
||||||
if err := s.cleanupSupersededVIPGiftResources(ctx, userID, resources, allResources, now); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := s.java.RemoveUserProfileCacheAll(ctx, userID); err != nil {
|
if err := s.java.RemoveUserProfileCacheAll(ctx, userID); err != nil {
|
||||||
return fmt.Errorf("remove vip user profile cache: %w", err)
|
return fmt.Errorf("remove vip user profile cache: %w", err)
|
||||||
}
|
}
|
||||||
@ -380,7 +374,6 @@ func vipGiftResourcesFromConfig(config model.VipLevelConfig) []vipGiftResource {
|
|||||||
{resourceID: config.EntryEffectResourceID, propsType: propsTypeRide},
|
{resourceID: config.EntryEffectResourceID, propsType: propsTypeRide},
|
||||||
{resourceID: config.ChatBubbleResourceID, propsType: propsTypeChatBubble},
|
{resourceID: config.ChatBubbleResourceID, propsType: propsTypeChatBubble},
|
||||||
{resourceID: config.FloatPictureResourceID, propsType: propsTypeFloatPicture},
|
{resourceID: config.FloatPictureResourceID, propsType: propsTypeFloatPicture},
|
||||||
{resourceID: config.BackgroundCardResourceID, propsType: propsTypeDataCard},
|
|
||||||
}
|
}
|
||||||
out := make([]vipGiftResource, 0, len(resources))
|
out := make([]vipGiftResource, 0, len(resources))
|
||||||
for _, resource := range resources {
|
for _, resource := range resources {
|
||||||
@ -391,104 +384,6 @@ func vipGiftResourcesFromConfig(config model.VipLevelConfig) []vipGiftResource {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func vipGiftResourcesFromState(state *model.UserVipState) []vipGiftResource {
|
|
||||||
if state == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return compactVIPGiftResources([]vipGiftResource{
|
|
||||||
{resourceID: state.BadgeResourceID, propsType: propsTypeBadge, badge: true},
|
|
||||||
{resourceID: state.ShortBadgeResourceID, propsType: propsTypeBadge, badge: true},
|
|
||||||
{resourceID: state.AvatarFrameResourceID, propsType: propsTypeAvatarFrame},
|
|
||||||
{resourceID: state.EntryEffectResourceID, propsType: propsTypeRide},
|
|
||||||
{resourceID: state.ChatBubbleResourceID, propsType: propsTypeChatBubble},
|
|
||||||
{resourceID: state.FloatPictureResourceID, propsType: propsTypeFloatPicture},
|
|
||||||
{resourceID: state.BackgroundCardResourceID, propsType: propsTypeDataCard},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func compactVIPGiftResources(resources []vipGiftResource) []vipGiftResource {
|
|
||||||
out := make([]vipGiftResource, 0, len(resources))
|
|
||||||
for _, resource := range resources {
|
|
||||||
if resource.resourceID > 0 {
|
|
||||||
out = append(out, resource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) vipGiftResourcesFromSysOrigin(ctx context.Context, sysOrigin string) ([]vipGiftResource, error) {
|
|
||||||
var configs []model.VipLevelConfig
|
|
||||||
if err := s.db.WithContext(ctx).
|
|
||||||
Where("sys_origin = ?", normalizeSysOrigin(sysOrigin)).
|
|
||||||
Find(&configs).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resources := make([]vipGiftResource, 0, len(configs)*7)
|
|
||||||
for _, config := range configs {
|
|
||||||
resources = append(resources, vipGiftResourcesFromConfig(config)...)
|
|
||||||
}
|
|
||||||
return resources, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) cleanupSupersededVIPGiftResources(ctx context.Context, userID int64, targetResources []vipGiftResource, allVIPResources []vipGiftResource, now time.Time) error {
|
|
||||||
targets := make(map[string]struct{}, len(targetResources))
|
|
||||||
for _, resource := range targetResources {
|
|
||||||
targets[vipGiftResourceKey(resource)] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
badgeIDs := make([]int64, 0)
|
|
||||||
propsIDsByType := map[string][]int64{}
|
|
||||||
seen := make(map[string]struct{}, len(allVIPResources))
|
|
||||||
for _, resource := range allVIPResources {
|
|
||||||
if resource.resourceID <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := vipGiftResourceKey(resource)
|
|
||||||
if _, exists := targets[key]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, exists := seen[key]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
if resource.badge {
|
|
||||||
badgeIDs = append(badgeIDs, resource.resourceID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(resource.propsType) != "" {
|
|
||||||
propsIDsByType[resource.propsType] = append(propsIDsByType[resource.propsType], resource.resourceID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(badgeIDs) > 0 {
|
|
||||||
if err := s.db.WithContext(ctx).Exec(
|
|
||||||
"UPDATE user_badge_backpack SET expire_type = ?, expire_time = ?, is_use_props = ?, update_time = ? WHERE user_id = ? AND badge_id IN ? AND expire_time > ?",
|
|
||||||
"TEMPORARY", now, false, now, userID, badgeIDs, now,
|
|
||||||
).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for propsType, propsIDs := range propsIDsByType {
|
|
||||||
if len(propsIDs) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := s.db.WithContext(ctx).Exec(
|
|
||||||
"UPDATE user_props_backpack SET expire_time = ?, is_use_props = ?, allow_give = ?, update_time = ? WHERE user_id = ? AND props_id IN ? AND type = ? AND expire_time > ?",
|
|
||||||
now, false, false, now, userID, propsIDs, propsType, now,
|
|
||||||
).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func vipGiftResourceKey(resource vipGiftResource) string {
|
|
||||||
if resource.badge {
|
|
||||||
return fmt.Sprintf("BADGE:%d", resource.resourceID)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s:%d", resource.propsType, resource.resourceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) managerApplicantName(ctx context.Context, managerID int64) string {
|
func (s *Service) managerApplicantName(ctx context.Context, managerID int64) string {
|
||||||
row, err := s.loadUserRowByID(ctx, managerID)
|
row, err := s.loadUserRowByID(ctx, managerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -20,11 +20,6 @@ import (
|
|||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
defaultTaskCenterArchiveBatchSize = 500
|
|
||||||
maxTaskCenterArchiveBatchSize = 1000
|
|
||||||
)
|
|
||||||
|
|
||||||
// StartArchiveWorker 启动任务中心事件 COS 归档 worker。
|
// StartArchiveWorker 启动任务中心事件 COS 归档 worker。
|
||||||
func (s *Service) StartArchiveWorker(ctx context.Context) error {
|
func (s *Service) StartArchiveWorker(ctx context.Context) error {
|
||||||
if !s.cfg.TaskCenter.Archive.Enabled {
|
if !s.cfg.TaskCenter.Archive.Enabled {
|
||||||
@ -73,7 +68,10 @@ func (s *Service) runArchiveWorker(ctx context.Context, workerIndex int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) archivePendingEvents(ctx context.Context) error {
|
func (s *Service) archivePendingEvents(ctx context.Context) error {
|
||||||
batchSize := normalizeTaskCenterArchiveBatchSize(s.cfg.TaskCenter.Archive.BatchSize)
|
batchSize := s.cfg.TaskCenter.Archive.BatchSize
|
||||||
|
if batchSize <= 0 {
|
||||||
|
batchSize = 10000
|
||||||
|
}
|
||||||
rows, err := s.claimArchiveRows(ctx, batchSize)
|
rows, err := s.claimArchiveRows(ctx, batchSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -99,46 +97,16 @@ func (s *Service) claimArchiveRows(ctx context.Context, batchSize int) ([]model.
|
|||||||
var rows []model.TaskCenterEventArchiveOutbox
|
var rows []model.TaskCenterEventArchiveOutbox
|
||||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
staleProcessingBefore := time.Now().Add(-10 * time.Minute)
|
staleProcessingBefore := time.Now().Add(-10 * time.Minute)
|
||||||
staleLimit, _ := taskCenterArchiveClaimLimits(batchSize)
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
|
||||||
if staleLimit > 0 {
|
Where("status IN ? OR (status = ? AND update_time < ?)",
|
||||||
var staleRows []model.TaskCenterEventArchiveOutbox
|
[]string{ArchiveStatusPending, ArchiveStatusFailed},
|
||||||
// PROCESSING 超时行保留固定接管额度,避免 PENDING/FAILED 持续堆积时旧锁永久饥饿。
|
ArchiveStatusProcessing,
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
|
staleProcessingBefore,
|
||||||
Where("status = ? AND update_time < ?", ArchiveStatusProcessing, staleProcessingBefore).
|
).
|
||||||
Order("update_time asc, id asc").
|
Order("create_time asc, id asc").
|
||||||
Limit(staleLimit).
|
Limit(batchSize).
|
||||||
Find(&staleRows).Error; err != nil {
|
Find(&rows).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
rows = append(rows, staleRows...)
|
|
||||||
}
|
|
||||||
if remaining := batchSize - len(rows); remaining > 0 {
|
|
||||||
pendingLimit := taskCenterArchivePendingClaimLimit(remaining)
|
|
||||||
for _, retryable := range []struct {
|
|
||||||
status string
|
|
||||||
limit int
|
|
||||||
}{
|
|
||||||
{status: ArchiveStatusPending, limit: pendingLimit},
|
|
||||||
{status: ArchiveStatusFailed, limit: remaining},
|
|
||||||
} {
|
|
||||||
if retryable.limit <= 0 || remaining <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if retryable.limit > remaining {
|
|
||||||
retryable.limit = remaining
|
|
||||||
}
|
|
||||||
var retryableRows []model.TaskCenterEventArchiveOutbox
|
|
||||||
// PENDING 和 FAILED 分开抢占,避免 IN 跨 status 排序触发 filesort,同时给 FAILED 保留重试额度。
|
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
|
|
||||||
Where("status = ?", retryable.status).
|
|
||||||
Order("create_time asc, id asc").
|
|
||||||
Limit(retryable.limit).
|
|
||||||
Find(&retryableRows).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rows = append(rows, retryableRows...)
|
|
||||||
remaining -= len(retryableRows)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@ -156,44 +124,6 @@ func (s *Service) claimArchiveRows(ctx context.Context, batchSize int) ([]model.
|
|||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeTaskCenterArchiveBatchSize(batchSize int) int {
|
|
||||||
if batchSize <= 0 {
|
|
||||||
return defaultTaskCenterArchiveBatchSize
|
|
||||||
}
|
|
||||||
if batchSize > maxTaskCenterArchiveBatchSize {
|
|
||||||
return maxTaskCenterArchiveBatchSize
|
|
||||||
}
|
|
||||||
return batchSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func taskCenterArchiveClaimLimits(batchSize int) (int, int) {
|
|
||||||
if batchSize <= 1 {
|
|
||||||
return batchSize, 0
|
|
||||||
}
|
|
||||||
staleLimit := batchSize / 10
|
|
||||||
if staleLimit < 1 {
|
|
||||||
staleLimit = 1
|
|
||||||
}
|
|
||||||
if staleLimit > 100 {
|
|
||||||
staleLimit = 100
|
|
||||||
}
|
|
||||||
return staleLimit, batchSize - staleLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
func taskCenterArchivePendingClaimLimit(retryableLimit int) int {
|
|
||||||
if retryableLimit <= 1 {
|
|
||||||
return retryableLimit
|
|
||||||
}
|
|
||||||
failedReserve := retryableLimit / 10
|
|
||||||
if failedReserve < 1 {
|
|
||||||
failedReserve = 1
|
|
||||||
}
|
|
||||||
if failedReserve > 100 {
|
|
||||||
failedReserve = 100
|
|
||||||
}
|
|
||||||
return retryableLimit - failedReserve
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) newCOSClient() (*cos.Client, error) {
|
func (s *Service) newCOSClient() (*cos.Client, error) {
|
||||||
bucketURL, err := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com",
|
bucketURL, err := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com",
|
||||||
strings.TrimSpace(s.cfg.TaskCenter.Archive.Bucket),
|
strings.TrimSpace(s.cfg.TaskCenter.Archive.Bucket),
|
||||||
|
|||||||
@ -42,73 +42,6 @@ func newTestService(t *testing.T, java taskCenterJavaGateway) (*Service, *gorm.D
|
|||||||
return service, db
|
return service, db
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeTaskCenterArchiveBatchSize(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input int
|
|
||||||
want int
|
|
||||||
}{
|
|
||||||
{name: "default", input: 0, want: defaultTaskCenterArchiveBatchSize},
|
|
||||||
{name: "negative default", input: -1, want: defaultTaskCenterArchiveBatchSize},
|
|
||||||
{name: "keeps configured value", input: 200, want: 200},
|
|
||||||
{name: "caps large batch", input: 10000, want: maxTaskCenterArchiveBatchSize},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := normalizeTaskCenterArchiveBatchSize(tt.input); got != tt.want {
|
|
||||||
t.Fatalf("normalizeTaskCenterArchiveBatchSize(%d) = %d, want %d", tt.input, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTaskCenterArchiveClaimLimitsReserveStaleRows(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
batchSize int
|
|
||||||
wantStale int
|
|
||||||
wantRetryable int
|
|
||||||
}{
|
|
||||||
{name: "single row", batchSize: 1, wantStale: 1, wantRetryable: 0},
|
|
||||||
{name: "small batch keeps one stale slot", batchSize: 5, wantStale: 1, wantRetryable: 4},
|
|
||||||
{name: "normal batch uses ten percent", batchSize: 500, wantStale: 50, wantRetryable: 450},
|
|
||||||
{name: "large batch caps stale slot", batchSize: 1000, wantStale: 100, wantRetryable: 900},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gotStale, gotRetryable := taskCenterArchiveClaimLimits(tt.batchSize)
|
|
||||||
if gotStale != tt.wantStale || gotRetryable != tt.wantRetryable {
|
|
||||||
t.Fatalf("taskCenterArchiveClaimLimits(%d) = (%d, %d), want (%d, %d)",
|
|
||||||
tt.batchSize, gotStale, gotRetryable, tt.wantStale, tt.wantRetryable)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTaskCenterArchivePendingClaimLimitReservesFailedRows(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
retryableLimit int
|
|
||||||
want int
|
|
||||||
}{
|
|
||||||
{name: "empty", retryableLimit: 0, want: 0},
|
|
||||||
{name: "single row stays pending first", retryableLimit: 1, want: 1},
|
|
||||||
{name: "small retry batch keeps one failed slot", retryableLimit: 4, want: 3},
|
|
||||||
{name: "normal retry batch reserves ten percent", retryableLimit: 450, want: 405},
|
|
||||||
{name: "large retry batch caps failed reserve", retryableLimit: 900, want: 810},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := taskCenterArchivePendingClaimLimit(tt.retryableLimit); got != tt.want {
|
|
||||||
t.Fatalf("taskCenterArchivePendingClaimLimit(%d) = %d, want %d", tt.retryableLimit, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDailyTaskConfigEventListAndClaim(t *testing.T) {
|
func TestDailyTaskConfigEventListAndClaim(t *testing.T) {
|
||||||
java := &taskCenterTestGateway{events: map[string]bool{}}
|
java := &taskCenterTestGateway{events: map[string]bool{}}
|
||||||
service, db := newTestService(t, java)
|
service, db := newTestService(t, java)
|
||||||
|
|||||||
@ -170,7 +170,7 @@ func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseReque
|
|||||||
if stateErr != nil {
|
if stateErr != nil {
|
||||||
return nil, stateErr
|
return nil, stateErr
|
||||||
}
|
}
|
||||||
if err := s.ensureStateResourcesGranted(ctx, sysOrigin, user.UserID, state, now); err != nil {
|
if err := s.ensureStateResourcesGranted(ctx, user.UserID, state, now); err != nil {
|
||||||
return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error())
|
return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error())
|
||||||
}
|
}
|
||||||
resources, resourceErr := s.loadResourceLookup(ctx, appendResourceIDsFromState(nil, state))
|
resources, resourceErr := s.loadResourceLookup(ctx, appendResourceIDsFromState(nil, state))
|
||||||
@ -274,7 +274,6 @@ func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseReque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
previousResources := vipGrantResourcesFromState(state)
|
|
||||||
applyPlanToState(state, plan, now)
|
applyPlanToState(state, plan, now)
|
||||||
order.Status = orderStatusSuccess
|
order.Status = orderStatusSuccess
|
||||||
order.ErrorMessage = ""
|
order.ErrorMessage = ""
|
||||||
@ -290,7 +289,7 @@ func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseReque
|
|||||||
}
|
}
|
||||||
committed = true
|
committed = true
|
||||||
|
|
||||||
if err := s.ensurePlanResourcesGranted(ctx, sysOrigin, user.UserID, plan, configs, previousResources, now); err != nil {
|
if err := s.ensurePlanResourcesGranted(ctx, user.UserID, plan, now); err != nil {
|
||||||
return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error())
|
return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -537,28 +536,15 @@ func expireState(state *model.UserVipState, now time.Time) {
|
|||||||
state.UpdateTime = now
|
state.UpdateTime = now
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensurePlanResourcesGranted(ctx context.Context, sysOrigin string, userID int64, plan purchasePlan, configs map[int]model.VipLevelConfig, additionalOldResources []vipGrantResource, now time.Time) error {
|
func (s *Service) ensurePlanResourcesGranted(ctx context.Context, userID int64, plan purchasePlan, now time.Time) error {
|
||||||
targetResources := vipGrantResourcesFromConfig(plan.TargetConfig)
|
return s.ensureVIPResourcesGranted(ctx, userID, vipGrantResourcesFromConfig(plan.TargetConfig), plan.ExpireAt, plan.DurationDays, now)
|
||||||
if err := s.ensureVIPResourcesGranted(ctx, userID, targetResources, plan.ExpireAt, plan.DurationDays, now); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
allVIPResources := append(vipGrantResourcesFromConfigs(configs), additionalOldResources...)
|
|
||||||
return s.cleanupSupersededVIPResources(ctx, userID, targetResources, allVIPResources, now)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureStateResourcesGranted(ctx context.Context, sysOrigin string, userID int64, state *model.UserVipState, now time.Time) error {
|
func (s *Service) ensureStateResourcesGranted(ctx context.Context, userID int64, state *model.UserVipState, now time.Time) error {
|
||||||
if !isActiveState(state, now) {
|
if !isActiveState(state, now) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
targetResources := vipGrantResourcesFromState(state)
|
return s.ensureVIPResourcesGranted(ctx, userID, vipGrantResourcesFromState(state), state.ExpireAt, vipGrantDaysUntil(now, state.ExpireAt), now)
|
||||||
if err := s.ensureVIPResourcesGranted(ctx, userID, targetResources, state.ExpireAt, vipGrantDaysUntil(now, state.ExpireAt), now); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
configs, err := s.loadConfigMap(ctx, sysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.cleanupSupersededVIPResources(ctx, userID, targetResources, vipGrantResourcesFromConfigs(configs), now)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureVIPResourcesGranted(ctx context.Context, userID int64, resources []vipGrantResource, expireAt time.Time, days int, now time.Time) error {
|
func (s *Service) ensureVIPResourcesGranted(ctx context.Context, userID int64, resources []vipGrantResource, expireAt time.Time, days int, now time.Time) error {
|
||||||
@ -699,17 +685,6 @@ func vipGrantResourcesFromState(state *model.UserVipState) []vipGrantResource {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func vipGrantResourcesFromConfigs(configs map[int]model.VipLevelConfig) []vipGrantResource {
|
|
||||||
if len(configs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resources := make([]vipGrantResource, 0, len(configs)*7)
|
|
||||||
for _, config := range configs {
|
|
||||||
resources = append(resources, vipGrantResourcesFromConfig(config)...)
|
|
||||||
}
|
|
||||||
return compactVIPGrantResources(resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
func compactVIPGrantResources(resources []vipGrantResource) []vipGrantResource {
|
func compactVIPGrantResources(resources []vipGrantResource) []vipGrantResource {
|
||||||
compacted := make([]vipGrantResource, 0, len(resources))
|
compacted := make([]vipGrantResource, 0, len(resources))
|
||||||
for _, resource := range resources {
|
for _, resource := range resources {
|
||||||
@ -720,65 +695,6 @@ func compactVIPGrantResources(resources []vipGrantResource) []vipGrantResource {
|
|||||||
return compacted
|
return compacted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) cleanupSupersededVIPResources(ctx context.Context, userID int64, targetResources []vipGrantResource, allVIPResources []vipGrantResource, now time.Time) error {
|
|
||||||
targets := make(map[string]struct{}, len(targetResources))
|
|
||||||
for _, resource := range targetResources {
|
|
||||||
targets[vipGrantResourceKey(resource)] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
badgeIDs := make([]int64, 0)
|
|
||||||
propsIDsByType := map[string][]int64{}
|
|
||||||
seen := make(map[string]struct{}, len(allVIPResources))
|
|
||||||
for _, resource := range allVIPResources {
|
|
||||||
if resource.ResourceID <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := vipGrantResourceKey(resource)
|
|
||||||
if _, exists := targets[key]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, exists := seen[key]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[key] = struct{}{}
|
|
||||||
if resource.Badge {
|
|
||||||
badgeIDs = append(badgeIDs, resource.ResourceID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(resource.PropsType) != "" {
|
|
||||||
propsIDsByType[resource.PropsType] = append(propsIDsByType[resource.PropsType], resource.ResourceID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(badgeIDs) > 0 {
|
|
||||||
if err := s.db.WithContext(ctx).Exec(
|
|
||||||
"UPDATE user_badge_backpack SET expire_type = ?, expire_time = ?, is_use_props = ?, update_time = ? WHERE user_id = ? AND badge_id IN ? AND expire_time > ?",
|
|
||||||
"TEMPORARY", now, false, now, userID, badgeIDs, now,
|
|
||||||
).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for propsType, propsIDs := range propsIDsByType {
|
|
||||||
if len(propsIDs) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := s.db.WithContext(ctx).Exec(
|
|
||||||
"UPDATE user_props_backpack SET expire_time = ?, is_use_props = ?, allow_give = ?, update_time = ? WHERE user_id = ? AND props_id IN ? AND type = ? AND expire_time > ?",
|
|
||||||
now, false, false, now, userID, propsIDs, propsType, now,
|
|
||||||
).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func vipGrantResourceKey(resource vipGrantResource) string {
|
|
||||||
if resource.Badge {
|
|
||||||
return fmt.Sprintf("BADGE:%d", resource.ResourceID)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s:%d", resource.PropsType, resource.ResourceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func vipGrantDaysUntil(now time.Time, expireAt time.Time) int {
|
func vipGrantDaysUntil(now time.Time, expireAt time.Time) int {
|
||||||
if !expireAt.After(now) {
|
if !expireAt.After(now) {
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package vip
|
package vip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -10,9 +9,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"chatapp3-golang/internal/model"
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildPurchasePlanNewPurchase(t *testing.T) {
|
func TestBuildPurchasePlanNewPurchase(t *testing.T) {
|
||||||
@ -326,47 +322,6 @@ func TestVIPGrantResourcesFromConfigMapsBackpackTypes(t *testing.T) {
|
|||||||
assertGrant(6, 16, vipPropsTypeDataCard, false)
|
assertGrant(6, 16, vipPropsTypeDataCard, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanupSupersededVIPResourcesExpiresOnlyOldVIPResources(t *testing.T) {
|
|
||||||
db := openVIPCleanupTestDB(t)
|
|
||||||
service := &Service{db: db}
|
|
||||||
now := time.Date(2026, 5, 30, 10, 0, 0, 0, time.UTC)
|
|
||||||
future := now.AddDate(0, 0, 20)
|
|
||||||
oldTime := now.AddDate(0, 0, -1)
|
|
||||||
userID := int64(1001)
|
|
||||||
|
|
||||||
seedVIPCleanupRows(t, db,
|
|
||||||
vipCleanupPropsRow{ID: 1, UserID: userID, PropsID: 101, Type: vipPropsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true, UpdateTime: oldTime},
|
|
||||||
vipCleanupPropsRow{ID: 2, UserID: userID, PropsID: 201, Type: vipPropsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true, UpdateTime: oldTime},
|
|
||||||
vipCleanupPropsRow{ID: 3, UserID: userID, PropsID: 999, Type: vipPropsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true, UpdateTime: oldTime},
|
|
||||||
vipCleanupPropsRow{ID: 4, UserID: userID, PropsID: 102, Type: vipPropsTypeRide, ExpireTime: future, UseProps: true, AllowGive: true, UpdateTime: oldTime},
|
|
||||||
)
|
|
||||||
seedVIPCleanupBadges(t, db,
|
|
||||||
vipCleanupBadgeRow{ID: 1, UserID: userID, BadgeID: 100, ExpireTime: future, UseProps: true, UpdateTime: oldTime},
|
|
||||||
vipCleanupBadgeRow{ID: 2, UserID: userID, BadgeID: 200, ExpireTime: future, UseProps: true, UpdateTime: oldTime},
|
|
||||||
)
|
|
||||||
|
|
||||||
target := vipGrantResourcesFromConfig(model.VipLevelConfig{
|
|
||||||
BadgeResourceID: 200,
|
|
||||||
AvatarFrameResourceID: 201,
|
|
||||||
})
|
|
||||||
all := append(vipGrantResourcesFromConfig(model.VipLevelConfig{
|
|
||||||
BadgeResourceID: 100,
|
|
||||||
AvatarFrameResourceID: 101,
|
|
||||||
EntryEffectResourceID: 102,
|
|
||||||
}), target...)
|
|
||||||
|
|
||||||
if err := service.cleanupSupersededVIPResources(context.Background(), userID, target, all, now); err != nil {
|
|
||||||
t.Fatalf("cleanupSupersededVIPResources() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertVIPCleanupProps(t, db, 101, now, false, false)
|
|
||||||
assertVIPCleanupProps(t, db, 102, now, false, false)
|
|
||||||
assertVIPCleanupProps(t, db, 201, future, true, true)
|
|
||||||
assertVIPCleanupProps(t, db, 999, future, true, true)
|
|
||||||
assertVIPCleanupBadge(t, db, 100, now, false)
|
|
||||||
assertVIPCleanupBadge(t, db, 200, future, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVIPGrantDaysUntilCeilsPartialDay(t *testing.T) {
|
func TestVIPGrantDaysUntilCeilsPartialDay(t *testing.T) {
|
||||||
now := time.Date(2026, 4, 29, 10, 0, 0, 0, time.UTC)
|
now := time.Date(2026, 4, 29, 10, 0, 0, 0, time.UTC)
|
||||||
expireAt := now.Add((24 * time.Hour) + time.Second)
|
expireAt := now.Add((24 * time.Hour) + time.Second)
|
||||||
@ -379,89 +334,6 @@ func TestVIPGrantDaysUntilCeilsPartialDay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type vipCleanupPropsRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
UserID int64 `gorm:"column:user_id"`
|
|
||||||
PropsID int64 `gorm:"column:props_id"`
|
|
||||||
Type string `gorm:"column:type"`
|
|
||||||
ExpireTime time.Time `gorm:"column:expire_time"`
|
|
||||||
UseProps bool `gorm:"column:is_use_props"`
|
|
||||||
AllowGive bool `gorm:"column:allow_give"`
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vipCleanupPropsRow) TableName() string {
|
|
||||||
return "user_props_backpack"
|
|
||||||
}
|
|
||||||
|
|
||||||
type vipCleanupBadgeRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
UserID int64 `gorm:"column:user_id"`
|
|
||||||
BadgeID int64 `gorm:"column:badge_id"`
|
|
||||||
ExpireType string `gorm:"column:expire_type"`
|
|
||||||
ExpireTime time.Time `gorm:"column:expire_time"`
|
|
||||||
UseProps bool `gorm:"column:is_use_props"`
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vipCleanupBadgeRow) TableName() string {
|
|
||||||
return "user_badge_backpack"
|
|
||||||
}
|
|
||||||
|
|
||||||
func openVIPCleanupTestDB(t *testing.T) *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(&vipCleanupPropsRow{}, &vipCleanupBadgeRow{}); err != nil {
|
|
||||||
t.Fatalf("auto migrate cleanup tables: %v", err)
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedVIPCleanupRows(t *testing.T, db *gorm.DB, rows ...vipCleanupPropsRow) {
|
|
||||||
t.Helper()
|
|
||||||
for _, row := range rows {
|
|
||||||
if err := db.Create(&row).Error; err != nil {
|
|
||||||
t.Fatalf("seed props row: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedVIPCleanupBadges(t *testing.T, db *gorm.DB, rows ...vipCleanupBadgeRow) {
|
|
||||||
t.Helper()
|
|
||||||
for _, row := range rows {
|
|
||||||
if err := db.Create(&row).Error; err != nil {
|
|
||||||
t.Fatalf("seed badge row: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertVIPCleanupProps(t *testing.T, db *gorm.DB, propsID int64, expireTime time.Time, useProps bool, allowGive bool) {
|
|
||||||
t.Helper()
|
|
||||||
var row vipCleanupPropsRow
|
|
||||||
if err := db.Where("props_id = ?", propsID).First(&row).Error; err != nil {
|
|
||||||
t.Fatalf("load props %d: %v", propsID, err)
|
|
||||||
}
|
|
||||||
if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps || row.AllowGive != allowGive {
|
|
||||||
t.Fatalf("props %d = expire:%s use:%v allow:%v, want expire:%s use:%v allow:%v",
|
|
||||||
propsID, row.ExpireTime, row.UseProps, row.AllowGive, expireTime, useProps, allowGive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertVIPCleanupBadge(t *testing.T, db *gorm.DB, badgeID int64, expireTime time.Time, useProps bool) {
|
|
||||||
t.Helper()
|
|
||||||
var row vipCleanupBadgeRow
|
|
||||||
if err := db.Where("badge_id = ?", badgeID).First(&row).Error; err != nil {
|
|
||||||
t.Fatalf("load badge %d: %v", badgeID, err)
|
|
||||||
}
|
|
||||||
if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps {
|
|
||||||
t.Fatalf("badge %d = expire:%s use:%v, want expire:%s use:%v",
|
|
||||||
badgeID, row.ExpireTime, row.UseProps, expireTime, useProps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func vipTestConfigs() map[int]model.VipLevelConfig {
|
func vipTestConfigs() map[int]model.VipLevelConfig {
|
||||||
configs := make(map[int]model.VipLevelConfig, levelCount)
|
configs := make(map[int]model.VipLevelConfig, levelCount)
|
||||||
for level := 1; level <= levelCount; level++ {
|
for level := 1; level <= levelCount; level++ {
|
||||||
|
|||||||
@ -126,18 +126,16 @@ func (s *Service) PageAdminRecords(
|
|||||||
records = append(records, payload)
|
records = append(records, payload)
|
||||||
}
|
}
|
||||||
return &RecordPageResponse{
|
return &RecordPageResponse{
|
||||||
Records: records,
|
Records: records,
|
||||||
Total: total,
|
Total: total,
|
||||||
Current: cursor,
|
Current: cursor,
|
||||||
Size: limit,
|
Size: limit,
|
||||||
TotalPaidGold: summary.TotalPaidGold,
|
TotalPaidGold: summary.TotalPaidGold,
|
||||||
TotalRewardGold: summary.TotalRewardGold,
|
TotalRewardGold: summary.TotalRewardGold,
|
||||||
TotalGiftRewardGold: summary.TotalGiftRewardGold,
|
TotalDrawTimes: summary.TotalDrawTimes,
|
||||||
TotalDecorationRewardGold: summary.TotalDecorationRewardGold,
|
ParticipantCount: summary.ParticipantCount,
|
||||||
TotalDrawTimes: summary.TotalDrawTimes,
|
StartTime: formatDateTime(filter.StartTime),
|
||||||
ParticipantCount: summary.ParticipantCount,
|
EndTime: formatDateTime(filter.EndTime),
|
||||||
StartTime: formatDateTime(filter.StartTime),
|
|
||||||
EndTime: formatDateTime(filter.EndTime),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,12 +250,10 @@ type wheelAdminRecordFilter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type wheelAdminRecordSummary struct {
|
type wheelAdminRecordSummary struct {
|
||||||
TotalPaidGold int64
|
TotalPaidGold int64
|
||||||
TotalRewardGold int64
|
TotalRewardGold int64
|
||||||
TotalGiftRewardGold int64
|
TotalDrawTimes int64
|
||||||
TotalDecorationRewardGold int64
|
ParticipantCount int64
|
||||||
TotalDrawTimes int64
|
|
||||||
ParticipantCount int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWheelAdminRecordFilter(sysOrigin string, category string, userID int64, status string, startRaw string, endRaw string) (wheelAdminRecordFilter, error) {
|
func newWheelAdminRecordFilter(sysOrigin string, category string, userID int64, status string, startRaw string, endRaw string) (wheelAdminRecordFilter, error) {
|
||||||
@ -352,16 +348,9 @@ func (s *Service) adminRecordQuery(ctx context.Context, filter wheelAdminRecordF
|
|||||||
|
|
||||||
func (s *Service) adminRecordSummary(ctx context.Context, filter wheelAdminRecordFilter) (wheelAdminRecordSummary, error) {
|
func (s *Service) adminRecordSummary(ctx context.Context, filter wheelAdminRecordFilter) (wheelAdminRecordSummary, error) {
|
||||||
var summary wheelAdminRecordSummary
|
var summary wheelAdminRecordSummary
|
||||||
// totalRewardGold keeps the historical dashboard number, including direct gold rewards.
|
|
||||||
// The two split fields only count RESOURCE rewards: GIFT is the gift value, every other
|
|
||||||
// resourceType is treated as decoration value for the admin cards.
|
|
||||||
if err := s.adminRecordQuery(ctx, filter).
|
if err := s.adminRecordQuery(ctx, filter).
|
||||||
Select(`
|
Select("COALESCE(SUM(CASE WHEN status = ? THEN CASE WHEN reward_type = ? THEN gold_amount ELSE display_gold_amount END ELSE 0 END), 0)", drawStatusSuccess, rewardTypeGold).
|
||||||
COALESCE(SUM(CASE WHEN status = ? THEN CASE WHEN reward_type = ? THEN gold_amount ELSE display_gold_amount END ELSE 0 END), 0) AS total_reward_gold,
|
Scan(&summary.TotalRewardGold).Error; err != nil {
|
||||||
COALESCE(SUM(CASE WHEN status = ? AND reward_type = ? AND UPPER(resource_type) = ? THEN display_gold_amount ELSE 0 END), 0) AS total_gift_reward_gold,
|
|
||||||
COALESCE(SUM(CASE WHEN status = ? AND reward_type = ? AND UPPER(resource_type) <> ? THEN display_gold_amount ELSE 0 END), 0) AS total_decoration_reward_gold
|
|
||||||
`, drawStatusSuccess, rewardTypeGold, drawStatusSuccess, rewardTypeResource, resourceTypeGift, drawStatusSuccess, rewardTypeResource, resourceTypeGift).
|
|
||||||
Scan(&summary).Error; err != nil {
|
|
||||||
return summary, err
|
return summary, err
|
||||||
}
|
}
|
||||||
if err := s.adminRecordQuery(ctx, filter).
|
if err := s.adminRecordQuery(ctx, filter).
|
||||||
|
|||||||
@ -104,8 +104,8 @@ func TestPageAdminRecordsIncludesUserInfoAndRangeSummary(t *testing.T) {
|
|||||||
PaidGold: 200,
|
PaidGold: 200,
|
||||||
RewardType: rewardTypeResource,
|
RewardType: rewardTypeResource,
|
||||||
ResourceID: &giftID,
|
ResourceID: &giftID,
|
||||||
ResourceType: "AVATAR_FRAME",
|
ResourceType: resourceTypeGift,
|
||||||
ResourceName: "Frame B",
|
ResourceName: "Gift B",
|
||||||
DisplayGoldAmount: 30,
|
DisplayGoldAmount: 30,
|
||||||
Probability: 500000,
|
Probability: 500000,
|
||||||
Status: drawStatusSuccess,
|
Status: drawStatusSuccess,
|
||||||
@ -149,12 +149,6 @@ func TestPageAdminRecordsIncludesUserInfoAndRangeSummary(t *testing.T) {
|
|||||||
if resp.TotalRewardGold != 150 {
|
if resp.TotalRewardGold != 150 {
|
||||||
t.Fatalf("total reward = %d, want 150", resp.TotalRewardGold)
|
t.Fatalf("total reward = %d, want 150", resp.TotalRewardGold)
|
||||||
}
|
}
|
||||||
if resp.TotalGiftRewardGold != 50 {
|
|
||||||
t.Fatalf("total gift reward = %d, want 50", resp.TotalGiftRewardGold)
|
|
||||||
}
|
|
||||||
if resp.TotalDecorationRewardGold != 30 {
|
|
||||||
t.Fatalf("total decoration reward = %d, want 30", resp.TotalDecorationRewardGold)
|
|
||||||
}
|
|
||||||
if resp.TotalDrawTimes != 12 {
|
if resp.TotalDrawTimes != 12 {
|
||||||
t.Fatalf("total draw times = %d, want 12", resp.TotalDrawTimes)
|
t.Fatalf("total draw times = %d, want 12", resp.TotalDrawTimes)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -307,18 +307,16 @@ type DrawRecordPayload struct {
|
|||||||
|
|
||||||
// RecordPageResponse is an admin record page.
|
// RecordPageResponse is an admin record page.
|
||||||
type RecordPageResponse struct {
|
type RecordPageResponse struct {
|
||||||
Records []DrawRecordPayload `json:"records"`
|
Records []DrawRecordPayload `json:"records"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
Current int `json:"current"`
|
Current int `json:"current"`
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
TotalPaidGold int64 `json:"totalPaidGold,omitempty"`
|
TotalPaidGold int64 `json:"totalPaidGold,omitempty"`
|
||||||
TotalRewardGold int64 `json:"totalRewardGold,omitempty"`
|
TotalRewardGold int64 `json:"totalRewardGold,omitempty"`
|
||||||
TotalGiftRewardGold int64 `json:"totalGiftRewardGold,omitempty"`
|
TotalDrawTimes int64 `json:"totalDrawTimes,omitempty"`
|
||||||
TotalDecorationRewardGold int64 `json:"totalDecorationRewardGold,omitempty"`
|
ParticipantCount int64 `json:"participantCount,omitempty"`
|
||||||
TotalDrawTimes int64 `json:"totalDrawTimes,omitempty"`
|
StartTime string `json:"startTime,omitempty"`
|
||||||
ParticipantCount int64 `json:"participantCount,omitempty"`
|
EndTime string `json:"endTime,omitempty"`
|
||||||
StartTime string `json:"startTime,omitempty"`
|
|
||||||
EndTime string `json:"endTime,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeSysOrigin(sysOrigin string) string {
|
func normalizeSysOrigin(sysOrigin string) string {
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
-- Limit LIKEI app game-list rows to the Turkey region.
|
|
||||||
-- Production Turkey Region observed in sys_region_config: 2046066409959649281.
|
|
||||||
-- Re-check sys_region_config before production execution if region data has been rebuilt.
|
|
||||||
-- After direct SQL execution, clear Redis GAME_LIST* cache or wait for its 1-hour TTL.
|
|
||||||
|
|
||||||
START TRANSACTION;
|
|
||||||
|
|
||||||
SET @sys_origin := 'LIKEI';
|
|
||||||
SET @turkey_region_id := '2046066409959649281';
|
|
||||||
|
|
||||||
UPDATE sys_game_list_config
|
|
||||||
SET regions = @turkey_region_id,
|
|
||||||
update_time = NOW(3)
|
|
||||||
WHERE sys_origin = @sys_origin
|
|
||||||
AND is_showcase = 1;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS hotgame_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,
|
|
||||||
app_key VARCHAR(255) NOT NULL DEFAULT '',
|
|
||||||
callback_base_url VARCHAR(1024) NOT NULL DEFAULT '',
|
|
||||||
test_uid VARCHAR(64) DEFAULT NULL,
|
|
||||||
test_token VARCHAR(1024) DEFAULT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_hotgame_provider_sys_origin_profile (sys_origin, profile),
|
|
||||||
KEY idx_hotgame_provider_active (sys_origin, active, update_time)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS hotgame_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 VARCHAR(64) NOT NULL,
|
|
||||||
name VARCHAR(128) NOT NULL,
|
|
||||||
cover VARCHAR(1024) DEFAULT NULL,
|
|
||||||
preview_url VARCHAR(1024) DEFAULT NULL,
|
|
||||||
download_url VARCHAR(1024) DEFAULT NULL,
|
|
||||||
package_version VARCHAR(32) DEFAULT NULL,
|
|
||||||
raw_json LONGTEXT DEFAULT NULL,
|
|
||||||
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_hotgame_catalog_sys_profile_vendor_game (sys_origin, profile, vendor_game_id),
|
|
||||||
KEY idx_hotgame_catalog_profile_internal_game (sys_origin, profile, internal_game_id, status)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
INSERT INTO hotgame_provider_config (
|
|
||||||
id, sys_origin, profile, active, app_key, callback_base_url, create_time, update_time
|
|
||||||
) VALUES
|
|
||||||
(202606100520001, 'LIKEI', 'PROD', 1, 'laluprodappkey', 'https://api.global-interaction.com/', NOW(), NOW()),
|
|
||||||
(202606100520002, 'LIKEI', 'TEST', 0, 'lalutestappkey', 'https://api-test.global-interaction.com/', NOW(), NOW())
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
app_key = IF(TRIM(app_key) = '', VALUES(app_key), app_key),
|
|
||||||
callback_base_url = IF(TRIM(callback_base_url) = '', VALUES(callback_base_url), callback_base_url),
|
|
||||||
update_time = NOW();
|
|
||||||
|
|
||||||
INSERT INTO hotgame_game_catalog (
|
|
||||||
id, sys_origin, profile, internal_game_id, vendor_game_id, name, cover,
|
|
||||||
preview_url, download_url, package_version, raw_json, status, create_time, update_time
|
|
||||||
) VALUES
|
|
||||||
(202606100520101, 'LIKEI', 'TEST', 'hg_1', '1', '幸运77(Lucky77)', '',
|
|
||||||
'https://hk.hotgame.win/Seven7/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxLCJtYWNoQXJlYSI6Mn0%3D&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://hk.hotgame.win/Seven7/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxLCJtYWNoQXJlYSI6Mn0%3D&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520102, 'LIKEI', 'TEST', 'hg_13', '13', '海盗捕鱼(PirateFishing)', '',
|
|
||||||
'https://hk.hotgame.win/PirateFishing/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxMywibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://hk.hotgame.win/PirateFishing/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxMywibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520103, 'LIKEI', 'TEST', 'hg_19', '19', 'FortuneSlot (宝石solt)', '',
|
|
||||||
'https://hk.hotgame.win/FortuneSlot/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxOSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://hk.hotgame.win/FortuneSlot/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxOSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520104, 'LIKEI', 'TEST', 'hg_15', '15', '美味摩天轮(Delicious)', '',
|
|
||||||
'https://hk.hotgame.win/Wheel_Delicious/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://hk.hotgame.win/Wheel_Delicious/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520105, 'LIKEI', 'TEST', 'hg_20', '20', '水果派对(FruitParty)', '',
|
|
||||||
'https://hk.hotgame.win/FruitParty2/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoyMCwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://hk.hotgame.win/FruitParty2/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoyMCwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520106, 'LIKEI', 'TEST', 'hg_16', '16', '太空摩天轮(GreedyStar)', '',
|
|
||||||
'https://hk.hotgame.win/Wheel_OuterSpace/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNiwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://hk.hotgame.win/Wheel_OuterSpace/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNiwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520201, 'LIKEI', 'PROD', 'hg_1', '1', '幸运77(Lucky77)', '',
|
|
||||||
'https://cdn-res.hotgame.win/Seven7/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxLCJtYWNoQXJlYSI6Mn0%3D&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://cdn-res.hotgame.win/Seven7/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxLCJtYWNoQXJlYSI6Mn0%3D&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520202, 'LIKEI', 'PROD', 'hg_13', '13', '海盗捕鱼(PirateFishing)', '',
|
|
||||||
'https://cdn-res.hotgame.win/PirateFishing/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxMywibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://cdn-res.hotgame.win/PirateFishing/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxMywibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520203, 'LIKEI', 'PROD', 'hg_19', '19', 'FortuneSlot (宝石solt)', '',
|
|
||||||
'https://cdn-res.hotgame.win/FortuneSlot/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxOSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://cdn-res.hotgame.win/FortuneSlot/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxOSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520204, 'LIKEI', 'PROD', 'hg_15', '15', '美味摩天轮(Delicious)', '',
|
|
||||||
'https://cdn-res.hotgame.win/Wheel_Delicious/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://cdn-res.hotgame.win/Wheel_Delicious/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNSwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520205, 'LIKEI', 'PROD', 'hg_20', '20', '水果派对(FruitParty)', '',
|
|
||||||
'https://cdn-res.hotgame.win/FruitParty2/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoyMCwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://cdn-res.hotgame.win/FruitParty2/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoyMCwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW()),
|
|
||||||
(202606100520206, 'LIKEI', 'PROD', 'hg_16', '16', '太空摩天轮(GreedyStar)', '',
|
|
||||||
'https://cdn-res.hotgame.win/Wheel_OuterSpace/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNiwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'https://cdn-res.hotgame.win/Wheel_OuterSpace/index.html?__inherit__=eyJwbGF0SWQiOjEwMTEsInBsYXRLZXkiOiJGWWRSMk02Z3FnIiwiZ2FtZUlkIjoxNiwibWFjaEFyZWEiOjJ9&uid={uid}&token={token}&lang={lang}',
|
|
||||||
'2026', '{"mode":"seven"}', 'ENABLED', NOW(), NOW())
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
internal_game_id = VALUES(internal_game_id),
|
|
||||||
name = VALUES(name),
|
|
||||||
cover = VALUES(cover),
|
|
||||||
preview_url = VALUES(preview_url),
|
|
||||||
download_url = VALUES(download_url),
|
|
||||||
package_version = VALUES(package_version),
|
|
||||||
raw_json = VALUES(raw_json),
|
|
||||||
status = VALUES(status),
|
|
||||||
update_time = NOW();
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
SET @current_schema := DATABASE();
|
|
||||||
|
|
||||||
SET @modify_sys_game_list_cover_sql := IF(
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = @current_schema
|
|
||||||
AND table_name = 'sys_game_list_config'
|
|
||||||
AND column_name = 'cover'
|
|
||||||
),
|
|
||||||
'ALTER TABLE `sys_game_list_config` MODIFY COLUMN `cover` VARCHAR(1024) DEFAULT NULL COMMENT ''封面图''',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE modify_sys_game_list_cover_stmt FROM @modify_sys_game_list_cover_sql;
|
|
||||||
EXECUTE modify_sys_game_list_cover_stmt;
|
|
||||||
DEALLOCATE PREPARE modify_sys_game_list_cover_stmt;
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
SET @table_schema = DATABASE();
|
|
||||||
|
|
||||||
SET @ddl = IF(
|
|
||||||
EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = @table_schema
|
|
||||||
AND TABLE_NAME = 'team_manager_base_info'
|
|
||||||
AND COLUMN_NAME = 'can_add_bd_leader'
|
|
||||||
),
|
|
||||||
'SELECT 1',
|
|
||||||
"ALTER TABLE `team_manager_base_info` ADD COLUMN `can_add_bd_leader` tinyint(1) NOT NULL DEFAULT 1 COMMENT '经理中心是否允许添加BD Leader' AFTER `region_id`"
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @ddl = IF(
|
|
||||||
EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = @table_schema
|
|
||||||
AND TABLE_NAME = 'team_manager_base_info'
|
|
||||||
AND COLUMN_NAME = 'can_gift_props'
|
|
||||||
),
|
|
||||||
'SELECT 1',
|
|
||||||
"ALTER TABLE `team_manager_base_info` ADD COLUMN `can_gift_props` tinyint(1) NOT NULL DEFAULT 1 COMMENT '经理中心是否允许赠送道具' AFTER `can_add_bd_leader`"
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @ddl = IF(
|
|
||||||
EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = @table_schema
|
|
||||||
AND TABLE_NAME = 'team_manager_base_info'
|
|
||||||
AND COLUMN_NAME = 'can_ban_user'
|
|
||||||
),
|
|
||||||
'SELECT 1',
|
|
||||||
"ALTER TABLE `team_manager_base_info` ADD COLUMN `can_ban_user` tinyint(1) NOT NULL DEFAULT 1 COMMENT '经理中心是否允许封禁用户' AFTER `can_gift_props`"
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @ddl = IF(
|
|
||||||
EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = @table_schema
|
|
||||||
AND TABLE_NAME = 'team_manager_base_info'
|
|
||||||
AND COLUMN_NAME = 'can_unban_user'
|
|
||||||
),
|
|
||||||
'SELECT 1',
|
|
||||||
"ALTER TABLE `team_manager_base_info` ADD COLUMN `can_unban_user` tinyint(1) NOT NULL DEFAULT 1 COMMENT '经理中心是否允许解封用户' AFTER `can_ban_user`"
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
SET @table_schema = DATABASE();
|
|
||||||
|
|
||||||
SET @ddl = IF(
|
|
||||||
EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = @table_schema
|
|
||||||
AND TABLE_NAME = 'team_manager_base_info'
|
|
||||||
AND COLUMN_NAME = 'can_change_country'
|
|
||||||
),
|
|
||||||
'SELECT 1',
|
|
||||||
"ALTER TABLE `team_manager_base_info` ADD COLUMN `can_change_country` tinyint(1) NOT NULL DEFAULT 1 COMMENT '经理中心是否允许修改用户国家' AFTER `can_unban_user`"
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
SET @current_schema := DATABASE();
|
|
||||||
|
|
||||||
SET @add_task_center_archive_claim_create_sql := IF(
|
|
||||||
NOT EXISTS (
|
|
||||||
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE table_schema = @current_schema
|
|
||||||
AND table_name = 'task_center_event_archive_outbox'
|
|
||||||
AND index_name = 'idx_task_center_archive_claim_create'
|
|
||||||
),
|
|
||||||
'ALTER TABLE `task_center_event_archive_outbox` ADD INDEX `idx_task_center_archive_claim_create` (`status`, `create_time`, `id`)',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE add_task_center_archive_claim_create_stmt FROM @add_task_center_archive_claim_create_sql;
|
|
||||||
EXECUTE add_task_center_archive_claim_create_stmt;
|
|
||||||
DEALLOCATE PREPARE add_task_center_archive_claim_create_stmt;
|
|
||||||
|
|
||||||
SET @add_task_center_archive_claim_stale_sql := IF(
|
|
||||||
NOT EXISTS (
|
|
||||||
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE table_schema = @current_schema
|
|
||||||
AND table_name = 'task_center_event_archive_outbox'
|
|
||||||
AND index_name = 'idx_task_center_archive_claim_stale'
|
|
||||||
),
|
|
||||||
'ALTER TABLE `task_center_event_archive_outbox` ADD INDEX `idx_task_center_archive_claim_stale` (`status`, `update_time`, `id`)',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE add_task_center_archive_claim_stale_stmt FROM @add_task_center_archive_claim_stale_sql;
|
|
||||||
EXECUTE add_task_center_archive_claim_stale_stmt;
|
|
||||||
DEALLOCATE PREPARE add_task_center_archive_claim_stale_stmt;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
-- dashboard-cdc-worker 每 5 分钟按 create_time 24 小时窗口回读 game_lucky_gift_count(1400 万行),
|
|
||||||
-- 原有索引均以 sys_origin,user_id 开头,无法按时间窗口定位,只能从 user_base_info 反向驱动全量探测
|
|
||||||
-- (单次 examined ~18 万行,是线上 MySQL CPU 波动来源之一)。
|
|
||||||
-- 本索引已于 2026-07-10 通过 ALGORITHM=INPLACE, LOCK=NONE 在生产手工应用(耗时 19s),此文件用于存档与新环境初始化。
|
|
||||||
|
|
||||||
SET @current_schema := DATABASE();
|
|
||||||
|
|
||||||
SET @add_game_lucky_gift_count_sorigin_ctime_sql := IF(
|
|
||||||
NOT EXISTS (
|
|
||||||
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE table_schema = @current_schema
|
|
||||||
AND table_name = 'game_lucky_gift_count'
|
|
||||||
AND index_name = 'idx_game_lucky_gift_count_sorigin_ctime'
|
|
||||||
),
|
|
||||||
'ALTER TABLE `game_lucky_gift_count` ADD INDEX `idx_game_lucky_gift_count_sorigin_ctime` (`sys_origin`, `create_time`), ALGORITHM=INPLACE, LOCK=NONE',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE add_game_lucky_gift_count_sorigin_ctime_stmt FROM @add_game_lucky_gift_count_sorigin_ctime_sql;
|
|
||||||
EXECUTE add_game_lucky_gift_count_sorigin_ctime_stmt;
|
|
||||||
DEALLOCATE PREPARE add_game_lucky_gift_count_sorigin_ctime_stmt;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user