update self game catalog handling
This commit is contained in:
parent
1b8241c3d2
commit
cee48232f8
@ -1794,22 +1794,23 @@ func (x *DiceStakeOption) GetSortOrder() int32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DiceConfig struct {
|
type DiceConfig struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
||||||
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
GameId string `protobuf:"bytes,2,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"`
|
||||||
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||||
StakeOptions []*DiceStakeOption `protobuf:"bytes,4,rep,name=stake_options,json=stakeOptions,proto3" json:"stake_options,omitempty"`
|
StakeOptions []*DiceStakeOption `protobuf:"bytes,4,rep,name=stake_options,json=stakeOptions,proto3" json:"stake_options,omitempty"`
|
||||||
FeeBps int32 `protobuf:"varint,5,opt,name=fee_bps,json=feeBps,proto3" json:"fee_bps,omitempty"`
|
FeeBps int32 `protobuf:"varint,5,opt,name=fee_bps,json=feeBps,proto3" json:"fee_bps,omitempty"`
|
||||||
PoolBps int32 `protobuf:"varint,6,opt,name=pool_bps,json=poolBps,proto3" json:"pool_bps,omitempty"`
|
PoolBps int32 `protobuf:"varint,6,opt,name=pool_bps,json=poolBps,proto3" json:"pool_bps,omitempty"`
|
||||||
MinPlayers int32 `protobuf:"varint,7,opt,name=min_players,json=minPlayers,proto3" json:"min_players,omitempty"`
|
MinPlayers int32 `protobuf:"varint,7,opt,name=min_players,json=minPlayers,proto3" json:"min_players,omitempty"`
|
||||||
MaxPlayers int32 `protobuf:"varint,8,opt,name=max_players,json=maxPlayers,proto3" json:"max_players,omitempty"`
|
MaxPlayers int32 `protobuf:"varint,8,opt,name=max_players,json=maxPlayers,proto3" json:"max_players,omitempty"`
|
||||||
RobotEnabled bool `protobuf:"varint,9,opt,name=robot_enabled,json=robotEnabled,proto3" json:"robot_enabled,omitempty"`
|
RobotEnabled bool `protobuf:"varint,9,opt,name=robot_enabled,json=robotEnabled,proto3" json:"robot_enabled,omitempty"`
|
||||||
RobotMatchWaitMs int64 `protobuf:"varint,10,opt,name=robot_match_wait_ms,json=robotMatchWaitMs,proto3" json:"robot_match_wait_ms,omitempty"`
|
RobotMatchWaitMs int64 `protobuf:"varint,10,opt,name=robot_match_wait_ms,json=robotMatchWaitMs,proto3" json:"robot_match_wait_ms,omitempty"`
|
||||||
PoolBalanceCoin int64 `protobuf:"varint,11,opt,name=pool_balance_coin,json=poolBalanceCoin,proto3" json:"pool_balance_coin,omitempty"`
|
PoolBalanceCoin int64 `protobuf:"varint,11,opt,name=pool_balance_coin,json=poolBalanceCoin,proto3" json:"pool_balance_coin,omitempty"`
|
||||||
CreatedAtMs int64 `protobuf:"varint,12,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
CreatedAtMs int64 `protobuf:"varint,12,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||||
UpdatedAtMs int64 `protobuf:"varint,13,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
|
UpdatedAtMs int64 `protobuf:"varint,13,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
RobotUserWinRatePercent *int32 `protobuf:"varint,14,opt,name=robot_user_win_rate_percent,json=robotUserWinRatePercent,proto3,oneof" json:"robot_user_win_rate_percent,omitempty"`
|
||||||
sizeCache protoimpl.SizeCache
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DiceConfig) Reset() {
|
func (x *DiceConfig) Reset() {
|
||||||
@ -1933,6 +1934,13 @@ func (x *DiceConfig) GetUpdatedAtMs() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *DiceConfig) GetRobotUserWinRatePercent() int32 {
|
||||||
|
if x != nil && x.RobotUserWinRatePercent != nil {
|
||||||
|
return *x.RobotUserWinRatePercent
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type GetDiceConfigRequest struct {
|
type GetDiceConfigRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||||
@ -5758,7 +5766,7 @@ const file_proto_game_v1_game_proto_rawDesc = "" +
|
|||||||
"stake_coin\x18\x01 \x01(\x03R\tstakeCoin\x12\x18\n" +
|
"stake_coin\x18\x01 \x01(\x03R\tstakeCoin\x12\x18\n" +
|
||||||
"\aenabled\x18\x02 \x01(\bR\aenabled\x12\x1d\n" +
|
"\aenabled\x18\x02 \x01(\bR\aenabled\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"sort_order\x18\x03 \x01(\x05R\tsortOrder\"\xdb\x03\n" +
|
"sort_order\x18\x03 \x01(\x05R\tsortOrder\"\xbe\x04\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"DiceConfig\x12\x19\n" +
|
"DiceConfig\x12\x19\n" +
|
||||||
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x17\n" +
|
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x17\n" +
|
||||||
@ -5776,7 +5784,9 @@ const file_proto_game_v1_game_proto_rawDesc = "" +
|
|||||||
" \x01(\x03R\x10robotMatchWaitMs\x12*\n" +
|
" \x01(\x03R\x10robotMatchWaitMs\x12*\n" +
|
||||||
"\x11pool_balance_coin\x18\v \x01(\x03R\x0fpoolBalanceCoin\x12\"\n" +
|
"\x11pool_balance_coin\x18\v \x01(\x03R\x0fpoolBalanceCoin\x12\"\n" +
|
||||||
"\rcreated_at_ms\x18\f \x01(\x03R\vcreatedAtMs\x12\"\n" +
|
"\rcreated_at_ms\x18\f \x01(\x03R\vcreatedAtMs\x12\"\n" +
|
||||||
"\rupdated_at_ms\x18\r \x01(\x03R\vupdatedAtMs\"_\n" +
|
"\rupdated_at_ms\x18\r \x01(\x03R\vupdatedAtMs\x12A\n" +
|
||||||
|
"\x1brobot_user_win_rate_percent\x18\x0e \x01(\x05H\x00R\x17robotUserWinRatePercent\x88\x01\x01B\x1e\n" +
|
||||||
|
"\x1c_robot_user_win_rate_percent\"_\n" +
|
||||||
"\x14GetDiceConfigRequest\x12.\n" +
|
"\x14GetDiceConfigRequest\x12.\n" +
|
||||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.game.v1.RequestMetaR\x04meta\x12\x17\n" +
|
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.game.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||||
"\agame_id\x18\x02 \x01(\tR\x06gameId\"m\n" +
|
"\agame_id\x18\x02 \x01(\tR\x06gameId\"m\n" +
|
||||||
@ -6364,6 +6374,7 @@ func file_proto_game_v1_game_proto_init() {
|
|||||||
if File_proto_game_v1_game_proto != nil {
|
if File_proto_game_v1_game_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[18].OneofWrappers = []any{}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
|
|||||||
@ -216,6 +216,7 @@ message DiceConfig {
|
|||||||
int64 pool_balance_coin = 11;
|
int64 pool_balance_coin = 11;
|
||||||
int64 created_at_ms = 12;
|
int64 created_at_ms = 12;
|
||||||
int64 updated_at_ms = 13;
|
int64 updated_at_ms = 13;
|
||||||
|
optional int32 robot_user_win_rate_percent = 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetDiceConfigRequest {
|
message GetDiceConfigRequest {
|
||||||
|
|||||||
@ -43,6 +43,22 @@ func (h *Handler) StatisticsOverview(c *gin.Context) {
|
|||||||
response.OK(c, overview)
|
response.OK(c, overview)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) SelfGameStatisticsOverview(c *gin.Context) {
|
||||||
|
overview, err := h.service.SelfGameStatisticsOverview(c.Request.Context(), SelfGameStatisticsQuery{
|
||||||
|
AppCode: c.DefaultQuery("app_code", "lalu"),
|
||||||
|
StartMS: parseInt64(c.Query("start_ms")),
|
||||||
|
EndMS: parseInt64(c.Query("end_ms")),
|
||||||
|
RegionID: parseInt64(c.Query("region_id")),
|
||||||
|
CountryID: parseInt64(c.Query("country_id")),
|
||||||
|
GameID: c.Query("game_id"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取自研游戏统计失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, overview)
|
||||||
|
}
|
||||||
|
|
||||||
func parseInt64(value string) int64 {
|
func parseInt64(value string) int64 {
|
||||||
parsed, _ := strconv.ParseInt(value, 10, 64)
|
parsed, _ := strconv.ParseInt(value, 10, 64)
|
||||||
return parsed
|
return parsed
|
||||||
|
|||||||
@ -9,4 +9,5 @@ import (
|
|||||||
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||||
protected.GET("/dashboard/overview", middleware.RequirePermission("overview:view"), h.DashboardOverview)
|
protected.GET("/dashboard/overview", middleware.RequirePermission("overview:view"), h.DashboardOverview)
|
||||||
protected.GET("/statistics/overview", middleware.RequirePermission("overview:view"), h.StatisticsOverview)
|
protected.GET("/statistics/overview", middleware.RequirePermission("overview:view"), h.StatisticsOverview)
|
||||||
|
protected.GET("/statistics/self-games/overview", middleware.RequirePermission("overview:view"), h.SelfGameStatisticsOverview)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"hyapp-admin-server/internal/config"
|
"hyapp-admin-server/internal/config"
|
||||||
"hyapp-admin-server/internal/repository"
|
"hyapp-admin-server/internal/repository"
|
||||||
@ -28,6 +29,15 @@ type StatisticsQuery struct {
|
|||||||
CountryID int64
|
CountryID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelfGameStatisticsQuery struct {
|
||||||
|
AppCode string
|
||||||
|
StartMS int64
|
||||||
|
EndMS int64
|
||||||
|
RegionID int64
|
||||||
|
CountryID int64
|
||||||
|
GameID string
|
||||||
|
}
|
||||||
|
|
||||||
func NewService(store *repository.Store, cfg config.Config) *DashboardService {
|
func NewService(store *repository.Store, cfg config.Config) *DashboardService {
|
||||||
return &DashboardService{
|
return &DashboardService{
|
||||||
store: store,
|
store: store,
|
||||||
@ -41,7 +51,6 @@ func (s *DashboardService) Overview() (*repository.DashboardOverview, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DashboardService) StatisticsOverview(ctx context.Context, query StatisticsQuery) (map[string]any, error) {
|
func (s *DashboardService) StatisticsOverview(ctx context.Context, query StatisticsQuery) (map[string]any, error) {
|
||||||
baseURL := s.cfg.StatisticsService.BaseURL + "/internal/v1/statistics/overview"
|
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
values.Set("app_code", query.AppCode)
|
values.Set("app_code", query.AppCode)
|
||||||
if query.StartMS > 0 {
|
if query.StartMS > 0 {
|
||||||
@ -62,6 +71,33 @@ func (s *DashboardService) StatisticsOverview(ctx context.Context, query Statist
|
|||||||
if query.CountryID > 0 {
|
if query.CountryID > 0 {
|
||||||
values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
|
values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
|
||||||
}
|
}
|
||||||
|
return s.statisticsGET(ctx, "/internal/v1/statistics/overview", values)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DashboardService) SelfGameStatisticsOverview(ctx context.Context, query SelfGameStatisticsQuery) (map[string]any, error) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("app_code", query.AppCode)
|
||||||
|
if query.StartMS > 0 {
|
||||||
|
values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
|
||||||
|
}
|
||||||
|
if query.EndMS > 0 {
|
||||||
|
values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
|
||||||
|
}
|
||||||
|
if query.RegionID > 0 {
|
||||||
|
values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
|
||||||
|
}
|
||||||
|
if query.CountryID > 0 {
|
||||||
|
values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
|
||||||
|
}
|
||||||
|
if gameID := strings.TrimSpace(query.GameID); gameID != "" && gameID != "all" {
|
||||||
|
values.Set("game_id", gameID)
|
||||||
|
}
|
||||||
|
// 数据大屏只通过 statistics-service 读取自研游戏聚合指标,避免 admin 服务绕过聚合层去扫业务明细表。
|
||||||
|
return s.statisticsGET(ctx, "/internal/v1/statistics/self-games/overview", values)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DashboardService) statisticsGET(ctx context.Context, path string, values url.Values) (map[string]any, error) {
|
||||||
|
baseURL := s.cfg.StatisticsService.BaseURL + path
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"?"+values.Encode(), nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"?"+values.Encode(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -48,19 +48,20 @@ type catalogDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type diceConfigDTO struct {
|
type diceConfigDTO struct {
|
||||||
AppCode string `json:"appCode"`
|
AppCode string `json:"appCode"`
|
||||||
GameID string `json:"gameId"`
|
GameID string `json:"gameId"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
StakeOptions []diceStakeDTO `json:"stakeOptions"`
|
StakeOptions []diceStakeDTO `json:"stakeOptions"`
|
||||||
FeeBPS int32 `json:"feeBps"`
|
FeeBPS int32 `json:"feeBps"`
|
||||||
PoolBPS int32 `json:"poolBps"`
|
PoolBPS int32 `json:"poolBps"`
|
||||||
MinPlayers int32 `json:"minPlayers"`
|
MinPlayers int32 `json:"minPlayers"`
|
||||||
MaxPlayers int32 `json:"maxPlayers"`
|
MaxPlayers int32 `json:"maxPlayers"`
|
||||||
RobotEnabled bool `json:"robotEnabled"`
|
RobotEnabled bool `json:"robotEnabled"`
|
||||||
RobotMatchWaitMS int64 `json:"robotMatchWaitMs"`
|
RobotMatchWaitMS int64 `json:"robotMatchWaitMs"`
|
||||||
PoolBalanceCoin int64 `json:"poolBalanceCoin"`
|
RobotUserWinRatePercent int32 `json:"robotUserWinRatePercent"`
|
||||||
CreatedAtMS int64 `json:"createdAtMs"`
|
PoolBalanceCoin int64 `json:"poolBalanceCoin"`
|
||||||
UpdatedAtMS int64 `json:"updatedAtMs"`
|
CreatedAtMS int64 `json:"createdAtMs"`
|
||||||
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type diceStakeDTO struct {
|
type diceStakeDTO struct {
|
||||||
@ -205,19 +206,20 @@ func diceConfigFromProto(item *gamev1.DiceConfig) diceConfigDTO {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return diceConfigDTO{
|
return diceConfigDTO{
|
||||||
AppCode: item.GetAppCode(),
|
AppCode: item.GetAppCode(),
|
||||||
GameID: item.GetGameId(),
|
GameID: item.GetGameId(),
|
||||||
Status: item.GetStatus(),
|
Status: item.GetStatus(),
|
||||||
StakeOptions: options,
|
StakeOptions: options,
|
||||||
FeeBPS: item.GetFeeBps(),
|
FeeBPS: item.GetFeeBps(),
|
||||||
PoolBPS: item.GetPoolBps(),
|
PoolBPS: item.GetPoolBps(),
|
||||||
MinPlayers: item.GetMinPlayers(),
|
MinPlayers: item.GetMinPlayers(),
|
||||||
MaxPlayers: item.GetMaxPlayers(),
|
MaxPlayers: item.GetMaxPlayers(),
|
||||||
RobotEnabled: item.GetRobotEnabled(),
|
RobotEnabled: item.GetRobotEnabled(),
|
||||||
RobotMatchWaitMS: item.GetRobotMatchWaitMs(),
|
RobotMatchWaitMS: item.GetRobotMatchWaitMs(),
|
||||||
PoolBalanceCoin: item.GetPoolBalanceCoin(),
|
RobotUserWinRatePercent: item.GetRobotUserWinRatePercent(),
|
||||||
CreatedAtMS: item.GetCreatedAtMs(),
|
PoolBalanceCoin: item.GetPoolBalanceCoin(),
|
||||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,8 @@ type diceConfigRequest struct {
|
|||||||
MaxPlayers int32 `json:"maxPlayers"`
|
MaxPlayers int32 `json:"maxPlayers"`
|
||||||
RobotEnabled bool `json:"robotEnabled"`
|
RobotEnabled bool `json:"robotEnabled"`
|
||||||
RobotMatchWaitMS int64 `json:"robotMatchWaitMs"`
|
RobotMatchWaitMS int64 `json:"robotMatchWaitMs"`
|
||||||
|
// RobotUserWinRatePercent 用指针保留“没传”和“明确传 0”的区别;0 代表老用户机器人局必输,是合法运营配置。
|
||||||
|
RobotUserWinRatePercent *int32 `json:"robotUserWinRatePercent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type diceStakeReq struct {
|
type diceStakeReq struct {
|
||||||
@ -146,15 +148,16 @@ func (r diceConfigRequest) toProto(gameID string) *gamev1.DiceConfig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return &gamev1.DiceConfig{
|
return &gamev1.DiceConfig{
|
||||||
GameId: strings.TrimSpace(gameID),
|
GameId: strings.TrimSpace(gameID),
|
||||||
Status: strings.TrimSpace(r.Status),
|
Status: strings.TrimSpace(r.Status),
|
||||||
StakeOptions: options,
|
StakeOptions: options,
|
||||||
FeeBps: r.FeeBPS,
|
FeeBps: r.FeeBPS,
|
||||||
PoolBps: r.PoolBPS,
|
PoolBps: r.PoolBPS,
|
||||||
MinPlayers: r.MinPlayers,
|
MinPlayers: r.MinPlayers,
|
||||||
MaxPlayers: r.MaxPlayers,
|
MaxPlayers: r.MaxPlayers,
|
||||||
RobotEnabled: r.RobotEnabled,
|
RobotEnabled: r.RobotEnabled,
|
||||||
RobotMatchWaitMs: r.RobotMatchWaitMS,
|
RobotMatchWaitMs: r.RobotMatchWaitMS,
|
||||||
|
RobotUserWinRatePercent: r.RobotUserWinRatePercent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -282,6 +282,7 @@ CREATE TABLE IF NOT EXISTS game_self_game_configs (
|
|||||||
max_players INT NOT NULL DEFAULT 2 COMMENT '最多参与人数',
|
max_players INT NOT NULL DEFAULT 2 COMMENT '最多参与人数',
|
||||||
robot_enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用机器人补位',
|
robot_enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用机器人补位',
|
||||||
robot_match_wait_ms BIGINT NOT NULL DEFAULT 3000 COMMENT '等待真人后机器人补位毫秒数',
|
robot_match_wait_ms BIGINT NOT NULL DEFAULT 3000 COMMENT '等待真人后机器人补位毫秒数',
|
||||||
|
robot_user_win_rate_percent INT NOT NULL DEFAULT 50 COMMENT '非新手保护机器人局真人胜率,50 表示不干预正常规则',
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
PRIMARY KEY(app_code, game_id)
|
PRIMARY KEY(app_code, game_id)
|
||||||
@ -328,6 +329,16 @@ CREATE TABLE IF NOT EXISTS game_self_game_robots (
|
|||||||
KEY idx_game_robot_random_pool(app_code, status, user_id)
|
KEY idx_game_robot_random_pool(app_code, status, user_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='自研游戏机器人登记表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='自研游戏机器人登记表';
|
||||||
|
|
||||||
|
-- 机器人胜率 50 表示不写 forced_result;旧表补默认 50,避免存量配置上线后突然变成 0% 必胜/必输控制。
|
||||||
|
SET @ddl := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'game_self_game_configs' AND COLUMN_NAME = 'robot_user_win_rate_percent') = 0,
|
||||||
|
'ALTER TABLE game_self_game_configs ADD COLUMN robot_user_win_rate_percent INT NOT NULL DEFAULT 50 COMMENT ''非新手保护机器人局真人胜率,50 表示不干预正常规则'' AFTER robot_match_wait_ms',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
-- 机器人使用时间和累计次数只用于后台观察随机命中分布;旧表补默认 0,避免存量机器人资料读取失败。
|
-- 机器人使用时间和累计次数只用于后台观察随机命中分布;旧表补默认 0,避免存量机器人资料读取失败。
|
||||||
SET @ddl := IF(
|
SET @ddl := IF(
|
||||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'game_self_game_robots' AND COLUMN_NAME = 'last_used_at_ms') = 0,
|
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'game_self_game_robots' AND COLUMN_NAME = 'last_used_at_ms') = 0,
|
||||||
|
|||||||
@ -17,8 +17,10 @@ const (
|
|||||||
DefaultPoolBPS = int64(100)
|
DefaultPoolBPS = int64(100)
|
||||||
DefaultJoinTTLMillis = int64(60_000)
|
DefaultJoinTTLMillis = int64(60_000)
|
||||||
DefaultRobotWaitMS = int64(3_000)
|
DefaultRobotWaitMS = int64(3_000)
|
||||||
DefaultGameID = "dice"
|
// DefaultRobotUserWinRatePercent 为 50 时不改写 forced_result,机器人局继续按游戏自身随机/手势规则结算。
|
||||||
SelfGameIDRock = "rock"
|
DefaultRobotUserWinRatePercent = int32(50)
|
||||||
|
DefaultGameID = "dice"
|
||||||
|
SelfGameIDRock = "rock"
|
||||||
// 首版禁止 1 人局,避免单人场景下“自己赢自己”的派奖语义不清;3 人局只是多一行 participant。
|
// 首版禁止 1 人局,避免单人场景下“自己赢自己”的派奖语义不清;3 人局只是多一行 participant。
|
||||||
DefaultMinPlayers = int32(2)
|
DefaultMinPlayers = int32(2)
|
||||||
DefaultMaxPlayers = int32(2)
|
DefaultMaxPlayers = int32(2)
|
||||||
@ -106,9 +108,12 @@ type Config struct {
|
|||||||
MaxPlayers int32
|
MaxPlayers int32
|
||||||
RobotEnabled bool
|
RobotEnabled bool
|
||||||
RobotMatchWaitMS int64
|
RobotMatchWaitMS int64
|
||||||
PoolBalanceCoin int64
|
// RobotUserWinRatePercent 只控制非新手保护机器人局的强制胜负;0 是合法配置,所以用 Set 标记区分“未配置”和“明确配置 0”。
|
||||||
CreatedAtMS int64
|
RobotUserWinRatePercent int32
|
||||||
UpdatedAtMS int64
|
RobotUserWinRatePercentSet bool
|
||||||
|
PoolBalanceCoin int64
|
||||||
|
CreatedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Robot 是 game-service 对“哪些真实 App 用户可作为机器人”的最小登记,不复制用户头像昵称。
|
// Robot 是 game-service 对“哪些真实 App 用户可作为机器人”的最小登记,不复制用户头像昵称。
|
||||||
@ -239,6 +244,13 @@ func NormalizeConfig(config Config) (Config, error) {
|
|||||||
if config.RobotMatchWaitMS <= 0 {
|
if config.RobotMatchWaitMS <= 0 {
|
||||||
config.RobotMatchWaitMS = DefaultRobotWaitMS
|
config.RobotMatchWaitMS = DefaultRobotWaitMS
|
||||||
}
|
}
|
||||||
|
if !config.RobotUserWinRatePercentSet {
|
||||||
|
config.RobotUserWinRatePercent = DefaultRobotUserWinRatePercent
|
||||||
|
config.RobotUserWinRatePercentSet = true
|
||||||
|
}
|
||||||
|
if config.RobotUserWinRatePercent < 0 || config.RobotUserWinRatePercent > 100 {
|
||||||
|
return Config{}, xerr.New(xerr.InvalidArgument, "dice robot_user_win_rate_percent is invalid")
|
||||||
|
}
|
||||||
config.StakeOptions = NormalizeStakeOptions(config.StakeOptions)
|
config.StakeOptions = NormalizeStakeOptions(config.StakeOptions)
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
32
services/game-service/internal/domain/dice/dice_test.go
Normal file
32
services/game-service/internal/domain/dice/dice_test.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package dice
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNormalizeConfigDefaultsRobotUserWinRatePercent(t *testing.T) {
|
||||||
|
config, err := NormalizeConfig(Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NormalizeConfig failed: %v", err)
|
||||||
|
}
|
||||||
|
if config.RobotUserWinRatePercent != DefaultRobotUserWinRatePercent || !config.RobotUserWinRatePercentSet {
|
||||||
|
t.Fatalf("robot win rate default mismatch: rate=%d set=%v", config.RobotUserWinRatePercent, config.RobotUserWinRatePercentSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeConfigKeepsExplicitRobotUserWinRatePercentZero(t *testing.T) {
|
||||||
|
config, err := NormalizeConfig(Config{RobotUserWinRatePercent: 0, RobotUserWinRatePercentSet: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NormalizeConfig failed: %v", err)
|
||||||
|
}
|
||||||
|
if config.RobotUserWinRatePercent != 0 {
|
||||||
|
t.Fatalf("explicit zero win rate should be kept, got %d", config.RobotUserWinRatePercent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeConfigRejectsInvalidRobotUserWinRatePercent(t *testing.T) {
|
||||||
|
tests := []int32{-1, 101}
|
||||||
|
for _, rate := range tests {
|
||||||
|
if _, err := NormalizeConfig(Config{RobotUserWinRatePercent: rate, RobotUserWinRatePercentSet: true}); err == nil {
|
||||||
|
t.Fatalf("NormalizeConfig should reject robot win rate %d", rate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -822,15 +822,17 @@ func (s *Service) configForMatch(ctx context.Context, appCode string, gameID str
|
|||||||
}
|
}
|
||||||
// 没有后台配置时把默认配置写成 MySQL 事实,再重新读取;这样 H5、大厅后台和机器人奖池判断都使用同一份配置/奖池快照。
|
// 没有后台配置时把默认配置写成 MySQL 事实,再重新读取;这样 H5、大厅后台和机器人奖池判断都使用同一份配置/奖池快照。
|
||||||
normalized, err := dicedomain.NormalizeConfig(dicedomain.Config{
|
normalized, err := dicedomain.NormalizeConfig(dicedomain.Config{
|
||||||
AppCode: appcode.Normalize(appCode),
|
AppCode: appcode.Normalize(appCode),
|
||||||
GameID: strings.TrimSpace(gameID),
|
GameID: strings.TrimSpace(gameID),
|
||||||
Status: dicedomain.ConfigStatusActive,
|
Status: dicedomain.ConfigStatusActive,
|
||||||
FeeBPS: int32(dicedomain.DefaultFeeBPS),
|
FeeBPS: int32(dicedomain.DefaultFeeBPS),
|
||||||
PoolBPS: int32(dicedomain.DefaultPoolBPS),
|
PoolBPS: int32(dicedomain.DefaultPoolBPS),
|
||||||
MinPlayers: dicedomain.DefaultMinPlayers,
|
MinPlayers: dicedomain.DefaultMinPlayers,
|
||||||
MaxPlayers: dicedomain.DefaultMaxPlayers,
|
MaxPlayers: dicedomain.DefaultMaxPlayers,
|
||||||
RobotEnabled: true,
|
RobotEnabled: true,
|
||||||
RobotMatchWaitMS: dicedomain.DefaultRobotWaitMS,
|
RobotMatchWaitMS: dicedomain.DefaultRobotWaitMS,
|
||||||
|
RobotUserWinRatePercent: dicedomain.DefaultRobotUserWinRatePercent,
|
||||||
|
RobotUserWinRatePercentSet: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dicedomain.Config{}, err
|
return dicedomain.Config{}, err
|
||||||
@ -894,6 +896,12 @@ func (s *Service) attachRobotIfDue(ctx context.Context, requestID string, match
|
|||||||
forcedResult = dicedomain.ForcedResultPlayerWin
|
forcedResult = dicedomain.ForcedResultPlayerWin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if forcedResult == "" {
|
||||||
|
forcedResult, err = s.forcedRobotResultByWinRate(config.RobotUserWinRatePercent)
|
||||||
|
if err != nil {
|
||||||
|
return dicedomain.Match{}, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
robotRPSGesture := ""
|
robotRPSGesture := ""
|
||||||
if dicedomain.IsRPSGameID(match.GameID) {
|
if dicedomain.IsRPSGameID(match.GameID) {
|
||||||
@ -909,6 +917,24 @@ func (s *Service) attachRobotIfDue(ctx context.Context, requestID string, match
|
|||||||
return joined, nowMS, nil
|
return joined, nowMS, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) forcedRobotResultByWinRate(winRatePercent int32) (string, error) {
|
||||||
|
if winRatePercent == dicedomain.DefaultRobotUserWinRatePercent {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
roll, err := s.randomInt(100)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if roll < 0 || roll >= 100 {
|
||||||
|
return "", xerr.New(xerr.InvalidArgument, "random robot win rate roll is invalid")
|
||||||
|
}
|
||||||
|
if int32(roll) < winRatePercent {
|
||||||
|
// 非新手保护机器人局按后台配置直接写入强制结果;骰子在结算时改点数,RPS 在机器人入座时反推手势。
|
||||||
|
return dicedomain.ForcedResultPlayerWin, nil
|
||||||
|
}
|
||||||
|
return dicedomain.ForcedResultPlayerLose, nil
|
||||||
|
}
|
||||||
|
|
||||||
func firstWinGuaranteeUser(match dicedomain.Match) (int64, bool) {
|
func firstWinGuaranteeUser(match dicedomain.Match) (int64, bool) {
|
||||||
var userID int64
|
var userID int64
|
||||||
for _, participant := range match.Participants {
|
for _, participant := range match.Participants {
|
||||||
|
|||||||
@ -306,6 +306,7 @@ func TestRPSRequiresGestureBeforeMatchmaking(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
repo := newFakeDiceRepository()
|
repo := newFakeDiceRepository()
|
||||||
configureFakeRock(repo)
|
configureFakeRock(repo)
|
||||||
@ -570,6 +571,142 @@ func TestRobotMatchUsesNormalFlowAfterUserHasSelfGameWin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRobotMatchAppliesConfiguredWinRateAfterUserHasSelfGameWin(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
winRate int32
|
||||||
|
randomRoll int
|
||||||
|
wantForced string
|
||||||
|
randomAllowed bool
|
||||||
|
}{
|
||||||
|
{name: "configured_100_forces_human_win", winRate: 100, randomRoll: 99, wantForced: dicedomain.ForcedResultPlayerWin, randomAllowed: true},
|
||||||
|
{name: "configured_0_forces_human_lose", winRate: 0, randomRoll: 0, wantForced: dicedomain.ForcedResultPlayerLose, randomAllowed: true},
|
||||||
|
{name: "configured_50_keeps_normal_flow", winRate: 50, wantForced: "", randomAllowed: false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
repo := newFakeDiceRepository()
|
||||||
|
repo.config.PoolBalanceCoin = 200
|
||||||
|
repo.poolBalance = 200
|
||||||
|
repo.config.RobotUserWinRatePercent = tt.winRate
|
||||||
|
repo.config.RobotUserWinRatePercentSet = true
|
||||||
|
repo.robot = dicedomain.Robot{AppCode: "lalu", GameID: "dice", UserID: 9100, Status: dicedomain.RobotStatusActive}
|
||||||
|
repo.userWins = map[string]bool{"dice:101": true}
|
||||||
|
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000}}
|
||||||
|
svc := New(Config{}, repo, wallet)
|
||||||
|
nowMS := int64(1700000000000)
|
||||||
|
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||||
|
svc.randomInt = func(max int) (int, error) {
|
||||||
|
if !tt.randomAllowed {
|
||||||
|
t.Fatalf("50 percent robot win rate should keep normal flow without attach-time random, max=%d", max)
|
||||||
|
}
|
||||||
|
if max != 100 {
|
||||||
|
t.Fatalf("robot win rate random should use 100 slots, got %d", max)
|
||||||
|
}
|
||||||
|
return tt.randomRoll, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
created, _, err := svc.CreateMatch(context.Background(), CreateMatchCommand{
|
||||||
|
AppCode: "lalu",
|
||||||
|
RequestID: "req-win-rate-create-" + tt.name,
|
||||||
|
UserID: 101,
|
||||||
|
GameID: "dice",
|
||||||
|
StakeCoin: 100,
|
||||||
|
MinPlayers: 2,
|
||||||
|
MaxPlayers: 2,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMatch failed: %v", err)
|
||||||
|
}
|
||||||
|
nowMS += dicedomain.DefaultRobotWaitMS
|
||||||
|
ready, _, err := svc.GetMatch(context.Background(), GetMatchCommand{AppCode: "lalu", UserID: 101, MatchID: created.MatchID, GameID: "dice"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetMatch attach robot failed: %v", err)
|
||||||
|
}
|
||||||
|
if ready.ForcedResult != tt.wantForced {
|
||||||
|
t.Fatalf("forced_result = %q, want %q", ready.ForcedResult, tt.wantForced)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRobotWinRateDoesNotOverrideFirstWinGuarantee(t *testing.T) {
|
||||||
|
repo := newFakeDiceRepository()
|
||||||
|
repo.config.PoolBalanceCoin = 200
|
||||||
|
repo.poolBalance = 200
|
||||||
|
repo.config.RobotUserWinRatePercent = 0
|
||||||
|
repo.config.RobotUserWinRatePercentSet = true
|
||||||
|
repo.robot = dicedomain.Robot{AppCode: "lalu", GameID: "dice", UserID: 9101, Status: dicedomain.RobotStatusActive}
|
||||||
|
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000}}
|
||||||
|
svc := New(Config{}, repo, wallet)
|
||||||
|
nowMS := int64(1700000000000)
|
||||||
|
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||||
|
svc.randomInt = func(max int) (int, error) {
|
||||||
|
t.Fatalf("first-win guarantee must skip configured robot win rate random, max=%d", max)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
created, _, err := svc.CreateMatch(context.Background(), CreateMatchCommand{
|
||||||
|
AppCode: "lalu",
|
||||||
|
RequestID: "req-first-win-over-rate-create",
|
||||||
|
UserID: 101,
|
||||||
|
GameID: "dice",
|
||||||
|
StakeCoin: 100,
|
||||||
|
MinPlayers: 2,
|
||||||
|
MaxPlayers: 2,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMatch failed: %v", err)
|
||||||
|
}
|
||||||
|
nowMS += dicedomain.DefaultRobotWaitMS
|
||||||
|
ready, _, err := svc.GetMatch(context.Background(), GetMatchCommand{AppCode: "lalu", UserID: 101, MatchID: created.MatchID, GameID: "dice"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetMatch attach robot failed: %v", err)
|
||||||
|
}
|
||||||
|
if ready.ForcedResult != dicedomain.ForcedResultPlayerWin {
|
||||||
|
t.Fatalf("first-win guarantee should force player win before configured rate, got %q", ready.ForcedResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRobotWinRateDoesNotOverrideShortPoolProtection(t *testing.T) {
|
||||||
|
repo := newFakeDiceRepository()
|
||||||
|
repo.config.PoolBalanceCoin = 87
|
||||||
|
repo.poolBalance = 87
|
||||||
|
repo.config.RobotUserWinRatePercent = 100
|
||||||
|
repo.config.RobotUserWinRatePercentSet = true
|
||||||
|
repo.robot = dicedomain.Robot{AppCode: "lalu", GameID: "dice", UserID: 9102, Status: dicedomain.RobotStatusActive}
|
||||||
|
repo.userWins = map[string]bool{"dice:101": true}
|
||||||
|
wallet := &fakeDiceWallet{balances: map[int64]int64{101: 1000}}
|
||||||
|
svc := New(Config{}, repo, wallet)
|
||||||
|
nowMS := int64(1700000000000)
|
||||||
|
svc.now = func() time.Time { return time.UnixMilli(nowMS) }
|
||||||
|
svc.randomInt = func(max int) (int, error) {
|
||||||
|
t.Fatalf("short pool protection must skip configured robot win rate random, max=%d", max)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
created, _, err := svc.CreateMatch(context.Background(), CreateMatchCommand{
|
||||||
|
AppCode: "lalu",
|
||||||
|
RequestID: "req-short-pool-over-rate-create",
|
||||||
|
UserID: 101,
|
||||||
|
GameID: "dice",
|
||||||
|
StakeCoin: 100,
|
||||||
|
MinPlayers: 2,
|
||||||
|
MaxPlayers: 2,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMatch failed: %v", err)
|
||||||
|
}
|
||||||
|
nowMS += dicedomain.DefaultRobotWaitMS
|
||||||
|
ready, _, err := svc.GetMatch(context.Background(), GetMatchCommand{AppCode: "lalu", UserID: 101, MatchID: created.MatchID, GameID: "dice"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetMatch attach robot failed: %v", err)
|
||||||
|
}
|
||||||
|
if ready.ForcedResult != dicedomain.ForcedResultPlayerLose {
|
||||||
|
t.Fatalf("short pool should force player lose before configured rate, got %q", ready.ForcedResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExploreGameCodeForMatchUsesSelfGameIDs(t *testing.T) {
|
func TestExploreGameCodeForMatchUsesSelfGameIDs(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
gameID string
|
gameID string
|
||||||
@ -681,16 +818,18 @@ func newFakeDiceRepository() *fakeDiceRepository {
|
|||||||
PlatformStatus: gamedomain.StatusActive,
|
PlatformStatus: gamedomain.StatusActive,
|
||||||
},
|
},
|
||||||
config: dicedomain.Config{
|
config: dicedomain.Config{
|
||||||
AppCode: "lalu",
|
AppCode: "lalu",
|
||||||
GameID: "dice",
|
GameID: "dice",
|
||||||
Status: dicedomain.ConfigStatusActive,
|
Status: dicedomain.ConfigStatusActive,
|
||||||
StakeOptions: dicedomain.NormalizeStakeOptions(nil),
|
StakeOptions: dicedomain.NormalizeStakeOptions(nil),
|
||||||
FeeBPS: int32(dicedomain.DefaultFeeBPS),
|
FeeBPS: int32(dicedomain.DefaultFeeBPS),
|
||||||
PoolBPS: int32(dicedomain.DefaultPoolBPS),
|
PoolBPS: int32(dicedomain.DefaultPoolBPS),
|
||||||
MinPlayers: dicedomain.DefaultMinPlayers,
|
MinPlayers: dicedomain.DefaultMinPlayers,
|
||||||
MaxPlayers: dicedomain.DefaultMaxPlayers,
|
MaxPlayers: dicedomain.DefaultMaxPlayers,
|
||||||
RobotEnabled: true,
|
RobotEnabled: true,
|
||||||
RobotMatchWaitMS: dicedomain.DefaultRobotWaitMS,
|
RobotMatchWaitMS: dicedomain.DefaultRobotWaitMS,
|
||||||
|
RobotUserWinRatePercent: dicedomain.DefaultRobotUserWinRatePercent,
|
||||||
|
RobotUserWinRatePercentSet: true,
|
||||||
},
|
},
|
||||||
orders: map[string]gamedomain.GameOrder{},
|
orders: map[string]gamedomain.GameOrder{},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ func (r *Repository) GetDiceConfig(ctx context.Context, appCode string, gameID s
|
|||||||
gameID = normalizeDiceGameID(gameID)
|
gameID = normalizeDiceGameID(gameID)
|
||||||
query := `SELECT c.app_code, c.game_id, c.status, COALESCE(CAST(c.stake_options_json AS CHAR), '[]'),
|
query := `SELECT c.app_code, c.game_id, c.status, COALESCE(CAST(c.stake_options_json AS CHAR), '[]'),
|
||||||
c.fee_bps, c.pool_bps, c.min_players, c.max_players, c.robot_enabled,
|
c.fee_bps, c.pool_bps, c.min_players, c.max_players, c.robot_enabled,
|
||||||
c.robot_match_wait_ms, c.created_at_ms, c.updated_at_ms,
|
c.robot_match_wait_ms, c.robot_user_win_rate_percent, c.created_at_ms, c.updated_at_ms,
|
||||||
COALESCE(p.balance_coin, 0)
|
COALESCE(p.balance_coin, 0)
|
||||||
FROM game_self_game_configs c
|
FROM game_self_game_configs c
|
||||||
LEFT JOIN game_self_game_pools p ON p.app_code = c.app_code AND p.game_id = c.game_id
|
LEFT JOIN game_self_game_pools p ON p.app_code = c.app_code AND p.game_id = c.game_id
|
||||||
@ -31,13 +31,14 @@ func (r *Repository) GetDiceConfig(ctx context.Context, appCode string, gameID s
|
|||||||
if err := r.db.QueryRowContext(ctx, query, app, gameID).Scan(
|
if err := r.db.QueryRowContext(ctx, query, app, gameID).Scan(
|
||||||
&config.AppCode, &config.GameID, &config.Status, &optionsJSON,
|
&config.AppCode, &config.GameID, &config.Status, &optionsJSON,
|
||||||
&config.FeeBPS, &config.PoolBPS, &config.MinPlayers, &config.MaxPlayers, &config.RobotEnabled,
|
&config.FeeBPS, &config.PoolBPS, &config.MinPlayers, &config.MaxPlayers, &config.RobotEnabled,
|
||||||
&config.RobotMatchWaitMS, &config.CreatedAtMS, &config.UpdatedAtMS, &config.PoolBalanceCoin,
|
&config.RobotMatchWaitMS, &config.RobotUserWinRatePercent, &config.CreatedAtMS, &config.UpdatedAtMS, &config.PoolBalanceCoin,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return dicedomain.Config{}, xerr.New(xerr.NotFound, "dice config not found")
|
return dicedomain.Config{}, xerr.New(xerr.NotFound, "dice config not found")
|
||||||
}
|
}
|
||||||
return dicedomain.Config{}, err
|
return dicedomain.Config{}, err
|
||||||
}
|
}
|
||||||
|
config.RobotUserWinRatePercentSet = true
|
||||||
config.StakeOptions = dicedomain.ParseStakeOptions(optionsJSON)
|
config.StakeOptions = dicedomain.ParseStakeOptions(optionsJSON)
|
||||||
return dicedomain.NormalizeConfig(config)
|
return dicedomain.NormalizeConfig(config)
|
||||||
}
|
}
|
||||||
@ -48,7 +49,8 @@ func (r *Repository) ListSelfGameConfigs(ctx context.Context, appCode string) ([
|
|||||||
rows, err := r.db.QueryContext(ctx,
|
rows, err := r.db.QueryContext(ctx,
|
||||||
`SELECT c.app_code, c.game_id, c.status, COALESCE(CAST(c.stake_options_json AS CHAR), '[]'),
|
`SELECT c.app_code, c.game_id, c.status, COALESCE(CAST(c.stake_options_json AS CHAR), '[]'),
|
||||||
c.fee_bps, c.pool_bps, c.min_players, c.max_players, c.robot_enabled,
|
c.fee_bps, c.pool_bps, c.min_players, c.max_players, c.robot_enabled,
|
||||||
c.robot_match_wait_ms, c.created_at_ms, c.updated_at_ms, COALESCE(p.balance_coin, 0)
|
c.robot_match_wait_ms, c.robot_user_win_rate_percent,
|
||||||
|
c.created_at_ms, c.updated_at_ms, COALESCE(p.balance_coin, 0)
|
||||||
FROM game_self_game_configs c
|
FROM game_self_game_configs c
|
||||||
LEFT JOIN game_self_game_pools p ON p.app_code = c.app_code AND p.game_id = c.game_id
|
LEFT JOIN game_self_game_pools p ON p.app_code = c.app_code AND p.game_id = c.game_id
|
||||||
WHERE c.app_code = ?
|
WHERE c.app_code = ?
|
||||||
@ -67,10 +69,11 @@ func (r *Repository) ListSelfGameConfigs(ctx context.Context, appCode string) ([
|
|||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&config.AppCode, &config.GameID, &config.Status, &optionsJSON,
|
&config.AppCode, &config.GameID, &config.Status, &optionsJSON,
|
||||||
&config.FeeBPS, &config.PoolBPS, &config.MinPlayers, &config.MaxPlayers, &config.RobotEnabled,
|
&config.FeeBPS, &config.PoolBPS, &config.MinPlayers, &config.MaxPlayers, &config.RobotEnabled,
|
||||||
&config.RobotMatchWaitMS, &config.CreatedAtMS, &config.UpdatedAtMS, &config.PoolBalanceCoin,
|
&config.RobotMatchWaitMS, &config.RobotUserWinRatePercent, &config.CreatedAtMS, &config.UpdatedAtMS, &config.PoolBalanceCoin,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
config.RobotUserWinRatePercentSet = true
|
||||||
config.StakeOptions = dicedomain.ParseStakeOptions(optionsJSON)
|
config.StakeOptions = dicedomain.ParseStakeOptions(optionsJSON)
|
||||||
normalized, err := dicedomain.NormalizeConfig(config)
|
normalized, err := dicedomain.NormalizeConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -97,8 +100,8 @@ func (r *Repository) UpsertDiceConfig(ctx context.Context, config dicedomain.Con
|
|||||||
if _, err := tx.ExecContext(ctx,
|
if _, err := tx.ExecContext(ctx,
|
||||||
`INSERT INTO game_self_game_configs (
|
`INSERT INTO game_self_game_configs (
|
||||||
app_code, game_id, status, stake_options_json, fee_bps, pool_bps,
|
app_code, game_id, status, stake_options_json, fee_bps, pool_bps,
|
||||||
min_players, max_players, robot_enabled, robot_match_wait_ms, created_at_ms, updated_at_ms
|
min_players, max_players, robot_enabled, robot_match_wait_ms, robot_user_win_rate_percent, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
status = VALUES(status),
|
status = VALUES(status),
|
||||||
stake_options_json = VALUES(stake_options_json),
|
stake_options_json = VALUES(stake_options_json),
|
||||||
@ -108,10 +111,11 @@ func (r *Repository) UpsertDiceConfig(ctx context.Context, config dicedomain.Con
|
|||||||
max_players = VALUES(max_players),
|
max_players = VALUES(max_players),
|
||||||
robot_enabled = VALUES(robot_enabled),
|
robot_enabled = VALUES(robot_enabled),
|
||||||
robot_match_wait_ms = VALUES(robot_match_wait_ms),
|
robot_match_wait_ms = VALUES(robot_match_wait_ms),
|
||||||
|
robot_user_win_rate_percent = VALUES(robot_user_win_rate_percent),
|
||||||
updated_at_ms = VALUES(updated_at_ms)`,
|
updated_at_ms = VALUES(updated_at_ms)`,
|
||||||
normalized.AppCode, normalized.GameID, normalized.Status, dicedomain.StakeOptionsJSON(normalized.StakeOptions),
|
normalized.AppCode, normalized.GameID, normalized.Status, dicedomain.StakeOptionsJSON(normalized.StakeOptions),
|
||||||
normalized.FeeBPS, normalized.PoolBPS, normalized.MinPlayers, normalized.MaxPlayers, normalized.RobotEnabled,
|
normalized.FeeBPS, normalized.PoolBPS, normalized.MinPlayers, normalized.MaxPlayers, normalized.RobotEnabled,
|
||||||
normalized.RobotMatchWaitMS, nowMs, nowMs,
|
normalized.RobotMatchWaitMS, normalized.RobotUserWinRatePercent, nowMs, nowMs,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return dicedomain.Config{}, err
|
return dicedomain.Config{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -221,6 +221,46 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
KEY idx_game_explore_winners_time(app_code, won_at_ms, win_id),
|
KEY idx_game_explore_winners_time(app_code, won_at_ms, win_id),
|
||||||
KEY idx_game_explore_winners_game(app_code, game_code, won_at_ms)
|
KEY idx_game_explore_winners_game(app_code, game_code, won_at_ms)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS game_self_game_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL,
|
||||||
|
game_id VARCHAR(96) NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||||
|
stake_options_json JSON NULL,
|
||||||
|
fee_bps INT NOT NULL DEFAULT 500,
|
||||||
|
pool_bps INT NOT NULL DEFAULT 100,
|
||||||
|
min_players INT NOT NULL DEFAULT 2,
|
||||||
|
max_players INT NOT NULL DEFAULT 2,
|
||||||
|
robot_enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
robot_match_wait_ms BIGINT NOT NULL DEFAULT 3000,
|
||||||
|
robot_user_win_rate_percent INT NOT NULL DEFAULT 50,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY(app_code, game_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS game_self_game_pools (
|
||||||
|
app_code VARCHAR(32) NOT NULL,
|
||||||
|
game_id VARCHAR(96) NOT NULL,
|
||||||
|
balance_coin BIGINT NOT NULL DEFAULT 0,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY(app_code, game_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS game_self_game_pool_transactions (
|
||||||
|
app_code VARCHAR(32) NOT NULL,
|
||||||
|
adjustment_id VARCHAR(96) NOT NULL,
|
||||||
|
game_id VARCHAR(96) NOT NULL,
|
||||||
|
match_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||||
|
user_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
direction VARCHAR(16) NOT NULL,
|
||||||
|
amount_coin BIGINT NOT NULL,
|
||||||
|
reason VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
balance_after BIGINT NOT NULL,
|
||||||
|
created_by BIGINT NOT NULL DEFAULT 0,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY(app_code, adjustment_id),
|
||||||
|
KEY idx_game_pool_tx_game_time(app_code, game_id, created_at_ms),
|
||||||
|
KEY idx_game_pool_tx_match(app_code, match_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS game_self_game_robots (
|
`CREATE TABLE IF NOT EXISTS game_self_game_robots (
|
||||||
app_code VARCHAR(32) NOT NULL,
|
app_code VARCHAR(32) NOT NULL,
|
||||||
game_id VARCHAR(96) NOT NULL,
|
game_id VARCHAR(96) NOT NULL,
|
||||||
@ -347,6 +387,9 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
if err := r.ensureColumn(ctx, "game_self_game_robots", "used_count", "used_count BIGINT NOT NULL DEFAULT 0 AFTER last_used_at_ms"); err != nil {
|
if err := r.ensureColumn(ctx, "game_self_game_robots", "used_count", "used_count BIGINT NOT NULL DEFAULT 0 AFTER last_used_at_ms"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := r.ensureColumn(ctx, "game_self_game_configs", "robot_user_win_rate_percent", "robot_user_win_rate_percent INT NOT NULL DEFAULT 50 AFTER robot_match_wait_ms"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := r.ensureColumn(ctx, "game_outbox", "worker_id", "worker_id VARCHAR(128) NOT NULL DEFAULT '' AFTER status"); err != nil {
|
if err := r.ensureColumn(ctx, "game_outbox", "worker_id", "worker_id VARCHAR(128) NOT NULL DEFAULT '' AFTER status"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -485,7 +528,17 @@ func equalStringSlices(left []string, right []string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isBuiltinSelfGameID(gameID string) bool {
|
||||||
|
switch strings.TrimSpace(gameID) {
|
||||||
|
case "dice", "rock":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQuery) ([]gamedomain.AppGame, error) {
|
func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQuery) ([]gamedomain.AppGame, error) {
|
||||||
|
// dice/rock 需要保留 catalog 行给专用匹配入口做可玩性校验,但 App 普通游戏列表只展示三方 H5 游戏,避免自研入口混进运营列表后被误删。
|
||||||
rows, err := r.db.QueryContext(ctx,
|
rows, err := r.db.QueryContext(ctx,
|
||||||
`SELECT c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category,
|
`SELECT c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category,
|
||||||
c.launch_mode, c.orientation, c.safe_height, c.min_coin, c.status, p.status,
|
c.launch_mode, c.orientation, c.safe_height, c.min_coin, c.status, p.status,
|
||||||
@ -496,6 +549,7 @@ func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQ
|
|||||||
WHERE c.app_code = ?
|
WHERE c.app_code = ?
|
||||||
AND c.status IN ('active', 'maintenance')
|
AND c.status IN ('active', 'maintenance')
|
||||||
AND p.status IN ('active', 'maintenance')
|
AND p.status IN ('active', 'maintenance')
|
||||||
|
AND c.game_id NOT IN ('dice', 'rock')
|
||||||
AND r.scene = ?
|
AND r.scene = ?
|
||||||
AND r.enabled = 1
|
AND r.enabled = 1
|
||||||
AND r.visible = 1
|
AND r.visible = 1
|
||||||
@ -551,6 +605,7 @@ func (r *Repository) ListRecentGames(ctx context.Context, query gameservice.List
|
|||||||
JOIN game_display_rules r ON r.app_code = c.app_code AND r.game_id = c.game_id
|
JOIN game_display_rules r ON r.app_code = c.app_code AND r.game_id = c.game_id
|
||||||
WHERE c.status IN ('active', 'maintenance')
|
WHERE c.status IN ('active', 'maintenance')
|
||||||
AND p.status IN ('active', 'maintenance')
|
AND p.status IN ('active', 'maintenance')
|
||||||
|
AND c.game_id NOT IN ('dice', 'rock')
|
||||||
AND r.scene = ?
|
AND r.scene = ?
|
||||||
AND r.enabled = 1
|
AND r.enabled = 1
|
||||||
AND r.visible = 1
|
AND r.visible = 1
|
||||||
@ -1455,6 +1510,10 @@ func (r *Repository) DeleteCatalog(ctx context.Context, appCode string, gameID s
|
|||||||
if id == "" {
|
if id == "" {
|
||||||
return xerr.New(xerr.InvalidArgument, "game_id is required")
|
return xerr.New(xerr.InvalidArgument, "game_id is required")
|
||||||
}
|
}
|
||||||
|
if isBuiltinSelfGameID(id) {
|
||||||
|
// 内置自研游戏的 catalog 是专用 /games/dice、/games/rps 入口的运行时前置校验,不属于普通三方游戏目录,不能通过删除目录下架。
|
||||||
|
return xerr.New(xerr.InvalidArgument, "self game catalog cannot be deleted")
|
||||||
|
}
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -9,10 +9,137 @@ import (
|
|||||||
|
|
||||||
"hyapp/internal/testutil/mysqlschema"
|
"hyapp/internal/testutil/mysqlschema"
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
dicedomain "hyapp/services/game-service/internal/domain/dice"
|
dicedomain "hyapp/services/game-service/internal/domain/dice"
|
||||||
gamedomain "hyapp/services/game-service/internal/domain/game"
|
gamedomain "hyapp/services/game-service/internal/domain/game"
|
||||||
|
gameservice "hyapp/services/game-service/internal/service/game"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestDiceConfigPersistsRobotUserWinRatePercent(t *testing.T) {
|
||||||
|
caller := mysqlschema.CallerFile(t, 1)
|
||||||
|
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||||
|
InitDBPath: mysqlschema.InitDBPath(t, caller, "../../../deploy/mysql/initdb/001_game_service.sql"),
|
||||||
|
DatabasePrefix: "hy_game_test",
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||||
|
repo, err := Open(ctx, schema.DSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open repository failed: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = repo.Close() })
|
||||||
|
|
||||||
|
config := dicedomain.Config{
|
||||||
|
AppCode: "lalu",
|
||||||
|
GameID: dicedomain.DefaultGameID,
|
||||||
|
Status: dicedomain.ConfigStatusActive,
|
||||||
|
StakeOptions: dicedomain.NormalizeStakeOptions(nil),
|
||||||
|
FeeBPS: int32(dicedomain.DefaultFeeBPS),
|
||||||
|
PoolBPS: int32(dicedomain.DefaultPoolBPS),
|
||||||
|
MinPlayers: dicedomain.DefaultMinPlayers,
|
||||||
|
MaxPlayers: dicedomain.DefaultMaxPlayers,
|
||||||
|
RobotEnabled: true,
|
||||||
|
RobotMatchWaitMS: dicedomain.DefaultRobotWaitMS,
|
||||||
|
RobotUserWinRatePercent: 70,
|
||||||
|
RobotUserWinRatePercentSet: true,
|
||||||
|
}
|
||||||
|
saved, err := repo.UpsertDiceConfig(ctx, config, 1700000000000)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("upsert dice config failed: %v", err)
|
||||||
|
}
|
||||||
|
if saved.RobotUserWinRatePercent != 70 {
|
||||||
|
t.Fatalf("saved robot win rate = %d, want 70", saved.RobotUserWinRatePercent)
|
||||||
|
}
|
||||||
|
fetched, err := repo.GetDiceConfig(ctx, "lalu", dicedomain.DefaultGameID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get dice config failed: %v", err)
|
||||||
|
}
|
||||||
|
if fetched.RobotUserWinRatePercent != 70 {
|
||||||
|
t.Fatalf("fetched robot win rate = %d, want 70", fetched.RobotUserWinRatePercent)
|
||||||
|
}
|
||||||
|
items, err := repo.ListSelfGameConfigs(ctx, "lalu")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list self game configs failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(items) != 1 || items[0].RobotUserWinRatePercent != 70 {
|
||||||
|
t.Fatalf("listed robot win rate mismatch: %+v", items)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.RobotUserWinRatePercent = 0
|
||||||
|
saved, err = repo.UpsertDiceConfig(ctx, config, 1700000001000)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("upsert zero robot win rate failed: %v", err)
|
||||||
|
}
|
||||||
|
if saved.RobotUserWinRatePercent != 0 {
|
||||||
|
t.Fatalf("explicit zero robot win rate should be persisted, got %d", saved.RobotUserWinRatePercent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppGameListsExcludeBuiltInSelfGames(t *testing.T) {
|
||||||
|
caller := mysqlschema.CallerFile(t, 1)
|
||||||
|
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||||
|
InitDBPath: mysqlschema.InitDBPath(t, caller, "../../../deploy/mysql/initdb/001_game_service.sql"),
|
||||||
|
DatabasePrefix: "hy_game_test",
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||||
|
repo, err := Open(ctx, schema.DSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open repository failed: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = repo.Close() })
|
||||||
|
|
||||||
|
nowMS := int64(1781200000000)
|
||||||
|
for _, platform := range []gamedomain.Platform{
|
||||||
|
{AppCode: "lalu", PlatformCode: "dice", PlatformName: "Dice Internal", Status: gamedomain.StatusActive, AdapterType: "dice_internal", CreatedAtMS: nowMS, UpdatedAtMS: nowMS},
|
||||||
|
{AppCode: "lalu", PlatformCode: "demo", PlatformName: "Demo", Status: gamedomain.StatusActive, AdapterType: gamedomain.AdapterDemo, CreatedAtMS: nowMS, UpdatedAtMS: nowMS},
|
||||||
|
} {
|
||||||
|
if _, err := repo.UpsertPlatform(ctx, platform); err != nil {
|
||||||
|
t.Fatalf("upsert platform %s failed: %v", platform.PlatformCode, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, item := range []gamedomain.CatalogItem{
|
||||||
|
{AppCode: "lalu", GameID: dicedomain.DefaultGameID, PlatformCode: "dice", ProviderGameID: "dice", GameName: "Dice", Category: "self", LaunchMode: gamedomain.LaunchModeH5Popup, Orientation: "portrait", Status: gamedomain.StatusActive, SortOrder: 1},
|
||||||
|
{AppCode: "lalu", GameID: dicedomain.SelfGameIDRock, PlatformCode: "dice", ProviderGameID: "rock", GameName: "Rock Paper Scissors", Category: "self", LaunchMode: gamedomain.LaunchModeH5Popup, Orientation: "portrait", Status: gamedomain.StatusActive, SortOrder: 2},
|
||||||
|
{AppCode: "lalu", GameID: "demo_slot_001", PlatformCode: "demo", ProviderGameID: "slot_001", GameName: "Demo Slot", Category: "slot", LaunchMode: gamedomain.LaunchModeH5Popup, Orientation: "portrait", Status: gamedomain.StatusActive, SortOrder: 3},
|
||||||
|
} {
|
||||||
|
if _, err := repo.UpsertCatalog(ctx, item); err != nil {
|
||||||
|
t.Fatalf("upsert catalog %s failed: %v", item.GameID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, session := range []gamedomain.LaunchSession{
|
||||||
|
{AppCode: "lalu", SessionID: "sess_dice", UserID: 42, Scene: gamedomain.SceneVoiceRoom, PlatformCode: "dice", GameID: dicedomain.DefaultGameID, ProviderGameID: "dice", LaunchTokenHash: "hash_dice", Status: gamedomain.SessionActive, ExpiresAtMS: nowMS + 60000, CreatedAtMS: nowMS + 10, UpdatedAtMS: nowMS + 10},
|
||||||
|
{AppCode: "lalu", SessionID: "sess_rock", UserID: 42, Scene: gamedomain.SceneVoiceRoom, PlatformCode: "dice", GameID: dicedomain.SelfGameIDRock, ProviderGameID: "rock", LaunchTokenHash: "hash_rock", Status: gamedomain.SessionActive, ExpiresAtMS: nowMS + 60000, CreatedAtMS: nowMS + 20, UpdatedAtMS: nowMS + 20},
|
||||||
|
{AppCode: "lalu", SessionID: "sess_demo", UserID: 42, Scene: gamedomain.SceneVoiceRoom, PlatformCode: "demo", GameID: "demo_slot_001", ProviderGameID: "slot_001", LaunchTokenHash: "hash_demo", Status: gamedomain.SessionActive, ExpiresAtMS: nowMS + 60000, CreatedAtMS: nowMS + 30, UpdatedAtMS: nowMS + 30},
|
||||||
|
} {
|
||||||
|
if err := repo.CreateLaunchSession(ctx, session); err != nil {
|
||||||
|
t.Fatalf("create launch session %s failed: %v", session.SessionID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := gameservice.ListGamesQuery{AppCode: "lalu", UserID: 42, Scene: gamedomain.SceneVoiceRoom}
|
||||||
|
games, err := repo.ListGames(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list games failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(games) != 1 || games[0].GameID != "demo_slot_001" {
|
||||||
|
t.Fatalf("app game list should hide built-in self games, got %+v", games)
|
||||||
|
}
|
||||||
|
recent, err := repo.ListRecentGames(ctx, gameservice.ListRecentGamesQuery{AppCode: "lalu", UserID: 42, Scene: gamedomain.SceneVoiceRoom, PageSize: 10})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list recent games failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(recent) != 1 || recent[0].GameID != "demo_slot_001" {
|
||||||
|
t.Fatalf("recent game list should hide built-in self games, got %+v", recent)
|
||||||
|
}
|
||||||
|
if _, err := repo.GetLaunchableGame(ctx, "lalu", dicedomain.DefaultGameID); err != nil {
|
||||||
|
t.Fatalf("dice catalog must remain launchable for dedicated endpoints: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := repo.GetLaunchableGame(ctx, "lalu", dicedomain.SelfGameIDRock); err != nil {
|
||||||
|
t.Fatalf("rock catalog must remain launchable for dedicated endpoints: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarkOrderSucceededCreatesClaimableLevelOutboxEvent(t *testing.T) {
|
func TestMarkOrderSucceededCreatesClaimableLevelOutboxEvent(t *testing.T) {
|
||||||
caller := mysqlschema.CallerFile(t, 1)
|
caller := mysqlschema.CallerFile(t, 1)
|
||||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||||
@ -545,3 +672,25 @@ func TestDeleteCatalogRemovesGameAndDisplayRule(t *testing.T) {
|
|||||||
t.Fatalf("game_display_rules count = %d, want 0", ruleCount)
|
t.Fatalf("game_display_rules count = %d, want 0", ruleCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteCatalogRejectsBuiltInSelfGames(t *testing.T) {
|
||||||
|
caller := mysqlschema.CallerFile(t, 1)
|
||||||
|
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||||
|
InitDBPath: mysqlschema.InitDBPath(t, caller, "../../../deploy/mysql/initdb/001_game_service.sql"),
|
||||||
|
DatabasePrefix: "hy_game_test",
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||||
|
repo, err := Open(ctx, schema.DSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open repository failed: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = repo.Close() })
|
||||||
|
|
||||||
|
for _, gameID := range []string{dicedomain.DefaultGameID, dicedomain.SelfGameIDRock} {
|
||||||
|
err := repo.DeleteCatalog(ctx, "lalu", gameID)
|
||||||
|
if !xerr.IsCode(err, xerr.InvalidArgument) {
|
||||||
|
t.Fatalf("delete built-in self game %s should be rejected, got %v", gameID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -755,20 +755,22 @@ func diceConfigToProto(config dicedomain.Config) *gamev1.DiceConfig {
|
|||||||
for _, option := range config.StakeOptions {
|
for _, option := range config.StakeOptions {
|
||||||
options = append(options, &gamev1.DiceStakeOption{StakeCoin: option.StakeCoin, Enabled: option.Enabled, SortOrder: option.SortOrder})
|
options = append(options, &gamev1.DiceStakeOption{StakeCoin: option.StakeCoin, Enabled: option.Enabled, SortOrder: option.SortOrder})
|
||||||
}
|
}
|
||||||
|
robotUserWinRatePercent := config.RobotUserWinRatePercent
|
||||||
return &gamev1.DiceConfig{
|
return &gamev1.DiceConfig{
|
||||||
AppCode: config.AppCode,
|
AppCode: config.AppCode,
|
||||||
GameId: config.GameID,
|
GameId: config.GameID,
|
||||||
Status: config.Status,
|
Status: config.Status,
|
||||||
StakeOptions: options,
|
StakeOptions: options,
|
||||||
FeeBps: config.FeeBPS,
|
FeeBps: config.FeeBPS,
|
||||||
PoolBps: config.PoolBPS,
|
PoolBps: config.PoolBPS,
|
||||||
MinPlayers: config.MinPlayers,
|
MinPlayers: config.MinPlayers,
|
||||||
MaxPlayers: config.MaxPlayers,
|
MaxPlayers: config.MaxPlayers,
|
||||||
RobotEnabled: config.RobotEnabled,
|
RobotEnabled: config.RobotEnabled,
|
||||||
RobotMatchWaitMs: config.RobotMatchWaitMS,
|
RobotMatchWaitMs: config.RobotMatchWaitMS,
|
||||||
PoolBalanceCoin: config.PoolBalanceCoin,
|
PoolBalanceCoin: config.PoolBalanceCoin,
|
||||||
CreatedAtMs: config.CreatedAtMS,
|
CreatedAtMs: config.CreatedAtMS,
|
||||||
UpdatedAtMs: config.UpdatedAtMS,
|
UpdatedAtMs: config.UpdatedAtMS,
|
||||||
|
RobotUserWinRatePercent: &robotUserWinRatePercent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -781,16 +783,18 @@ func diceConfigFromProto(config *gamev1.DiceConfig) dicedomain.Config {
|
|||||||
options = append(options, dicedomain.StakeOption{StakeCoin: option.GetStakeCoin(), Enabled: option.GetEnabled(), SortOrder: option.GetSortOrder()})
|
options = append(options, dicedomain.StakeOption{StakeCoin: option.GetStakeCoin(), Enabled: option.GetEnabled(), SortOrder: option.GetSortOrder()})
|
||||||
}
|
}
|
||||||
return dicedomain.Config{
|
return dicedomain.Config{
|
||||||
AppCode: config.GetAppCode(),
|
AppCode: config.GetAppCode(),
|
||||||
GameID: config.GetGameId(),
|
GameID: config.GetGameId(),
|
||||||
Status: config.GetStatus(),
|
Status: config.GetStatus(),
|
||||||
StakeOptions: options,
|
StakeOptions: options,
|
||||||
FeeBPS: config.GetFeeBps(),
|
FeeBPS: config.GetFeeBps(),
|
||||||
PoolBPS: config.GetPoolBps(),
|
PoolBPS: config.GetPoolBps(),
|
||||||
MinPlayers: config.GetMinPlayers(),
|
MinPlayers: config.GetMinPlayers(),
|
||||||
MaxPlayers: config.GetMaxPlayers(),
|
MaxPlayers: config.GetMaxPlayers(),
|
||||||
RobotEnabled: config.GetRobotEnabled(),
|
RobotEnabled: config.GetRobotEnabled(),
|
||||||
RobotMatchWaitMS: config.GetRobotMatchWaitMs(),
|
RobotMatchWaitMS: config.GetRobotMatchWaitMs(),
|
||||||
|
RobotUserWinRatePercent: config.GetRobotUserWinRatePercent(),
|
||||||
|
RobotUserWinRatePercentSet: config.RobotUserWinRatePercent != nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user