Compare commits
No commits in common. "8f8ae40947aa4163258826393c47b9b2792137bb" and "df9a779b621d0b5ffc80d62b01b0771685932b2d" have entirely different histories.
8f8ae40947
...
df9a779b62
@ -4,8 +4,6 @@ import (
|
|||||||
"chatapp3-golang/internal/bootstrap"
|
"chatapp3-golang/internal/bootstrap"
|
||||||
"chatapp3-golang/internal/router"
|
"chatapp3-golang/internal/router"
|
||||||
"chatapp3-golang/internal/service/baishun"
|
"chatapp3-golang/internal/service/baishun"
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
|
||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
"chatapp3-golang/internal/service/registerreward"
|
"chatapp3-golang/internal/service/registerreward"
|
||||||
@ -32,19 +30,13 @@ func main() {
|
|||||||
|
|
||||||
inviteService := invite.NewInviteService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
inviteService := invite.NewInviteService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
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)
|
||||||
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)
|
||||||
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
gameProviders := gameprovider.NewRegistry(
|
|
||||||
baishun.NewAppProvider(baishunService),
|
|
||||||
)
|
|
||||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||||
Invite: inviteService,
|
Invite: inviteService,
|
||||||
Baishun: baishunService,
|
Baishun: baishunService,
|
||||||
GameOpen: gameOpenService,
|
|
||||||
GameProviders: gameProviders,
|
|
||||||
LuckyGift: luckyGiftService,
|
LuckyGift: luckyGiftService,
|
||||||
RegisterReward: registerRewardService,
|
RegisterReward: registerRewardService,
|
||||||
SignInReward: signInRewardService,
|
SignInReward: signInRewardService,
|
||||||
|
|||||||
@ -17,7 +17,6 @@ type Config struct {
|
|||||||
Invite InviteConfig
|
Invite InviteConfig
|
||||||
WeekStar WeekStarConfig
|
WeekStar WeekStarConfig
|
||||||
Baishun BaishunConfig
|
Baishun BaishunConfig
|
||||||
GameOpen GameOpenConfig
|
|
||||||
LuckyGift LuckyGiftConfig
|
LuckyGift LuckyGiftConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,21 +101,11 @@ type BaishunConfig struct {
|
|||||||
SSTokenTTLSeconds int
|
SSTokenTTLSeconds int
|
||||||
}
|
}
|
||||||
|
|
||||||
// GameOpenConfig 保存统一三方游戏回调配置。
|
|
||||||
type GameOpenConfig struct {
|
|
||||||
PublicBaseURL string
|
|
||||||
AppKey string
|
|
||||||
TestUserID int64
|
|
||||||
TestAccount string
|
|
||||||
TestPassword string
|
|
||||||
}
|
|
||||||
|
|
||||||
// LuckyGiftConfig 保存幸运礼物模块配置。
|
// LuckyGiftConfig 保存幸运礼物模块配置。
|
||||||
type LuckyGiftConfig struct {
|
type LuckyGiftConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
RewardStreamKey string
|
Providers []LuckyGiftProviderConfig
|
||||||
Providers []LuckyGiftProviderConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LuckyGiftProviderConfig struct {
|
type LuckyGiftProviderConfig struct {
|
||||||
@ -193,18 +182,10 @@ func Load() Config {
|
|||||||
LaunchCodeTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_LAUNCH_CODE_TTL_SECONDS", "BAISHUN_LAUNCH_CODE_TTL_SECONDS"}, 300),
|
LaunchCodeTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_LAUNCH_CODE_TTL_SECONDS", "BAISHUN_LAUNCH_CODE_TTL_SECONDS"}, 300),
|
||||||
SSTokenTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_SS_TOKEN_TTL_SECONDS", "BAISHUN_SS_TOKEN_TTL_SECONDS"}, 86400),
|
SSTokenTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_SS_TOKEN_TTL_SECONDS", "BAISHUN_SS_TOKEN_TTL_SECONDS"}, 86400),
|
||||||
},
|
},
|
||||||
GameOpen: GameOpenConfig{
|
|
||||||
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"),
|
|
||||||
TestUserID: getEnvInt64Any([]string{"CHATAPP_GAME_OPEN_TEST_USER_ID", "GAME_OPEN_TEST_USER_ID"}, 1234567),
|
|
||||||
TestAccount: getEnvAny([]string{"CHATAPP_GAME_OPEN_TEST_ACCOUNT", "GAME_OPEN_TEST_ACCOUNT"}, "1234567"),
|
|
||||||
TestPassword: getEnvAny([]string{"CHATAPP_GAME_OPEN_TEST_PASSWORD", "GAME_OPEN_TEST_PASSWORD"}, "1234567"),
|
|
||||||
},
|
|
||||||
LuckyGift: LuckyGiftConfig{
|
LuckyGift: LuckyGiftConfig{
|
||||||
Enabled: getEnvBoolAny([]string{"CHATAPP_LUCKY_GIFT_ENABLED", "LUCKY_GIFT_ENABLED", "LUCKY_GIFT_GO_ENABLED"}, false),
|
Enabled: getEnvBoolAny([]string{"CHATAPP_LUCKY_GIFT_ENABLED", "LUCKY_GIFT_ENABLED", "LUCKY_GIFT_GO_ENABLED"}, false),
|
||||||
Timeout: time.Duration(getEnvIntAny([]string{"CHATAPP_LUCKY_GIFT_TIMEOUT_MILLIS", "LUCKY_GIFT_TIMEOUT_MILLIS", "LUCKY_GIFT_GO_TIMEOUT_MILLIS"}, 8000)) * time.Millisecond,
|
Timeout: time.Duration(getEnvIntAny([]string{"CHATAPP_LUCKY_GIFT_TIMEOUT_MILLIS", "LUCKY_GIFT_TIMEOUT_MILLIS", "LUCKY_GIFT_GO_TIMEOUT_MILLIS"}, 8000)) * time.Millisecond,
|
||||||
RewardStreamKey: getEnvAny([]string{"CHATAPP_LUCKY_GIFT_REWARD_STREAM_KEY", "LUCKY_GIFT_REWARD_STREAM_KEY"}, "lucky-gift:reward"),
|
Providers: loadLuckyGiftConfigs(),
|
||||||
Providers: loadLuckyGiftConfigs(),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,11 +38,6 @@ func (g *Gateways) AuthenticateToken(ctx context.Context, token string) (UserCre
|
|||||||
return g.Auth.AuthenticateToken(ctx, token)
|
return g.Auth.AuthenticateToken(ctx, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateIfAbsentUserCredentialToken 透传到认证网关。
|
|
||||||
func (g *Gateways) CreateIfAbsentUserCredentialToken(ctx context.Context, userID int64) (string, error) {
|
|
||||||
return g.Auth.CreateIfAbsentUserCredentialToken(ctx, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticateConsoleToken 透传到后台认证网关。
|
// AuthenticateConsoleToken 透传到后台认证网关。
|
||||||
func (g *Gateways) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
|
func (g *Gateways) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
|
||||||
return g.Auth.AuthenticateConsoleToken(ctx, authorization)
|
return g.Auth.AuthenticateConsoleToken(ctx, authorization)
|
||||||
@ -118,11 +113,6 @@ func (g *AuthGateway) AuthenticateToken(ctx context.Context, token string) (User
|
|||||||
return g.client.AuthenticateToken(ctx, token)
|
return g.client.AuthenticateToken(ctx, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateIfAbsentUserCredentialToken 创建或返回用户 token。
|
|
||||||
func (g *AuthGateway) CreateIfAbsentUserCredentialToken(ctx context.Context, userID int64) (string, error) {
|
|
||||||
return g.client.CreateIfAbsentUserCredentialToken(ctx, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticateConsoleToken 校验后台 console token。
|
// AuthenticateConsoleToken 校验后台 console token。
|
||||||
func (g *AuthGateway) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
|
func (g *AuthGateway) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
|
||||||
return g.client.AuthenticateConsoleToken(ctx, authorization)
|
return g.client.AuthenticateConsoleToken(ctx, authorization)
|
||||||
|
|||||||
@ -192,19 +192,6 @@ func (c *Client) AuthenticateToken(ctx context.Context, token string) (UserCrede
|
|||||||
return tokenCredential, nil
|
return tokenCredential, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) CreateIfAbsentUserCredentialToken(ctx context.Context, userID int64) (string, error) {
|
|
||||||
endpoint := c.cfg.Java.AuthBaseURL + "/auth/client/createIfAbsentUserCredentialToken?userId=" +
|
|
||||||
url.QueryEscape(strconv.FormatInt(userID, 10))
|
|
||||||
var resp resultResponse[string]
|
|
||||||
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(resp.Body) == "" {
|
|
||||||
return "", fmt.Errorf("empty token response")
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(resp.Body), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticateConsoleToken 校验后台 console Bearer token,并返回管理员基础信息。
|
// AuthenticateConsoleToken 校验后台 console Bearer token,并返回管理员基础信息。
|
||||||
func (c *Client) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
|
func (c *Client) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
|
||||||
authorization = strings.TrimSpace(authorization)
|
authorization = strings.TrimSpace(authorization)
|
||||||
|
|||||||
@ -4,23 +4,20 @@ import "time"
|
|||||||
|
|
||||||
// SysGameListConfig 是系统游戏列表主表,描述平台可展示的游戏。
|
// SysGameListConfig 是系统游戏列表主表,描述平台可展示的游戏。
|
||||||
type SysGameListConfig struct {
|
type SysGameListConfig struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32"` // 系统标识
|
SysOrigin string `gorm:"column:sys_origin;size:32"` // 系统标识
|
||||||
GameOrigin string `gorm:"column:game_origin;size:32"` // 游戏来源
|
GameOrigin string `gorm:"column:game_origin;size:32"` // 游戏来源
|
||||||
GameID string `gorm:"column:game_id;size:64"` // 内部游戏 ID
|
GameID string `gorm:"column:game_id;size:64"` // 内部游戏 ID
|
||||||
Name string `gorm:"column:name;size:128"` // 游戏名称
|
Name string `gorm:"column:name;size:128"` // 游戏名称
|
||||||
Category string `gorm:"column:category;size:64"` // 分类
|
Category string `gorm:"column:category;size:64"` // 分类
|
||||||
GameCode string `gorm:"column:game_code;size:512"` // 游戏码或入口标识
|
GameCode string `gorm:"column:game_code;size:512"` // 游戏码或入口标识
|
||||||
Cover string `gorm:"column:cover;size:1024"` // 封面图
|
Cover string `gorm:"column:cover;size:1024"` // 封面图
|
||||||
Amounts string `gorm:"column:amounts;size:512"` // 金额配置
|
Showcase bool `gorm:"column:is_showcase"` // 是否首页推荐
|
||||||
Showcase bool `gorm:"column:is_showcase"` // 是否首页推荐
|
Sort int64 `gorm:"column:sort"` // 排序值
|
||||||
Sort int64 `gorm:"column:sort"` // 排序值
|
FullScreen bool `gorm:"column:full_screen"` // 是否全屏展示
|
||||||
Height float64 `gorm:"column:height"` // 高度
|
ClientOrigin string `gorm:"column:client_origin;size:32"` // 客户端来源
|
||||||
Width float64 `gorm:"column:width"` // 宽度
|
Regions string `gorm:"column:regions;size:255"` // 可投放地区
|
||||||
FullScreen bool `gorm:"column:full_screen"` // 是否全屏展示
|
GameMode string `gorm:"column:game_mode;size:255"` // 游戏模式配置
|
||||||
ClientOrigin string `gorm:"column:client_origin;size:32"` // 客户端来源
|
|
||||||
Regions string `gorm:"column:regions;size:255"` // 可投放地区
|
|
||||||
GameMode string `gorm:"column:game_mode;size:255"` // 游戏模式配置
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 返回系统游戏列表表名。
|
// TableName 返回系统游戏列表表名。
|
||||||
@ -52,25 +49,6 @@ type SysGameListVendorExt struct {
|
|||||||
// TableName 返回游戏厂商扩展表名。
|
// TableName 返回游戏厂商扩展表名。
|
||||||
func (SysGameListVendorExt) TableName() string { return "sys_game_list_vendor_ext" }
|
func (SysGameListVendorExt) TableName() string { return "sys_game_list_vendor_ext" }
|
||||||
|
|
||||||
// BaishunProviderConfig 保存每个系统的百顺接入配置。
|
|
||||||
type BaishunProviderConfig struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_provider_sys_origin"` // 系统标识
|
|
||||||
PlatformBaseURL string `gorm:"column:platform_base_url;size:1024"` // 百顺平台基础地址
|
|
||||||
AppID int64 `gorm:"column:app_id;index:idx_baishun_provider_app,priority:1"` // 百顺应用 ID
|
|
||||||
AppName string `gorm:"column:app_name;size:128"` // 百顺应用名
|
|
||||||
AppChannel string `gorm:"column:app_channel;size:64;index:idx_baishun_provider_app,priority:2"` // 百顺渠道
|
|
||||||
AppKey string `gorm:"column:app_key;size:255"` // 百顺密钥
|
|
||||||
GSP int `gorm:"column:gsp"` // 默认 GSP
|
|
||||||
LaunchCodeTTLSeconds int `gorm:"column:launch_code_ttl_seconds"` // 启动码过期秒数
|
|
||||||
SSTokenTTLSeconds int `gorm:"column:ss_token_ttl_seconds"` // ss_token 过期秒数
|
|
||||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 返回百顺配置表名。
|
|
||||||
func (BaishunProviderConfig) TableName() string { return "baishun_provider_config" }
|
|
||||||
|
|
||||||
// BaishunGameCatalog 是百顺平台同步到本地的游戏目录。
|
// BaishunGameCatalog 是百顺平台同步到本地的游戏目录。
|
||||||
type BaishunGameCatalog struct {
|
type BaishunGameCatalog struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// GameOpenCallbackLog 保存统一三方游戏回调日志。
|
|
||||||
type GameOpenCallbackLog struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
|
||||||
RequestType string `gorm:"column:request_type;size:32;index:idx_game_open_log_type_time,priority:1"` // 回调类型
|
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32"` // 系统标识
|
|
||||||
OrderID string `gorm:"column:order_id;size:128;index:idx_game_open_log_order"` // 订单号
|
|
||||||
GameID string `gorm:"column:game_id;size:64"` // 游戏 ID
|
|
||||||
RoundID string `gorm:"column:round_id;size:128"` // 局号
|
|
||||||
RoomID string `gorm:"column:room_id;size:64"` // 房间 ID
|
|
||||||
UserID *int64 `gorm:"column:user_id;index:idx_game_open_log_user_time,priority:1"` // 用户 ID
|
|
||||||
RequestJSON string `gorm:"column:request_json;type:longtext"` // 请求报文
|
|
||||||
ResponseJSON string `gorm:"column:response_json;type:longtext"` // 响应报文
|
|
||||||
BizCode int `gorm:"column:biz_code"` // 业务状态码
|
|
||||||
BizMessage string `gorm:"column:biz_message;type:text"` // 业务说明
|
|
||||||
Status string `gorm:"column:status;size:32"` // 处理状态
|
|
||||||
CreateTime time.Time `gorm:"column:create_time;index:idx_game_open_log_type_time,priority:2;index:idx_game_open_log_user_time,priority:2"` // 创建时间
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 返回统一游戏开放接口日志表名。
|
|
||||||
func (GameOpenCallbackLog) TableName() string { return "game_open_callback_log" }
|
|
||||||
@ -26,6 +26,22 @@ func registerBaishunRoutes(
|
|||||||
|
|
||||||
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) {
|
||||||
|
resp, err := baishunService.ListShortcutGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/room/list", func(c *gin.Context) {
|
||||||
|
resp, err := baishunService.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
appGroup.GET("/baishun/state", func(c *gin.Context) {
|
appGroup.GET("/baishun/state", func(c *gin.Context) {
|
||||||
resp, err := baishunService.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
resp, err := baishunService.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -93,123 +109,6 @@ func registerBaishunRoutes(
|
|||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
|
|
||||||
consoleGroup := engine.Group("/operate/baishun-game")
|
|
||||||
consoleGroup.Use(consoleAuthMiddleware(javaClient))
|
|
||||||
consoleGroup.GET("/config", func(c *gin.Context) {
|
|
||||||
resp, err := baishunService.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.POST("/config", func(c *gin.Context) {
|
|
||||||
var req baishun.SaveBaishunProviderConfigRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := baishunService.SaveProviderConfig(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.GET("/page", func(c *gin.Context) {
|
|
||||||
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
|
||||||
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
|
||||||
resp, err := baishunService.PageAdminGames(
|
|
||||||
c.Request.Context(),
|
|
||||||
c.Query("sysOrigin"),
|
|
||||||
c.Query("keyword"),
|
|
||||||
parseOptionalBool(c.Query("showcase")),
|
|
||||||
cursor,
|
|
||||||
limit,
|
|
||||||
)
|
|
||||||
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 := baishunService.PageCatalog(
|
|
||||||
c.Request.Context(),
|
|
||||||
c.Query("sysOrigin"),
|
|
||||||
c.Query("keyword"),
|
|
||||||
cursor,
|
|
||||||
limit,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.POST("/save", func(c *gin.Context) {
|
|
||||||
var req baishun.SaveAdminBaishunGameRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := baishunService.SaveAdminGame(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.DELETE("", func(c *gin.Context) {
|
|
||||||
id, _ := strconv.ParseInt(strings.TrimSpace(c.Query("id")), 10, 64)
|
|
||||||
if err := baishunService.DeleteAdminGame(c.Request.Context(), c.Query("sysOrigin"), id); err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, gin.H{"deleted": true})
|
|
||||||
})
|
|
||||||
consoleGroup.POST("/catalog/fetch", func(c *gin.Context) {
|
|
||||||
var req baishun.SyncCatalogRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := baishunService.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 baishun.SyncCatalogRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := baishunService.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.VendorGameIDs)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
consoleGroup.POST("/sync", func(c *gin.Context) {
|
|
||||||
var req baishun.SyncCatalogRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := baishunService.SyncAdminGames(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
|
|
||||||
callbackGroup := engine.Group("/game/baishun")
|
callbackGroup := engine.Group("/game/baishun")
|
||||||
callbackGroup.POST("/token", func(c *gin.Context) {
|
callbackGroup.POST("/token", func(c *gin.Context) {
|
||||||
raw, payload := readRawPayload(c)
|
raw, payload := readRawPayload(c)
|
||||||
@ -369,16 +268,3 @@ func asInt64(value any) int64 {
|
|||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptionalBool(raw string) *bool {
|
|
||||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
|
||||||
case "0", "false":
|
|
||||||
value := false
|
|
||||||
return &value
|
|
||||||
case "1", "true":
|
|
||||||
value := true
|
|
||||||
return &value
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,79 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/config"
|
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// registerGameOpenRoutes 注册统一三方游戏回调接口。
|
|
||||||
func registerGameOpenRoutes(engine *gin.Engine, cfg config.Config, service *gameopen.GameOpenService) {
|
|
||||||
if service == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callbackGroup := engine.Group("/game/open")
|
|
||||||
callbackGroup.POST("/user-info", func(c *gin.Context) {
|
|
||||||
raw, _ := c.GetRawData()
|
|
||||||
var req gameopen.QueryUserRequest
|
|
||||||
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.HandleQueryUser(c.Request.Context(), req, string(raw)))
|
|
||||||
})
|
|
||||||
callbackGroup.POST("/update-coin", func(c *gin.Context) {
|
|
||||||
raw, _ := c.GetRawData()
|
|
||||||
var req gameopen.UpdateCoinRequest
|
|
||||||
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.HandleUpdateCoin(c.Request.Context(), req, string(raw)))
|
|
||||||
})
|
|
||||||
callbackGroup.POST("/supplement", func(c *gin.Context) {
|
|
||||||
raw, _ := c.GetRawData()
|
|
||||||
var req gameopen.SupplementRequest
|
|
||||||
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.HandleSupplement(c.Request.Context(), req, string(raw)))
|
|
||||||
})
|
|
||||||
|
|
||||||
internalGroup := engine.Group("/internal/game/open")
|
|
||||||
internalGroup.Use(internalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
|
||||||
internalGroup.GET("/info", func(c *gin.Context) {
|
|
||||||
resp, err := service.GetIntegrationInfo(c.Request.Context(), resolvePublicBaseURL(c, cfg))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolvePublicBaseURL(c *gin.Context, cfg config.Config) string {
|
|
||||||
if value := strings.TrimRight(strings.TrimSpace(cfg.GameOpen.PublicBaseURL), "/"); value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
scheme := "http"
|
|
||||||
if forwarded := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); forwarded != "" {
|
|
||||||
scheme = forwarded
|
|
||||||
} else if c.Request.TLS != nil {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
host := strings.TrimSpace(c.GetHeader("X-Forwarded-Host"))
|
|
||||||
if host == "" {
|
|
||||||
host = strings.TrimSpace(c.Request.Host)
|
|
||||||
}
|
|
||||||
if host == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return scheme + "://" + host
|
|
||||||
}
|
|
||||||
@ -1,291 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// registerGameProviderRoutes 注册统一游戏厂商 app 路由。
|
|
||||||
func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, registry *gameprovider.Registry) {
|
|
||||||
if registry == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appGroup := engine.Group("/app/game")
|
|
||||||
appGroup.Use(authMiddleware(javaClient))
|
|
||||||
appGroup.GET("/room/shortcut", func(c *gin.Context) {
|
|
||||||
resp, err := registry.ListShortcutGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, publicRoomGameItems(resp))
|
|
||||||
})
|
|
||||||
appGroup.GET("/room/list", func(c *gin.Context) {
|
|
||||||
resp, err := registry.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, publicRoomGameListResponse(resp))
|
|
||||||
})
|
|
||||||
appGroup.GET("/state", func(c *gin.Context) {
|
|
||||||
resp, err := registry.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, publicRoomState(resp))
|
|
||||||
})
|
|
||||||
appGroup.POST("/launch", func(c *gin.Context) {
|
|
||||||
var req gameprovider.LaunchRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := registry.LaunchGame(c.Request.Context(), mustAuthUser(c), req, resolveClientIP(c))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, publicLaunch(resp))
|
|
||||||
})
|
|
||||||
appGroup.POST("/close", func(c *gin.Context) {
|
|
||||||
var req gameprovider.CloseRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := registry.CloseGame(c.Request.Context(), mustAuthUser(c), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, publicRoomState(resp))
|
|
||||||
})
|
|
||||||
|
|
||||||
providerGroup := engine.Group("/app/game/providers")
|
|
||||||
providerGroup.Use(authMiddleware(javaClient))
|
|
||||||
providerGroup.GET("", func(c *gin.Context) {
|
|
||||||
writeOK(c, registry.List())
|
|
||||||
})
|
|
||||||
providerGroup.GET("/:provider/room/shortcut", func(c *gin.Context) {
|
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := provider.ListShortcutGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, gin.H{"items": resp})
|
|
||||||
})
|
|
||||||
providerGroup.GET("/:provider/room/list", func(c *gin.Context) {
|
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := provider.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
providerGroup.GET("/:provider/state", func(c *gin.Context) {
|
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := provider.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
providerGroup.POST("/:provider/launch", func(c *gin.Context) {
|
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req gameprovider.LaunchRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := provider.LaunchGame(c.Request.Context(), mustAuthUser(c), req, resolveClientIP(c))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
providerGroup.POST("/:provider/close", func(c *gin.Context) {
|
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req gameprovider.CloseRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := provider.CloseGame(c.Request.Context(), mustAuthUser(c), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gameprovider.Provider, bool) {
|
|
||||||
provider, err := registry.Resolve(c.Param("provider"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return provider, true
|
|
||||||
}
|
|
||||||
|
|
||||||
type publicRoomGameItem struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
GameType string `json:"gameType,omitempty"`
|
|
||||||
Provider string `json:"provider,omitempty"`
|
|
||||||
VendorType string `json:"vendorType,omitempty"`
|
|
||||||
ProviderGameID string `json:"providerGameId,omitempty"`
|
|
||||||
VendorGameID string `json:"vendorGameId,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Cover string `json:"cover"`
|
|
||||||
Category string `json:"category,omitempty"`
|
|
||||||
Sort int64 `json:"sort,omitempty"`
|
|
||||||
LaunchMode string `json:"launchMode,omitempty"`
|
|
||||||
FullScreen bool `json:"fullScreen"`
|
|
||||||
GameMode int `json:"gameMode,omitempty"`
|
|
||||||
SafeHeight int `json:"safeHeight,omitempty"`
|
|
||||||
Orientation int `json:"orientation,omitempty"`
|
|
||||||
PackageVersion string `json:"packageVersion,omitempty"`
|
|
||||||
Status string `json:"status,omitempty"`
|
|
||||||
LaunchParams map[string]any `json:"launchParams,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type publicRoomGameList struct {
|
|
||||||
Items []publicRoomGameItem `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type publicRoomStateResponse struct {
|
|
||||||
RoomID string `json:"roomId"`
|
|
||||||
State string `json:"state"`
|
|
||||||
Provider string `json:"provider,omitempty"`
|
|
||||||
VendorType string `json:"vendorType,omitempty"`
|
|
||||||
GameSessionID string `json:"gameSessionId,omitempty"`
|
|
||||||
CurrentGameID string `json:"currentGameId,omitempty"`
|
|
||||||
CurrentProviderGameID string `json:"currentProviderGameId,omitempty"`
|
|
||||||
CurrentVendorGameID string `json:"currentVendorGameId,omitempty"`
|
|
||||||
CurrentGameName string `json:"currentGameName,omitempty"`
|
|
||||||
CurrentGameCover string `json:"currentGameCover,omitempty"`
|
|
||||||
HostUserID int64 `json:"hostUserId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type publicLaunchResponse struct {
|
|
||||||
ID int64 `json:"id,omitempty"`
|
|
||||||
GameSessionID string `json:"gameSessionId"`
|
|
||||||
Provider string `json:"provider,omitempty"`
|
|
||||||
VendorType string `json:"vendorType,omitempty"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
ProviderGameID string `json:"providerGameId,omitempty"`
|
|
||||||
VendorGameID string `json:"vendorGameId,omitempty"`
|
|
||||||
Entry gameprovider.LaunchEntry `json:"entry"`
|
|
||||||
LaunchConfig any `json:"launchConfig,omitempty"`
|
|
||||||
BridgeConfig any `json:"bridgeConfig,omitempty"`
|
|
||||||
RoomState publicRoomStateResponse `json:"roomState"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func publicRoomGameItems(items []gameprovider.RoomGameListItem) []publicRoomGameItem {
|
|
||||||
if len(items) == 0 {
|
|
||||||
return []publicRoomGameItem{}
|
|
||||||
}
|
|
||||||
result := make([]publicRoomGameItem, 0, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
result = append(result, publicRoomGameItem{
|
|
||||||
ID: item.ID,
|
|
||||||
GameID: item.GameID,
|
|
||||||
GameType: item.GameType,
|
|
||||||
Provider: item.Provider,
|
|
||||||
VendorType: item.Provider,
|
|
||||||
ProviderGameID: item.ProviderGameID,
|
|
||||||
VendorGameID: item.ProviderGameID,
|
|
||||||
Name: item.Name,
|
|
||||||
Cover: item.Cover,
|
|
||||||
Category: item.Category,
|
|
||||||
Sort: item.Sort,
|
|
||||||
LaunchMode: item.LaunchMode,
|
|
||||||
FullScreen: item.FullScreen,
|
|
||||||
GameMode: item.GameMode,
|
|
||||||
SafeHeight: item.SafeHeight,
|
|
||||||
Orientation: item.Orientation,
|
|
||||||
PackageVersion: item.PackageVersion,
|
|
||||||
Status: item.Status,
|
|
||||||
LaunchParams: item.LaunchParams,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func publicRoomGameListResponse(resp *gameprovider.RoomGameListResponse) *publicRoomGameList {
|
|
||||||
if resp == nil {
|
|
||||||
return &publicRoomGameList{Items: []publicRoomGameItem{}}
|
|
||||||
}
|
|
||||||
return &publicRoomGameList{
|
|
||||||
Items: publicRoomGameItems(resp.Items),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func publicRoomState(resp *gameprovider.RoomStateResponse) *publicRoomStateResponse {
|
|
||||||
if resp == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &publicRoomStateResponse{
|
|
||||||
RoomID: resp.RoomID,
|
|
||||||
State: resp.State,
|
|
||||||
Provider: resp.Provider,
|
|
||||||
VendorType: resp.Provider,
|
|
||||||
GameSessionID: resp.GameSessionID,
|
|
||||||
CurrentGameID: resp.CurrentGameID,
|
|
||||||
CurrentProviderGameID: resp.CurrentProviderGameID,
|
|
||||||
CurrentVendorGameID: resp.CurrentProviderGameID,
|
|
||||||
CurrentGameName: resp.CurrentGameName,
|
|
||||||
CurrentGameCover: resp.CurrentGameCover,
|
|
||||||
HostUserID: resp.HostUserID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func publicLaunch(resp *gameprovider.LaunchResponse) *publicLaunchResponse {
|
|
||||||
if resp == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
roomState := publicRoomState(&resp.RoomState)
|
|
||||||
if roomState == nil {
|
|
||||||
roomState = &publicRoomStateResponse{}
|
|
||||||
}
|
|
||||||
return &publicLaunchResponse{
|
|
||||||
ID: resp.ID,
|
|
||||||
GameSessionID: resp.GameSessionID,
|
|
||||||
Provider: resp.Provider,
|
|
||||||
VendorType: resp.Provider,
|
|
||||||
GameID: resp.GameID,
|
|
||||||
ProviderGameID: resp.ProviderGameID,
|
|
||||||
VendorGameID: resp.ProviderGameID,
|
|
||||||
Entry: resp.Entry,
|
|
||||||
LaunchConfig: resp.LaunchConfig,
|
|
||||||
BridgeConfig: resp.LaunchConfig,
|
|
||||||
RoomState: *roomState,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPublicRoomGameListResponseKeepsLegacyProviderFields(t *testing.T) {
|
|
||||||
resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{
|
|
||||||
Items: []gameprovider.RoomGameListItem{
|
|
||||||
{
|
|
||||||
ID: 9527,
|
|
||||||
GameID: "bs_1046",
|
|
||||||
GameType: "BAISHUN",
|
|
||||||
Provider: "BAISHUN",
|
|
||||||
ProviderGameID: "1046",
|
|
||||||
Name: "Lucky77Pro",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(resp.Items) != 1 {
|
|
||||||
t.Fatalf("items = %d, want 1", len(resp.Items))
|
|
||||||
}
|
|
||||||
item := resp.Items[0]
|
|
||||||
if item.Provider != "BAISHUN" {
|
|
||||||
t.Fatalf("provider = %q, want BAISHUN", item.Provider)
|
|
||||||
}
|
|
||||||
if item.VendorType != "BAISHUN" {
|
|
||||||
t.Fatalf("vendorType = %q, want BAISHUN", item.VendorType)
|
|
||||||
}
|
|
||||||
if item.ProviderGameID != "1046" {
|
|
||||||
t.Fatalf("providerGameId = %q, want 1046", item.ProviderGameID)
|
|
||||||
}
|
|
||||||
if item.VendorGameID != "1046" {
|
|
||||||
t.Fatalf("vendorGameId = %q, want 1046", item.VendorGameID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPublicLaunchKeepsLegacyBridgeAndProviderFields(t *testing.T) {
|
|
||||||
launchConfig := map[string]any{"code": "launch-code"}
|
|
||||||
resp := publicLaunch(&gameprovider.LaunchResponse{
|
|
||||||
ID: 9527,
|
|
||||||
GameSessionID: "bs_room_1",
|
|
||||||
Provider: "BAISHUN",
|
|
||||||
GameID: "bs_1046",
|
|
||||||
ProviderGameID: "1046",
|
|
||||||
LaunchConfig: launchConfig,
|
|
||||||
RoomState: gameprovider.RoomStateResponse{
|
|
||||||
RoomID: "room-1",
|
|
||||||
State: "PLAYING",
|
|
||||||
Provider: "BAISHUN",
|
|
||||||
CurrentProviderGameID: "1046",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if resp.Provider != "BAISHUN" {
|
|
||||||
t.Fatalf("provider = %q, want BAISHUN", resp.Provider)
|
|
||||||
}
|
|
||||||
if resp.VendorType != "BAISHUN" {
|
|
||||||
t.Fatalf("vendorType = %q, want BAISHUN", resp.VendorType)
|
|
||||||
}
|
|
||||||
if resp.VendorGameID != "1046" {
|
|
||||||
t.Fatalf("vendorGameId = %q, want 1046", resp.VendorGameID)
|
|
||||||
}
|
|
||||||
bridgeConfig, ok := resp.BridgeConfig.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("bridgeConfig type = %T, want map[string]any", resp.BridgeConfig)
|
|
||||||
}
|
|
||||||
if got := bridgeConfig["code"]; got != "launch-code" {
|
|
||||||
t.Fatalf("bridgeConfig[code] = %v, want launch-code", got)
|
|
||||||
}
|
|
||||||
if resp.RoomState.Provider != "BAISHUN" {
|
|
||||||
t.Fatalf("roomState.provider = %q, want BAISHUN", resp.RoomState.Provider)
|
|
||||||
}
|
|
||||||
if resp.RoomState.CurrentVendorGameID != "1046" {
|
|
||||||
t.Fatalf("roomState.currentVendorGameId = %q, want 1046", resp.RoomState.CurrentVendorGameID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,8 +7,6 @@ import (
|
|||||||
"chatapp3-golang/internal/config"
|
"chatapp3-golang/internal/config"
|
||||||
"chatapp3-golang/internal/repo"
|
"chatapp3-golang/internal/repo"
|
||||||
"chatapp3-golang/internal/service/baishun"
|
"chatapp3-golang/internal/service/baishun"
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
|
||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
"chatapp3-golang/internal/service/registerreward"
|
"chatapp3-golang/internal/service/registerreward"
|
||||||
@ -22,8 +20,6 @@ import (
|
|||||||
type Services struct {
|
type Services struct {
|
||||||
Invite *invite.InviteService
|
Invite *invite.InviteService
|
||||||
Baishun *baishun.BaishunService
|
Baishun *baishun.BaishunService
|
||||||
GameOpen *gameopen.GameOpenService
|
|
||||||
GameProviders *gameprovider.Registry
|
|
||||||
LuckyGift *luckygift.LuckyGiftService
|
LuckyGift *luckygift.LuckyGiftService
|
||||||
RegisterReward *registerreward.RegisterRewardService
|
RegisterReward *registerreward.RegisterRewardService
|
||||||
SignInReward *signinreward.SignInRewardService
|
SignInReward *signinreward.SignInRewardService
|
||||||
@ -45,8 +41,6 @@ func NewRouter(
|
|||||||
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
|
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
|
||||||
registerSignInRewardRoutes(engine, javaClient, services.SignInReward)
|
registerSignInRewardRoutes(engine, javaClient, services.SignInReward)
|
||||||
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
||||||
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
|
||||||
registerGameProviderRoutes(engine, javaClient, services.GameProviders)
|
|
||||||
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
||||||
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
||||||
|
|
||||||
@ -67,6 +61,6 @@ func registerHealthRoutes(engine *gin.Engine, cfg config.Config, repository *rep
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"code": "ok", "message": "success4"})
|
c.JSON(http.StatusOK, gin.H{"code": "ok", "message": "success3"})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,759 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/utils"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const adminTimeLayout = "2006-01-02 15:04:05"
|
|
||||||
|
|
||||||
type adminGameRow struct {
|
|
||||||
ID int64
|
|
||||||
SysOrigin string
|
|
||||||
InternalGameID string
|
|
||||||
Name string
|
|
||||||
Category string
|
|
||||||
GameCode string
|
|
||||||
Cover string
|
|
||||||
Amounts string
|
|
||||||
Showcase bool
|
|
||||||
Sort int64
|
|
||||||
Height float64
|
|
||||||
Width float64
|
|
||||||
FullScreen bool
|
|
||||||
ClientOrigin string
|
|
||||||
Regions string
|
|
||||||
GameModeRaw string
|
|
||||||
CreateTime time.Time
|
|
||||||
UpdateTime time.Time
|
|
||||||
VendorGameID string
|
|
||||||
LaunchMode string
|
|
||||||
ExtPackageVersion string
|
|
||||||
ExtPreviewURL string
|
|
||||||
PackageURL string
|
|
||||||
ExtOrientation *int
|
|
||||||
ExtSafeHeight int
|
|
||||||
ExtGSP string
|
|
||||||
CurrencyIcon string
|
|
||||||
ExtExtraJSON string
|
|
||||||
CatalogStatus string
|
|
||||||
CatalogPreviewURL string
|
|
||||||
CatalogDownloadURL string
|
|
||||||
CatalogPackage string
|
|
||||||
CatalogOrientation *int
|
|
||||||
CatalogSafeHeight int
|
|
||||||
CatalogGameModeJSON string
|
|
||||||
}
|
|
||||||
|
|
||||||
// PageAdminGames 返回后台百顺游戏管理分页数据。
|
|
||||||
func (s *BaishunService) PageAdminGames(
|
|
||||||
ctx context.Context,
|
|
||||||
sysOrigin string,
|
|
||||||
keyword string,
|
|
||||||
showcase *bool,
|
|
||||||
cursor int,
|
|
||||||
limit int,
|
|
||||||
) (*AdminBaishunGamePageResponse, error) {
|
|
||||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
|
||||||
cursor = normalizePageCursor(cursor)
|
|
||||||
limit = normalizePageLimit(limit)
|
|
||||||
|
|
||||||
countQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase)
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
if err := countQuery.Count(&total).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []adminGameRow
|
|
||||||
listQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase)
|
|
||||||
if err := listQuery.
|
|
||||||
Select(`
|
|
||||||
cfg.id AS id,
|
|
||||||
cfg.sys_origin AS sys_origin,
|
|
||||||
cfg.game_id AS internal_game_id,
|
|
||||||
cfg.name AS name,
|
|
||||||
cfg.category AS category,
|
|
||||||
cfg.game_code AS game_code,
|
|
||||||
cfg.cover AS cover,
|
|
||||||
cfg.amounts AS amounts,
|
|
||||||
cfg.is_showcase AS showcase,
|
|
||||||
cfg.sort AS sort,
|
|
||||||
cfg.height AS height,
|
|
||||||
cfg.width AS width,
|
|
||||||
cfg.full_screen AS full_screen,
|
|
||||||
cfg.client_origin AS client_origin,
|
|
||||||
cfg.regions AS regions,
|
|
||||||
cfg.game_mode AS game_mode_raw,
|
|
||||||
cfg.create_time AS create_time,
|
|
||||||
cfg.update_time AS update_time,
|
|
||||||
ext.vendor_game_id AS vendor_game_id,
|
|
||||||
ext.launch_mode AS launch_mode,
|
|
||||||
ext.package_version AS ext_package_version,
|
|
||||||
ext.preview_url AS ext_preview_url,
|
|
||||||
ext.package_url AS package_url,
|
|
||||||
ext.orientation AS ext_orientation,
|
|
||||||
ext.safe_height AS ext_safe_height,
|
|
||||||
ext.gsp AS ext_gsp,
|
|
||||||
ext.currency_icon AS currency_icon,
|
|
||||||
ext.extra_json AS ext_extra_json,
|
|
||||||
cat.status AS catalog_status,
|
|
||||||
cat.preview_url AS catalog_preview_url,
|
|
||||||
cat.download_url AS catalog_download_url,
|
|
||||||
cat.package_version AS catalog_package,
|
|
||||||
cat.orientation AS catalog_orientation,
|
|
||||||
cat.safe_height AS catalog_safe_height,
|
|
||||||
cat.game_mode_json AS catalog_game_mode_json
|
|
||||||
`).
|
|
||||||
Order("cfg.is_showcase DESC").
|
|
||||||
Order("cfg.sort DESC").
|
|
||||||
Order("cfg.update_time DESC").
|
|
||||||
Limit(limit).
|
|
||||||
Offset((cursor - 1) * limit).
|
|
||||||
Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
items := make([]AdminBaishunGameItem, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
items = append(items, row.toAdminItem())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AdminBaishunGamePageResponse{
|
|
||||||
Cursor: cursor,
|
|
||||||
Limit: limit,
|
|
||||||
Records: items,
|
|
||||||
Total: total,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAdminGame 保存后台百顺游戏配置。
|
|
||||||
func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishunGameRequest) (*AdminBaishunGameItem, error) {
|
|
||||||
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
|
||||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
catalog, err := s.findCatalogForAdmin(ctx, sysOrigin, req.VendorGameID, req.GameID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if req.VendorGameID <= 0 {
|
|
||||||
req.VendorGameID = catalog.VendorGameID
|
|
||||||
}
|
|
||||||
if req.VendorGameID <= 0 {
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "vendor_game_id_required", "vendorGameId is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
gameID := strings.TrimSpace(req.GameID)
|
|
||||||
if gameID == "" {
|
|
||||||
gameID = defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", req.VendorGameID))
|
|
||||||
}
|
|
||||||
if gameID == "" {
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "game_id_required", "gameId is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
name := defaultIfBlank(strings.TrimSpace(req.Name), defaultIfBlank(catalog.Name, gameID))
|
|
||||||
if name == "" {
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "name_required", "name is required")
|
|
||||||
}
|
|
||||||
category := defaultIfBlank(strings.TrimSpace(req.Category), "OTHER")
|
|
||||||
cover := defaultIfBlank(strings.TrimSpace(req.Cover), defaultIfBlank(catalog.Cover, catalog.PreviewURL))
|
|
||||||
clientOrigin := defaultIfBlank(strings.TrimSpace(req.ClientOrigin), "COMMON")
|
|
||||||
gameCode := defaultIfBlank(strings.TrimSpace(req.GameCode), strconv.Itoa(req.VendorGameID))
|
|
||||||
launchMode := defaultIfBlank(strings.TrimSpace(req.LaunchMode), baishunLaunchModeRemote)
|
|
||||||
packageVersion := defaultIfBlank(strings.TrimSpace(req.PackageVersion), catalog.PackageVersion)
|
|
||||||
previewURL := defaultIfBlank(strings.TrimSpace(req.PreviewURL), catalog.PreviewURL)
|
|
||||||
packageURL := defaultIfBlank(strings.TrimSpace(req.PackageURL), catalog.DownloadURL)
|
|
||||||
gsp := defaultIfBlank(strings.TrimSpace(req.GSP), defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()))
|
|
||||||
safeHeight := req.SafeHeight
|
|
||||||
if safeHeight <= 0 {
|
|
||||||
safeHeight = catalog.SafeHeight
|
|
||||||
}
|
|
||||||
orientation := req.Orientation
|
|
||||||
if orientation == nil && catalog.Orientation != nil {
|
|
||||||
value := *catalog.Orientation
|
|
||||||
orientation = &value
|
|
||||||
}
|
|
||||||
gameMode := baishunFixedGameMode
|
|
||||||
|
|
||||||
tx := s.repo.DB.WithContext(ctx).Begin()
|
|
||||||
if tx.Error != nil {
|
|
||||||
return nil, tx.Error
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
panic(r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
cfg, err := s.findAdminConfigForSave(tx, sysOrigin, req.ID, gameID, req.VendorGameID)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
showcase := resolveAdminShowcase(req.Showcase, cfg.Showcase, cfg.ID == 0)
|
|
||||||
|
|
||||||
cfgValues := map[string]any{
|
|
||||||
"sys_origin": sysOrigin,
|
|
||||||
"game_origin": baishunVendorType,
|
|
||||||
"game_id": gameID,
|
|
||||||
"name": name,
|
|
||||||
"category": category,
|
|
||||||
"game_code": gameCode,
|
|
||||||
"cover": cover,
|
|
||||||
"amounts": strings.TrimSpace(req.Amounts),
|
|
||||||
"is_showcase": showcase,
|
|
||||||
"sort": req.Sort,
|
|
||||||
"height": req.Height,
|
|
||||||
"width": req.Width,
|
|
||||||
"full_screen": req.FullScreen,
|
|
||||||
"client_origin": clientOrigin,
|
|
||||||
"regions": strings.TrimSpace(req.Regions),
|
|
||||||
"game_mode": formatAdminGameModeRaw(gameMode),
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.ID == 0 {
|
|
||||||
nextID, idErr := utils.NextID()
|
|
||||||
if idErr != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, idErr
|
|
||||||
}
|
|
||||||
cfg = model.SysGameListConfig{
|
|
||||||
ID: nextID,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
GameOrigin: baishunVendorType,
|
|
||||||
GameID: gameID,
|
|
||||||
Name: name,
|
|
||||||
Category: category,
|
|
||||||
GameCode: gameCode,
|
|
||||||
Cover: cover,
|
|
||||||
Amounts: strings.TrimSpace(req.Amounts),
|
|
||||||
Showcase: showcase,
|
|
||||||
Sort: req.Sort,
|
|
||||||
Height: req.Height,
|
|
||||||
Width: req.Width,
|
|
||||||
FullScreen: req.FullScreen,
|
|
||||||
ClientOrigin: clientOrigin,
|
|
||||||
Regions: strings.TrimSpace(req.Regions),
|
|
||||||
GameMode: formatAdminGameModeRaw(gameMode),
|
|
||||||
}
|
|
||||||
if err := tx.Create(&cfg).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := tx.Model(&model.SysGameListConfig{}).
|
|
||||||
Where("id = ?", cfg.ID).
|
|
||||||
Updates(cfgValues).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ext model.SysGameListVendorExt
|
|
||||||
err = tx.Where("game_list_config_id = ? AND vendor_type = ?", cfg.ID, baishunVendorType).First(&ext).Error
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
extraJSON := updateExtExtraJSONWithGameType(ext.ExtraJSON, req.LaunchParams, baishunVendorType, ext.ID != 0)
|
|
||||||
if ext.ID == 0 {
|
|
||||||
nextID, idErr := utils.NextID()
|
|
||||||
if idErr != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, idErr
|
|
||||||
}
|
|
||||||
ext = model.SysGameListVendorExt{
|
|
||||||
ID: nextID,
|
|
||||||
GameListConfigID: cfg.ID,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
VendorType: baishunVendorType,
|
|
||||||
VendorGameID: strconv.Itoa(req.VendorGameID),
|
|
||||||
LaunchMode: launchMode,
|
|
||||||
PackageVersion: packageVersion,
|
|
||||||
PackageURL: packageURL,
|
|
||||||
PreviewURL: previewURL,
|
|
||||||
Orientation: orientation,
|
|
||||||
SafeHeight: safeHeight,
|
|
||||||
GSP: gsp,
|
|
||||||
CurrencyIcon: strings.TrimSpace(req.CurrencyIcon),
|
|
||||||
ExtraJSON: extraJSON,
|
|
||||||
Enabled: true,
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&ext).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := tx.Model(&model.SysGameListVendorExt{}).
|
|
||||||
Where("id = ?", ext.ID).
|
|
||||||
Updates(map[string]any{
|
|
||||||
"sys_origin": sysOrigin,
|
|
||||||
"vendor_game_id": strconv.Itoa(req.VendorGameID),
|
|
||||||
"launch_mode": launchMode,
|
|
||||||
"package_version": packageVersion,
|
|
||||||
"package_url": packageURL,
|
|
||||||
"preview_url": previewURL,
|
|
||||||
"orientation": orientation,
|
|
||||||
"safe_height": safeHeight,
|
|
||||||
"gsp": gsp,
|
|
||||||
"currency_icon": strings.TrimSpace(req.CurrencyIcon),
|
|
||||||
"extra_json": extraJSON,
|
|
||||||
"enabled": true,
|
|
||||||
"bridge_schema_version": defaultIfBlank(ext.BridgeSchemaVersion, "1.0"),
|
|
||||||
"update_time": now,
|
|
||||||
}).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s.getAdminGame(ctx, sysOrigin, cfg.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAdminGame 删除后台百顺游戏配置,不影响目录表。
|
|
||||||
func (s *BaishunService) DeleteAdminGame(ctx context.Context, sysOrigin string, id int64) error {
|
|
||||||
if id <= 0 {
|
|
||||||
return NewAppError(http.StatusBadRequest, "id_required", "id is required")
|
|
||||||
}
|
|
||||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
|
||||||
|
|
||||||
tx := s.repo.DB.WithContext(ctx).Begin()
|
|
||||||
if tx.Error != nil {
|
|
||||||
return tx.Error
|
|
||||||
}
|
|
||||||
if err := tx.Where("game_list_config_id = ? AND vendor_type = ?", id, baishunVendorType).
|
|
||||||
Delete(&model.SysGameListVendorExt{}).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).
|
|
||||||
Delete(&model.SysGameListConfig{}).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Commit().Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportCatalogGames 把本地目录中尚未进入后台列表的百顺游戏补齐成可配置记录。
|
|
||||||
func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin string, vendorGameIDs []int) (*ImportCatalogResponse, error) {
|
|
||||||
tx := s.repo.DB.WithContext(ctx).Begin()
|
|
||||||
if tx.Error != nil {
|
|
||||||
return nil, tx.Error
|
|
||||||
}
|
|
||||||
resp, err := s.importCatalogGamesTx(tx, normalizeAdminSysOrigin(sysOrigin), buildVendorGameIDFilter(vendorGameIDs))
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncAdminGames 先同步百顺平台目录,再把缺失配置导入后台列表。
|
|
||||||
func (s *BaishunService) SyncAdminGames(ctx context.Context, req SyncCatalogRequest) (*SyncAdminCatalogResponse, error) {
|
|
||||||
syncResp, err := s.SyncCatalog(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.VendorGameIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &SyncAdminCatalogResponse{
|
|
||||||
Inserted: syncResp.Inserted,
|
|
||||||
Updated: syncResp.Updated,
|
|
||||||
Skipped: syncResp.Skipped,
|
|
||||||
Existing: importResp.Existing,
|
|
||||||
Imported: importResp.Imported,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) buildAdminGameQuery(ctx context.Context, sysOrigin, keyword string, showcase *bool) *gorm.DB {
|
|
||||||
query := s.repo.DB.WithContext(ctx).
|
|
||||||
Table("sys_game_list_config AS cfg").
|
|
||||||
Joins("LEFT JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.vendor_type = ?", baishunVendorType).
|
|
||||||
Joins(`
|
|
||||||
LEFT JOIN baishun_game_catalog cat
|
|
||||||
ON cat.sys_origin = cfg.sys_origin
|
|
||||||
AND (
|
|
||||||
CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id
|
|
||||||
OR cat.internal_game_id = cfg.game_id
|
|
||||||
)
|
|
||||||
`).
|
|
||||||
Where("cfg.sys_origin = ? AND cfg.game_origin = ?", sysOrigin, baishunVendorType)
|
|
||||||
if showcase != nil {
|
|
||||||
query = query.Where("cfg.is_showcase = ?", *showcase)
|
|
||||||
}
|
|
||||||
keyword = strings.TrimSpace(keyword)
|
|
||||||
if keyword != "" {
|
|
||||||
like := "%" + keyword + "%"
|
|
||||||
query = query.Where(`
|
|
||||||
cfg.name LIKE ?
|
|
||||||
OR cfg.game_id LIKE ?
|
|
||||||
OR cfg.game_code LIKE ?
|
|
||||||
OR ext.vendor_game_id LIKE ?
|
|
||||||
`, like, like, like, like)
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) getAdminGame(ctx context.Context, sysOrigin string, id int64) (*AdminBaishunGameItem, error) {
|
|
||||||
var rows []adminGameRow
|
|
||||||
err := s.buildAdminGameQuery(ctx, sysOrigin, "", nil).
|
|
||||||
Where("cfg.id = ?", id).
|
|
||||||
Select(`
|
|
||||||
cfg.id AS id,
|
|
||||||
cfg.sys_origin AS sys_origin,
|
|
||||||
cfg.game_id AS internal_game_id,
|
|
||||||
cfg.name AS name,
|
|
||||||
cfg.category AS category,
|
|
||||||
cfg.game_code AS game_code,
|
|
||||||
cfg.cover AS cover,
|
|
||||||
cfg.amounts AS amounts,
|
|
||||||
cfg.is_showcase AS showcase,
|
|
||||||
cfg.sort AS sort,
|
|
||||||
cfg.height AS height,
|
|
||||||
cfg.width AS width,
|
|
||||||
cfg.full_screen AS full_screen,
|
|
||||||
cfg.client_origin AS client_origin,
|
|
||||||
cfg.regions AS regions,
|
|
||||||
cfg.game_mode AS game_mode_raw,
|
|
||||||
cfg.create_time AS create_time,
|
|
||||||
cfg.update_time AS update_time,
|
|
||||||
ext.vendor_game_id AS vendor_game_id,
|
|
||||||
ext.launch_mode AS launch_mode,
|
|
||||||
ext.package_version AS ext_package_version,
|
|
||||||
ext.preview_url AS ext_preview_url,
|
|
||||||
ext.package_url AS package_url,
|
|
||||||
ext.orientation AS ext_orientation,
|
|
||||||
ext.safe_height AS ext_safe_height,
|
|
||||||
ext.gsp AS ext_gsp,
|
|
||||||
ext.currency_icon AS currency_icon,
|
|
||||||
ext.extra_json AS ext_extra_json,
|
|
||||||
cat.status AS catalog_status,
|
|
||||||
cat.preview_url AS catalog_preview_url,
|
|
||||||
cat.download_url AS catalog_download_url,
|
|
||||||
cat.package_version AS catalog_package,
|
|
||||||
cat.orientation AS catalog_orientation,
|
|
||||||
cat.safe_height AS catalog_safe_height,
|
|
||||||
cat.game_mode_json AS catalog_game_mode_json
|
|
||||||
`).
|
|
||||||
Limit(1).
|
|
||||||
Scan(&rows).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(rows) == 1 {
|
|
||||||
item := rows[0].toAdminItem()
|
|
||||||
return &item, nil
|
|
||||||
}
|
|
||||||
return nil, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) findCatalogForAdmin(ctx context.Context, sysOrigin string, vendorGameID int, gameID string) (model.BaishunGameCatalog, error) {
|
|
||||||
query := s.repo.DB.WithContext(ctx).Where("sys_origin = ?", sysOrigin)
|
|
||||||
switch {
|
|
||||||
case vendorGameID > 0:
|
|
||||||
query = query.Where("vendor_game_id = ?", vendorGameID)
|
|
||||||
case strings.TrimSpace(gameID) != "":
|
|
||||||
query = query.Where("internal_game_id = ?", strings.TrimSpace(gameID))
|
|
||||||
default:
|
|
||||||
return model.BaishunGameCatalog{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var catalog model.BaishunGameCatalog
|
|
||||||
if err := query.First(&catalog).Error; err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
return model.BaishunGameCatalog{}, nil
|
|
||||||
}
|
|
||||||
return model.BaishunGameCatalog{}, err
|
|
||||||
}
|
|
||||||
return catalog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, id int64, gameID string, vendorGameID int) (model.SysGameListConfig, error) {
|
|
||||||
var cfg model.SysGameListConfig
|
|
||||||
if id > 0 {
|
|
||||||
err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).First(&cfg).Error
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
return model.SysGameListConfig{}, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
|
|
||||||
}
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if vendorGameID > 0 {
|
|
||||||
var ext model.SysGameListVendorExt
|
|
||||||
err := tx.Where("sys_origin = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, baishunVendorType, strconv.Itoa(vendorGameID)).First(&ext).Error
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
return model.SysGameListConfig{}, err
|
|
||||||
}
|
|
||||||
if ext.GameListConfigID > 0 {
|
|
||||||
err = tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", ext.GameListConfigID, sysOrigin, baishunVendorType).First(&cfg).Error
|
|
||||||
if err == nil {
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
if err != gorm.ErrRecordNotFound {
|
|
||||||
return model.SysGameListConfig{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(gameID) == "" {
|
|
||||||
return model.SysGameListConfig{}, nil
|
|
||||||
}
|
|
||||||
err := tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, strings.TrimSpace(gameID)).First(&cfg).Error
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
return model.SysGameListConfig{}, nil
|
|
||||||
}
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, filter map[int]struct{}) (*ImportCatalogResponse, error) {
|
|
||||||
ctx := tx.Statement.Context
|
|
||||||
if ctx == nil {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
query := tx.Where("sys_origin = ? AND status = ?", sysOrigin, baishunCatalogEnabled)
|
|
||||||
if len(filter) > 0 {
|
|
||||||
ids := make([]int, 0, len(filter))
|
|
||||||
for id := range filter {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
query = query.Where("vendor_game_id IN ?", ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
var catalogs []model.BaishunGameCatalog
|
|
||||||
if err := query.Order("vendor_game_id ASC").Find(&catalogs).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &ImportCatalogResponse{}
|
|
||||||
for _, catalog := range catalogs {
|
|
||||||
vendorGameID := strconv.Itoa(catalog.VendorGameID)
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
var ext model.SysGameListVendorExt
|
|
||||||
err := tx.Where("sys_origin = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, baishunVendorType, vendorGameID).First(&ext).Error
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err == nil && ext.GameListConfigID > 0 {
|
|
||||||
resp.Existing++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg model.SysGameListConfig
|
|
||||||
err = tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, catalog.InternalGameID).First(&cfg).Error
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
nextID, idErr := utils.NextID()
|
|
||||||
if idErr != nil {
|
|
||||||
return nil, idErr
|
|
||||||
}
|
|
||||||
cfg = model.SysGameListConfig{
|
|
||||||
ID: nextID,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
GameOrigin: baishunVendorType,
|
|
||||||
GameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
|
|
||||||
Name: catalog.Name,
|
|
||||||
Category: "OTHER",
|
|
||||||
GameCode: vendorGameID,
|
|
||||||
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
|
||||||
Showcase: true,
|
|
||||||
Sort: int64(catalog.VendorGameID),
|
|
||||||
FullScreen: true,
|
|
||||||
ClientOrigin: "COMMON",
|
|
||||||
GameMode: formatAdminGameModeRaw(baishunFixedGameMode),
|
|
||||||
}
|
|
||||||
if err := tx.Create(&cfg).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextID, idErr := utils.NextID()
|
|
||||||
if idErr != nil {
|
|
||||||
return nil, idErr
|
|
||||||
}
|
|
||||||
newExt := model.SysGameListVendorExt{
|
|
||||||
ID: nextID,
|
|
||||||
GameListConfigID: cfg.ID,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
VendorType: baishunVendorType,
|
|
||||||
VendorGameID: vendorGameID,
|
|
||||||
LaunchMode: baishunLaunchModeRemote,
|
|
||||||
PackageVersion: catalog.PackageVersion,
|
|
||||||
PackageURL: catalog.DownloadURL,
|
|
||||||
PreviewURL: catalog.PreviewURL,
|
|
||||||
Orientation: catalog.Orientation,
|
|
||||||
SafeHeight: catalog.SafeHeight,
|
|
||||||
GSP: defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()),
|
|
||||||
BridgeSchemaVersion: "1.0",
|
|
||||||
ExtraJSON: updateExtExtraJSONWithGameType("", nil, baishunVendorType, false),
|
|
||||||
Enabled: true,
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&newExt).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.Imported++
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r adminGameRow) toAdminItem() AdminBaishunGameItem {
|
|
||||||
return AdminBaishunGameItem{
|
|
||||||
ID: r.ID,
|
|
||||||
SysOrigin: r.SysOrigin,
|
|
||||||
GameID: r.InternalGameID,
|
|
||||||
GameType: gameTypeFromExtraJSON(r.ExtExtraJSON, baishunVendorType),
|
|
||||||
VendorGameID: parseAdminVendorGameID(r.VendorGameID, r.InternalGameID),
|
|
||||||
Name: r.Name,
|
|
||||||
Category: r.Category,
|
|
||||||
GameCode: r.GameCode,
|
|
||||||
Cover: r.Cover,
|
|
||||||
Amounts: r.Amounts,
|
|
||||||
Showcase: r.Showcase,
|
|
||||||
Sort: r.Sort,
|
|
||||||
Height: r.Height,
|
|
||||||
Width: r.Width,
|
|
||||||
FullScreen: r.FullScreen,
|
|
||||||
ClientOrigin: r.ClientOrigin,
|
|
||||||
Regions: r.Regions,
|
|
||||||
GameMode: baishunFixedGameMode,
|
|
||||||
LaunchMode: defaultIfBlank(r.LaunchMode, baishunLaunchModeRemote),
|
|
||||||
PackageVersion: defaultIfBlank(r.ExtPackageVersion, r.CatalogPackage),
|
|
||||||
PreviewURL: defaultIfBlank(r.ExtPreviewURL, r.CatalogPreviewURL),
|
|
||||||
PackageURL: defaultIfBlank(r.PackageURL, r.CatalogDownloadURL),
|
|
||||||
Orientation: chooseAdminOrientation(r.ExtOrientation, r.CatalogOrientation),
|
|
||||||
SafeHeight: chooseAdminSafeHeight(r.ExtSafeHeight, r.CatalogSafeHeight),
|
|
||||||
GSP: r.ExtGSP,
|
|
||||||
CurrencyIcon: r.CurrencyIcon,
|
|
||||||
CatalogStatus: r.CatalogStatus,
|
|
||||||
LaunchParams: launchParamsWithGameType(r.ExtExtraJSON, baishunVendorType),
|
|
||||||
CreateTime: formatAdminTime(r.CreateTime),
|
|
||||||
UpdateTime: formatAdminTime(r.UpdateTime),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeAdminSysOrigin(sysOrigin string) string {
|
|
||||||
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
|
||||||
if sysOrigin == "" {
|
|
||||||
return "LIKEI"
|
|
||||||
}
|
|
||||||
return sysOrigin
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePageCursor(cursor int) int {
|
|
||||||
if cursor <= 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePageLimit(limit int) int {
|
|
||||||
if limit <= 0 {
|
|
||||||
return 20
|
|
||||||
}
|
|
||||||
if limit > 200 {
|
|
||||||
return 200
|
|
||||||
}
|
|
||||||
return limit
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveAdminShowcase(showcase *bool, current bool, isCreate bool) bool {
|
|
||||||
if showcase != nil {
|
|
||||||
return *showcase
|
|
||||||
}
|
|
||||||
if isCreate {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultAdminGSP(catalogGSP string, fallback int) string {
|
|
||||||
if strings.TrimSpace(catalogGSP) != "" {
|
|
||||||
return strings.TrimSpace(catalogGSP)
|
|
||||||
}
|
|
||||||
if fallback > 0 {
|
|
||||||
return strconv.Itoa(fallback)
|
|
||||||
}
|
|
||||||
return "101"
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatAdminGameModeRaw(gameMode int) string {
|
|
||||||
return fmt.Sprintf("[%d]", baishunFixedGameMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAdminVendorGameID(raw string, gameID string) int {
|
|
||||||
if parsed, err := strconv.Atoi(strings.TrimSpace(raw)); err == nil {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(strings.TrimSpace(gameID), "bs_") {
|
|
||||||
if parsed, err := strconv.Atoi(strings.TrimPrefix(strings.TrimSpace(gameID), "bs_")); err == nil {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func chooseAdminOrientation(extOrientation, catalogOrientation *int) int {
|
|
||||||
if extOrientation != nil {
|
|
||||||
return *extOrientation
|
|
||||||
}
|
|
||||||
if catalogOrientation != nil {
|
|
||||||
return *catalogOrientation
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func chooseAdminSafeHeight(extSafeHeight, catalogSafeHeight int) int {
|
|
||||||
if extSafeHeight > 0 {
|
|
||||||
return extSafeHeight
|
|
||||||
}
|
|
||||||
return catalogSafeHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatAdminTime(value time.Time) string {
|
|
||||||
if value.IsZero() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return value.Format(adminTimeLayout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildVendorGameIDFilter(vendorGameIDs []int) map[int]struct{} {
|
|
||||||
filter := make(map[int]struct{}, len(vendorGameIDs))
|
|
||||||
for _, id := range vendorGameIDs {
|
|
||||||
if id > 0 {
|
|
||||||
filter[id] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filter
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestResolveAdminShowcase(t *testing.T) {
|
|
||||||
t.Run("create defaults to enabled", func(t *testing.T) {
|
|
||||||
if got := resolveAdminShowcase(nil, false, true); !got {
|
|
||||||
t.Fatalf("resolveAdminShowcase(nil, false, true) = %v, want true", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("update keeps current when omitted", func(t *testing.T) {
|
|
||||||
if got := resolveAdminShowcase(nil, false, false); got {
|
|
||||||
t.Fatalf("resolveAdminShowcase(nil, false, false) = %v, want false", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("explicit false remains false", func(t *testing.T) {
|
|
||||||
showcase := false
|
|
||||||
if got := resolveAdminShowcase(&showcase, true, true); got {
|
|
||||||
t.Fatalf("resolveAdminShowcase(false, true, true) = %v, want false", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ gameprovider.Provider = (*AppProvider)(nil)
|
|
||||||
|
|
||||||
// AppProvider 把百顺服务适配成统一厂商接口。
|
|
||||||
type AppProvider struct {
|
|
||||||
service *BaishunService
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAppProvider 创建百顺统一厂商适配器。
|
|
||||||
func NewAppProvider(service *BaishunService) *AppProvider {
|
|
||||||
if service == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &AppProvider{service: service}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key 返回统一厂商标识。
|
|
||||||
func (p *AppProvider) Key() string {
|
|
||||||
return baishunVendorType
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisplayName 返回厂商展示名。
|
|
||||||
func (p *AppProvider) DisplayName() string {
|
|
||||||
return "百顺"
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportsLaunch 判断启动请求是否属于百顺。
|
|
||||||
func (p *AppProvider) SupportsLaunch(ctx context.Context, user gameprovider.AuthUser, req gameprovider.LaunchRequest) (bool, error) {
|
|
||||||
_, err := p.service.resolveLaunchGameRow(ctx, user.SysOrigin, BaishunLaunchAppRequest{
|
|
||||||
ID: req.ID,
|
|
||||||
GameID: req.GameID,
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
var appErr *AppError
|
|
||||||
if errors.As(err, &appErr) && appErr.Code == "game_not_found" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListShortcutGames 返回百顺房间快捷游戏。
|
|
||||||
func (p *AppProvider) ListShortcutGames(ctx context.Context, user gameprovider.AuthUser, roomID string) ([]gameprovider.RoomGameListItem, error) {
|
|
||||||
items, err := p.service.ListShortcutGames(ctx, user, roomID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return mapRoomGameListItems(items), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListRoomGames 返回百顺房间游戏列表。
|
|
||||||
func (p *AppProvider) ListRoomGames(ctx context.Context, user gameprovider.AuthUser, roomID, category string) (*gameprovider.RoomGameListResponse, error) {
|
|
||||||
resp, err := p.service.ListRoomGames(ctx, user, roomID, category)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &gameprovider.RoomGameListResponse{Items: mapRoomGameListItems(resp.Items)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoomState 返回百顺房间状态。
|
|
||||||
func (p *AppProvider) GetRoomState(ctx context.Context, user gameprovider.AuthUser, roomID string) (*gameprovider.RoomStateResponse, error) {
|
|
||||||
resp, err := p.service.GetRoomState(ctx, user, roomID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return mapRoomStateResponse(resp), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LaunchGame 启动百顺游戏。
|
|
||||||
func (p *AppProvider) LaunchGame(ctx context.Context, user gameprovider.AuthUser, req gameprovider.LaunchRequest, clientIP string) (*gameprovider.LaunchResponse, error) {
|
|
||||||
resp, err := p.service.LaunchGame(ctx, user, BaishunLaunchAppRequest{
|
|
||||||
ID: req.ID,
|
|
||||||
RoomID: req.RoomID,
|
|
||||||
GameID: req.GameID,
|
|
||||||
SceneMode: req.SceneMode,
|
|
||||||
ClientOrigin: req.ClientOrigin,
|
|
||||||
}, clientIP)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &gameprovider.LaunchResponse{
|
|
||||||
ID: resp.ID,
|
|
||||||
GameSessionID: resp.GameSessionID,
|
|
||||||
Provider: baishunVendorType,
|
|
||||||
GameID: resp.GameID,
|
|
||||||
ProviderGameID: strconv.Itoa(resp.VendorGameID),
|
|
||||||
Entry: gameprovider.LaunchEntry{
|
|
||||||
LaunchMode: resp.Entry.LaunchMode,
|
|
||||||
EntryURL: resp.Entry.EntryURL,
|
|
||||||
PreviewURL: resp.Entry.PreviewURL,
|
|
||||||
DownloadURL: resp.Entry.DownloadURL,
|
|
||||||
PackageVersion: resp.Entry.PackageVersion,
|
|
||||||
Orientation: resp.Entry.Orientation,
|
|
||||||
SafeHeight: resp.Entry.SafeHeight,
|
|
||||||
},
|
|
||||||
LaunchConfig: resp.BridgeConfig,
|
|
||||||
RoomState: *mapRoomStateResponse(&resp.RoomState),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseGame 关闭百顺游戏。
|
|
||||||
func (p *AppProvider) CloseGame(ctx context.Context, user gameprovider.AuthUser, req gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) {
|
|
||||||
resp, err := p.service.CloseGame(ctx, user, BaishunCloseAppRequest{
|
|
||||||
RoomID: req.RoomID,
|
|
||||||
GameSessionID: req.GameSessionID,
|
|
||||||
Reason: req.Reason,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return mapRoomStateResponse(resp), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapRoomGameListItems(items []RoomGameListItem) []gameprovider.RoomGameListItem {
|
|
||||||
result := make([]gameprovider.RoomGameListItem, 0, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
result = append(result, gameprovider.RoomGameListItem{
|
|
||||||
ID: item.ID,
|
|
||||||
GameID: item.GameID,
|
|
||||||
GameType: item.GameType,
|
|
||||||
Provider: baishunVendorType,
|
|
||||||
ProviderGameID: strconv.Itoa(item.VendorGameID),
|
|
||||||
Name: item.Name,
|
|
||||||
Cover: item.Cover,
|
|
||||||
Category: item.Category,
|
|
||||||
Sort: item.Sort,
|
|
||||||
LaunchMode: item.LaunchMode,
|
|
||||||
FullScreen: item.FullScreen,
|
|
||||||
GameMode: item.GameMode,
|
|
||||||
SafeHeight: item.SafeHeight,
|
|
||||||
Orientation: item.Orientation,
|
|
||||||
PackageVersion: item.PackageVersion,
|
|
||||||
Status: item.Status,
|
|
||||||
LaunchParams: item.LaunchParams,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapRoomStateResponse(resp *BaishunRoomStateResponse) *gameprovider.RoomStateResponse {
|
|
||||||
if resp == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
state := &gameprovider.RoomStateResponse{
|
|
||||||
RoomID: resp.RoomID,
|
|
||||||
State: resp.State,
|
|
||||||
Provider: baishunVendorType,
|
|
||||||
GameSessionID: resp.GameSessionID,
|
|
||||||
CurrentGameID: resp.CurrentGameID,
|
|
||||||
CurrentGameName: resp.CurrentGameName,
|
|
||||||
CurrentGameCover: resp.CurrentGameCover,
|
|
||||||
HostUserID: resp.HostUserID,
|
|
||||||
}
|
|
||||||
if resp.CurrentVendorGameID > 0 {
|
|
||||||
state.CurrentProviderGameID = strconv.Itoa(resp.CurrentVendorGameID)
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
@ -19,8 +19,7 @@ import (
|
|||||||
|
|
||||||
// HandleToken 处理百顺获取启动 code/token 的回调。
|
// HandleToken 处理百顺获取启动 code/token 的回调。
|
||||||
func (s *BaishunService) HandleToken(ctx context.Context, req BaishunTokenRequest, rawJSON string) baishunStandardResponse {
|
func (s *BaishunService) HandleToken(ctx context.Context, req BaishunTokenRequest, rawJSON string) baishunStandardResponse {
|
||||||
runtimeCfg, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp)
|
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||||
if !ok {
|
|
||||||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||||||
}
|
}
|
||||||
var session model.BaishunLaunchSession
|
var session model.BaishunLaunchSession
|
||||||
@ -48,7 +47,7 @@ func (s *BaishunService) HandleToken(ctx context.Context, req BaishunTokenReques
|
|||||||
s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
expire := time.Now().Add(time.Duration(runtimeCfg.ssTokenTTLSeconds()) * time.Second)
|
expire := time.Now().Add(time.Duration(s.cfg.Baishun.SSTokenTTLSeconds) * time.Second)
|
||||||
if err := s.repo.DB.WithContext(ctx).
|
if err := s.repo.DB.WithContext(ctx).
|
||||||
Model(&model.BaishunLaunchSession{}).
|
Model(&model.BaishunLaunchSession{}).
|
||||||
Where("id = ?", session.ID).
|
Where("id = ?", session.ID).
|
||||||
@ -86,7 +85,7 @@ func (s *BaishunService) HandleToken(ctx context.Context, req BaishunTokenReques
|
|||||||
|
|
||||||
// HandleProfile 处理百顺获取用户资料的回调。
|
// HandleProfile 处理百顺获取用户资料的回调。
|
||||||
func (s *BaishunService) HandleProfile(ctx context.Context, req BaishunProfileRequest, rawJSON string) baishunStandardResponse {
|
func (s *BaishunService) HandleProfile(ctx context.Context, req BaishunProfileRequest, rawJSON string) baishunStandardResponse {
|
||||||
if _, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp); !ok {
|
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||||||
}
|
}
|
||||||
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
||||||
@ -120,8 +119,7 @@ func (s *BaishunService) HandleProfile(ctx context.Context, req BaishunProfileRe
|
|||||||
|
|
||||||
// HandleUpdateToken 处理百顺刷新 ss_token 的回调。
|
// HandleUpdateToken 处理百顺刷新 ss_token 的回调。
|
||||||
func (s *BaishunService) HandleUpdateToken(ctx context.Context, req BaishunUpdateTokenRequest, rawJSON string) baishunStandardResponse {
|
func (s *BaishunService) HandleUpdateToken(ctx context.Context, req BaishunUpdateTokenRequest, rawJSON string) baishunStandardResponse {
|
||||||
runtimeCfg, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp)
|
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||||
if !ok {
|
|
||||||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||||||
}
|
}
|
||||||
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
||||||
@ -136,7 +134,7 @@ func (s *BaishunService) HandleUpdateToken(ctx context.Context, req BaishunUpdat
|
|||||||
s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
expire := time.Now().Add(time.Duration(runtimeCfg.ssTokenTTLSeconds()) * time.Second)
|
expire := time.Now().Add(time.Duration(s.cfg.Baishun.SSTokenTTLSeconds) * time.Second)
|
||||||
if err := s.repo.DB.WithContext(ctx).
|
if err := s.repo.DB.WithContext(ctx).
|
||||||
Model(&model.BaishunLaunchSession{}).
|
Model(&model.BaishunLaunchSession{}).
|
||||||
Where("id = ?", session.ID).
|
Where("id = ?", session.ID).
|
||||||
@ -165,7 +163,7 @@ func (s *BaishunService) HandleUpdateToken(ctx context.Context, req BaishunUpdat
|
|||||||
|
|
||||||
// HandleChangeBalance 处理百顺扣增币回调,并驱动钱包变更。
|
// HandleChangeBalance 处理百顺扣增币回调,并驱动钱包变更。
|
||||||
func (s *BaishunService) HandleChangeBalance(ctx context.Context, req BaishunChangeBalanceRequest, rawJSON string) baishunStandardResponse {
|
func (s *BaishunService) HandleChangeBalance(ctx context.Context, req BaishunChangeBalanceRequest, rawJSON string) baishunStandardResponse {
|
||||||
if _, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp); !ok {
|
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||||||
}
|
}
|
||||||
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
||||||
@ -257,7 +255,7 @@ func (s *BaishunService) HandleReport(ctx context.Context, payload map[string]an
|
|||||||
|
|
||||||
// HandleBalanceInfo 处理百顺查询余额回调。
|
// HandleBalanceInfo 处理百顺查询余额回调。
|
||||||
func (s *BaishunService) HandleBalanceInfo(ctx context.Context, req BaishunBalanceInfoRequest, rawJSON string) baishunBalanceInfoResponse {
|
func (s *BaishunService) HandleBalanceInfo(ctx context.Context, req BaishunBalanceInfoRequest, rawJSON string) baishunBalanceInfoResponse {
|
||||||
if _, ok := s.validateCommonSignature(ctx, req.AppID, req.AppChannel, req.Signature, req.SignatureNonce, req.Timestamp); !ok {
|
if !s.validateCommonSignature(ctx, req.AppID, req.AppChannel, req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||||
return baishunBalanceInfoResponse{Code: baishunCodeSignatureError, Msg: "signature error"}
|
return baishunBalanceInfoResponse{Code: baishunCodeSignatureError, Msg: "signature error"}
|
||||||
}
|
}
|
||||||
userID, _ := strconv.ParseInt(req.UserID, 10, 64)
|
userID, _ := strconv.ParseInt(req.UserID, 10, 64)
|
||||||
@ -272,31 +270,24 @@ func (s *BaishunService) HandleBalanceInfo(ctx context.Context, req BaishunBalan
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateCommonSignature 校验百顺回调签名。
|
// validateCommonSignature 校验百顺回调签名。
|
||||||
func (s *BaishunService) validateCommonSignature(ctx context.Context, appID int64, appChannel, signature, nonce string, timestamp int64) (baishunRuntimeConfig, bool) {
|
func (s *BaishunService) validateCommonSignature(ctx context.Context, appID int64, appChannel, signature, nonce string, timestamp int64) bool {
|
||||||
runtimeCfg, err := s.resolveRuntimeConfigByApp(ctx, appID, appChannel)
|
if s.cfg.Baishun.AppID != 0 && appID != 0 && appID != s.cfg.Baishun.AppID {
|
||||||
if err != nil {
|
return false
|
||||||
return baishunRuntimeConfig{}, false
|
|
||||||
}
|
}
|
||||||
if runtimeCfg.AppID != 0 && appID != 0 && appID != runtimeCfg.AppID {
|
if strings.TrimSpace(appChannel) != "" && s.cfg.Baishun.AppChannel != "" && appChannel != s.cfg.Baishun.AppChannel {
|
||||||
return baishunRuntimeConfig{}, false
|
return false
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(appChannel) != "" && runtimeCfg.appChannel() != "" && appChannel != runtimeCfg.appChannel() {
|
if strings.TrimSpace(signature) == "" || strings.TrimSpace(nonce) == "" || timestamp == 0 || strings.TrimSpace(s.cfg.Baishun.AppKey) == "" {
|
||||||
return baishunRuntimeConfig{}, false
|
return false
|
||||||
}
|
|
||||||
if strings.TrimSpace(signature) == "" || strings.TrimSpace(nonce) == "" || timestamp == 0 || strings.TrimSpace(runtimeCfg.AppKey) == "" {
|
|
||||||
return baishunRuntimeConfig{}, false
|
|
||||||
}
|
}
|
||||||
if abs64(time.Now().Unix()-timestamp) > 15 {
|
if abs64(time.Now().Unix()-timestamp) > 15 {
|
||||||
return baishunRuntimeConfig{}, false
|
return false
|
||||||
}
|
}
|
||||||
expected := signBAISHUN(runtimeCfg.AppKey, timestamp, nonce)
|
expected := s.signBAISHUN(timestamp, nonce)
|
||||||
if !strings.EqualFold(expected, signature) {
|
if !strings.EqualFold(expected, signature) {
|
||||||
return baishunRuntimeConfig{}, false
|
return false
|
||||||
}
|
}
|
||||||
if !s.rememberNonce(ctx, nonce) {
|
return s.rememberNonce(ctx, nonce)
|
||||||
return baishunRuntimeConfig{}, false
|
|
||||||
}
|
|
||||||
return runtimeCfg, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// rememberNonce 记录已用 nonce,防止回调重放。
|
// rememberNonce 记录已用 nonce,防止回调重放。
|
||||||
@ -306,8 +297,8 @@ func (s *BaishunService) rememberNonce(ctx context.Context, nonce string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// signBAISHUN 按百顺约定生成签名。
|
// signBAISHUN 按百顺约定生成签名。
|
||||||
func signBAISHUN(appKey string, timestamp int64, nonce string) string {
|
func (s *BaishunService) signBAISHUN(timestamp int64, nonce string) string {
|
||||||
sum := md5.Sum([]byte(nonce + appKey + strconv.FormatInt(timestamp, 10)))
|
sum := md5.Sum([]byte(nonce + s.cfg.Baishun.AppKey + strconv.FormatInt(timestamp, 10)))
|
||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,16 +18,15 @@ import (
|
|||||||
|
|
||||||
// SyncCatalog 从百顺平台拉取游戏目录并同步到本地表。
|
// SyncCatalog 从百顺平台拉取游戏目录并同步到本地表。
|
||||||
func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
||||||
|
if strings.TrimSpace(s.cfg.Baishun.PlatformBaseURL) == "" || s.cfg.Baishun.AppID == 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "baishun_config_missing", "baishun platform config is missing")
|
||||||
|
}
|
||||||
sysOrigin := defaultIfBlank(req.SysOrigin, "LIKEI")
|
sysOrigin := defaultIfBlank(req.SysOrigin, "LIKEI")
|
||||||
if strings.TrimSpace(sysOrigin) == "" {
|
if strings.TrimSpace(sysOrigin) == "" {
|
||||||
sysOrigin = "LIKEI"
|
sysOrigin = "LIKEI"
|
||||||
}
|
}
|
||||||
runtimeCfg, err := s.requireProviderConfig(ctx, sysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
games, err := s.fetchPlatformGames(ctx, runtimeCfg)
|
games, err := s.fetchPlatformGames(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -62,7 +61,7 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
|
|||||||
Orientation: intPtr(game.GameOrientation),
|
Orientation: intPtr(game.GameOrientation),
|
||||||
SafeHeight: game.SafeHeight,
|
SafeHeight: game.SafeHeight,
|
||||||
VenueLevelJSON: utils.MustJSONString(game.VenueLevel, ""),
|
VenueLevelJSON: utils.MustJSONString(game.VenueLevel, ""),
|
||||||
GSP: strconv.Itoa(runtimeCfg.gsp()),
|
GSP: strconv.Itoa(s.cfg.Baishun.GSP),
|
||||||
RawJSON: utils.MustJSONString(game, ""),
|
RawJSON: utils.MustJSONString(game, ""),
|
||||||
Status: baishunCatalogEnabled,
|
Status: baishunCatalogEnabled,
|
||||||
CreateTime: time.Now(),
|
CreateTime: time.Now(),
|
||||||
@ -89,23 +88,22 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetchPlatformGames 从百顺平台拉取游戏目录。
|
// fetchPlatformGames 从百顺平台拉取游戏目录。
|
||||||
func (s *BaishunService) fetchPlatformGames(ctx context.Context, runtimeCfg baishunRuntimeConfig) ([]baishunPlatformGame, error) {
|
func (s *BaishunService) fetchPlatformGames(ctx context.Context) ([]baishunPlatformGame, error) {
|
||||||
timestamp := time.Now().Unix()
|
timestamp := time.Now().Unix()
|
||||||
nonce := s.randomNonce()
|
nonce := s.randomNonce()
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"game_list_type": baishunGameListTypeGame,
|
"game_list_type": baishunGameListTypeGame,
|
||||||
"app_channel": runtimeCfg.appChannel(),
|
"app_channel": s.cfg.Baishun.AppChannel,
|
||||||
"app_id": runtimeCfg.AppID,
|
"app_id": s.cfg.Baishun.AppID,
|
||||||
"signature": signBAISHUN(runtimeCfg.AppKey, timestamp, nonce),
|
"signature": s.signBAISHUN(timestamp, nonce),
|
||||||
"signature_nonce": nonce,
|
"signature_nonce": nonce,
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(runtimeCfg.AppName) != "" {
|
if strings.TrimSpace(s.cfg.Baishun.AppName) != "" {
|
||||||
payload["app_name"] = runtimeCfg.AppName
|
payload["app_name"] = s.cfg.Baishun.AppName
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
endpoint := strings.TrimRight(runtimeCfg.PlatformBaseURL, "/") + "/v1/api/gamelist"
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(s.cfg.Baishun.PlatformBaseURL, "/")+"/v1/api/gamelist", bytes.NewReader(body))
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -119,56 +117,15 @@ func (s *BaishunService) fetchPlatformGames(ctx context.Context, runtimeCfg bais
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
|
|
||||||
if resp.StatusCode >= http.StatusBadRequest {
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("baishun platform failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(raw)))
|
||||||
"baishun platform failed: url=%s status=%d contentType=%s body=%s",
|
|
||||||
endpoint,
|
|
||||||
resp.StatusCode,
|
|
||||||
contentType,
|
|
||||||
baishunShortBody(raw),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if baishunLooksLikeHTML(raw, contentType) {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"baishun platform returned non-json html: url=%s contentType=%s body=%s",
|
|
||||||
endpoint,
|
|
||||||
contentType,
|
|
||||||
baishunShortBody(raw),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
var result baishunPlatformResponse
|
var result baishunPlatformResponse
|
||||||
if err := json.Unmarshal(raw, &result); err != nil {
|
if err := json.Unmarshal(raw, &result); err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, err
|
||||||
"baishun platform returned invalid json: url=%s contentType=%s error=%v body=%s",
|
|
||||||
endpoint,
|
|
||||||
contentType,
|
|
||||||
err,
|
|
||||||
baishunShortBody(raw),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if result.Code != 0 {
|
if result.Code != 0 {
|
||||||
return nil, fmt.Errorf("baishun platform failed: code=%d message=%s", result.Code, result.Message)
|
return nil, fmt.Errorf("baishun platform failed: code=%d message=%s", result.Code, result.Message)
|
||||||
}
|
}
|
||||||
return result.Data, nil
|
return result.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func baishunLooksLikeHTML(raw []byte, contentType string) bool {
|
|
||||||
contentType = strings.ToLower(strings.TrimSpace(contentType))
|
|
||||||
if strings.Contains(contentType, "text/html") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
body := strings.ToLower(strings.TrimSpace(string(raw)))
|
|
||||||
return strings.HasPrefix(body, "<!doctype html") ||
|
|
||||||
strings.HasPrefix(body, "<!doctypehtml") ||
|
|
||||||
strings.HasPrefix(body, "<html")
|
|
||||||
}
|
|
||||||
|
|
||||||
func baishunShortBody(raw []byte) string {
|
|
||||||
body := strings.TrimSpace(string(raw))
|
|
||||||
const max = 512
|
|
||||||
if len(body) <= max {
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
return body[:max] + "..."
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,136 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type adminCatalogRow struct {
|
|
||||||
VendorGameID int
|
|
||||||
InternalGameID string
|
|
||||||
Name string
|
|
||||||
Cover string
|
|
||||||
PreviewURL string
|
|
||||||
DownloadURL string
|
|
||||||
PackageVersion string
|
|
||||||
GSP string
|
|
||||||
Status string
|
|
||||||
SafeHeight int
|
|
||||||
Orientation *int
|
|
||||||
AddedConfigID *int64
|
|
||||||
Showcase *bool
|
|
||||||
UpdateTime string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) PageCatalog(
|
|
||||||
ctx context.Context,
|
|
||||||
sysOrigin string,
|
|
||||||
keyword string,
|
|
||||||
cursor int,
|
|
||||||
limit int,
|
|
||||||
) (*AdminBaishunCatalogPageResponse, error) {
|
|
||||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
|
||||||
cursor = normalizePageCursor(cursor)
|
|
||||||
limit = normalizePageLimit(limit)
|
|
||||||
|
|
||||||
countQuery := s.buildAdminCatalogQuery(ctx, sysOrigin, keyword)
|
|
||||||
var total int64
|
|
||||||
if err := countQuery.Count(&total).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []adminCatalogRow
|
|
||||||
err := s.buildAdminCatalogQuery(ctx, sysOrigin, keyword).
|
|
||||||
Select(`
|
|
||||||
cat.vendor_game_id AS vendor_game_id,
|
|
||||||
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.gsp AS gsp,
|
|
||||||
cat.status AS status,
|
|
||||||
cat.safe_height AS safe_height,
|
|
||||||
cat.orientation AS orientation,
|
|
||||||
ext.game_list_config_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("cat.vendor_game_id ASC").
|
|
||||||
Limit(limit).
|
|
||||||
Offset((cursor - 1) * limit).
|
|
||||||
Scan(&rows).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
items := make([]AdminBaishunCatalogItem, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
items = append(items, AdminBaishunCatalogItem{
|
|
||||||
VendorGameID: row.VendorGameID,
|
|
||||||
GameID: row.InternalGameID,
|
|
||||||
Name: row.Name,
|
|
||||||
Cover: row.Cover,
|
|
||||||
PreviewURL: row.PreviewURL,
|
|
||||||
DownloadURL: row.DownloadURL,
|
|
||||||
PackageVersion: row.PackageVersion,
|
|
||||||
GSP: row.GSP,
|
|
||||||
Status: row.Status,
|
|
||||||
SafeHeight: row.SafeHeight,
|
|
||||||
Orientation: derefInt(row.Orientation),
|
|
||||||
Added: row.AddedConfigID != nil && *row.AddedConfigID > 0,
|
|
||||||
Showcase: row.Showcase != nil && *row.Showcase,
|
|
||||||
UpdateTime: row.UpdateTime,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AdminBaishunCatalogPageResponse{
|
|
||||||
Cursor: cursor,
|
|
||||||
Limit: limit,
|
|
||||||
Records: items,
|
|
||||||
Total: total,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) FetchAdminCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
|
||||||
return s.SyncCatalog(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) buildAdminCatalogQuery(ctx context.Context, sysOrigin string, keyword string) *gorm.DB {
|
|
||||||
query := s.repo.DB.WithContext(ctx).
|
|
||||||
Table("baishun_game_catalog AS cat").
|
|
||||||
Joins(`
|
|
||||||
LEFT JOIN sys_game_list_vendor_ext ext
|
|
||||||
ON ext.sys_origin = cat.sys_origin
|
|
||||||
AND ext.vendor_type = ?
|
|
||||||
AND ext.vendor_game_id = CAST(cat.vendor_game_id AS CHAR)
|
|
||||||
AND ext.enabled = 1
|
|
||||||
`, baishunVendorType).
|
|
||||||
Joins("LEFT JOIN sys_game_list_config cfg ON cfg.id = ext.game_list_config_id").
|
|
||||||
Where("cat.sys_origin = ?", sysOrigin)
|
|
||||||
keyword = strings.TrimSpace(keyword)
|
|
||||||
if keyword != "" {
|
|
||||||
like := "%" + keyword + "%"
|
|
||||||
query = query.Where(`
|
|
||||||
cat.name LIKE ?
|
|
||||||
OR cat.internal_game_id LIKE ?
|
|
||||||
OR CAST(cat.vendor_game_id AS CHAR) LIKE ?
|
|
||||||
`, like, like, like)
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) requireProviderConfig(ctx context.Context, sysOrigin string) (baishunRuntimeConfig, error) {
|
|
||||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return baishunRuntimeConfig{}, err
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(runtimeCfg.PlatformBaseURL) == "" || runtimeCfg.AppID <= 0 || strings.TrimSpace(runtimeCfg.AppKey) == "" {
|
|
||||||
return baishunRuntimeConfig{}, NewAppError(http.StatusBadRequest, "baishun_config_missing", "baishun provider config is missing")
|
|
||||||
}
|
|
||||||
return runtimeCfg, nil
|
|
||||||
}
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const extExtraLaunchParamsKey = "launchParams"
|
|
||||||
const extLaunchParamGameTypeKey = "gameType"
|
|
||||||
|
|
||||||
func launchParamsFromExtraJSON(raw string) map[string]any {
|
|
||||||
extra := parseExtExtraJSON(raw)
|
|
||||||
value, ok := extra[extExtraLaunchParamsKey]
|
|
||||||
if !ok || value == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if params, ok := value.(map[string]any); ok {
|
|
||||||
if len(params) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
bytes, err := json.Marshal(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var params map[string]any
|
|
||||||
if err := json.Unmarshal(bytes, ¶ms); err != nil || len(params) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
func launchParamsWithGameType(raw string, gameType string) map[string]any {
|
|
||||||
params := cloneMapAny(launchParamsFromExtraJSON(raw))
|
|
||||||
gameType = normalizeGameType(gameType)
|
|
||||||
if gameType == "" {
|
|
||||||
if len(params) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
if params == nil {
|
|
||||||
params = map[string]any{}
|
|
||||||
}
|
|
||||||
params[extLaunchParamGameTypeKey] = gameType
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
func gameTypeFromExtraJSON(raw string, fallback string) string {
|
|
||||||
params := launchParamsFromExtraJSON(raw)
|
|
||||||
if value, ok := params[extLaunchParamGameTypeKey].(string); ok {
|
|
||||||
if normalized := normalizeGameType(value); normalized != "" {
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return normalizeGameType(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateExtExtraJSON(existing string, launchParams map[string]any, preserveExisting bool) string {
|
|
||||||
if launchParams == nil {
|
|
||||||
if preserveExisting {
|
|
||||||
return strings.TrimSpace(existing)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
extra := parseExtExtraJSON(existing)
|
|
||||||
if len(launchParams) == 0 {
|
|
||||||
delete(extra, extExtraLaunchParamsKey)
|
|
||||||
} else {
|
|
||||||
extra[extExtraLaunchParamsKey] = launchParams
|
|
||||||
}
|
|
||||||
if len(extra) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
bytes, err := json.Marshal(extra)
|
|
||||||
if err != nil {
|
|
||||||
return strings.TrimSpace(existing)
|
|
||||||
}
|
|
||||||
return string(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateExtExtraJSONWithGameType(existing string, launchParams map[string]any, gameType string, preserveExisting bool) string {
|
|
||||||
params := cloneMapAny(launchParams)
|
|
||||||
if params == nil && preserveExisting {
|
|
||||||
params = cloneMapAny(launchParamsFromExtraJSON(existing))
|
|
||||||
}
|
|
||||||
if params == nil {
|
|
||||||
params = map[string]any{}
|
|
||||||
}
|
|
||||||
gameType = normalizeGameType(gameType)
|
|
||||||
if gameType != "" {
|
|
||||||
params[extLaunchParamGameTypeKey] = gameType
|
|
||||||
}
|
|
||||||
return updateExtExtraJSON(existing, params, preserveExisting)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseExtExtraJSON(raw string) map[string]any {
|
|
||||||
raw = strings.TrimSpace(raw)
|
|
||||||
if raw == "" {
|
|
||||||
return map[string]any{}
|
|
||||||
}
|
|
||||||
var extra map[string]any
|
|
||||||
if err := json.Unmarshal([]byte(raw), &extra); err != nil || extra == nil {
|
|
||||||
return map[string]any{}
|
|
||||||
}
|
|
||||||
return extra
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneMapAny(source map[string]any) map[string]any {
|
|
||||||
if len(source) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
target := make(map[string]any, len(source))
|
|
||||||
for key, value := range source {
|
|
||||||
target[key] = value
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeGameType(value string) string {
|
|
||||||
value = strings.ToUpper(strings.TrimSpace(value))
|
|
||||||
if value == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestUpdateExtExtraJSON(t *testing.T) {
|
|
||||||
raw := updateExtExtraJSON(`{"keep":"value"}`, map[string]any{
|
|
||||||
"sdkMode": "H5",
|
|
||||||
}, true)
|
|
||||||
|
|
||||||
if got := parseExtExtraJSON(raw)["keep"]; got != "value" {
|
|
||||||
t.Fatalf("extra keep = %v, want value", got)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleared := updateExtExtraJSON(raw, map[string]any{}, true)
|
|
||||||
if params := launchParamsFromExtraJSON(cleared); params != nil {
|
|
||||||
t.Fatalf("launchParams after clear = %#v, want nil", params)
|
|
||||||
}
|
|
||||||
if got := parseExtExtraJSON(cleared)["keep"]; got != "value" {
|
|
||||||
t.Fatalf("extra keep after clear = %v, want value", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateExtExtraJSONWithGameType(t *testing.T) {
|
|
||||||
raw := updateExtExtraJSONWithGameType(`{"keep":"value"}`, map[string]any{
|
|
||||||
"sdkMode": "H5",
|
|
||||||
}, baishunVendorType, true)
|
|
||||||
|
|
||||||
params := launchParamsFromExtraJSON(raw)
|
|
||||||
if got := params["gameType"]; got != baishunVendorType {
|
|
||||||
t.Fatalf("launchParams[gameType] = %v, want %s", got, baishunVendorType)
|
|
||||||
}
|
|
||||||
if got := params["sdkMode"]; got != "H5" {
|
|
||||||
t.Fatalf("launchParams[sdkMode] = %v, want H5", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLaunchParamsExposeToListAndAdmin(t *testing.T) {
|
|
||||||
raw := `{"launchParams":{"sdkMode":"H5"}}`
|
|
||||||
|
|
||||||
listItem := gameListRow{
|
|
||||||
ConfigID: 9527,
|
|
||||||
InternalGameID: "g-1",
|
|
||||||
VendorType: baishunVendorType,
|
|
||||||
VendorGameID: "1001",
|
|
||||||
LaunchMode: baishunLaunchModeRemote,
|
|
||||||
ExtExtraJSON: raw,
|
|
||||||
}.toListItem()
|
|
||||||
if got := listItem.ID; got != 9527 {
|
|
||||||
t.Fatalf("list id = %v, want 9527", got)
|
|
||||||
}
|
|
||||||
if got := listItem.GameType; got != baishunVendorType {
|
|
||||||
t.Fatalf("list gameType = %v, want %s", got, baishunVendorType)
|
|
||||||
}
|
|
||||||
if got := listItem.LaunchParams["gameType"]; got != baishunVendorType {
|
|
||||||
t.Fatalf("list launchParams[gameType] = %v, want %s", got, baishunVendorType)
|
|
||||||
}
|
|
||||||
|
|
||||||
adminItem := adminGameRow{
|
|
||||||
InternalGameID: "g-1",
|
|
||||||
VendorGameID: "1001",
|
|
||||||
ExtExtraJSON: raw,
|
|
||||||
}.toAdminItem()
|
|
||||||
if got := adminItem.GameType; got != baishunVendorType {
|
|
||||||
t.Fatalf("admin gameType = %v, want %s", got, baishunVendorType)
|
|
||||||
}
|
|
||||||
if got := adminItem.LaunchParams["sdkMode"]; got != "H5" {
|
|
||||||
t.Fatalf("admin launchParams[sdkMode] = %v, want H5", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
const baishunFixedGameMode = 2
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestGameListRowGameModeAlwaysFixed(t *testing.T) {
|
|
||||||
testCases := []string{"", "[1]", "[2]", "[3]", "CHAT_ROOM"}
|
|
||||||
for _, raw := range testCases {
|
|
||||||
row := gameListRow{GameModeRaw: raw}
|
|
||||||
if got := row.gameMode(); got != baishunFixedGameMode {
|
|
||||||
t.Fatalf("gameMode() = %d, want %d for raw %q", got, baishunFixedGameMode, raw)
|
|
||||||
}
|
|
||||||
if got := row.toListItem().GameMode; got != baishunFixedGameMode {
|
|
||||||
t.Fatalf("toListItem().GameMode = %d, want %d for raw %q", got, baishunFixedGameMode, raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdminGameModeAlwaysFixed(t *testing.T) {
|
|
||||||
row := adminGameRow{
|
|
||||||
GameModeRaw: "[3]",
|
|
||||||
CatalogGameModeJSON: "[1]",
|
|
||||||
}
|
|
||||||
if got := row.toAdminItem().GameMode; got != baishunFixedGameMode {
|
|
||||||
t.Fatalf("toAdminItem().GameMode = %d, want %d", got, baishunFixedGameMode)
|
|
||||||
}
|
|
||||||
if got := formatAdminGameModeRaw(0); got != "[2]" {
|
|
||||||
t.Fatalf("formatAdminGameModeRaw(0) = %q, want [2]", got)
|
|
||||||
}
|
|
||||||
if got := formatAdminGameModeRaw(99); got != "[2]" {
|
|
||||||
t.Fatalf("formatAdminGameModeRaw(99) = %q, want [2]", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,9 +8,7 @@ import (
|
|||||||
// toListItem 把联表行模型转换成游戏列表项。
|
// toListItem 把联表行模型转换成游戏列表项。
|
||||||
func (r gameListRow) toListItem() RoomGameListItem {
|
func (r gameListRow) toListItem() RoomGameListItem {
|
||||||
return RoomGameListItem{
|
return RoomGameListItem{
|
||||||
ID: r.ConfigID,
|
|
||||||
GameID: r.InternalGameID,
|
GameID: r.InternalGameID,
|
||||||
GameType: r.gameTypeName(),
|
|
||||||
VendorType: defaultIfBlank(r.VendorType, baishunVendorType),
|
VendorType: defaultIfBlank(r.VendorType, baishunVendorType),
|
||||||
VendorGameID: r.vendorGameIDInt(),
|
VendorGameID: r.vendorGameIDInt(),
|
||||||
Name: r.name(),
|
Name: r.name(),
|
||||||
@ -24,7 +22,6 @@ func (r gameListRow) toListItem() RoomGameListItem {
|
|||||||
Orientation: r.orientation(),
|
Orientation: r.orientation(),
|
||||||
PackageVersion: r.packageVersion(),
|
PackageVersion: r.packageVersion(),
|
||||||
Status: defaultIfBlank(r.CatalogStatus, baishunCatalogEnabled),
|
Status: defaultIfBlank(r.CatalogStatus, baishunCatalogEnabled),
|
||||||
LaunchParams: r.launchParams(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +87,10 @@ func (r gameListRow) launchMode() string {
|
|||||||
|
|
||||||
// gameMode 返回游戏模式。
|
// gameMode 返回游戏模式。
|
||||||
func (r gameListRow) gameMode() int {
|
func (r gameListRow) gameMode() int {
|
||||||
return baishunFixedGameMode
|
if parsed := pickFirstInt(r.GameModeRaw, 3); parsed > 0 {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 3
|
||||||
}
|
}
|
||||||
|
|
||||||
// vendorGameIDInt 返回厂商游戏 ID 的整数值。
|
// vendorGameIDInt 返回厂商游戏 ID 的整数值。
|
||||||
@ -103,12 +103,3 @@ func (r gameListRow) vendorGameIDInt() int {
|
|||||||
func (r gameListRow) currencyIcon() string {
|
func (r gameListRow) currencyIcon() string {
|
||||||
return r.CurrencyIcon
|
return r.CurrencyIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
// launchParams 返回 Flutter 统一启动接口使用的扩展参数。
|
|
||||||
func (r gameListRow) launchParams() map[string]any {
|
|
||||||
return launchParamsWithGameType(r.ExtExtraJSON, r.gameTypeName())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r gameListRow) gameTypeName() string {
|
|
||||||
return gameTypeFromExtraJSON(r.ExtExtraJSON, defaultIfBlank(r.VendorType, baishunVendorType))
|
|
||||||
}
|
|
||||||
|
|||||||
@ -20,15 +20,11 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
|||||||
if strings.TrimSpace(req.RoomID) == "" {
|
if strings.TrimSpace(req.RoomID) == "" {
|
||||||
return nil, NewAppError(http.StatusBadRequest, "missing_room_id", "roomId is required")
|
return nil, NewAppError(http.StatusBadRequest, "missing_room_id", "roomId is required")
|
||||||
}
|
}
|
||||||
if req.ID <= 0 && strings.TrimSpace(req.GameID) == "" {
|
if strings.TrimSpace(req.GameID) == "" {
|
||||||
return nil, NewAppError(http.StatusBadRequest, "missing_game_id", "id or gameId is required")
|
return nil, NewAppError(http.StatusBadRequest, "missing_game_id", "gameId is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
row, err := s.resolveLaunchGameRow(ctx, user.SysOrigin, req)
|
row, err := s.findGameRow(ctx, user.SysOrigin, req.GameID)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
runtimeCfg, err := s.requireProviderConfig(ctx, user.SysOrigin)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -60,9 +56,9 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
|||||||
VendorGameID: row.vendorGameIDInt(),
|
VendorGameID: row.vendorGameIDInt(),
|
||||||
GameSessionID: fmt.Sprintf("bs_%s_%d", sanitizeRoomID(req.RoomID), sessionID),
|
GameSessionID: fmt.Sprintf("bs_%s_%d", sanitizeRoomID(req.RoomID), sessionID),
|
||||||
LaunchCode: launchCode,
|
LaunchCode: launchCode,
|
||||||
LaunchCodeExpireTime: now.Add(time.Duration(runtimeCfg.launchCodeTTLSeconds()) * time.Second),
|
LaunchCodeExpireTime: now.Add(time.Duration(s.cfg.Baishun.LaunchCodeTTLSeconds) * time.Second),
|
||||||
Language: s.resolveLanguage(ctx, user.UserID),
|
Language: s.resolveLanguage(ctx, user.UserID),
|
||||||
GSP: s.resolveGSP(row, runtimeCfg),
|
GSP: s.resolveGSP(row),
|
||||||
CurrencyType: 0,
|
CurrencyType: 0,
|
||||||
CurrencyIcon: row.currencyIcon(),
|
CurrencyIcon: row.currencyIcon(),
|
||||||
SceneMode: req.SceneMode,
|
SceneMode: req.SceneMode,
|
||||||
@ -110,7 +106,6 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &BaishunLaunchAppResponse{
|
return &BaishunLaunchAppResponse{
|
||||||
ID: row.ConfigID,
|
|
||||||
GameSessionID: session.GameSessionID,
|
GameSessionID: session.GameSessionID,
|
||||||
VendorType: baishunVendorType,
|
VendorType: baishunVendorType,
|
||||||
GameID: row.InternalGameID,
|
GameID: row.InternalGameID,
|
||||||
@ -125,9 +120,9 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
|||||||
SafeHeight: row.safeHeight(),
|
SafeHeight: row.safeHeight(),
|
||||||
},
|
},
|
||||||
BridgeConfig: BaishunBridgeConfig{
|
BridgeConfig: BaishunBridgeConfig{
|
||||||
AppName: runtimeCfg.AppName,
|
AppName: s.cfg.Baishun.AppName,
|
||||||
AppChannel: runtimeCfg.appChannel(),
|
AppChannel: defaultIfBlank(s.cfg.Baishun.AppChannel, "skychat"),
|
||||||
AppID: runtimeCfg.AppID,
|
AppID: s.cfg.Baishun.AppID,
|
||||||
UserID: strconv.FormatInt(user.UserID, 10),
|
UserID: strconv.FormatInt(user.UserID, 10),
|
||||||
Code: launchCode,
|
Code: launchCode,
|
||||||
RoomID: req.RoomID,
|
RoomID: req.RoomID,
|
||||||
@ -152,13 +147,6 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaishunService) resolveLaunchGameRow(ctx context.Context, sysOrigin string, req BaishunLaunchAppRequest) (gameListRow, error) {
|
|
||||||
if req.ID > 0 {
|
|
||||||
return s.findGameRowByConfigID(ctx, sysOrigin, req.ID)
|
|
||||||
}
|
|
||||||
return s.findGameRow(ctx, sysOrigin, req.GameID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseGame 关闭房间内当前游戏会话并回写房间状态。
|
// CloseGame 关闭房间内当前游戏会话并回写房间状态。
|
||||||
func (s *BaishunService) CloseGame(ctx context.Context, user AuthUser, req BaishunCloseAppRequest) (*BaishunRoomStateResponse, error) {
|
func (s *BaishunService) CloseGame(ctx context.Context, user AuthUser, req BaishunCloseAppRequest) (*BaishunRoomStateResponse, error) {
|
||||||
if strings.TrimSpace(req.RoomID) == "" {
|
if strings.TrimSpace(req.RoomID) == "" {
|
||||||
|
|||||||
@ -1,118 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/config"
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type stubGateway struct{}
|
|
||||||
|
|
||||||
func (stubGateway) GetUserProfile(context.Context, int64) (integration.UserProfile, error) {
|
|
||||||
return integration.UserProfile{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stubGateway) MapGoldBalance(context.Context, []int64) (map[int64]int64, error) {
|
|
||||||
return map[int64]int64{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stubGateway) GetUserLanguage(context.Context, int64) (string, error) {
|
|
||||||
return "en", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stubGateway) ExistsGoldEvent(context.Context, string) (bool, error) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stubGateway) ChangeGoldBalance(context.Context, integration.GoldReceiptCommand) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestBaishunService(t *testing.T) (*BaishunService, *gorm.DB) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("open sqlite: %v", err)
|
|
||||||
}
|
|
||||||
if err := db.AutoMigrate(
|
|
||||||
&model.SysGameListConfig{},
|
|
||||||
&model.SysGameListVendorExt{},
|
|
||||||
&model.BaishunProviderConfig{},
|
|
||||||
&model.BaishunGameCatalog{},
|
|
||||||
&model.BaishunLaunchSession{},
|
|
||||||
&model.BaishunRoomState{},
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("auto migrate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
service := NewBaishunService(config.Config{
|
|
||||||
Baishun: config.BaishunConfig{
|
|
||||||
PlatformBaseURL: "https://mesh-channels-online.jieyou.shop",
|
|
||||||
AppID: 7485318192,
|
|
||||||
AppName: "yumiparty_app",
|
|
||||||
AppChannel: "yumiparty",
|
|
||||||
AppKey: "test-app-key",
|
|
||||||
GSP: 111,
|
|
||||||
LaunchCodeTTLSeconds: 300,
|
|
||||||
SSTokenTTLSeconds: 86400,
|
|
||||||
},
|
|
||||||
}, db, nil, stubGateway{})
|
|
||||||
return service, db
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLaunchGameFallsBackToCatalogWhenConfigMissing(t *testing.T) {
|
|
||||||
service, db := newTestBaishunService(t)
|
|
||||||
|
|
||||||
if err := db.Create(&model.BaishunGameCatalog{
|
|
||||||
ID: 1,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
InternalGameID: "bs_1146",
|
|
||||||
VendorGameID: 1146,
|
|
||||||
Name: "LordOfOlympus",
|
|
||||||
Cover: "https://cdn.example.com/cover.png",
|
|
||||||
PreviewURL: "https://cdn.example.com/preview.png",
|
|
||||||
DownloadURL: "https://cdn.example.com/game/index.html",
|
|
||||||
PackageVersion: "1.0.0",
|
|
||||||
SafeHeight: 24,
|
|
||||||
GSP: "222",
|
|
||||||
Status: baishunCatalogEnabled,
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create catalog: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := service.LaunchGame(context.Background(), AuthUser{
|
|
||||||
UserID: 1234567,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
}, BaishunLaunchAppRequest{
|
|
||||||
RoomID: "room-1",
|
|
||||||
GameID: "bs_1146",
|
|
||||||
SceneMode: 1,
|
|
||||||
ClientOrigin: "COMMON",
|
|
||||||
}, "127.0.0.1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("LaunchGame() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.GameID != "bs_1146" {
|
|
||||||
t.Fatalf("gameId = %q, want bs_1146", resp.GameID)
|
|
||||||
}
|
|
||||||
if resp.VendorGameID != 1146 {
|
|
||||||
t.Fatalf("vendorGameId = %d, want 1146", resp.VendorGameID)
|
|
||||||
}
|
|
||||||
if resp.Entry.EntryURL != "https://cdn.example.com/game/index.html" {
|
|
||||||
t.Fatalf("entryUrl = %q, want catalog download url", resp.Entry.EntryURL)
|
|
||||||
}
|
|
||||||
if resp.BridgeConfig.GSP != 222 {
|
|
||||||
t.Fatalf("gsp = %d, want 222", resp.BridgeConfig.GSP)
|
|
||||||
}
|
|
||||||
if resp.RoomState.State != baishunRoomStatePlaying {
|
|
||||||
t.Fatalf("roomState.state = %q, want %q", resp.RoomState.State, baishunRoomStatePlaying)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,230 +0,0 @@
|
|||||||
package baishun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/utils"
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
type baishunRuntimeConfig struct {
|
|
||||||
SysOrigin string
|
|
||||||
PlatformBaseURL string
|
|
||||||
AppID int64
|
|
||||||
AppName string
|
|
||||||
AppChannel string
|
|
||||||
AppKey string
|
|
||||||
GSP int
|
|
||||||
LaunchCodeTTLSeconds int
|
|
||||||
SSTokenTTLSeconds int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c baishunRuntimeConfig) appChannel() string {
|
|
||||||
return defaultIfBlank(strings.TrimSpace(c.AppChannel), "skychat")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c baishunRuntimeConfig) gsp() int {
|
|
||||||
if c.GSP > 0 {
|
|
||||||
return c.GSP
|
|
||||||
}
|
|
||||||
return 101
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c baishunRuntimeConfig) launchCodeTTLSeconds() int {
|
|
||||||
if c.LaunchCodeTTLSeconds > 0 {
|
|
||||||
return c.LaunchCodeTTLSeconds
|
|
||||||
}
|
|
||||||
return 300
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c baishunRuntimeConfig) ssTokenTTLSeconds() int {
|
|
||||||
if c.SSTokenTTLSeconds > 0 {
|
|
||||||
return c.SSTokenTTLSeconds
|
|
||||||
}
|
|
||||||
return 86400
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) defaultRuntimeConfig(sysOrigin string) baishunRuntimeConfig {
|
|
||||||
return baishunRuntimeConfig{
|
|
||||||
SysOrigin: normalizeAdminSysOrigin(sysOrigin),
|
|
||||||
PlatformBaseURL: strings.TrimSpace(s.cfg.Baishun.PlatformBaseURL),
|
|
||||||
AppID: s.cfg.Baishun.AppID,
|
|
||||||
AppName: strings.TrimSpace(s.cfg.Baishun.AppName),
|
|
||||||
AppChannel: strings.TrimSpace(s.cfg.Baishun.AppChannel),
|
|
||||||
AppKey: strings.TrimSpace(s.cfg.Baishun.AppKey),
|
|
||||||
GSP: s.cfg.Baishun.GSP,
|
|
||||||
LaunchCodeTTLSeconds: s.cfg.Baishun.LaunchCodeTTLSeconds,
|
|
||||||
SSTokenTTLSeconds: s.cfg.Baishun.SSTokenTTLSeconds,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyProviderRow(base baishunRuntimeConfig, row model.BaishunProviderConfig) baishunRuntimeConfig {
|
|
||||||
base.SysOrigin = normalizeAdminSysOrigin(defaultIfBlank(row.SysOrigin, base.SysOrigin))
|
|
||||||
if strings.TrimSpace(row.PlatformBaseURL) != "" {
|
|
||||||
base.PlatformBaseURL = strings.TrimSpace(row.PlatformBaseURL)
|
|
||||||
}
|
|
||||||
if row.AppID > 0 {
|
|
||||||
base.AppID = row.AppID
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(row.AppName) != "" {
|
|
||||||
base.AppName = strings.TrimSpace(row.AppName)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(row.AppChannel) != "" {
|
|
||||||
base.AppChannel = strings.TrimSpace(row.AppChannel)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(row.AppKey) != "" {
|
|
||||||
base.AppKey = strings.TrimSpace(row.AppKey)
|
|
||||||
}
|
|
||||||
if row.GSP > 0 {
|
|
||||||
base.GSP = row.GSP
|
|
||||||
}
|
|
||||||
if row.LaunchCodeTTLSeconds > 0 {
|
|
||||||
base.LaunchCodeTTLSeconds = row.LaunchCodeTTLSeconds
|
|
||||||
}
|
|
||||||
if row.SSTokenTTLSeconds > 0 {
|
|
||||||
base.SSTokenTTLSeconds = row.SSTokenTTLSeconds
|
|
||||||
}
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) resolveRuntimeConfig(ctx context.Context, sysOrigin string) (baishunRuntimeConfig, error) {
|
|
||||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
|
||||||
base := s.defaultRuntimeConfig(sysOrigin)
|
|
||||||
|
|
||||||
var row model.BaishunProviderConfig
|
|
||||||
err := s.repo.DB.WithContext(ctx).
|
|
||||||
Where("sys_origin = ?", sysOrigin).
|
|
||||||
Limit(1).
|
|
||||||
First(&row).Error
|
|
||||||
if err == nil {
|
|
||||||
return applyProviderRow(base, row), nil
|
|
||||||
}
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
return base, nil
|
|
||||||
}
|
|
||||||
return baishunRuntimeConfig{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) resolveRuntimeConfigByApp(ctx context.Context, appID int64, appChannel string) (baishunRuntimeConfig, error) {
|
|
||||||
base := s.defaultRuntimeConfig("LIKEI")
|
|
||||||
appChannel = strings.TrimSpace(appChannel)
|
|
||||||
|
|
||||||
queries := make([]func(*gorm.DB) *gorm.DB, 0, 3)
|
|
||||||
if appID > 0 && appChannel != "" {
|
|
||||||
queries = append(queries, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Where("app_id = ? AND app_channel = ?", appID, appChannel)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if appID > 0 {
|
|
||||||
queries = append(queries, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Where("app_id = ?", appID)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if appChannel != "" {
|
|
||||||
queries = append(queries, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Where("app_channel = ?", appChannel)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, build := range queries {
|
|
||||||
var row model.BaishunProviderConfig
|
|
||||||
err := build(s.repo.DB.WithContext(ctx)).Limit(1).First(&row).Error
|
|
||||||
if err == nil {
|
|
||||||
return applyProviderRow(s.defaultRuntimeConfig(row.SysOrigin), row), nil
|
|
||||||
}
|
|
||||||
if err != gorm.ErrRecordNotFound {
|
|
||||||
return baishunRuntimeConfig{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return base, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) GetProviderConfig(ctx context.Context, sysOrigin string) (*BaishunProviderConfigItem, error) {
|
|
||||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var row model.BaishunProviderConfig
|
|
||||||
_ = s.repo.DB.WithContext(ctx).
|
|
||||||
Where("sys_origin = ?", normalizeAdminSysOrigin(sysOrigin)).
|
|
||||||
Limit(1).
|
|
||||||
First(&row).Error
|
|
||||||
|
|
||||||
return &BaishunProviderConfigItem{
|
|
||||||
ID: row.ID,
|
|
||||||
SysOrigin: runtimeCfg.SysOrigin,
|
|
||||||
PlatformBaseURL: runtimeCfg.PlatformBaseURL,
|
|
||||||
AppID: runtimeCfg.AppID,
|
|
||||||
AppName: runtimeCfg.AppName,
|
|
||||||
AppChannel: runtimeCfg.appChannel(),
|
|
||||||
AppKey: runtimeCfg.AppKey,
|
|
||||||
GSP: runtimeCfg.gsp(),
|
|
||||||
LaunchCodeTTLSeconds: runtimeCfg.launchCodeTTLSeconds(),
|
|
||||||
SSTokenTTLSeconds: runtimeCfg.ssTokenTTLSeconds(),
|
|
||||||
UpdateTime: formatAdminTime(row.UpdateTime),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishunProviderConfigRequest) (*BaishunProviderConfigItem, error) {
|
|
||||||
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
|
||||||
platformBaseURL := strings.TrimSpace(req.PlatformBaseURL)
|
|
||||||
appChannel := strings.TrimSpace(req.AppChannel)
|
|
||||||
appKey := strings.TrimSpace(req.AppKey)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case platformBaseURL == "":
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "platform_base_url_required", "platformBaseUrl is required")
|
|
||||||
case req.AppID <= 0:
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "app_id_required", "appId is required")
|
|
||||||
case appChannel == "":
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "app_channel_required", "appChannel is required")
|
|
||||||
case appKey == "":
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "app_key_required", "appKey is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := utils.NextID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
row := model.BaishunProviderConfig{
|
|
||||||
ID: id,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
PlatformBaseURL: platformBaseURL,
|
|
||||||
AppID: req.AppID,
|
|
||||||
AppName: strings.TrimSpace(req.AppName),
|
|
||||||
AppChannel: appChannel,
|
|
||||||
AppKey: appKey,
|
|
||||||
GSP: req.GSP,
|
|
||||||
LaunchCodeTTLSeconds: req.LaunchCodeTTLSeconds,
|
|
||||||
SSTokenTTLSeconds: req.SSTokenTTLSeconds,
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
}
|
|
||||||
if row.GSP <= 0 {
|
|
||||||
row.GSP = 101
|
|
||||||
}
|
|
||||||
if row.LaunchCodeTTLSeconds <= 0 {
|
|
||||||
row.LaunchCodeTTLSeconds = 300
|
|
||||||
}
|
|
||||||
if row.SSTokenTTLSeconds <= 0 {
|
|
||||||
row.SSTokenTTLSeconds = 86400
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
|
||||||
Columns: []clause.Column{{Name: "sys_origin"}},
|
|
||||||
DoUpdates: clause.AssignmentColumns([]string{
|
|
||||||
"platform_base_url", "app_id", "app_name", "app_channel", "app_key",
|
|
||||||
"gsp", "launch_code_ttl_seconds", "ss_token_ttl_seconds", "update_time",
|
|
||||||
}),
|
|
||||||
}).Create(&row).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s.GetProviderConfig(ctx, sysOrigin)
|
|
||||||
}
|
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -79,13 +80,12 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
|
|||||||
ext.package_version AS ext_package_version,
|
ext.package_version AS ext_package_version,
|
||||||
ext.preview_url AS ext_preview_url,
|
ext.preview_url AS ext_preview_url,
|
||||||
ext.package_url AS package_url,
|
ext.package_url AS package_url,
|
||||||
ext.orientation AS ext_orientation,
|
ext.orientation AS ext_orientation,
|
||||||
ext.safe_height AS ext_safe_height,
|
ext.safe_height AS ext_safe_height,
|
||||||
ext.currency_icon AS currency_icon,
|
ext.currency_icon AS currency_icon,
|
||||||
ext.gsp AS ext_gsp,
|
ext.gsp AS ext_gsp,
|
||||||
ext.extra_json AS ext_extra_json,
|
cat.name AS catalog_name,
|
||||||
cat.name AS catalog_name,
|
cat.cover AS catalog_cover,
|
||||||
cat.cover AS catalog_cover,
|
|
||||||
cat.preview_url AS catalog_preview_url,
|
cat.preview_url AS catalog_preview_url,
|
||||||
cat.download_url AS catalog_download_url,
|
cat.download_url AS catalog_download_url,
|
||||||
cat.package_version AS catalog_package_version,
|
cat.package_version AS catalog_package_version,
|
||||||
@ -99,6 +99,9 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(roomID) != "" {
|
||||||
|
query = query.Where("cfg.game_mode LIKE ?", "%CHAT_ROOM%")
|
||||||
|
}
|
||||||
if err := query.Order("cfg.sort DESC").Scan(&rows).Error; err != nil {
|
if err := query.Order("cfg.sort DESC").Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -107,6 +110,42 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
|
|||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
items = append(items, row.toListItem())
|
items = append(items, row.toListItem())
|
||||||
}
|
}
|
||||||
|
if len(items) > 0 {
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var catalogs []model.BaishunGameCatalog
|
||||||
|
if err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("sys_origin = ? AND status = ?", sysOrigin, baishunCatalogEnabled).
|
||||||
|
Order("vendor_game_id ASC").
|
||||||
|
Find(&catalogs).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, catalog := range catalogs {
|
||||||
|
gameID := defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID))
|
||||||
|
items = append(items, RoomGameListItem{
|
||||||
|
GameID: gameID,
|
||||||
|
VendorType: baishunVendorType,
|
||||||
|
VendorGameID: catalog.VendorGameID,
|
||||||
|
Name: catalog.Name,
|
||||||
|
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
||||||
|
Category: "CHAT_ROOM",
|
||||||
|
Sort: int64(catalog.VendorGameID),
|
||||||
|
LaunchMode: baishunLaunchModeRemote,
|
||||||
|
FullScreen: true,
|
||||||
|
GameMode: pickFirstInt(catalog.GameModeJSON, 3),
|
||||||
|
SafeHeight: catalog.SafeHeight,
|
||||||
|
Orientation: derefInt(catalog.Orientation),
|
||||||
|
PackageVersion: catalog.PackageVersion,
|
||||||
|
Status: catalog.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
if items[i].Sort == items[j].Sort {
|
||||||
|
return items[i].VendorGameID < items[j].VendorGameID
|
||||||
|
}
|
||||||
|
return items[i].Sort > items[j].Sort
|
||||||
|
})
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,13 +170,12 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
|
|||||||
ext.package_version AS ext_package_version,
|
ext.package_version AS ext_package_version,
|
||||||
ext.preview_url AS ext_preview_url,
|
ext.preview_url AS ext_preview_url,
|
||||||
ext.package_url AS package_url,
|
ext.package_url AS package_url,
|
||||||
ext.orientation AS ext_orientation,
|
ext.orientation AS ext_orientation,
|
||||||
ext.safe_height AS ext_safe_height,
|
ext.safe_height AS ext_safe_height,
|
||||||
ext.currency_icon AS currency_icon,
|
ext.currency_icon AS currency_icon,
|
||||||
ext.gsp AS ext_gsp,
|
ext.gsp AS ext_gsp,
|
||||||
ext.extra_json AS ext_extra_json,
|
cat.name AS catalog_name,
|
||||||
cat.name AS catalog_name,
|
cat.cover AS catalog_cover,
|
||||||
cat.cover AS catalog_cover,
|
|
||||||
cat.preview_url AS catalog_preview_url,
|
cat.preview_url AS catalog_preview_url,
|
||||||
cat.download_url AS catalog_download_url,
|
cat.download_url AS catalog_download_url,
|
||||||
cat.package_version AS catalog_package_version,
|
cat.package_version AS catalog_package_version,
|
||||||
@ -163,13 +201,12 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
|
|||||||
First(&catalog).Error; err == nil {
|
First(&catalog).Error; err == nil {
|
||||||
return gameListRow{
|
return gameListRow{
|
||||||
SysOrigin: sysOrigin,
|
SysOrigin: sysOrigin,
|
||||||
InternalGameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
|
InternalGameID: catalog.InternalGameID,
|
||||||
Name: catalog.Name,
|
Name: catalog.Name,
|
||||||
Cover: catalog.Cover,
|
Cover: catalog.Cover,
|
||||||
VendorType: baishunVendorType,
|
VendorType: baishunVendorType,
|
||||||
VendorGameID: strconv.Itoa(catalog.VendorGameID),
|
VendorGameID: strconv.Itoa(catalog.VendorGameID),
|
||||||
LaunchMode: baishunLaunchModeRemote,
|
LaunchMode: baishunLaunchModeRemote,
|
||||||
ExtGSP: catalog.GSP,
|
|
||||||
CatalogCover: catalog.Cover,
|
CatalogCover: catalog.Cover,
|
||||||
CatalogPreviewURL: catalog.PreviewURL,
|
CatalogPreviewURL: catalog.PreviewURL,
|
||||||
CatalogDownloadURL: catalog.DownloadURL,
|
CatalogDownloadURL: catalog.DownloadURL,
|
||||||
@ -177,68 +214,19 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
|
|||||||
CatalogSafeHeight: catalog.SafeHeight,
|
CatalogSafeHeight: catalog.SafeHeight,
|
||||||
CatalogOrientation: catalog.Orientation,
|
CatalogOrientation: catalog.Orientation,
|
||||||
CatalogStatus: catalog.Status,
|
CatalogStatus: catalog.Status,
|
||||||
GameModeRaw: formatAdminGameModeRaw(baishunFixedGameMode),
|
GameModeRaw: catalog.GameModeJSON,
|
||||||
}, nil
|
}, nil
|
||||||
} else if err != gorm.ErrRecordNotFound {
|
|
||||||
return gameListRow{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return gameListRow{}, NewAppError(http.StatusNotFound, "game_not_found", "room game config not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// findGameRowByConfigID 按系统游戏配置主键查询游戏聚合信息。
|
|
||||||
func (s *BaishunService) findGameRowByConfigID(ctx context.Context, sysOrigin string, configID int64) (gameListRow, error) {
|
|
||||||
var row gameListRow
|
|
||||||
err := s.repo.DB.WithContext(ctx).
|
|
||||||
Table("sys_game_list_config AS cfg").
|
|
||||||
Select(`
|
|
||||||
cfg.id AS config_id,
|
|
||||||
cfg.sys_origin AS sys_origin,
|
|
||||||
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,
|
|
||||||
cfg.game_mode AS game_mode_raw,
|
|
||||||
ext.vendor_type AS vendor_type,
|
|
||||||
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.currency_icon AS currency_icon,
|
|
||||||
ext.gsp AS ext_gsp,
|
|
||||||
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.orientation AS catalog_orientation,
|
|
||||||
cat.safe_height AS catalog_safe_height,
|
|
||||||
cat.status AS catalog_status
|
|
||||||
`).
|
|
||||||
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1").
|
|
||||||
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
|
||||||
Where("cfg.sys_origin = ? AND cfg.id = ? AND ext.vendor_type = ?", sysOrigin, configID, baishunVendorType).
|
|
||||||
Limit(1).
|
|
||||||
Scan(&row).Error
|
|
||||||
if err != nil {
|
|
||||||
return gameListRow{}, err
|
|
||||||
}
|
|
||||||
if row.ConfigID > 0 {
|
|
||||||
return row, nil
|
|
||||||
}
|
}
|
||||||
return gameListRow{}, NewAppError(http.StatusNotFound, "game_not_found", "room game config not found")
|
return gameListRow{}, NewAppError(http.StatusNotFound, "game_not_found", "room game config not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveGSP 解析行模型中的 GSP 值。
|
// resolveGSP 解析行模型中的 GSP 值。
|
||||||
func (s *BaishunService) resolveGSP(row gameListRow, runtimeCfg baishunRuntimeConfig) int {
|
func (s *BaishunService) resolveGSP(row gameListRow) int {
|
||||||
if parsed, err := strconv.Atoi(strings.TrimSpace(row.ExtGSP)); err == nil && parsed > 0 {
|
if parsed, err := strconv.Atoi(strings.TrimSpace(row.ExtGSP)); err == nil && parsed > 0 {
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
return runtimeCfg.gsp()
|
if s.cfg.Baishun.GSP > 0 {
|
||||||
|
return s.cfg.Baishun.GSP
|
||||||
|
}
|
||||||
|
return 101
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,23 +77,20 @@ func NewBaishunService(cfg config.Config, db baishunDB, cache redis.Cmdable, jav
|
|||||||
|
|
||||||
// RoomGameListItem 是房间游戏列表中的单个游戏项。
|
// RoomGameListItem 是房间游戏列表中的单个游戏项。
|
||||||
type RoomGameListItem struct {
|
type RoomGameListItem struct {
|
||||||
ID int64 `json:"id"`
|
GameID string `json:"gameId"`
|
||||||
GameID string `json:"gameId"`
|
VendorType string `json:"vendorType"`
|
||||||
GameType string `json:"gameType,omitempty"`
|
VendorGameID int `json:"vendorGameId"`
|
||||||
VendorType string `json:"vendorType"`
|
Name string `json:"name"`
|
||||||
VendorGameID int `json:"vendorGameId"`
|
Cover string `json:"cover"`
|
||||||
Name string `json:"name"`
|
Category string `json:"category,omitempty"`
|
||||||
Cover string `json:"cover"`
|
Sort int64 `json:"sort,omitempty"`
|
||||||
Category string `json:"category,omitempty"`
|
LaunchMode string `json:"launchMode"`
|
||||||
Sort int64 `json:"sort,omitempty"`
|
FullScreen bool `json:"fullScreen"`
|
||||||
LaunchMode string `json:"launchMode"`
|
GameMode int `json:"gameMode"`
|
||||||
FullScreen bool `json:"fullScreen"`
|
SafeHeight int `json:"safeHeight"`
|
||||||
GameMode int `json:"gameMode"`
|
Orientation int `json:"orientation"`
|
||||||
SafeHeight int `json:"safeHeight"`
|
PackageVersion string `json:"packageVersion,omitempty"`
|
||||||
Orientation int `json:"orientation"`
|
Status string `json:"status,omitempty"`
|
||||||
PackageVersion string `json:"packageVersion,omitempty"`
|
|
||||||
Status string `json:"status,omitempty"`
|
|
||||||
LaunchParams map[string]any `json:"launchParams,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomGameListResponse 是房间游戏列表响应。
|
// RoomGameListResponse 是房间游戏列表响应。
|
||||||
@ -115,7 +112,6 @@ type BaishunRoomStateResponse struct {
|
|||||||
|
|
||||||
// BaishunLaunchAppRequest 是启动百顺游戏入参。
|
// BaishunLaunchAppRequest 是启动百顺游戏入参。
|
||||||
type BaishunLaunchAppRequest struct {
|
type BaishunLaunchAppRequest struct {
|
||||||
ID int64 `json:"id,omitempty"`
|
|
||||||
RoomID string `json:"roomId"`
|
RoomID string `json:"roomId"`
|
||||||
GameID string `json:"gameId"`
|
GameID string `json:"gameId"`
|
||||||
SceneMode int `json:"sceneMode"`
|
SceneMode int `json:"sceneMode"`
|
||||||
@ -162,7 +158,6 @@ type BaishunLaunchEntry struct {
|
|||||||
|
|
||||||
// BaishunLaunchAppResponse 是启动游戏接口返回体。
|
// BaishunLaunchAppResponse 是启动游戏接口返回体。
|
||||||
type BaishunLaunchAppResponse struct {
|
type BaishunLaunchAppResponse struct {
|
||||||
ID int64 `json:"id,omitempty"`
|
|
||||||
GameSessionID string `json:"gameSessionId"`
|
GameSessionID string `json:"gameSessionId"`
|
||||||
VendorType string `json:"vendorType"`
|
VendorType string `json:"vendorType"`
|
||||||
GameID string `json:"gameId"`
|
GameID string `json:"gameId"`
|
||||||
@ -186,147 +181,6 @@ type SyncCatalogResponse struct {
|
|||||||
Skipped int `json:"skipped"`
|
Skipped int `json:"skipped"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminBaishunGameItem 是后台管理页展示的百顺游戏配置行。
|
|
||||||
type AdminBaishunGameItem struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
GameType string `json:"gameType,omitempty"`
|
|
||||||
VendorGameID int `json:"vendorGameId"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Category string `json:"category"`
|
|
||||||
GameCode string `json:"gameCode"`
|
|
||||||
Cover string `json:"cover"`
|
|
||||||
Amounts string `json:"amounts"`
|
|
||||||
Showcase bool `json:"showcase"`
|
|
||||||
Sort int64 `json:"sort"`
|
|
||||||
Height float64 `json:"height"`
|
|
||||||
Width float64 `json:"width"`
|
|
||||||
FullScreen bool `json:"fullScreen"`
|
|
||||||
ClientOrigin string `json:"clientOrigin"`
|
|
||||||
Regions string `json:"regions"`
|
|
||||||
GameMode int `json:"gameMode"`
|
|
||||||
LaunchMode string `json:"launchMode"`
|
|
||||||
PackageVersion string `json:"packageVersion"`
|
|
||||||
PreviewURL string `json:"previewUrl"`
|
|
||||||
PackageURL string `json:"packageUrl"`
|
|
||||||
Orientation int `json:"orientation"`
|
|
||||||
SafeHeight int `json:"safeHeight"`
|
|
||||||
GSP string `json:"gsp"`
|
|
||||||
CurrencyIcon string `json:"currencyIcon"`
|
|
||||||
CatalogStatus string `json:"catalogStatus"`
|
|
||||||
LaunchParams map[string]any `json:"launchParams,omitempty"`
|
|
||||||
CreateTime string `json:"createTime"`
|
|
||||||
UpdateTime string `json:"updateTime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminBaishunGamePageResponse 是后台页分页响应。
|
|
||||||
type AdminBaishunGamePageResponse struct {
|
|
||||||
Cursor int `json:"cursor"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Records []AdminBaishunGameItem `json:"records"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAdminBaishunGameRequest 是后台保存百顺游戏配置的入参。
|
|
||||||
type SaveAdminBaishunGameRequest struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
VendorGameID int `json:"vendorGameId"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Category string `json:"category"`
|
|
||||||
GameCode string `json:"gameCode"`
|
|
||||||
Cover string `json:"cover"`
|
|
||||||
Amounts string `json:"amounts"`
|
|
||||||
Showcase *bool `json:"showcase"`
|
|
||||||
Sort int64 `json:"sort"`
|
|
||||||
Height float64 `json:"height"`
|
|
||||||
Width float64 `json:"width"`
|
|
||||||
FullScreen bool `json:"fullScreen"`
|
|
||||||
ClientOrigin string `json:"clientOrigin"`
|
|
||||||
Regions string `json:"regions"`
|
|
||||||
GameMode int `json:"gameMode"`
|
|
||||||
LaunchMode string `json:"launchMode"`
|
|
||||||
PackageVersion string `json:"packageVersion"`
|
|
||||||
PreviewURL string `json:"previewUrl"`
|
|
||||||
PackageURL string `json:"packageUrl"`
|
|
||||||
Orientation *int `json:"orientation"`
|
|
||||||
SafeHeight int `json:"safeHeight"`
|
|
||||||
GSP string `json:"gsp"`
|
|
||||||
CurrencyIcon string `json:"currencyIcon"`
|
|
||||||
LaunchParams map[string]any `json:"launchParams,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BaishunProviderConfigItem 是后台页展示的百顺配置。
|
|
||||||
type BaishunProviderConfigItem struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
PlatformBaseURL string `json:"platformBaseUrl"`
|
|
||||||
AppID int64 `json:"appId"`
|
|
||||||
AppName string `json:"appName"`
|
|
||||||
AppChannel string `json:"appChannel"`
|
|
||||||
AppKey string `json:"appKey"`
|
|
||||||
GSP int `json:"gsp"`
|
|
||||||
LaunchCodeTTLSeconds int `json:"launchCodeTtlSeconds"`
|
|
||||||
SSTokenTTLSeconds int `json:"ssTokenTtlSeconds"`
|
|
||||||
UpdateTime string `json:"updateTime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveBaishunProviderConfigRequest 是后台保存百顺配置的入参。
|
|
||||||
type SaveBaishunProviderConfigRequest struct {
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
PlatformBaseURL string `json:"platformBaseUrl"`
|
|
||||||
AppID int64 `json:"appId"`
|
|
||||||
AppName string `json:"appName"`
|
|
||||||
AppChannel string `json:"appChannel"`
|
|
||||||
AppKey string `json:"appKey"`
|
|
||||||
GSP int `json:"gsp"`
|
|
||||||
LaunchCodeTTLSeconds int `json:"launchCodeTtlSeconds"`
|
|
||||||
SSTokenTTLSeconds int `json:"ssTokenTtlSeconds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminBaishunCatalogItem 是后台目录页展示的单个百顺目录项。
|
|
||||||
type AdminBaishunCatalogItem struct {
|
|
||||||
VendorGameID int `json:"vendorGameId"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Cover string `json:"cover"`
|
|
||||||
PreviewURL string `json:"previewUrl"`
|
|
||||||
DownloadURL string `json:"downloadUrl"`
|
|
||||||
PackageVersion string `json:"packageVersion"`
|
|
||||||
GSP string `json:"gsp"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
SafeHeight int `json:"safeHeight"`
|
|
||||||
Orientation int `json:"orientation"`
|
|
||||||
Added bool `json:"added"`
|
|
||||||
Showcase bool `json:"showcase"`
|
|
||||||
UpdateTime string `json:"updateTime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminBaishunCatalogPageResponse 是后台目录页分页响应。
|
|
||||||
type AdminBaishunCatalogPageResponse struct {
|
|
||||||
Cursor int `json:"cursor"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Records []AdminBaishunCatalogItem `json:"records"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BaishunTokenRequest 是百顺拉取 token/code 回调入参。
|
// BaishunTokenRequest 是百顺拉取 token/code 回调入参。
|
||||||
type BaishunTokenRequest struct {
|
type BaishunTokenRequest struct {
|
||||||
AppID int64 `json:"app_id"`
|
AppID int64 `json:"app_id"`
|
||||||
@ -427,7 +281,6 @@ type gameListRow struct {
|
|||||||
ExtSafeHeight int
|
ExtSafeHeight int
|
||||||
CurrencyIcon string
|
CurrencyIcon string
|
||||||
ExtGSP string
|
ExtGSP string
|
||||||
ExtExtraJSON string
|
|
||||||
CatalogName string
|
CatalogName string
|
||||||
CatalogCover string
|
CatalogCover string
|
||||||
CatalogPreviewURL string
|
CatalogPreviewURL string
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
package gameopen
|
|
||||||
|
|
||||||
import "chatapp3-golang/internal/common"
|
|
||||||
|
|
||||||
type AppError = common.AppError
|
|
||||||
|
|
||||||
var NewAppError = common.NewAppError
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
package gameopen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildQueryUserSign(req QueryUserRequest, key string) string {
|
|
||||||
return md5Hex(req.GameID + req.UID + req.Token + req.RoomID + key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildUpdateCoinSign(req UpdateCoinRequest, key string) string {
|
|
||||||
return md5Hex(
|
|
||||||
req.OrderID +
|
|
||||||
req.GameID +
|
|
||||||
req.RoundID +
|
|
||||||
req.UID +
|
|
||||||
fmt.Sprintf("%d", req.Coin) +
|
|
||||||
fmt.Sprintf("%d", req.Type) +
|
|
||||||
fmt.Sprintf("%d", req.RewardType) +
|
|
||||||
req.Token +
|
|
||||||
req.WinID +
|
|
||||||
req.RoomID +
|
|
||||||
key,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSupplementSign(req SupplementRequest, key string) string {
|
|
||||||
return md5Hex(
|
|
||||||
req.OrderID +
|
|
||||||
req.GameID +
|
|
||||||
req.RoundID +
|
|
||||||
req.UID +
|
|
||||||
fmt.Sprintf("%d", req.Coin) +
|
|
||||||
fmt.Sprintf("%d", req.RewardType) +
|
|
||||||
req.WinID +
|
|
||||||
req.RoomID +
|
|
||||||
key,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifySign(actual, expected string) bool {
|
|
||||||
return strings.EqualFold(strings.TrimSpace(actual), strings.TrimSpace(expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
func receiptTypeFromType(typed int) string {
|
|
||||||
if typed == 2 {
|
|
||||||
return "INCOME"
|
|
||||||
}
|
|
||||||
return "EXPENDITURE"
|
|
||||||
}
|
|
||||||
|
|
||||||
func md5Hex(value string) string {
|
|
||||||
sum := md5.Sum([]byte(value))
|
|
||||||
return hex.EncodeToString(sum[:])
|
|
||||||
}
|
|
||||||
@ -1,306 +0,0 @@
|
|||||||
package gameopen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/utils"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HandleQueryUser 处理统一查询用户信息回调。
|
|
||||||
func (s *GameOpenService) HandleQueryUser(ctx context.Context, req QueryUserRequest, rawJSON string) CallbackResponse {
|
|
||||||
if err := s.validateQueryUserRequest(req); err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeBadRequest, err.Error())
|
|
||||||
s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
if !verifySign(req.Sign, buildQueryUserSign(req, s.cfg.GameOpen.AppKey)) {
|
|
||||||
resp := failResponse(gameOpenCodeSignatureError, "signature error")
|
|
||||||
s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
credential, err := s.java.AuthenticateToken(ctx, req.Token)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeTokenInvalid, "token invalid")
|
|
||||||
s.saveCallbackLog(ctx, "query-user", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
userID := int64(credential.UserID)
|
|
||||||
if req.UID != strconv.FormatInt(userID, 10) {
|
|
||||||
resp := failResponse(gameOpenCodeTokenInvalid, "uid token mismatch")
|
|
||||||
s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
profile, err := s.java.GetUserProfile(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeServerError, "profile load failed")
|
|
||||||
s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
balance, err := s.readUserBalance(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeServerError, "balance load failed")
|
|
||||||
s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := successResponse(map[string]any{
|
|
||||||
"uid": req.UID,
|
|
||||||
"nickname": profile.UserNickname,
|
|
||||||
"avatar": profile.UserAvatar,
|
|
||||||
"coin": balance,
|
|
||||||
})
|
|
||||||
s.saveCallbackLog(ctx, "query-user", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusSuccess)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleUpdateCoin 处理统一游戏币变更回调。
|
|
||||||
func (s *GameOpenService) HandleUpdateCoin(ctx context.Context, req UpdateCoinRequest, rawJSON string) CallbackResponse {
|
|
||||||
if err := s.validateUpdateCoinRequest(req); err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeBadRequest, err.Error())
|
|
||||||
s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
if !verifySign(req.Sign, buildUpdateCoinSign(req, s.cfg.GameOpen.AppKey)) {
|
|
||||||
resp := failResponse(gameOpenCodeSignatureError, "signature error")
|
|
||||||
s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
credential, err := s.java.AuthenticateToken(ctx, req.Token)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeTokenInvalid, "token invalid")
|
|
||||||
s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
userID := int64(credential.UserID)
|
|
||||||
if req.UID != strconv.FormatInt(userID, 10) {
|
|
||||||
resp := failResponse(gameOpenCodeTokenInvalid, "uid token mismatch")
|
|
||||||
s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
eventID := "GAME_OPEN:" + strings.TrimSpace(req.OrderID)
|
|
||||||
exists, err := s.java.ExistsGoldEvent(ctx, eventID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeServerError, "event check failed")
|
|
||||||
s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
if err := s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{
|
|
||||||
ReceiptType: receiptTypeFromType(req.Type),
|
|
||||||
UserID: userID,
|
|
||||||
SysOrigin: credential.SysOrigin,
|
|
||||||
EventID: eventID,
|
|
||||||
Remark: fmt.Sprintf("game=%s rewardType=%d round=%s", req.GameID, req.RewardType, req.RoundID),
|
|
||||||
Amount: integration.NewPennyAmountPayloadFromDollar(req.Coin),
|
|
||||||
CloseDelayAsset: false,
|
|
||||||
OpUserType: "APP",
|
|
||||||
CustomizeOrigin: "GAME_OPEN_" + strings.TrimSpace(req.GameID),
|
|
||||||
CustomizeOriginDesc: "GAME OPEN[" + strings.TrimSpace(req.GameID) + "]",
|
|
||||||
}); err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeServerError, err.Error())
|
|
||||||
s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
balance, err := s.readUserBalance(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeServerError, "balance load failed")
|
|
||||||
s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := successResponse(map[string]any{"coin": balance})
|
|
||||||
s.saveCallbackLog(ctx, "update-coin", rawJSON, resp, credential.SysOrigin, req, &userID, gameOpenLogStatusSuccess)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleSupplement 处理统一补单回调,当前只做签名校验和日志落库。
|
|
||||||
func (s *GameOpenService) HandleSupplement(ctx context.Context, req SupplementRequest, rawJSON string) CallbackResponse {
|
|
||||||
if err := s.validateSupplementRequest(req); err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeBadRequest, err.Error())
|
|
||||||
s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
if !verifySign(req.Sign, buildSupplementSign(req, s.cfg.GameOpen.AppKey)) {
|
|
||||||
resp := failResponse(gameOpenCodeSignatureError, "signature error")
|
|
||||||
s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, nil, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := parseInt64Default(req.UID)
|
|
||||||
balance, err := s.readUserBalance(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
resp := failResponse(gameOpenCodeServerError, "balance load failed")
|
|
||||||
s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, &userID, gameOpenLogStatusFailed)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := successResponse(map[string]any{"coin": balance})
|
|
||||||
s.saveCallbackLog(ctx, "supplement", rawJSON, resp, "", req, &userID, gameOpenLogStatusPending)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIntegrationInfo 返回当前统一三方游戏联调信息。
|
|
||||||
func (s *GameOpenService) GetIntegrationInfo(ctx context.Context, publicBaseURL string) (*IntegrationInfoResponse, error) {
|
|
||||||
baseURL := strings.TrimRight(strings.TrimSpace(publicBaseURL), "/")
|
|
||||||
if baseURL == "" {
|
|
||||||
baseURL = strings.TrimRight(strings.TrimSpace(s.cfg.GameOpen.PublicBaseURL), "/")
|
|
||||||
}
|
|
||||||
if baseURL == "" {
|
|
||||||
return nil, NewAppError(http.StatusServiceUnavailable, "public_base_url_required", "public base url is required")
|
|
||||||
}
|
|
||||||
if s.cfg.GameOpen.TestUserID <= 0 {
|
|
||||||
return nil, NewAppError(http.StatusServiceUnavailable, "test_user_required", "test user is required")
|
|
||||||
}
|
|
||||||
token, err := s.java.CreateIfAbsentUserCredentialToken(ctx, s.cfg.GameOpen.TestUserID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &IntegrationInfoResponse{
|
|
||||||
Domain: baseURL,
|
|
||||||
QueryUser: baseURL + "/game/open/user-info",
|
|
||||||
UpdateCoin: baseURL + "/game/open/update-coin",
|
|
||||||
Supplement: baseURL + "/game/open/supplement",
|
|
||||||
UID: defaultIfBlank(s.cfg.GameOpen.TestAccount, strconv.FormatInt(s.cfg.GameOpen.TestUserID, 10)),
|
|
||||||
Password: s.cfg.GameOpen.TestPassword,
|
|
||||||
Token: strings.TrimSpace(token),
|
|
||||||
Key: s.cfg.GameOpen.AppKey,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) readUserBalance(ctx context.Context, userID int64) (int64, error) {
|
|
||||||
balances, err := s.java.MapGoldBalance(ctx, []int64{userID})
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return balances[userID], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) saveCallbackLog(ctx context.Context, requestType, rawJSON string, resp CallbackResponse, sysOrigin string, req any, userID *int64, status string) {
|
|
||||||
if s == nil || s.repo.DB == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id, err := utils.NextID()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
record := model.GameOpenCallbackLog{
|
|
||||||
ID: id,
|
|
||||||
RequestType: requestType,
|
|
||||||
SysOrigin: strings.TrimSpace(sysOrigin),
|
|
||||||
RequestJSON: rawJSON,
|
|
||||||
ResponseJSON: utils.MustJSONString(resp, ""),
|
|
||||||
BizCode: resp.ErrorCode,
|
|
||||||
BizMessage: resp.ErrorMsg,
|
|
||||||
Status: status,
|
|
||||||
CreateTime: time.Now(),
|
|
||||||
UpdateTime: time.Now(),
|
|
||||||
}
|
|
||||||
if userID != nil && *userID > 0 {
|
|
||||||
value := *userID
|
|
||||||
record.UserID = &value
|
|
||||||
}
|
|
||||||
switch typed := req.(type) {
|
|
||||||
case QueryUserRequest:
|
|
||||||
record.GameID = typed.GameID
|
|
||||||
record.RoomID = typed.RoomID
|
|
||||||
case UpdateCoinRequest:
|
|
||||||
record.OrderID = typed.OrderID
|
|
||||||
record.GameID = typed.GameID
|
|
||||||
record.RoundID = typed.RoundID
|
|
||||||
record.RoomID = typed.RoomID
|
|
||||||
case SupplementRequest:
|
|
||||||
record.OrderID = typed.OrderID
|
|
||||||
record.GameID = typed.GameID
|
|
||||||
record.RoundID = typed.RoundID
|
|
||||||
record.RoomID = typed.RoomID
|
|
||||||
}
|
|
||||||
_ = s.repo.DB.WithContext(ctx).Create(&record).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) validateQueryUserRequest(req QueryUserRequest) 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")
|
|
||||||
case strings.TrimSpace(s.cfg.GameOpen.AppKey) == "":
|
|
||||||
return newBadRequest("app key is blank")
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) validateUpdateCoinRequest(req UpdateCoinRequest) 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(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")
|
|
||||||
case strings.TrimSpace(s.cfg.GameOpen.AppKey) == "":
|
|
||||||
return newBadRequest("app key is blank")
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameOpenService) validateSupplementRequest(req SupplementRequest) 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(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 strings.TrimSpace(req.Sign) == "":
|
|
||||||
return newBadRequest("sign is required")
|
|
||||||
case strings.TrimSpace(s.cfg.GameOpen.AppKey) == "":
|
|
||||||
return newBadRequest("app key is blank")
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultIfBlank(value, fallback string) string {
|
|
||||||
if strings.TrimSpace(value) == "" {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInt64Default(value string) int64 {
|
|
||||||
parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
package gameopen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/config"
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type stubGateway struct {
|
|
||||||
credential integration.UserCredential
|
|
||||||
token string
|
|
||||||
profile integration.UserProfile
|
|
||||||
balance int64
|
|
||||||
exists bool
|
|
||||||
cmd integration.GoldReceiptCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubGateway) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
|
||||||
return s.credential, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubGateway) CreateIfAbsentUserCredentialToken(context.Context, int64) (string, error) {
|
|
||||||
return s.token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubGateway) GetUserProfile(context.Context, int64) (integration.UserProfile, error) {
|
|
||||||
return s.profile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubGateway) MapGoldBalance(context.Context, []int64) (map[int64]int64, error) {
|
|
||||||
return map[int64]int64{int64(s.credential.UserID): s.balance}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubGateway) ExistsGoldEvent(context.Context, string) (bool, error) {
|
|
||||||
return s.exists, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubGateway) ChangeGoldBalance(_ context.Context, cmd integration.GoldReceiptCommand) error {
|
|
||||||
s.cmd = cmd
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleQueryUser(t *testing.T) {
|
|
||||||
gateway := &stubGateway{
|
|
||||||
credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"},
|
|
||||||
profile: integration.UserProfile{
|
|
||||||
ID: 1234567,
|
|
||||||
UserNickname: "tester",
|
|
||||||
UserAvatar: "https://example.com/avatar.png",
|
|
||||||
},
|
|
||||||
balance: 88,
|
|
||||||
}
|
|
||||||
service := NewGameOpenService(config.Config{
|
|
||||||
GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"},
|
|
||||||
}, nil, gateway)
|
|
||||||
req := QueryUserRequest{
|
|
||||||
GameID: "101",
|
|
||||||
UID: "1234567",
|
|
||||||
Token: "token",
|
|
||||||
RoomID: "room-1",
|
|
||||||
}
|
|
||||||
req.Sign = buildQueryUserSign(req, "game-open-test-key")
|
|
||||||
|
|
||||||
resp := service.HandleQueryUser(context.Background(), req, "{}")
|
|
||||||
if resp.ErrorCode != 0 {
|
|
||||||
t.Fatalf("HandleQueryUser() error = %+v", resp)
|
|
||||||
}
|
|
||||||
data, ok := resp.Data.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("HandleQueryUser() data type = %T", resp.Data)
|
|
||||||
}
|
|
||||||
if got := data["coin"]; got != int64(88) {
|
|
||||||
t.Fatalf("HandleQueryUser() coin = %v, want 88", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleUpdateCoin(t *testing.T) {
|
|
||||||
gateway := &stubGateway{
|
|
||||||
credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"},
|
|
||||||
balance: 99,
|
|
||||||
}
|
|
||||||
service := NewGameOpenService(config.Config{
|
|
||||||
GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"},
|
|
||||||
}, nil, gateway)
|
|
||||||
req := UpdateCoinRequest{
|
|
||||||
OrderID: "order-1",
|
|
||||||
GameID: "101",
|
|
||||||
RoundID: "round-1",
|
|
||||||
UID: "1234567",
|
|
||||||
Coin: 15,
|
|
||||||
Type: 2,
|
|
||||||
RewardType: 2,
|
|
||||||
Token: "token",
|
|
||||||
WinID: "win-1",
|
|
||||||
RoomID: "room-1",
|
|
||||||
}
|
|
||||||
req.Sign = buildUpdateCoinSign(req, "game-open-test-key")
|
|
||||||
|
|
||||||
resp := service.HandleUpdateCoin(context.Background(), req, "{}")
|
|
||||||
if resp.ErrorCode != 0 {
|
|
||||||
t.Fatalf("HandleUpdateCoin() error = %+v", resp)
|
|
||||||
}
|
|
||||||
if gateway.cmd.EventID != "GAME_OPEN:order-1" {
|
|
||||||
t.Fatalf("HandleUpdateCoin() event id = %q", gateway.cmd.EventID)
|
|
||||||
}
|
|
||||||
if gateway.cmd.ReceiptType != "INCOME" {
|
|
||||||
t.Fatalf("HandleUpdateCoin() receipt type = %q", gateway.cmd.ReceiptType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetIntegrationInfo(t *testing.T) {
|
|
||||||
gateway := &stubGateway{token: "token-123"}
|
|
||||||
service := NewGameOpenService(config.Config{
|
|
||||||
GameOpen: config.GameOpenConfig{
|
|
||||||
AppKey: "game-open-test-key",
|
|
||||||
TestUserID: 1234567,
|
|
||||||
TestAccount: "1234567",
|
|
||||||
TestPassword: "1234567",
|
|
||||||
},
|
|
||||||
}, nil, gateway)
|
|
||||||
|
|
||||||
info, err := service.GetIntegrationInfo(context.Background(), "https://jvapi.haiyihy.com")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetIntegrationInfo() error = %v", err)
|
|
||||||
}
|
|
||||||
if info.QueryUser != "https://jvapi.haiyihy.com/game/open/user-info" {
|
|
||||||
t.Fatalf("GetIntegrationInfo() query url = %q", info.QueryUser)
|
|
||||||
}
|
|
||||||
if info.Token != "token-123" {
|
|
||||||
t.Fatalf("GetIntegrationInfo() token = %q", info.Token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
package gameopen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/config"
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
gameOpenCodeSuccess = 0
|
|
||||||
gameOpenCodeBadRequest = 4001
|
|
||||||
gameOpenCodeSignatureError = 4002
|
|
||||||
gameOpenCodeTokenInvalid = 4003
|
|
||||||
gameOpenCodeServerError = 5000
|
|
||||||
|
|
||||||
gameOpenLogStatusSuccess = "SUCCESS"
|
|
||||||
gameOpenLogStatusFailed = "FAILED"
|
|
||||||
gameOpenLogStatusPending = "PENDING"
|
|
||||||
)
|
|
||||||
|
|
||||||
type gameOpenDB interface {
|
|
||||||
WithContext(ctx context.Context) *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
type gameOpenGateway interface {
|
|
||||||
AuthenticateToken(ctx context.Context, token string) (integration.UserCredential, error)
|
|
||||||
CreateIfAbsentUserCredentialToken(ctx context.Context, userID int64) (string, error)
|
|
||||||
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
|
||||||
MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error)
|
|
||||||
ExistsGoldEvent(ctx context.Context, eventID string) (bool, error)
|
|
||||||
ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type gameOpenPorts struct {
|
|
||||||
DB gameOpenDB
|
|
||||||
}
|
|
||||||
|
|
||||||
// GameOpenService 负责统一三方游戏回调接口。
|
|
||||||
type GameOpenService struct {
|
|
||||||
cfg config.Config
|
|
||||||
repo gameOpenPorts
|
|
||||||
java gameOpenGateway
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGameOpenService 创建统一三方游戏回调服务。
|
|
||||||
func NewGameOpenService(cfg config.Config, db gameOpenDB, javaGateway gameOpenGateway) *GameOpenService {
|
|
||||||
if javaGateway == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &GameOpenService{
|
|
||||||
cfg: cfg,
|
|
||||||
repo: gameOpenPorts{DB: db},
|
|
||||||
java: javaGateway,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryUserRequest 是统一查询用户信息入参。
|
|
||||||
type QueryUserRequest struct {
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
UID string `json:"uid"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
RoomID string `json:"roomId"`
|
|
||||||
Sign string `json:"sign"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateCoinRequest 是统一更新游戏币入参。
|
|
||||||
type UpdateCoinRequest struct {
|
|
||||||
OrderID string `json:"orderId"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
RoundID string `json:"roundId"`
|
|
||||||
UID string `json:"uid"`
|
|
||||||
Coin int64 `json:"coin"`
|
|
||||||
Type int `json:"type"`
|
|
||||||
RewardType int `json:"rewardType"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
WinID string `json:"winId"`
|
|
||||||
RoomID string `json:"roomId"`
|
|
||||||
Sign string `json:"sign"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupplementRequest 是统一补单入参。
|
|
||||||
type SupplementRequest struct {
|
|
||||||
OrderID string `json:"orderId"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
RoundID string `json:"roundId"`
|
|
||||||
UID string `json:"uid"`
|
|
||||||
Coin int64 `json:"coin"`
|
|
||||||
RewardType int `json:"rewardType"`
|
|
||||||
WinID string `json:"winId"`
|
|
||||||
RoomID string `json:"roomId"`
|
|
||||||
Sign string `json:"sign"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CallbackResponse 是统一三方回调响应。
|
|
||||||
type CallbackResponse struct {
|
|
||||||
ErrorCode int `json:"errorCode"`
|
|
||||||
ErrorMsg string `json:"errorMsg,omitempty"`
|
|
||||||
Data any `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntegrationInfoResponse 描述要提供给三方的联调信息。
|
|
||||||
type IntegrationInfoResponse struct {
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
QueryUser string `json:"queryUser"`
|
|
||||||
UpdateCoin string `json:"updateCoin"`
|
|
||||||
Supplement string `json:"supplement"`
|
|
||||||
UID string `json:"uid"`
|
|
||||||
Password string `json:"password,omitempty"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func successResponse(data any) CallbackResponse {
|
|
||||||
return CallbackResponse{ErrorCode: gameOpenCodeSuccess, Data: data}
|
|
||||||
}
|
|
||||||
|
|
||||||
func failResponse(code int, message string) CallbackResponse {
|
|
||||||
return CallbackResponse{ErrorCode: code, ErrorMsg: message}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBadRequest(message string) error {
|
|
||||||
return NewAppError(http.StatusBadRequest, "bad_request", message)
|
|
||||||
}
|
|
||||||
@ -1,229 +0,0 @@
|
|||||||
package gameprovider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Registry 保存所有已注册的游戏厂商。
|
|
||||||
type Registry struct {
|
|
||||||
providers map[string]Provider
|
|
||||||
ordered []Provider
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRegistry 创建厂商注册表。
|
|
||||||
func NewRegistry(providers ...Provider) *Registry {
|
|
||||||
r := &Registry{
|
|
||||||
providers: make(map[string]Provider, len(providers)),
|
|
||||||
ordered: make([]Provider, 0, len(providers)),
|
|
||||||
}
|
|
||||||
for _, provider := range providers {
|
|
||||||
r.Register(provider)
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register 注册一个游戏厂商。
|
|
||||||
func (r *Registry) Register(provider Provider) {
|
|
||||||
if provider == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
key := normalizeKey(provider.Key())
|
|
||||||
if key == "" {
|
|
||||||
panic("gameprovider: empty provider key")
|
|
||||||
}
|
|
||||||
if _, exists := r.providers[key]; exists {
|
|
||||||
panic("gameprovider: duplicate provider key " + key)
|
|
||||||
}
|
|
||||||
r.providers[key] = provider
|
|
||||||
r.ordered = append(r.ordered, provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve 根据名称解析厂商,名称大小写不敏感。
|
|
||||||
func (r *Registry) Resolve(key string) (Provider, error) {
|
|
||||||
if r == nil {
|
|
||||||
return nil, common.NewAppError(http.StatusNotFound, "provider_not_found", "game provider not found")
|
|
||||||
}
|
|
||||||
provider, ok := r.providers[normalizeKey(key)]
|
|
||||||
if !ok {
|
|
||||||
return nil, common.NewAppError(http.StatusNotFound, "provider_not_found", "game provider not found")
|
|
||||||
}
|
|
||||||
return provider, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List 返回已注册厂商列表。
|
|
||||||
func (r *Registry) List() ProviderListResponse {
|
|
||||||
if r == nil || len(r.ordered) == 0 {
|
|
||||||
return ProviderListResponse{Items: []ProviderSummary{}}
|
|
||||||
}
|
|
||||||
items := make([]ProviderSummary, 0, len(r.ordered))
|
|
||||||
for _, provider := range r.ordered {
|
|
||||||
items = append(items, ProviderSummary{
|
|
||||||
Key: provider.Key(),
|
|
||||||
DisplayName: provider.DisplayName(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ProviderListResponse{Items: items}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListShortcutGames 返回聚合后的快捷游戏列表。
|
|
||||||
func (r *Registry) ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error) {
|
|
||||||
if r == nil || len(r.ordered) == 0 {
|
|
||||||
return []RoomGameListItem{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
items := make([]RoomGameListItem, 0, len(r.ordered))
|
|
||||||
for _, provider := range r.ordered {
|
|
||||||
resp, err := provider.ListShortcutGames(ctx, user, roomID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, resp...)
|
|
||||||
}
|
|
||||||
sortRoomGameListItems(items)
|
|
||||||
if len(items) > 5 {
|
|
||||||
items = items[:5]
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListRoomGames 返回聚合后的房间游戏列表。
|
|
||||||
func (r *Registry) ListRoomGames(ctx context.Context, user AuthUser, roomID, category string) (*RoomGameListResponse, error) {
|
|
||||||
if r == nil || len(r.ordered) == 0 {
|
|
||||||
return &RoomGameListResponse{Items: []RoomGameListItem{}}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
items := make([]RoomGameListItem, 0, len(r.ordered))
|
|
||||||
for _, provider := range r.ordered {
|
|
||||||
resp, err := provider.ListRoomGames(ctx, user, roomID, category)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
items = append(items, resp.Items...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortRoomGameListItems(items)
|
|
||||||
return &RoomGameListResponse{Items: items}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveByLaunchRequest 根据启动请求解析内部对应的厂商。
|
|
||||||
func (r *Registry) ResolveByLaunchRequest(ctx context.Context, user AuthUser, req LaunchRequest) (Provider, error) {
|
|
||||||
if r == nil {
|
|
||||||
return nil, common.NewAppError(http.StatusNotFound, "provider_not_found", "game provider not found")
|
|
||||||
}
|
|
||||||
if req.ID <= 0 && strings.TrimSpace(req.GameID) == "" {
|
|
||||||
return nil, common.NewAppError(http.StatusBadRequest, "missing_game_id", "id or gameId is required")
|
|
||||||
}
|
|
||||||
for _, provider := range r.ordered {
|
|
||||||
ok, err := provider.SupportsLaunch(ctx, user, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
return provider, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, common.NewAppError(http.StatusNotFound, "game_not_found", "game provider not found for gameId")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoomState 返回聚合后的房间状态。
|
|
||||||
func (r *Registry) GetRoomState(ctx context.Context, user AuthUser, roomID string) (*RoomStateResponse, error) {
|
|
||||||
if strings.TrimSpace(roomID) == "" {
|
|
||||||
return nil, common.NewAppError(http.StatusBadRequest, "missing_room_id", "roomId is required")
|
|
||||||
}
|
|
||||||
state, _, err := r.resolveProviderByRoomState(ctx, user, roomID, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if state != nil {
|
|
||||||
return state, nil
|
|
||||||
}
|
|
||||||
return &RoomStateResponse{RoomID: roomID, State: roomStateIdle}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LaunchGame 按 gameId 自动分发到内部对应的厂商实现。
|
|
||||||
func (r *Registry) LaunchGame(ctx context.Context, user AuthUser, req LaunchRequest, clientIP string) (*LaunchResponse, error) {
|
|
||||||
provider, err := r.ResolveByLaunchRequest(ctx, user, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return provider.LaunchGame(ctx, user, req, clientIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseGame 根据当前房间状态自动路由到真实厂商。
|
|
||||||
func (r *Registry) CloseGame(ctx context.Context, user AuthUser, req CloseRequest) (*RoomStateResponse, error) {
|
|
||||||
if strings.TrimSpace(req.RoomID) == "" {
|
|
||||||
return nil, common.NewAppError(http.StatusBadRequest, "missing_room_id", "roomId is required")
|
|
||||||
}
|
|
||||||
_, provider, err := r.resolveProviderByRoomState(ctx, user, req.RoomID, req.GameSessionID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if provider == nil {
|
|
||||||
return &RoomStateResponse{RoomID: req.RoomID, State: roomStateIdle}, nil
|
|
||||||
}
|
|
||||||
return provider.CloseGame(ctx, user, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomStateIdle = "IDLE"
|
|
||||||
|
|
||||||
func (r *Registry) resolveProviderByRoomState(ctx context.Context, user AuthUser, roomID, gameSessionID string) (*RoomStateResponse, Provider, error) {
|
|
||||||
if r == nil {
|
|
||||||
return nil, nil, common.NewAppError(http.StatusNotFound, "provider_not_found", "game provider not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID := strings.TrimSpace(gameSessionID)
|
|
||||||
var activeState *RoomStateResponse
|
|
||||||
var activeProvider Provider
|
|
||||||
for _, provider := range r.ordered {
|
|
||||||
state, err := provider.GetRoomState(ctx, user, roomID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if state == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if sessionID != "" && strings.EqualFold(strings.TrimSpace(state.GameSessionID), sessionID) {
|
|
||||||
return state, provider, nil
|
|
||||||
}
|
|
||||||
if !isIdleRoomState(state) && activeProvider == nil {
|
|
||||||
activeState = state
|
|
||||||
activeProvider = provider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return activeState, activeProvider, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isIdleRoomState(state *RoomStateResponse) bool {
|
|
||||||
if state == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(state.GameSessionID) != "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
value := strings.ToUpper(strings.TrimSpace(state.State))
|
|
||||||
if value == "" {
|
|
||||||
value = roomStateIdle
|
|
||||||
}
|
|
||||||
return value == roomStateIdle
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortRoomGameListItems(items []RoomGameListItem) {
|
|
||||||
sort.SliceStable(items, func(i, j int) bool {
|
|
||||||
if items[i].Sort != items[j].Sort {
|
|
||||||
return items[i].Sort > items[j].Sort
|
|
||||||
}
|
|
||||||
if items[i].GameID != items[j].GameID {
|
|
||||||
return items[i].GameID < items[j].GameID
|
|
||||||
}
|
|
||||||
return strings.ToUpper(strings.TrimSpace(items[i].GameType)) < strings.ToUpper(strings.TrimSpace(items[j].GameType))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeKey(key string) string {
|
|
||||||
return strings.ToUpper(strings.TrimSpace(key))
|
|
||||||
}
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
package gameprovider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type stubProvider struct {
|
|
||||||
key string
|
|
||||||
name string
|
|
||||||
matchIDs map[int64]bool
|
|
||||||
matchIDsByGame map[string]bool
|
|
||||||
shortcut []RoomGameListItem
|
|
||||||
room []RoomGameListItem
|
|
||||||
state *RoomStateResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubProvider) Key() string { return s.key }
|
|
||||||
|
|
||||||
func (s *stubProvider) DisplayName() string { return s.name }
|
|
||||||
|
|
||||||
func (s *stubProvider) SupportsLaunch(_ context.Context, _ AuthUser, req LaunchRequest) (bool, error) {
|
|
||||||
if req.ID > 0 {
|
|
||||||
if s.matchIDs == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return s.matchIDs[req.ID], nil
|
|
||||||
}
|
|
||||||
if s.matchIDsByGame == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return s.matchIDsByGame[req.GameID], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubProvider) ListShortcutGames(context.Context, AuthUser, string) ([]RoomGameListItem, error) {
|
|
||||||
return append([]RoomGameListItem(nil), s.shortcut...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubProvider) ListRoomGames(context.Context, AuthUser, string, string) (*RoomGameListResponse, error) {
|
|
||||||
return &RoomGameListResponse{Items: append([]RoomGameListItem(nil), s.room...)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubProvider) GetRoomState(context.Context, AuthUser, string) (*RoomStateResponse, error) {
|
|
||||||
if s.state == nil {
|
|
||||||
return &RoomStateResponse{State: roomStateIdle}, nil
|
|
||||||
}
|
|
||||||
copied := *s.state
|
|
||||||
return &copied, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubProvider) LaunchGame(context.Context, AuthUser, LaunchRequest, string) (*LaunchResponse, error) {
|
|
||||||
return &LaunchResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stubProvider) CloseGame(context.Context, AuthUser, CloseRequest) (*RoomStateResponse, error) {
|
|
||||||
return &RoomStateResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegistryResolveIsCaseInsensitive(t *testing.T) {
|
|
||||||
registry := NewRegistry(&stubProvider{key: "BAISHUN", name: "百顺"})
|
|
||||||
|
|
||||||
provider, err := registry.Resolve("baishun")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Resolve() error = %v", err)
|
|
||||||
}
|
|
||||||
if provider.Key() != "BAISHUN" {
|
|
||||||
t.Fatalf("Resolve() key = %q, want %q", provider.Key(), "BAISHUN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegistryListKeepsRegistrationOrder(t *testing.T) {
|
|
||||||
registry := NewRegistry(
|
|
||||||
&stubProvider{key: "BAISHUN", name: "百顺"},
|
|
||||||
&stubProvider{key: "LINGXIAN", name: "灵仙"},
|
|
||||||
)
|
|
||||||
|
|
||||||
list := registry.List()
|
|
||||||
if len(list.Items) != 2 {
|
|
||||||
t.Fatalf("List() items = %d, want 2", len(list.Items))
|
|
||||||
}
|
|
||||||
if list.Items[0].Key != "BAISHUN" || list.Items[1].Key != "LINGXIAN" {
|
|
||||||
t.Fatalf("List() order = %#v", list.Items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegistryListRoomGamesAggregatesAndSorts(t *testing.T) {
|
|
||||||
registry := NewRegistry(
|
|
||||||
&stubProvider{
|
|
||||||
key: "BAISHUN",
|
|
||||||
name: "百顺",
|
|
||||||
room: []RoomGameListItem{{GameID: "b", Sort: 10}, {GameID: "d", Sort: 10}},
|
|
||||||
},
|
|
||||||
&stubProvider{
|
|
||||||
key: "LINGXIAN",
|
|
||||||
name: "灵仙",
|
|
||||||
room: []RoomGameListItem{{GameID: "a", Sort: 30}, {GameID: "c", Sort: 10}},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
resp, err := registry.ListRoomGames(context.Background(), AuthUser{}, "room-1", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListRoomGames() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(resp.Items) != 4 {
|
|
||||||
t.Fatalf("ListRoomGames() items = %d, want 4", len(resp.Items))
|
|
||||||
}
|
|
||||||
got := []string{resp.Items[0].GameID, resp.Items[1].GameID, resp.Items[2].GameID, resp.Items[3].GameID}
|
|
||||||
want := []string{"a", "b", "c", "d"}
|
|
||||||
for i := range want {
|
|
||||||
if got[i] != want[i] {
|
|
||||||
t.Fatalf("ListRoomGames() order = %#v, want %#v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegistryLaunchGameResolvesProviderByGameID(t *testing.T) {
|
|
||||||
baishunProvider := &stubProvider{
|
|
||||||
key: "BAISHUN",
|
|
||||||
name: "百顺",
|
|
||||||
matchIDs: map[int64]bool{101: true},
|
|
||||||
}
|
|
||||||
lingxianProvider := &stubProvider{
|
|
||||||
key: "LINGXIAN",
|
|
||||||
name: "灵仙",
|
|
||||||
matchIDs: map[int64]bool{202: true},
|
|
||||||
}
|
|
||||||
registry := NewRegistry(baishunProvider, lingxianProvider)
|
|
||||||
|
|
||||||
provider, err := registry.ResolveByLaunchRequest(context.Background(), AuthUser{}, LaunchRequest{ID: 202})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ResolveByLaunchRequest() error = %v", err)
|
|
||||||
}
|
|
||||||
if provider.Key() != "LINGXIAN" {
|
|
||||||
t.Fatalf("ResolveByLaunchRequest() key = %q, want %q", provider.Key(), "LINGXIAN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegistryCloseGameUsesMatchingSessionProvider(t *testing.T) {
|
|
||||||
baishunProvider := &stubProvider{
|
|
||||||
key: "BAISHUN",
|
|
||||||
name: "百顺",
|
|
||||||
state: &RoomStateResponse{RoomID: "room-1", State: "PLAYING", GameSessionID: "bs-session", Provider: "BAISHUN"},
|
|
||||||
}
|
|
||||||
lingxianProvider := &stubProvider{
|
|
||||||
key: "LINGXIAN",
|
|
||||||
name: "灵仙",
|
|
||||||
state: &RoomStateResponse{RoomID: "room-1", State: roomStateIdle, Provider: "LINGXIAN"},
|
|
||||||
}
|
|
||||||
registry := NewRegistry(baishunProvider, lingxianProvider)
|
|
||||||
|
|
||||||
state, provider, err := registry.resolveProviderByRoomState(context.Background(), AuthUser{}, "room-1", "bs-session")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolveProviderByRoomState() error = %v", err)
|
|
||||||
}
|
|
||||||
if provider == nil || provider.Key() != "BAISHUN" {
|
|
||||||
t.Fatalf("resolveProviderByRoomState() provider = %#v", provider)
|
|
||||||
}
|
|
||||||
if state == nil || state.GameSessionID != "bs-session" {
|
|
||||||
t.Fatalf("resolveProviderByRoomState() state = %#v", state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
package gameprovider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AuthUser 表示已经通过平台鉴权的用户。
|
|
||||||
type AuthUser = common.AuthUser
|
|
||||||
|
|
||||||
// Provider 抽象出房间游戏类厂商在 app 侧通用的查询、启动和关闭能力。
|
|
||||||
type Provider interface {
|
|
||||||
Key() string
|
|
||||||
DisplayName() string
|
|
||||||
SupportsLaunch(ctx context.Context, user AuthUser, req LaunchRequest) (bool, error)
|
|
||||||
ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error)
|
|
||||||
ListRoomGames(ctx context.Context, user AuthUser, roomID, category string) (*RoomGameListResponse, error)
|
|
||||||
GetRoomState(ctx context.Context, user AuthUser, roomID string) (*RoomStateResponse, error)
|
|
||||||
LaunchGame(ctx context.Context, user AuthUser, req LaunchRequest, clientIP string) (*LaunchResponse, error)
|
|
||||||
CloseGame(ctx context.Context, user AuthUser, req CloseRequest) (*RoomStateResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProviderSummary 描述一个已注册的游戏厂商。
|
|
||||||
type ProviderSummary struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProviderListResponse 是统一厂商列表返回结构。
|
|
||||||
type ProviderListResponse struct {
|
|
||||||
Items []ProviderSummary `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomGameListItem 是统一房间游戏列表项。
|
|
||||||
type RoomGameListItem struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
GameType string `json:"gameType,omitempty"`
|
|
||||||
Provider string `json:"provider,omitempty"`
|
|
||||||
ProviderGameID string `json:"providerGameId,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Cover string `json:"cover"`
|
|
||||||
Category string `json:"category,omitempty"`
|
|
||||||
Sort int64 `json:"sort,omitempty"`
|
|
||||||
LaunchMode string `json:"launchMode,omitempty"`
|
|
||||||
FullScreen bool `json:"fullScreen"`
|
|
||||||
GameMode int `json:"gameMode,omitempty"`
|
|
||||||
SafeHeight int `json:"safeHeight,omitempty"`
|
|
||||||
Orientation int `json:"orientation,omitempty"`
|
|
||||||
PackageVersion string `json:"packageVersion,omitempty"`
|
|
||||||
Status string `json:"status,omitempty"`
|
|
||||||
LaunchParams map[string]any `json:"launchParams,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomGameListResponse 是统一房间游戏列表响应。
|
|
||||||
type RoomGameListResponse struct {
|
|
||||||
Items []RoomGameListItem `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomStateResponse 是统一房间状态响应。
|
|
||||||
type RoomStateResponse struct {
|
|
||||||
RoomID string `json:"roomId"`
|
|
||||||
State string `json:"state"`
|
|
||||||
Provider string `json:"provider,omitempty"`
|
|
||||||
GameSessionID string `json:"gameSessionId,omitempty"`
|
|
||||||
CurrentGameID string `json:"currentGameId,omitempty"`
|
|
||||||
CurrentProviderGameID string `json:"currentProviderGameId,omitempty"`
|
|
||||||
CurrentGameName string `json:"currentGameName,omitempty"`
|
|
||||||
CurrentGameCover string `json:"currentGameCover,omitempty"`
|
|
||||||
HostUserID int64 `json:"hostUserId,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LaunchRequest 是统一启动入参。
|
|
||||||
type LaunchRequest struct {
|
|
||||||
ID int64 `json:"id,omitempty"`
|
|
||||||
RoomID string `json:"roomId"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
SceneMode int `json:"sceneMode"`
|
|
||||||
ClientOrigin string `json:"clientOrigin"`
|
|
||||||
Params map[string]any `json:"params,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseRequest 是统一关闭入参。
|
|
||||||
type CloseRequest struct {
|
|
||||||
RoomID string `json:"roomId"`
|
|
||||||
GameSessionID string `json:"gameSessionId"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
Params map[string]any `json:"params,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LaunchEntry 描述游戏真正的加载入口。
|
|
||||||
type LaunchEntry struct {
|
|
||||||
LaunchMode string `json:"launchMode"`
|
|
||||||
EntryURL string `json:"entryUrl"`
|
|
||||||
PreviewURL string `json:"previewUrl"`
|
|
||||||
DownloadURL string `json:"downloadUrl"`
|
|
||||||
PackageVersion string `json:"packageVersion"`
|
|
||||||
Orientation int `json:"orientation"`
|
|
||||||
SafeHeight int `json:"safeHeight"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LaunchResponse 是统一启动结果。
|
|
||||||
type LaunchResponse struct {
|
|
||||||
ID int64 `json:"id,omitempty"`
|
|
||||||
GameSessionID string `json:"gameSessionId"`
|
|
||||||
Provider string `json:"provider,omitempty"`
|
|
||||||
GameID string `json:"gameId"`
|
|
||||||
ProviderGameID string `json:"providerGameId,omitempty"`
|
|
||||||
Entry LaunchEntry `json:"entry"`
|
|
||||||
LaunchConfig any `json:"launchConfig,omitempty"`
|
|
||||||
RoomState RoomStateResponse `json:"roomState"`
|
|
||||||
}
|
|
||||||
@ -75,12 +75,7 @@ func (s *LuckyGiftService) Draw(ctx context.Context, req LuckyGiftDrawRequest) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.publishRewardEvent(ctx, req, results, rewardTotal); err != nil {
|
// 三方有中奖收益时,再调用钱包做一次金币入账,并用 walletEventID 保证幂等。
|
||||||
_ = s.markRequestFailed(ctx, req.BusinessID, providerCode, rawProviderResponse, err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 三方有中奖收益时,再调用钱包给送礼人做金币返奖,并用 walletEventID 保证幂等。
|
|
||||||
if rewardTotal > 0 {
|
if rewardTotal > 0 {
|
||||||
exists, err := s.java.ExistsGoldEvent(ctx, walletEventID)
|
exists, err := s.java.ExistsGoldEvent(ctx, walletEventID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
package luckygift
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/utils"
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LuckyGiftRewardDispatchEvent 是发送给 Java 房间侧统计消费者的中奖事件。
|
|
||||||
type LuckyGiftRewardDispatchEvent struct {
|
|
||||||
EventID string `json:"eventId"`
|
|
||||||
BusinessID string `json:"businessId"`
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
RoomID int64 `json:"roomId"`
|
|
||||||
SendUserID int64 `json:"sendUserId"`
|
|
||||||
GiftID int64 `json:"giftId"`
|
|
||||||
GiftPrice int64 `json:"giftPrice"`
|
|
||||||
GiftNum int64 `json:"giftNum"`
|
|
||||||
RewardTotal int64 `json:"rewardTotal"`
|
|
||||||
Results []LuckyGiftRewardDispatchResult `json:"results"`
|
|
||||||
PublishedAt int64 `json:"publishedAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LuckyGiftRewardDispatchResult 是单个中奖子单的房间侧结算载体。
|
|
||||||
type LuckyGiftRewardDispatchResult struct {
|
|
||||||
OrderSeed string `json:"orderSeed"`
|
|
||||||
AcceptUserID int64 `json:"acceptUserId"`
|
|
||||||
RewardNum int64 `json:"rewardNum"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LuckyGiftService) publishRewardEvent(
|
|
||||||
ctx context.Context,
|
|
||||||
req LuckyGiftDrawRequest,
|
|
||||||
results []LuckyGiftDrawResult,
|
|
||||||
rewardTotal int64,
|
|
||||||
) error {
|
|
||||||
if rewardTotal <= 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
streamKey := strings.TrimSpace(s.cfg.LuckyGift.RewardStreamKey)
|
|
||||||
if streamKey == "" {
|
|
||||||
return NewAppError(http.StatusInternalServerError, "lucky_gift_reward_stream_not_configured", "lucky gift reward stream is not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
winResults := make([]LuckyGiftRewardDispatchResult, 0, len(results))
|
|
||||||
for _, item := range results {
|
|
||||||
if item.RewardNum <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
winResults = append(winResults, LuckyGiftRewardDispatchResult{
|
|
||||||
OrderSeed: item.ID,
|
|
||||||
AcceptUserID: item.AcceptUserID,
|
|
||||||
RewardNum: item.RewardNum,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(winResults) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
event := LuckyGiftRewardDispatchEvent{
|
|
||||||
EventID: luckyGiftRewardEventID(req.BusinessID),
|
|
||||||
BusinessID: req.BusinessID,
|
|
||||||
SysOrigin: req.SysOrigin,
|
|
||||||
RoomID: req.RoomID,
|
|
||||||
SendUserID: req.SendUserID,
|
|
||||||
GiftID: req.GiftID,
|
|
||||||
GiftPrice: req.GiftPrice,
|
|
||||||
GiftNum: req.GiftNum,
|
|
||||||
RewardTotal: rewardTotal,
|
|
||||||
Results: winResults,
|
|
||||||
PublishedAt: time.Now().UnixMilli(),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := s.repo.Redis.XAdd(ctx, &redis.XAddArgs{
|
|
||||||
Stream: streamKey,
|
|
||||||
Values: map[string]any{
|
|
||||||
"eventId": event.EventID,
|
|
||||||
"businessId": event.BusinessID,
|
|
||||||
"payload": utils.MustJSONString(event, "{}"),
|
|
||||||
"publishedAt": event.PublishedAt,
|
|
||||||
},
|
|
||||||
}).Result()
|
|
||||||
if err != nil {
|
|
||||||
return NewAppError(http.StatusBadGateway, "lucky_gift_reward_stream_publish_failed", err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func luckyGiftRewardEventID(businessID string) string {
|
|
||||||
return "LUCKY_GIFT_REWARD:" + strings.TrimSpace(businessID)
|
|
||||||
}
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
package luckygift
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/config"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/alicebob/miniredis/v2"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPublishRewardEventWritesWinningResultsToStream(t *testing.T) {
|
|
||||||
mr, err := miniredis.Run()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("miniredis run failed: %v", err)
|
|
||||||
}
|
|
||||||
defer mr.Close()
|
|
||||||
|
|
||||||
client := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
service := &LuckyGiftService{
|
|
||||||
cfg: config.Config{
|
|
||||||
LuckyGift: config.LuckyGiftConfig{
|
|
||||||
RewardStreamKey: "lucky-gift:reward",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
repo: luckyGiftPorts{Redis: client},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := LuckyGiftDrawRequest{
|
|
||||||
BusinessID: "biz-1001",
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
RoomID: 2001,
|
|
||||||
SendUserID: 3001,
|
|
||||||
GiftID: 4001,
|
|
||||||
GiftPrice: 500,
|
|
||||||
GiftNum: 2,
|
|
||||||
}
|
|
||||||
results := []LuckyGiftDrawResult{
|
|
||||||
{ID: "order-a", AcceptUserID: 9001, RewardNum: 88},
|
|
||||||
{ID: "order-b", AcceptUserID: 9002, RewardNum: 0},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := service.publishRewardEvent(context.Background(), req, results, 88); err != nil {
|
|
||||||
t.Fatalf("publishRewardEvent returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
streams, err := client.XRange(context.Background(), "lucky-gift:reward", "-", "+").Result()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("xrange returned error: %v", err)
|
|
||||||
}
|
|
||||||
if len(streams) != 1 {
|
|
||||||
t.Fatalf("expected 1 stream record, got %d", len(streams))
|
|
||||||
}
|
|
||||||
|
|
||||||
payloadRaw, ok := streams[0].Values["payload"].(string)
|
|
||||||
if !ok || payloadRaw == "" {
|
|
||||||
t.Fatalf("expected payload field, got %#v", streams[0].Values["payload"])
|
|
||||||
}
|
|
||||||
|
|
||||||
var event LuckyGiftRewardDispatchEvent
|
|
||||||
if err := json.Unmarshal([]byte(payloadRaw), &event); err != nil {
|
|
||||||
t.Fatalf("unmarshal payload failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.EventID != "LUCKY_GIFT_REWARD:biz-1001" {
|
|
||||||
t.Fatalf("unexpected event id: %s", event.EventID)
|
|
||||||
}
|
|
||||||
if len(event.Results) != 1 {
|
|
||||||
t.Fatalf("expected only winning results to be published, got %d", len(event.Results))
|
|
||||||
}
|
|
||||||
if event.Results[0].OrderSeed != "order-a" || event.Results[0].AcceptUserID != 9001 || event.Results[0].RewardNum != 88 {
|
|
||||||
t.Fatalf("unexpected winning result payload: %+v", event.Results[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPublishRewardEventSkipsZeroRewardTotal(t *testing.T) {
|
|
||||||
mr, err := miniredis.Run()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("miniredis run failed: %v", err)
|
|
||||||
}
|
|
||||||
defer mr.Close()
|
|
||||||
|
|
||||||
client := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
service := &LuckyGiftService{
|
|
||||||
cfg: config.Config{
|
|
||||||
LuckyGift: config.LuckyGiftConfig{
|
|
||||||
RewardStreamKey: "lucky-gift:reward",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
repo: luckyGiftPorts{Redis: client},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := service.publishRewardEvent(context.Background(), LuckyGiftDrawRequest{BusinessID: "biz-1002"}, nil, 0); err != nil {
|
|
||||||
t.Fatalf("publishRewardEvent returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
streams, err := client.XRange(context.Background(), "lucky-gift:reward", "-", "+").Result()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("xrange returned error: %v", err)
|
|
||||||
}
|
|
||||||
if len(streams) != 0 {
|
|
||||||
t.Fatalf("expected no stream records for zero reward total, got %d", len(streams))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS baishun_provider_config (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
sys_origin VARCHAR(32) NOT NULL,
|
|
||||||
platform_base_url VARCHAR(1024) NOT NULL,
|
|
||||||
app_id BIGINT NOT NULL,
|
|
||||||
app_name VARCHAR(128) DEFAULT NULL,
|
|
||||||
app_channel VARCHAR(64) NOT NULL,
|
|
||||||
app_key VARCHAR(255) NOT NULL,
|
|
||||||
gsp INT NOT NULL DEFAULT 101,
|
|
||||||
launch_code_ttl_seconds INT NOT NULL DEFAULT 300,
|
|
||||||
ss_token_ttl_seconds INT NOT NULL DEFAULT 86400,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_baishun_provider_sys_origin (sys_origin),
|
|
||||||
KEY idx_baishun_provider_app (app_id, app_channel)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS game_open_callback_log (
|
|
||||||
id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
request_type VARCHAR(32) NOT NULL,
|
|
||||||
sys_origin VARCHAR(32) DEFAULT NULL,
|
|
||||||
order_id VARCHAR(128) DEFAULT NULL,
|
|
||||||
game_id VARCHAR(64) DEFAULT NULL,
|
|
||||||
round_id VARCHAR(128) DEFAULT NULL,
|
|
||||||
room_id VARCHAR(64) DEFAULT NULL,
|
|
||||||
user_id BIGINT DEFAULT NULL,
|
|
||||||
request_json LONGTEXT NOT NULL,
|
|
||||||
response_json LONGTEXT NOT NULL,
|
|
||||||
biz_code INT NOT NULL DEFAULT 0,
|
|
||||||
biz_message TEXT,
|
|
||||||
status VARCHAR(32) NOT NULL,
|
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_game_open_log_type_time (request_type, create_time),
|
|
||||||
KEY idx_game_open_log_order (order_id),
|
|
||||||
KEY idx_game_open_log_user_time (user_id, create_time)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user