增加游戏类型
This commit is contained in:
parent
0701bd0795
commit
6e379d62ac
@ -4,6 +4,8 @@ import (
|
||||
"chatapp3-golang/internal/bootstrap"
|
||||
"chatapp3-golang/internal/router"
|
||||
"chatapp3-golang/internal/service/baishun"
|
||||
"chatapp3-golang/internal/service/gameopen"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
"chatapp3-golang/internal/service/invite"
|
||||
"chatapp3-golang/internal/service/luckygift"
|
||||
"chatapp3-golang/internal/service/registerreward"
|
||||
@ -30,13 +32,19 @@ func main() {
|
||||
|
||||
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)
|
||||
gameOpenService := gameopen.NewGameOpenService(app.Config, app.Repository.DB, &app.Gateways)
|
||||
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)
|
||||
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)
|
||||
gameProviders := gameprovider.NewRegistry(
|
||||
baishun.NewAppProvider(baishunService),
|
||||
)
|
||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||
Invite: inviteService,
|
||||
Baishun: baishunService,
|
||||
GameOpen: gameOpenService,
|
||||
GameProviders: gameProviders,
|
||||
LuckyGift: luckyGiftService,
|
||||
RegisterReward: registerRewardService,
|
||||
SignInReward: signInRewardService,
|
||||
|
||||
@ -17,6 +17,7 @@ type Config struct {
|
||||
Invite InviteConfig
|
||||
WeekStar WeekStarConfig
|
||||
Baishun BaishunConfig
|
||||
GameOpen GameOpenConfig
|
||||
LuckyGift LuckyGiftConfig
|
||||
}
|
||||
|
||||
@ -101,11 +102,21 @@ type BaishunConfig struct {
|
||||
SSTokenTTLSeconds int
|
||||
}
|
||||
|
||||
// GameOpenConfig 保存统一三方游戏回调配置。
|
||||
type GameOpenConfig struct {
|
||||
PublicBaseURL string
|
||||
AppKey string
|
||||
TestUserID int64
|
||||
TestAccount string
|
||||
TestPassword string
|
||||
}
|
||||
|
||||
// LuckyGiftConfig 保存幸运礼物模块配置。
|
||||
type LuckyGiftConfig struct {
|
||||
Enabled bool
|
||||
Timeout time.Duration
|
||||
Providers []LuckyGiftProviderConfig
|
||||
Enabled bool
|
||||
Timeout time.Duration
|
||||
RewardStreamKey string
|
||||
Providers []LuckyGiftProviderConfig
|
||||
}
|
||||
|
||||
type LuckyGiftProviderConfig struct {
|
||||
@ -182,10 +193,18 @@ func Load() Config {
|
||||
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),
|
||||
},
|
||||
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{
|
||||
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,
|
||||
Providers: loadLuckyGiftConfigs(),
|
||||
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,
|
||||
RewardStreamKey: getEnvAny([]string{"CHATAPP_LUCKY_GIFT_REWARD_STREAM_KEY", "LUCKY_GIFT_REWARD_STREAM_KEY"}, "lucky-gift:reward"),
|
||||
Providers: loadLuckyGiftConfigs(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,11 @@ func (g *Gateways) AuthenticateToken(ctx context.Context, token string) (UserCre
|
||||
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 透传到后台认证网关。
|
||||
func (g *Gateways) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
|
||||
return g.Auth.AuthenticateConsoleToken(ctx, authorization)
|
||||
@ -113,6 +118,11 @@ func (g *AuthGateway) AuthenticateToken(ctx context.Context, token string) (User
|
||||
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。
|
||||
func (g *AuthGateway) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
|
||||
return g.client.AuthenticateConsoleToken(ctx, authorization)
|
||||
|
||||
@ -192,6 +192,19 @@ func (c *Client) AuthenticateToken(ctx context.Context, token string) (UserCrede
|
||||
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,并返回管理员基础信息。
|
||||
func (c *Client) AuthenticateConsoleToken(ctx context.Context, authorization string) (ConsoleAccount, error) {
|
||||
authorization = strings.TrimSpace(authorization)
|
||||
|
||||
@ -52,6 +52,25 @@ type SysGameListVendorExt struct {
|
||||
// TableName 返回游戏厂商扩展表名。
|
||||
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 是百顺平台同步到本地的游戏目录。
|
||||
type BaishunGameCatalog struct {
|
||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||
|
||||
25
internal/model/game_open_models.go
Normal file
25
internal/model/game_open_models.go
Normal file
@ -0,0 +1,25 @@
|
||||
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,22 +26,6 @@ func registerBaishunRoutes(
|
||||
|
||||
appGroup := engine.Group("/app/game")
|
||||
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) {
|
||||
resp, err := baishunService.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
||||
if err != nil {
|
||||
@ -111,6 +95,27 @@ func registerBaishunRoutes(
|
||||
|
||||
consoleGroup := engine.Group("/operate/baishun-game")
|
||||
consoleGroup.Use(consoleAuthMiddleware(javaClient))
|
||||
consoleGroup.GET("/config", func(c *gin.Context) {
|
||||
resp, err := baishunService.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||
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")))
|
||||
@ -128,6 +133,22 @@ func registerBaishunRoutes(
|
||||
}
|
||||
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 {
|
||||
@ -149,6 +170,19 @@ func registerBaishunRoutes(
|
||||
}
|
||||
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 {
|
||||
|
||||
79
internal/router/game_open_routes.go
Normal file
79
internal/router/game_open_routes.go
Normal file
@ -0,0 +1,79 @@
|
||||
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
|
||||
}
|
||||
195
internal/router/game_provider_routes.go
Normal file
195
internal/router/game_provider_routes.go
Normal file
@ -0,0 +1,195 @@
|
||||
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, stripProvidersFromRoomGameItems(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, stripProvidersFromRoomGameListResponse(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, stripProviderFromRoomState(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, stripProviderFromLaunch(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, stripProviderFromRoomState(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
|
||||
}
|
||||
|
||||
func stripProvidersFromRoomGameItems(items []gameprovider.RoomGameListItem) []gameprovider.RoomGameListItem {
|
||||
if len(items) == 0 {
|
||||
return []gameprovider.RoomGameListItem{}
|
||||
}
|
||||
result := make([]gameprovider.RoomGameListItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
item.Provider = ""
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func stripProvidersFromRoomGameListResponse(resp *gameprovider.RoomGameListResponse) *gameprovider.RoomGameListResponse {
|
||||
if resp == nil {
|
||||
return &gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}}
|
||||
}
|
||||
return &gameprovider.RoomGameListResponse{
|
||||
Items: stripProvidersFromRoomGameItems(resp.Items),
|
||||
}
|
||||
}
|
||||
|
||||
func stripProviderFromRoomState(resp *gameprovider.RoomStateResponse) *gameprovider.RoomStateResponse {
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
copied := *resp
|
||||
copied.Provider = ""
|
||||
return &copied
|
||||
}
|
||||
|
||||
func stripProviderFromLaunch(resp *gameprovider.LaunchResponse) *gameprovider.LaunchResponse {
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
copied := *resp
|
||||
copied.Provider = ""
|
||||
copied.RoomState = *stripProviderFromRoomState(&resp.RoomState)
|
||||
return &copied
|
||||
}
|
||||
@ -7,6 +7,8 @@ import (
|
||||
"chatapp3-golang/internal/config"
|
||||
"chatapp3-golang/internal/repo"
|
||||
"chatapp3-golang/internal/service/baishun"
|
||||
"chatapp3-golang/internal/service/gameopen"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
"chatapp3-golang/internal/service/invite"
|
||||
"chatapp3-golang/internal/service/luckygift"
|
||||
"chatapp3-golang/internal/service/registerreward"
|
||||
@ -20,6 +22,8 @@ import (
|
||||
type Services struct {
|
||||
Invite *invite.InviteService
|
||||
Baishun *baishun.BaishunService
|
||||
GameOpen *gameopen.GameOpenService
|
||||
GameProviders *gameprovider.Registry
|
||||
LuckyGift *luckygift.LuckyGiftService
|
||||
RegisterReward *registerreward.RegisterRewardService
|
||||
SignInReward *signinreward.SignInRewardService
|
||||
@ -41,6 +45,8 @@ func NewRouter(
|
||||
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
|
||||
registerSignInRewardRoutes(engine, javaClient, services.SignInReward)
|
||||
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
||||
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
||||
registerGameProviderRoutes(engine, javaClient, services.GameProviders)
|
||||
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
||||
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
||||
|
||||
@ -61,6 +67,6 @@ func registerHealthRoutes(engine *gin.Engine, cfg config.Config, repository *rep
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": "ok", "message": "success3"})
|
||||
c.JSON(http.StatusOK, gin.H{"code": "ok", "message": "success4"})
|
||||
})
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ type adminGameRow struct {
|
||||
ExtSafeHeight int
|
||||
ExtGSP string
|
||||
CurrencyIcon string
|
||||
ExtExtraJSON string
|
||||
CatalogStatus string
|
||||
CatalogPreviewURL string
|
||||
CatalogDownloadURL string
|
||||
@ -99,11 +100,12 @@ func (s *BaishunService) PageAdminGames(
|
||||
ext.package_version AS ext_package_version,
|
||||
ext.preview_url AS ext_preview_url,
|
||||
ext.package_url AS package_url,
|
||||
ext.orientation AS ext_orientation,
|
||||
ext.safe_height AS ext_safe_height,
|
||||
ext.gsp AS ext_gsp,
|
||||
ext.currency_icon AS currency_icon,
|
||||
cat.status AS catalog_status,
|
||||
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,
|
||||
@ -136,6 +138,10 @@ func (s *BaishunService) PageAdminGames(
|
||||
// SaveAdminGame 保存后台百顺游戏配置。
|
||||
func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishunGameRequest) (*AdminBaishunGameItem, error) {
|
||||
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
catalog, err := s.findCatalogForAdmin(ctx, sysOrigin, req.VendorGameID, req.GameID)
|
||||
if err != nil {
|
||||
@ -168,7 +174,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
||||
packageVersion := defaultIfBlank(strings.TrimSpace(req.PackageVersion), catalog.PackageVersion)
|
||||
previewURL := defaultIfBlank(strings.TrimSpace(req.PreviewURL), catalog.PreviewURL)
|
||||
packageURL := defaultIfBlank(strings.TrimSpace(req.PackageURL), catalog.DownloadURL)
|
||||
gsp := defaultIfBlank(strings.TrimSpace(req.GSP), defaultAdminGSP(catalog.GSP, s.cfg.Baishun.GSP))
|
||||
gsp := defaultIfBlank(strings.TrimSpace(req.GSP), defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()))
|
||||
safeHeight := req.SafeHeight
|
||||
if safeHeight <= 0 {
|
||||
safeHeight = catalog.SafeHeight
|
||||
@ -261,6 +267,8 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
||||
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 {
|
||||
@ -281,7 +289,10 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
||||
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()
|
||||
@ -301,8 +312,10 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
||||
"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
|
||||
@ -436,6 +449,7 @@ func (s *BaishunService) getAdminGame(ctx context.Context, sysOrigin string, id
|
||||
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,
|
||||
@ -515,6 +529,14 @@ func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, i
|
||||
}
|
||||
|
||||
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))
|
||||
@ -532,6 +554,7 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, fil
|
||||
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
|
||||
@ -589,9 +612,12 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, fil
|
||||
PreviewURL: catalog.PreviewURL,
|
||||
Orientation: catalog.Orientation,
|
||||
SafeHeight: catalog.SafeHeight,
|
||||
GSP: defaultAdminGSP(catalog.GSP, s.cfg.Baishun.GSP),
|
||||
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
|
||||
@ -606,6 +632,7 @@ func (r adminGameRow) toAdminItem() 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,
|
||||
@ -629,6 +656,7 @@ func (r adminGameRow) toAdminItem() AdminBaishunGameItem {
|
||||
GSP: r.ExtGSP,
|
||||
CurrencyIcon: r.CurrencyIcon,
|
||||
CatalogStatus: r.CatalogStatus,
|
||||
LaunchParams: launchParamsWithGameType(r.ExtExtraJSON, baishunVendorType),
|
||||
CreateTime: formatAdminTime(r.CreateTime),
|
||||
UpdateTime: formatAdminTime(r.UpdateTime),
|
||||
}
|
||||
|
||||
167
internal/service/baishun/app_provider.go
Normal file
167
internal/service/baishun/app_provider.go
Normal file
@ -0,0 +1,167 @@
|
||||
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,7 +19,8 @@ import (
|
||||
|
||||
// HandleToken 处理百顺获取启动 code/token 的回调。
|
||||
func (s *BaishunService) HandleToken(ctx context.Context, req BaishunTokenRequest, rawJSON string) baishunStandardResponse {
|
||||
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||
runtimeCfg, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp)
|
||||
if !ok {
|
||||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||||
}
|
||||
var session model.BaishunLaunchSession
|
||||
@ -47,7 +48,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)
|
||||
return resp
|
||||
}
|
||||
expire := time.Now().Add(time.Duration(s.cfg.Baishun.SSTokenTTLSeconds) * time.Second)
|
||||
expire := time.Now().Add(time.Duration(runtimeCfg.ssTokenTTLSeconds()) * time.Second)
|
||||
if err := s.repo.DB.WithContext(ctx).
|
||||
Model(&model.BaishunLaunchSession{}).
|
||||
Where("id = ?", session.ID).
|
||||
@ -85,7 +86,7 @@ func (s *BaishunService) HandleToken(ctx context.Context, req BaishunTokenReques
|
||||
|
||||
// HandleProfile 处理百顺获取用户资料的回调。
|
||||
func (s *BaishunService) HandleProfile(ctx context.Context, req BaishunProfileRequest, rawJSON string) baishunStandardResponse {
|
||||
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||
if _, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp); !ok {
|
||||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||||
}
|
||||
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
||||
@ -119,7 +120,8 @@ func (s *BaishunService) HandleProfile(ctx context.Context, req BaishunProfileRe
|
||||
|
||||
// HandleUpdateToken 处理百顺刷新 ss_token 的回调。
|
||||
func (s *BaishunService) HandleUpdateToken(ctx context.Context, req BaishunUpdateTokenRequest, rawJSON string) baishunStandardResponse {
|
||||
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||
runtimeCfg, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp)
|
||||
if !ok {
|
||||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||||
}
|
||||
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
||||
@ -134,7 +136,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)
|
||||
return resp
|
||||
}
|
||||
expire := time.Now().Add(time.Duration(s.cfg.Baishun.SSTokenTTLSeconds) * time.Second)
|
||||
expire := time.Now().Add(time.Duration(runtimeCfg.ssTokenTTLSeconds()) * time.Second)
|
||||
if err := s.repo.DB.WithContext(ctx).
|
||||
Model(&model.BaishunLaunchSession{}).
|
||||
Where("id = ?", session.ID).
|
||||
@ -163,7 +165,7 @@ func (s *BaishunService) HandleUpdateToken(ctx context.Context, req BaishunUpdat
|
||||
|
||||
// HandleChangeBalance 处理百顺扣增币回调,并驱动钱包变更。
|
||||
func (s *BaishunService) HandleChangeBalance(ctx context.Context, req BaishunChangeBalanceRequest, rawJSON string) baishunStandardResponse {
|
||||
if !s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||
if _, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp); !ok {
|
||||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||||
}
|
||||
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
||||
@ -255,7 +257,7 @@ func (s *BaishunService) HandleReport(ctx context.Context, payload map[string]an
|
||||
|
||||
// HandleBalanceInfo 处理百顺查询余额回调。
|
||||
func (s *BaishunService) HandleBalanceInfo(ctx context.Context, req BaishunBalanceInfoRequest, rawJSON string) baishunBalanceInfoResponse {
|
||||
if !s.validateCommonSignature(ctx, req.AppID, req.AppChannel, req.Signature, req.SignatureNonce, req.Timestamp) {
|
||||
if _, ok := s.validateCommonSignature(ctx, req.AppID, req.AppChannel, req.Signature, req.SignatureNonce, req.Timestamp); !ok {
|
||||
return baishunBalanceInfoResponse{Code: baishunCodeSignatureError, Msg: "signature error"}
|
||||
}
|
||||
userID, _ := strconv.ParseInt(req.UserID, 10, 64)
|
||||
@ -270,24 +272,31 @@ func (s *BaishunService) HandleBalanceInfo(ctx context.Context, req BaishunBalan
|
||||
}
|
||||
|
||||
// validateCommonSignature 校验百顺回调签名。
|
||||
func (s *BaishunService) validateCommonSignature(ctx context.Context, appID int64, appChannel, signature, nonce string, timestamp int64) bool {
|
||||
if s.cfg.Baishun.AppID != 0 && appID != 0 && appID != s.cfg.Baishun.AppID {
|
||||
return false
|
||||
func (s *BaishunService) validateCommonSignature(ctx context.Context, appID int64, appChannel, signature, nonce string, timestamp int64) (baishunRuntimeConfig, bool) {
|
||||
runtimeCfg, err := s.resolveRuntimeConfigByApp(ctx, appID, appChannel)
|
||||
if err != nil {
|
||||
return baishunRuntimeConfig{}, false
|
||||
}
|
||||
if strings.TrimSpace(appChannel) != "" && s.cfg.Baishun.AppChannel != "" && appChannel != s.cfg.Baishun.AppChannel {
|
||||
return false
|
||||
if runtimeCfg.AppID != 0 && appID != 0 && appID != runtimeCfg.AppID {
|
||||
return baishunRuntimeConfig{}, false
|
||||
}
|
||||
if strings.TrimSpace(signature) == "" || strings.TrimSpace(nonce) == "" || timestamp == 0 || strings.TrimSpace(s.cfg.Baishun.AppKey) == "" {
|
||||
return false
|
||||
if strings.TrimSpace(appChannel) != "" && runtimeCfg.appChannel() != "" && appChannel != runtimeCfg.appChannel() {
|
||||
return baishunRuntimeConfig{}, false
|
||||
}
|
||||
if strings.TrimSpace(signature) == "" || strings.TrimSpace(nonce) == "" || timestamp == 0 || strings.TrimSpace(runtimeCfg.AppKey) == "" {
|
||||
return baishunRuntimeConfig{}, false
|
||||
}
|
||||
if abs64(time.Now().Unix()-timestamp) > 15 {
|
||||
return false
|
||||
return baishunRuntimeConfig{}, false
|
||||
}
|
||||
expected := s.signBAISHUN(timestamp, nonce)
|
||||
expected := signBAISHUN(runtimeCfg.AppKey, timestamp, nonce)
|
||||
if !strings.EqualFold(expected, signature) {
|
||||
return false
|
||||
return baishunRuntimeConfig{}, false
|
||||
}
|
||||
return s.rememberNonce(ctx, nonce)
|
||||
if !s.rememberNonce(ctx, nonce) {
|
||||
return baishunRuntimeConfig{}, false
|
||||
}
|
||||
return runtimeCfg, true
|
||||
}
|
||||
|
||||
// rememberNonce 记录已用 nonce,防止回调重放。
|
||||
@ -297,8 +306,8 @@ func (s *BaishunService) rememberNonce(ctx context.Context, nonce string) bool {
|
||||
}
|
||||
|
||||
// signBAISHUN 按百顺约定生成签名。
|
||||
func (s *BaishunService) signBAISHUN(timestamp int64, nonce string) string {
|
||||
sum := md5.Sum([]byte(nonce + s.cfg.Baishun.AppKey + strconv.FormatInt(timestamp, 10)))
|
||||
func signBAISHUN(appKey string, timestamp int64, nonce string) string {
|
||||
sum := md5.Sum([]byte(nonce + appKey + strconv.FormatInt(timestamp, 10)))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
|
||||
@ -18,15 +18,16 @@ import (
|
||||
|
||||
// SyncCatalog 从百顺平台拉取游戏目录并同步到本地表。
|
||||
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")
|
||||
if strings.TrimSpace(sysOrigin) == "" {
|
||||
sysOrigin = "LIKEI"
|
||||
}
|
||||
runtimeCfg, err := s.requireProviderConfig(ctx, sysOrigin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
games, err := s.fetchPlatformGames(ctx)
|
||||
games, err := s.fetchPlatformGames(ctx, runtimeCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -61,7 +62,7 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
|
||||
Orientation: intPtr(game.GameOrientation),
|
||||
SafeHeight: game.SafeHeight,
|
||||
VenueLevelJSON: utils.MustJSONString(game.VenueLevel, ""),
|
||||
GSP: strconv.Itoa(s.cfg.Baishun.GSP),
|
||||
GSP: strconv.Itoa(runtimeCfg.gsp()),
|
||||
RawJSON: utils.MustJSONString(game, ""),
|
||||
Status: baishunCatalogEnabled,
|
||||
CreateTime: time.Now(),
|
||||
@ -88,22 +89,23 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
|
||||
}
|
||||
|
||||
// fetchPlatformGames 从百顺平台拉取游戏目录。
|
||||
func (s *BaishunService) fetchPlatformGames(ctx context.Context) ([]baishunPlatformGame, error) {
|
||||
func (s *BaishunService) fetchPlatformGames(ctx context.Context, runtimeCfg baishunRuntimeConfig) ([]baishunPlatformGame, error) {
|
||||
timestamp := time.Now().Unix()
|
||||
nonce := s.randomNonce()
|
||||
payload := map[string]any{
|
||||
"game_list_type": baishunGameListTypeGame,
|
||||
"app_channel": s.cfg.Baishun.AppChannel,
|
||||
"app_id": s.cfg.Baishun.AppID,
|
||||
"signature": s.signBAISHUN(timestamp, nonce),
|
||||
"app_channel": runtimeCfg.appChannel(),
|
||||
"app_id": runtimeCfg.AppID,
|
||||
"signature": signBAISHUN(runtimeCfg.AppKey, timestamp, nonce),
|
||||
"signature_nonce": nonce,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
if strings.TrimSpace(s.cfg.Baishun.AppName) != "" {
|
||||
payload["app_name"] = s.cfg.Baishun.AppName
|
||||
if strings.TrimSpace(runtimeCfg.AppName) != "" {
|
||||
payload["app_name"] = runtimeCfg.AppName
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(s.cfg.Baishun.PlatformBaseURL, "/")+"/v1/api/gamelist", bytes.NewReader(body))
|
||||
endpoint := strings.TrimRight(runtimeCfg.PlatformBaseURL, "/") + "/v1/api/gamelist"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -117,15 +119,56 @@ func (s *BaishunService) fetchPlatformGames(ctx context.Context) ([]baishunPlatf
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return nil, fmt.Errorf("baishun platform failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(raw)))
|
||||
return nil, fmt.Errorf(
|
||||
"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
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf(
|
||||
"baishun platform returned invalid json: url=%s contentType=%s error=%v body=%s",
|
||||
endpoint,
|
||||
contentType,
|
||||
err,
|
||||
baishunShortBody(raw),
|
||||
)
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("baishun platform failed: code=%d message=%s", result.Code, result.Message)
|
||||
}
|
||||
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] + "..."
|
||||
}
|
||||
|
||||
136
internal/service/baishun/catalog_admin.go
Normal file
136
internal/service/baishun/catalog_admin.go
Normal file
@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
127
internal/service/baishun/extra_json.go
Normal file
127
internal/service/baishun/extra_json.go
Normal file
@ -0,0 +1,127 @@
|
||||
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
|
||||
}
|
||||
69
internal/service/baishun/extra_json_test.go
Normal file
69
internal/service/baishun/extra_json_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,9 @@ import (
|
||||
// toListItem 把联表行模型转换成游戏列表项。
|
||||
func (r gameListRow) toListItem() RoomGameListItem {
|
||||
return RoomGameListItem{
|
||||
ID: r.ConfigID,
|
||||
GameID: r.InternalGameID,
|
||||
GameType: r.gameTypeName(),
|
||||
VendorType: defaultIfBlank(r.VendorType, baishunVendorType),
|
||||
VendorGameID: r.vendorGameIDInt(),
|
||||
Name: r.name(),
|
||||
@ -22,6 +24,7 @@ func (r gameListRow) toListItem() RoomGameListItem {
|
||||
Orientation: r.orientation(),
|
||||
PackageVersion: r.packageVersion(),
|
||||
Status: defaultIfBlank(r.CatalogStatus, baishunCatalogEnabled),
|
||||
LaunchParams: r.launchParams(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,3 +103,12 @@ func (r gameListRow) vendorGameIDInt() int {
|
||||
func (r gameListRow) currencyIcon() string {
|
||||
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,11 +20,15 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
||||
if strings.TrimSpace(req.RoomID) == "" {
|
||||
return nil, NewAppError(http.StatusBadRequest, "missing_room_id", "roomId is required")
|
||||
}
|
||||
if strings.TrimSpace(req.GameID) == "" {
|
||||
return nil, NewAppError(http.StatusBadRequest, "missing_game_id", "gameId is required")
|
||||
if req.ID <= 0 && strings.TrimSpace(req.GameID) == "" {
|
||||
return nil, NewAppError(http.StatusBadRequest, "missing_game_id", "id or gameId is required")
|
||||
}
|
||||
|
||||
row, err := s.findGameRow(ctx, user.SysOrigin, req.GameID)
|
||||
row, err := s.resolveLaunchGameRow(ctx, user.SysOrigin, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtimeCfg, err := s.requireProviderConfig(ctx, user.SysOrigin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -56,9 +60,9 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
||||
VendorGameID: row.vendorGameIDInt(),
|
||||
GameSessionID: fmt.Sprintf("bs_%s_%d", sanitizeRoomID(req.RoomID), sessionID),
|
||||
LaunchCode: launchCode,
|
||||
LaunchCodeExpireTime: now.Add(time.Duration(s.cfg.Baishun.LaunchCodeTTLSeconds) * time.Second),
|
||||
LaunchCodeExpireTime: now.Add(time.Duration(runtimeCfg.launchCodeTTLSeconds()) * time.Second),
|
||||
Language: s.resolveLanguage(ctx, user.UserID),
|
||||
GSP: s.resolveGSP(row),
|
||||
GSP: s.resolveGSP(row, runtimeCfg),
|
||||
CurrencyType: 0,
|
||||
CurrencyIcon: row.currencyIcon(),
|
||||
SceneMode: req.SceneMode,
|
||||
@ -106,6 +110,7 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
||||
}
|
||||
|
||||
return &BaishunLaunchAppResponse{
|
||||
ID: row.ConfigID,
|
||||
GameSessionID: session.GameSessionID,
|
||||
VendorType: baishunVendorType,
|
||||
GameID: row.InternalGameID,
|
||||
@ -120,9 +125,9 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
||||
SafeHeight: row.safeHeight(),
|
||||
},
|
||||
BridgeConfig: BaishunBridgeConfig{
|
||||
AppName: s.cfg.Baishun.AppName,
|
||||
AppChannel: defaultIfBlank(s.cfg.Baishun.AppChannel, "skychat"),
|
||||
AppID: s.cfg.Baishun.AppID,
|
||||
AppName: runtimeCfg.AppName,
|
||||
AppChannel: runtimeCfg.appChannel(),
|
||||
AppID: runtimeCfg.AppID,
|
||||
UserID: strconv.FormatInt(user.UserID, 10),
|
||||
Code: launchCode,
|
||||
RoomID: req.RoomID,
|
||||
@ -147,6 +152,13 @@ func (s *BaishunService) LaunchGame(ctx context.Context, user AuthUser, req Bais
|
||||
}, 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 关闭房间内当前游戏会话并回写房间状态。
|
||||
func (s *BaishunService) CloseGame(ctx context.Context, user AuthUser, req BaishunCloseAppRequest) (*BaishunRoomStateResponse, error) {
|
||||
if strings.TrimSpace(req.RoomID) == "" {
|
||||
|
||||
230
internal/service/baishun/provider_config.go
Normal file
230
internal/service/baishun/provider_config.go
Normal file
@ -0,0 +1,230 @@
|
||||
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)
|
||||
}
|
||||
@ -3,9 +3,7 @@ package baishun
|
||||
import (
|
||||
"chatapp3-golang/internal/model"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -80,12 +78,13 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
|
||||
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,
|
||||
cat.name AS catalog_name,
|
||||
cat.cover AS catalog_cover,
|
||||
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,
|
||||
@ -107,42 +106,6 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
|
||||
for _, row := range rows {
|
||||
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: baishunFixedGameMode,
|
||||
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
|
||||
}
|
||||
|
||||
@ -167,12 +130,13 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
|
||||
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,
|
||||
cat.name AS catalog_name,
|
||||
cat.cover AS catalog_cover,
|
||||
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,
|
||||
@ -191,39 +155,62 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
|
||||
if strings.TrimSpace(row.InternalGameID) != "" {
|
||||
return row, nil
|
||||
}
|
||||
return gameListRow{}, NewAppError(http.StatusNotFound, "game_not_found", "room game config not found")
|
||||
}
|
||||
|
||||
var catalog model.BaishunGameCatalog
|
||||
if err := s.repo.DB.WithContext(ctx).
|
||||
Where("sys_origin = ? AND internal_game_id = ?", sysOrigin, gameID).
|
||||
First(&catalog).Error; err == nil {
|
||||
return gameListRow{
|
||||
SysOrigin: sysOrigin,
|
||||
InternalGameID: catalog.InternalGameID,
|
||||
Name: catalog.Name,
|
||||
Cover: catalog.Cover,
|
||||
VendorType: baishunVendorType,
|
||||
VendorGameID: strconv.Itoa(catalog.VendorGameID),
|
||||
LaunchMode: baishunLaunchModeRemote,
|
||||
CatalogCover: catalog.Cover,
|
||||
CatalogPreviewURL: catalog.PreviewURL,
|
||||
CatalogDownloadURL: catalog.DownloadURL,
|
||||
CatalogPackageVersion: catalog.PackageVersion,
|
||||
CatalogSafeHeight: catalog.SafeHeight,
|
||||
CatalogOrientation: catalog.Orientation,
|
||||
CatalogStatus: catalog.Status,
|
||||
GameModeRaw: formatAdminGameModeRaw(baishunFixedGameMode),
|
||||
}, nil
|
||||
// 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")
|
||||
}
|
||||
|
||||
// resolveGSP 解析行模型中的 GSP 值。
|
||||
func (s *BaishunService) resolveGSP(row gameListRow) int {
|
||||
func (s *BaishunService) resolveGSP(row gameListRow, runtimeCfg baishunRuntimeConfig) int {
|
||||
if parsed, err := strconv.Atoi(strings.TrimSpace(row.ExtGSP)); err == nil && parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
if s.cfg.Baishun.GSP > 0 {
|
||||
return s.cfg.Baishun.GSP
|
||||
}
|
||||
return 101
|
||||
return runtimeCfg.gsp()
|
||||
}
|
||||
|
||||
@ -77,20 +77,23 @@ func NewBaishunService(cfg config.Config, db baishunDB, cache redis.Cmdable, jav
|
||||
|
||||
// RoomGameListItem 是房间游戏列表中的单个游戏项。
|
||||
type RoomGameListItem struct {
|
||||
GameID string `json:"gameId"`
|
||||
VendorType string `json:"vendorType"`
|
||||
VendorGameID int `json:"vendorGameId"`
|
||||
Name string `json:"name"`
|
||||
Cover string `json:"cover"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Sort int64 `json:"sort,omitempty"`
|
||||
LaunchMode string `json:"launchMode"`
|
||||
FullScreen bool `json:"fullScreen"`
|
||||
GameMode int `json:"gameMode"`
|
||||
SafeHeight int `json:"safeHeight"`
|
||||
Orientation int `json:"orientation"`
|
||||
PackageVersion string `json:"packageVersion,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
ID int64 `json:"id"`
|
||||
GameID string `json:"gameId"`
|
||||
GameType string `json:"gameType,omitempty"`
|
||||
VendorType string `json:"vendorType"`
|
||||
VendorGameID int `json:"vendorGameId"`
|
||||
Name string `json:"name"`
|
||||
Cover string `json:"cover"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Sort int64 `json:"sort,omitempty"`
|
||||
LaunchMode string `json:"launchMode"`
|
||||
FullScreen bool `json:"fullScreen"`
|
||||
GameMode int `json:"gameMode"`
|
||||
SafeHeight int `json:"safeHeight"`
|
||||
Orientation int `json:"orientation"`
|
||||
PackageVersion string `json:"packageVersion,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
LaunchParams map[string]any `json:"launchParams,omitempty"`
|
||||
}
|
||||
|
||||
// RoomGameListResponse 是房间游戏列表响应。
|
||||
@ -112,6 +115,7 @@ type BaishunRoomStateResponse struct {
|
||||
|
||||
// BaishunLaunchAppRequest 是启动百顺游戏入参。
|
||||
type BaishunLaunchAppRequest struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
RoomID string `json:"roomId"`
|
||||
GameID string `json:"gameId"`
|
||||
SceneMode int `json:"sceneMode"`
|
||||
@ -158,6 +162,7 @@ type BaishunLaunchEntry struct {
|
||||
|
||||
// BaishunLaunchAppResponse 是启动游戏接口返回体。
|
||||
type BaishunLaunchAppResponse struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
GameSessionID string `json:"gameSessionId"`
|
||||
VendorType string `json:"vendorType"`
|
||||
GameID string `json:"gameId"`
|
||||
@ -183,34 +188,36 @@ type SyncCatalogResponse struct {
|
||||
|
||||
// AdminBaishunGameItem 是后台管理页展示的百顺游戏配置行。
|
||||
type AdminBaishunGameItem struct {
|
||||
ID int64 `json:"id"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
GameID string `json:"gameId"`
|
||||
VendorGameID int `json:"vendorGameId"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
GameCode string `json:"gameCode"`
|
||||
Cover string `json:"cover"`
|
||||
Amounts string `json:"amounts"`
|
||||
Showcase bool `json:"showcase"`
|
||||
Sort int64 `json:"sort"`
|
||||
Height float64 `json:"height"`
|
||||
Width float64 `json:"width"`
|
||||
FullScreen bool `json:"fullScreen"`
|
||||
ClientOrigin string `json:"clientOrigin"`
|
||||
Regions string `json:"regions"`
|
||||
GameMode int `json:"gameMode"`
|
||||
LaunchMode string `json:"launchMode"`
|
||||
PackageVersion string `json:"packageVersion"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
PackageURL string `json:"packageUrl"`
|
||||
Orientation int `json:"orientation"`
|
||||
SafeHeight int `json:"safeHeight"`
|
||||
GSP string `json:"gsp"`
|
||||
CurrencyIcon string `json:"currencyIcon"`
|
||||
CatalogStatus string `json:"catalogStatus"`
|
||||
CreateTime string `json:"createTime"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
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 是后台页分页响应。
|
||||
@ -223,31 +230,32 @@ type AdminBaishunGamePageResponse struct {
|
||||
|
||||
// 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"`
|
||||
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 描述从本地目录补齐后台配置的结果。
|
||||
@ -265,6 +273,60 @@ type SyncAdminCatalogResponse struct {
|
||||
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 回调入参。
|
||||
type BaishunTokenRequest struct {
|
||||
AppID int64 `json:"app_id"`
|
||||
@ -365,6 +427,7 @@ type gameListRow struct {
|
||||
ExtSafeHeight int
|
||||
CurrencyIcon string
|
||||
ExtGSP string
|
||||
ExtExtraJSON string
|
||||
CatalogName string
|
||||
CatalogCover string
|
||||
CatalogPreviewURL string
|
||||
|
||||
7
internal/service/gameopen/aliases.go
Normal file
7
internal/service/gameopen/aliases.go
Normal file
@ -0,0 +1,7 @@
|
||||
package gameopen
|
||||
|
||||
import "chatapp3-golang/internal/common"
|
||||
|
||||
type AppError = common.AppError
|
||||
|
||||
var NewAppError = common.NewAppError
|
||||
58
internal/service/gameopen/convert.go
Normal file
58
internal/service/gameopen/convert.go
Normal file
@ -0,0 +1,58 @@
|
||||
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[:])
|
||||
}
|
||||
306
internal/service/gameopen/service.go
Normal file
306
internal/service/gameopen/service.go
Normal file
@ -0,0 +1,306 @@
|
||||
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
|
||||
}
|
||||
133
internal/service/gameopen/service_test.go
Normal file
133
internal/service/gameopen/service_test.go
Normal file
@ -0,0 +1,133 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
126
internal/service/gameopen/types.go
Normal file
126
internal/service/gameopen/types.go
Normal file
@ -0,0 +1,126 @@
|
||||
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)
|
||||
}
|
||||
229
internal/service/gameprovider/registry.go
Normal file
229
internal/service/gameprovider/registry.go
Normal file
@ -0,0 +1,229 @@
|
||||
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))
|
||||
}
|
||||
161
internal/service/gameprovider/registry_test.go
Normal file
161
internal/service/gameprovider/registry_test.go
Normal file
@ -0,0 +1,161 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
112
internal/service/gameprovider/types.go
Normal file
112
internal/service/gameprovider/types.go
Normal file
@ -0,0 +1,112 @@
|
||||
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,7 +75,12 @@ func (s *LuckyGiftService) Draw(ctx context.Context, req LuckyGiftDrawRequest) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 三方有中奖收益时,再调用钱包做一次金币入账,并用 walletEventID 保证幂等。
|
||||
if err := s.publishRewardEvent(ctx, req, results, rewardTotal); err != nil {
|
||||
_ = s.markRequestFailed(ctx, req.BusinessID, providerCode, rawProviderResponse, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 三方有中奖收益时,再调用钱包给送礼人做金币返奖,并用 walletEventID 保证幂等。
|
||||
if rewardTotal > 0 {
|
||||
exists, err := s.java.ExistsGoldEvent(ctx, walletEventID)
|
||||
if err != nil {
|
||||
|
||||
96
internal/service/luckygift/reward_event.go
Normal file
96
internal/service/luckygift/reward_event.go
Normal file
@ -0,0 +1,96 @@
|
||||
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)
|
||||
}
|
||||
109
internal/service/luckygift/reward_event_test.go
Normal file
109
internal/service/luckygift/reward_event_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
16
migrations/007_baishun_provider_config.sql
Normal file
16
migrations/007_baishun_provider_config.sql
Normal file
@ -0,0 +1,16 @@
|
||||
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;
|
||||
20
migrations/008_game_open.sql
Normal file
20
migrations/008_game_open.sql
Normal file
@ -0,0 +1,20 @@
|
||||
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