26 KiB
自研游戏机器人策略与档位奖池技术方案
本文只描述《自研游戏机器人策略与档位奖池产品文档》里功能设计的技术实现。产品目标、功能原因和运营策略以产品文档为准。
技术结论
- 策略、奖池、结算和
forced_result仍在game-service内实现。 robot-service只负责机器人账号池和机器人抽取,不参与胜负策略。- 奖池从
app_code + game_id升级为app_code + game_id + stake_coin。 - 档位奖池必须支持余额、冻结额度、健康状态和黑色熔断。
- 机器人策略引擎只输出决策,不直接改钱包,不直接改数据库余额。
- 新用户保护按有效机器人局计数,平局不计数、不消耗补贴额度。
- 结算仍复用现有骰子点数和 RPS 手势反推机制。
- 所有策略决策、奖池变更和保护状态变更都必须幂等。
功能一:档位独立奖池技术实现
数据表
新增 game_self_game_stake_pools。
CREATE TABLE IF NOT EXISTS game_self_game_stake_pools (
app_code VARCHAR(32) NOT NULL,
game_id VARCHAR(64) NOT NULL,
stake_coin BIGINT NOT NULL,
balance_coin BIGINT NOT NULL DEFAULT 0,
reserved_coin BIGINT NOT NULL DEFAULT 0,
target_balance_coin BIGINT NOT NULL DEFAULT 0,
safe_balance_coin BIGINT NOT NULL DEFAULT 0,
warning_balance_coin BIGINT NOT NULL DEFAULT 0,
danger_balance_coin BIGINT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL DEFAULT 'active',
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, game_id, stake_coin)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='自研游戏档位奖池';
新增 game_self_game_stake_pool_transactions。
CREATE TABLE IF NOT EXISTS game_self_game_stake_pool_transactions (
app_code VARCHAR(32) NOT NULL,
adjustment_id VARCHAR(96) NOT NULL,
game_id VARCHAR(64) NOT NULL,
stake_coin BIGINT 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,
reserved_delta_coin BIGINT NOT NULL DEFAULT 0,
reason VARCHAR(64) NOT NULL,
balance_after BIGINT NOT NULL,
reserved_after BIGINT NOT NULL DEFAULT 0,
created_by BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, adjustment_id),
KEY idx_self_game_stake_pool_tx_pool_time(app_code, game_id, stake_coin, created_at_ms),
KEY idx_self_game_stake_pool_tx_match(app_code, match_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='自研游戏档位奖池流水';
仓储能力
新增 repository 方法:
GetStakePool(ctx, appCode, gameID string, stakeCoin int64) (StakePool, error)
ListStakePools(ctx, appCode, gameID string) ([]StakePool, error)
UpsertStakePoolConfig(ctx, pool StakePool, nowMS int64) (StakePool, error)
ReserveStakePool(ctx, reservation StakePoolReservation) (StakePoolReservation, error)
ReleaseStakePoolReservation(ctx, reservation StakePoolReservation) (StakePoolReservation, error)
AdjustStakePool(ctx, adjustment StakePoolAdjustment) (StakePoolAdjustment, error)
实现要求:
AdjustStakePool必须在事务内SELECT ... FOR UPDATE锁定单个档位奖池。ReserveStakePool必须在机器人入座前冻结本局最大可能支出,防止并发局同时判断余额足够后扣穿奖池。ReleaseStakePoolReservation必须在结算完成、取消或失败补偿时释放冻结额度。direction=out时余额不能扣成负数。- 同一个
adjustment_id不能重复写流水。 - 同一个
reservation_id不能重复冻结或重复释放。 - 对局结算写奖池流水后,要同步更新 match 的
pool_delta_coin。
兜底奖池账务评估
机器人局账务需要从现有 fee_bps + pool_bps 口径中单独评估。
目标口径:
- 用户赢机器人:用户收到派奖,平台扣点从派奖侧体现,兜底奖池承担用户净赢额。
- 用户输机器人:用户扣除积分进入当前档位兜底奖池,机器人赢局不再从兜底奖池额外抽平台扣点。
- 平局:不改用户积分,不改奖池,不计有效机器人局。
- 成熟期机器人局回到 50% 基础体感时,兜底奖池长期期望接近不流失。
技术评估项:
- 当前
SettleByHighestScoreWithPool和SettleRPSByGestureWithPool是否把机器人赢局也按fee_bps/pool_bps扣点。 - 当前
dicePoolDelta是否需要按match_mode=robot分出新账务公式。 game_orders中平台收入、用户派奖和奖池变化是否能对账。- 历史局继续按旧口径解释,新策略上线后只影响新机器人局。
兼容迁移
旧 game_self_game_pools 不删除。上线迁移时新增档位奖池:
- 如果旧游戏奖池有余额,由运营按档位分配。
- 如果没有明确分配规则,默认只建档位奖池,余额为 0。
- 旧奖池只保留查询和回滚,不再作为新逻辑写入目标。
功能二:新用户独立保护技术实现
数据表
新增 game_self_game_new_user_policies。
CREATE TABLE IF NOT EXISTS game_self_game_new_user_policies (
app_code VARCHAR(32) NOT NULL,
game_id VARCHAR(64) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
protection_rounds INT NOT NULL DEFAULT 30,
protection_hours INT NOT NULL DEFAULT 168,
max_stake_coin BIGINT NOT NULL DEFAULT 5000,
lifetime_subsidy_quota_coin BIGINT NOT NULL DEFAULT 30000,
day1_subsidy_quota_coin BIGINT NOT NULL DEFAULT 15000,
single_round_subsidy_cap_coin BIGINT NOT NULL DEFAULT 5000,
strategy_level VARCHAR(32) NOT NULL DEFAULT 'soft_boost',
lose_streak_protection_enabled TINYINT(1) NOT NULL DEFAULT 1,
lose_streak_trigger INT NOT NULL DEFAULT 2,
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 COMMENT='自研游戏新用户保护策略';
新增 game_self_game_user_protection_states。
CREATE TABLE IF NOT EXISTS game_self_game_user_protection_states (
app_code VARCHAR(32) NOT NULL,
game_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
used_rounds INT NOT NULL DEFAULT 0,
lifetime_subsidy_coin BIGINT NOT NULL DEFAULT 0,
day1_subsidy_coin BIGINT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL DEFAULT 'active',
first_applied_at_ms BIGINT NOT NULL DEFAULT 0,
last_applied_at_ms BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, game_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='自研游戏用户新手保护状态';
新增保护消费幂等表。
CREATE TABLE IF NOT EXISTS game_self_game_user_protection_events (
app_code VARCHAR(32) NOT NULL,
game_id VARCHAR(64) NOT NULL,
match_id VARCHAR(96) NOT NULL,
user_id BIGINT NOT NULL,
used_round_delta INT NOT NULL DEFAULT 0,
lifetime_subsidy_coin_delta BIGINT NOT NULL DEFAULT 0,
day1_subsidy_coin_delta BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, game_id, match_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='自研游戏新手保护消耗幂等事件';
用户阶段判断
新增 UserStageResolver:
type UserStageResolver interface {
ResolveSelfGameUserStage(ctx context.Context, appCode, gameID string, userID int64, nowMS int64) (UserStage, error)
}
第一阶段数据来源:
- user-service 返回注册时间。
- game-service 自己统计该用户自研游戏有效机器人局结算次数。
- protection state 返回已使用保护次数、生命周期补贴金额和首日补贴金额。
有效机器人局计数
有效机器人局必须满足:
match_mode=robot。- 对局已完成结算。
- 对局结果不是平局。
- 钱包和奖池账务已经成功。
不计入有效机器人局:
- 真人局。
- 平局。
- 取消局。
- 结算失败局。
- 钱包扣款或派奖失败局。
实现要求:
ConsumeProtectionIfNeeded只在有效机器人局后运行。- 平局不插入
game_self_game_user_protection_events。 - 平局不增加
used_rounds。 - 平局不增加补贴消耗。
状态更新时机
新用户保护状态只在机器人局结算成功后更新。
不更新:
- 匹配取消。
- 平局。
- 钱包扣款失败。
- 钱包派奖失败。
- 对局失败。
实现:
- 在
RollMatch完成钱包和奖池更新后调用ConsumeProtectionIfNeeded。 ConsumeProtectionIfNeeded先插入game_self_game_user_protection_events。- 插入成功后再更新
game_self_game_user_protection_states。 - 如果事件已存在,直接返回,避免重复消费保护次数。
- 剩余生命周期额度、首日额度或单局额度不足时,策略引擎必须返回
none决策并降级到正常随机,不能继续输出新用户保护决策。
功能三:成熟用户自然期目标胜率技术实现
数据表
不新增成熟用户消耗配置表。自然期目标胜率复用 game_self_game_new_user_policies 作为同一套机器人补位体验策略配置。
新增字段:
normal_phase_target_win_rate_percent- 默认值:53
- 范围:50-100
- 50:完全自然随机
- 大于 50:只补正用户赢,不输出用户强制输
线上如果已经存在旧 game_self_game_consume_policies,只保留数据库对象,不做读取、不做写入、不在 admin 暴露。
用户统计快照
保留查询:
GetSelfGameUserStats(ctx, appCode, gameID string, userID int64, nowMS int64) (UserGameStats, error)
字段:
- 今日机器人局数。
- 今日净盈亏。
- 近 7 日净盈亏。
- 连赢次数。
- 连输次数。
- 今日新手保护干预次数。
- 最近一次策略干预时间。
统计快照用于新手连输保护、日志解释和后续看板;成熟用户阶段不使用这些统计去强制用户输。
策略输出
成熟用户策略只输出:
nonePOOL_RED_NORMAL_RANDOMNORMAL_PHASE_TARGET_BASELINENORMAL_PHASE_TARGET_MISSNORMAL_PHASE_TARGET_HIT
不输出 force_human_lose。
当配置值大于 50 且随机命中目标补正时,输出 force_human_win,但 forced_result 使用不消耗新手保护额度的内部值,结算后不写入新手保护消耗事件。
换算规则:
- dice 无平局:补正概率 =
2 * target - 100。例如目标 53%,补正概率约 6%。 - rock 有平局且平局不计有效机器人局:补正概率 =
(2 * target - 100) / (200 - target)。例如目标 53%,补正概率约 4.1%。 - 红色水位仍回到完全自然随机。
- 黑色水位仍不抽机器人。
说明:
- dice 继续按自然随机点数结算。
- rock 继续按自然随机机器人手势结算,平局退款。
- 红色水位只取消保护加成,不改写成用户必输。
- 黑色水位在奖池保护层熔断机器人补位。
功能四:奖池水位保护技术实现
水位判断
新增:
type PoolLevel string
const (
PoolLevelGreen PoolLevel = "green"
PoolLevelYellow PoolLevel = "yellow"
PoolLevelRed PoolLevel = "red"
PoolLevelBlack PoolLevel = "black"
)
判断规则:
available_coin = balance_coin - reserved_coin
available_coin < required_pool_coin -> black
balance_coin <= danger_balance_coin -> red
balance_coin <= warning_balance_coin -> yellow
balance_coin >= safe_balance_coin -> green
其它 -> yellow
策略行为
black:暂停该档机器人匹配,或只允许真人匹配。red:回到 50% 基础体感,不能输出新用户保护加成。yellow:按奖池健康度缩小保护强度,新用户强正反馈降级为轻度正反馈。green:允许执行配置策略。
资金校验
真人赢之前必须计算 required_pool_coin:
required_pool_coin = payout_coin - human_stake_coin
如果当前档位奖池余额小于 required_pool_coin,直接进入 pool_guard,输出 none 并熔断机器人补位。
黑色熔断
黑色状态处理:
- 当前档位不再触发机器人补位。
- 已经冻结的机器人局继续按已写入策略完成结算。
- 新进入的用户只等待真人匹配,或提示当前档位匹配繁忙。
- 后台必须展示黑色状态和触发原因。
实现要求:
attachRobotIfDue调用策略引擎前先判断当前档位是否黑色。- 黑色时不调用
robot-service抽机器人,避免抽到机器人后又无法入座。 - 黑色状态恢复到红色以上后,才允许重新开启该档机器人补位。
功能五:风控准入技术实现
数据表
新增 game_self_game_risk_rules。
CREATE TABLE IF NOT EXISTS game_self_game_risk_rules (
app_code VARCHAR(32) NOT NULL,
game_id VARCHAR(64) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
device_user_limit INT NOT NULL DEFAULT 3,
ip_user_limit INT NOT NULL DEFAULT 5,
protection_profit_limit_coin BIGINT NOT NULL DEFAULT 0,
high_stake_after_protection_limit_ms BIGINT NOT NULL DEFAULT 3600000,
full_protection_max_score INT NOT NULL DEFAULT 29,
reduced_protection_max_score INT NOT NULL DEFAULT 49,
weak_protection_max_score INT NOT NULL DEFAULT 69,
disable_protection_min_score INT NOT NULL DEFAULT 70,
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 COMMENT='自研游戏机器人策略风控规则';
风控接口
新增内部接口:
type RiskEvaluator interface {
EvaluateSelfGameRobotRisk(ctx context.Context, input RiskInput) (RiskResult, error)
}
第一阶段本地判断:
- protection state 中补贴收益是否超限。
- strategy logs 中保护命中是否异常。
- 用户是否保护期低档获利后快速进入高档。
第二阶段接 user-service 或风控服务:
- 同设备账号数。
- 同 IP 账号数。
- 黑名单。
风控结果
type RiskResult struct {
RiskScore int32
RiskTier string
BlockProtection bool
BlockReason string
ReasonJSON string
}
只阻断保护,不阻断正常游戏。
风控分层
0-29 -> normal,完整保护
30-49 -> reduced,降低保护强度,只保留低档位保护
50-69 -> weak,只保留 1000 档弱保护,关闭连输保护
70+ -> blocked,关闭全部新用户保护
实现要求:
- 风控分层结果写入策略日志
reason_json。 - 新用户保护策略根据
risk_tier降级,而不是只看BlockProtection。 - 高风险只关闭保护,不阻止用户正常进入游戏。
功能六:全量策略日志技术实现
数据表
新增 game_self_game_strategy_logs。
CREATE TABLE IF NOT EXISTS game_self_game_strategy_logs (
app_code VARCHAR(32) NOT NULL,
log_id VARCHAR(96) NOT NULL,
game_id VARCHAR(64) NOT NULL,
stake_coin BIGINT NOT NULL,
match_id VARCHAR(96) NOT NULL,
user_id BIGINT NOT NULL,
strategy_code VARCHAR(64) NOT NULL,
decision VARCHAR(64) NOT NULL,
force_result VARCHAR(32) NOT NULL DEFAULT '',
reason_code VARCHAR(64) NOT NULL,
reason_json JSON NULL,
pool_balance_coin BIGINT NOT NULL DEFAULT 0,
pool_level VARCHAR(32) NOT NULL DEFAULT '',
created_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, log_id),
UNIQUE KEY uk_self_game_strategy_match_user(app_code, match_id, user_id),
KEY idx_self_game_strategy_user_time(app_code, user_id, created_at_ms),
KEY idx_self_game_strategy_game_time(app_code, game_id, stake_coin, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='自研游戏机器人策略决策日志';
写入时机
在机器人入座前写策略日志。
原因:
- 机器人入座时已经确定
forced_result。 - 后续结算失败也能查到当时为什么做这个决策。
日志内容
reason_json 必须包含:
user_stagerisk_resultrisk_scorerisk_tierpool_levelpool_balance_coinpool_reserved_coinrequired_pool_coineffective_bot_game_countlifetime_subsidy_leftday1_subsidy_leftnew_user_policy_snapshotuser_stats_snapshot
策略引擎
输入
type RobotStrategyInput struct {
AppCode string
GameID string
MatchID string
UserID int64
StakeCoin int64
NowMS int64
UserStage UserStage
EffectiveBotGameCount int32
RecentStats UserGameStats
StakePool StakePoolSnapshot
NewUserPolicy NewUserPolicy
RiskResult RiskResult
}
输出
type RobotStrategyDecision struct {
StrategyCode string
Decision string
ReasonCode string
ReasonJSON string
ForceResult string
RequiredPoolCoin int64
ReservationID string
ShouldConsumeProtection bool
}
决策顺序
pool_guard
-> risk_block
-> new_user_protection
-> new_user_lose_streak_protection
-> normal_random
实现要求:
- 每个分支都返回明确
strategy_code和reason_code。 - 每个分支都必须可单测。
50%、70%这类绝对胜率不再作为主配置。- 平局必须返回不消耗有效机器人局和不消耗补贴额度的原因码。
- 当补贴额度不足时,必须返回
NO_USER_QUOTA类原因码并降级为正常随机。 - 新手连输保护的
LoseStreak必须来自同一自研游戏下真人局和机器人局的已结算非平局结果;平局不打断也不累计。 - 新手连输保护命中后只在机器人补位局输出
force_human_win,不能回写或改写真人对战结果。
现有链路改造点
attachRobotIfDue
旧链路在机器人入座时直接判断奖池、新用户首胜和固定胜负控制。改造后:
读取 match
读取 config
读取 stake pool
判断黑色熔断
抽机器人
读取 user stage
读取 risk result
读取 new user policy
读取 consume policy
调用 strategy engine
冻结 stake pool reservation
写 strategy log
按 decision 设置 forced_result
JoinDiceRobot
RollMatch
当前结算后调用奖池更新。改造后:
按现有逻辑扣款、结算、派奖
按 stake_coin 更新档位奖池
释放 stake pool reservation
如果策略命中新用户保护,结算成功后消费保护状态
MarkDiceMatchSettled
失败处理:
- 结算失败时释放冻结额度。
- 钱包扣款失败时释放冻结额度。
- 用户取消或匹配过期时释放冻结额度。
- 释放失败时写补偿任务或人工对账队列。
dicePoolDelta
输入不变,输出增加 stake_coin。
func dicePoolDelta(match dicedomain.Match) (direction string, amount int64, userID int64, stakeCoin int64)
原因:奖池流水必须写入具体档位。
Admin 后台技术实现
接口
档位奖池列表
- 地址:
GET /admin/self-games/:game_id/stake-pools - 参数:
game_id
- 返回值:
items[].stakeCoinitems[].balanceCoinitems[].reservedCoinitems[].availableCoinitems[].targetBalanceCoinitems[].safeBalanceCoinitems[].warningBalanceCoinitems[].dangerBalanceCoinitems[].poolLevelitems[].recentInCoinitems[].recentOutCoinitems[].recentNetCoinitems[].humanWinCapacityitems[].botMatchEnabled
- 相关 IM:无
档位奖池配置
- 地址:
PUT /admin/self-games/:game_id/stake-pools/:stake_coin - 参数:
targetBalanceCoinsafeBalanceCoinwarningBalanceCoindangerBalanceCoinstatus
- 返回值:更新后的档位奖池
- 相关 IM:无
档位奖池加扣
- 地址:
POST /admin/self-games/:game_id/stake-pools/:stake_coin/adjust - 参数:
directionamountCoinreason
- 返回值:
adjustmentIdbalanceAfterreservedAfter
- 相关 IM:无
新用户保护配置
-
地址:
GET /v1/admin/game/self-games/:game_id/new-user-policy -
参数:无
-
返回值:当前配置
-
相关 IM:无
-
地址:
PATCH /v1/admin/game/self-games/:game_id/new-user-policy -
参数:
enabledprotectionRoundsprotectionHoursmaxStakeCoinlifetimeSubsidyQuotaCoinday1SubsidyQuotaCoinsingleRoundSubsidyCapCoinstrategyLevelloseStreakProtectionEnabledloseStreakTrigger
-
返回值:更新后的配置
-
相关 IM:无
风控规则配置
-
地址:
GET /admin/self-games/:game_id/risk-rules -
参数:无
-
返回值:当前配置
-
相关 IM:无
-
地址:
PUT /admin/self-games/:game_id/risk-rules -
参数:
enableddeviceUserLimitipUserLimitprotectionProfitLimitCoinhighStakeAfterProtectionLimitMsfullProtectionMaxScorereducedProtectionMaxScoreweakProtectionMaxScoredisableProtectionMinScore
-
返回值:更新后的配置
-
相关 IM:无
策略日志查询
- 地址:
GET /admin/self-games/strategy-logs - 参数:
gameIdstakeCoinuserIdmatchIdstrategyCodedecisionriskTierpoolLevelstartTimeMsendTimeMspageSizecursor
- 返回值:
itemsnextCursor
- 相关 IM:无
前端页面
后台前端按产品文档拆成 6 个页签:
- 基础配置。
- 档位奖池。
- 新用户保护。
- 成熟用户观察。
- 风控规则。
- 策略日志。
保存配置时校验:
- 水位阈值必须满足
danger(red) <= warning(yellow) <= safe(green)。 targetBalanceCoin必须大于等于safeBalanceCoin,否则健康度比例没有参考值。- 新用户最大保护档位必须存在于下注档位。
- 生命周期补贴额度、首日补贴额度、单局保护上限不能为负数。
- 单局保护上限不能大于新用户最大保护档位。
- 风险评分阈值必须满足
fullProtectionMaxScore < reducedProtectionMaxScore < weakProtectionMaxScore < disableProtectionMinScore。 - 金额不能为负数。
灰度步骤
第一阶段:shadow 日志
- 建表。
- 接入策略引擎。
- 只计算决策,不执行
forced_result。 - 全量写策略日志。
验证:
- 日志覆盖所有机器人局。
- 策略命中分布符合预期。
第二阶段:档位奖池切换
- 新结算只写
game_self_game_stake_pools。 - 旧
game_self_game_pools不再写入。 - 后台奖池加扣只作用于档位奖池。
验证:
- 每档余额等于初始余额加流水净额。
- 并发扣减不会出现负数。
- 黑色状态时不再抽机器人补位。
- 冻结额度能在结算、取消和失败补偿后释放。
第三阶段:新用户保护灰度
- 只开低档位。
- 只开部分 App 或部分用户。
- 观察保护成本和复玩。
验证:
- 新用户策略不读取成熟用户正常随机逻辑。
- 命中风控的用户不消耗保护。
- 平局不计有效机器人局。
- 平局不消耗新用户补贴额度。
- 剩余额度不足时降级为正常随机。
- 成熟用户连赢或净赢后仍然不输出强制输。
测试计划
Go 单测
- 风控命中时不进入新用户保护。
- 奖池黑色状态时不抽机器人。
- 奖池红色水位优先于新用户保护。
- 新用户保护不读取成熟用户正常随机逻辑。
- 成熟用户连赢、净赢和高频下注后不输出强制输。
- 自然期目标胜率 50 时不输出强制结果。
- 自然期目标胜率大于 50 命中时输出不消耗新手额度的用户赢。
- 平局不计有效机器人局。
- 平局不消耗补贴额度。
- 生命周期额度、首日额度、单局额度不足时降级为正常随机。
- 风险评分分层能正确降级保护。
- 新手连输保护命中后输出
force_human_win。 - 非新手保护场景不输出
force_human_lose。 force_human_win、none输出稳定。- 策略日志 reason 完整。
MySQL 单测
- 档位奖池并发扣减不能扣成负数。
- 档位奖池并发冻结不能超过可用余额。
- 结算成功后冻结额度释放。
- 取消或失败后冻结额度释放。
- 同一个
adjustment_id不重复入流水。 - 同一局重复结算不重复扣奖池。
- 同一局重复结算不重复消耗新手保护次数。
- 平局不插入保护消费事件。
- 策略日志唯一键生效。
Admin 测试
- 档位奖池列表展示水位和可承受次数。
- 档位奖池列表展示冻结额度、可用余额和黑色熔断状态。
- 档位奖池加扣成功后余额和流水一致。
- 新用户保护配置校验生命周期额度、首日额度、单局保护上限。
- 风控规则配置校验 risk_score 阈值递增。
- 策略日志分页查询正常。
产品验收校验
- 3 秒内开局率可按小时、游戏和档位统计。
- 100000 档新手保护命中率必须为 0。
- 40000 档强保护命中率必须为 0。
- 高风险用户保护命中率必须为 0。
- 日终奖池账务差异必须为 0。
- 50% 基础体感机器人局下,兜底奖池净变化需要接近 0,若不接近必须回查账务公式。
- 自然期默认 53% 下,用户长期净变化应接近抽水后的体验目标,且新手补贴消耗不增加。
需要保留的兼容点
- 旧
game_self_game_pools不删除。 - 旧
game_self_game_pool_transactions不删除。 - 旧
robot_user_win_rate_percent不再做任何 server/admin/API 兼容;线上库已有遗留列不删除,但新代码不读写、不暴露。 - 已结算历史局不回算档位奖池。
- 历史局仍按原
pool_delta_coin解释。
实施顺序
- 建表和仓储。
- 策略引擎 shadow 模式。
- 策略日志后台查询。
- 档位奖池后台列表和加扣。
- 档位奖池切换结算。
- 新用户保护灰度。
- 风控准入接入。
- 成熟用户观察看板。
- 告警。