# 自研游戏机器人策略与档位奖池技术方案 本文只描述《自研游戏机器人策略与档位奖池产品文档》里功能设计的技术实现。产品目标、功能原因和运营策略以产品文档为准。 ## 技术结论 - 策略、奖池、结算和 `forced_result` 仍在 `game-service` 内实现。 - `robot-service` 只负责机器人账号池和机器人抽取,不参与胜负策略。 - 奖池从 `app_code + game_id` 升级为 `app_code + game_id + stake_coin`。 - 档位奖池必须支持余额、冻结额度、健康状态和黑色熔断。 - 机器人策略引擎只输出决策,不直接改钱包,不直接改数据库余额。 - 新用户保护按有效机器人局计数,平局不计数、不消耗补贴额度。 - 结算仍复用现有骰子点数和 RPS 手势反推机制。 - 所有策略决策、奖池变更和保护状态变更都必须幂等。 ## 功能一:档位独立奖池技术实现 ### 数据表 新增 `game_self_game_stake_pools`。 ```sql 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`。 ```sql 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 方法: ```go 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`。 ```sql 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`。 ```sql 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='自研游戏用户新手保护状态'; ``` 新增保护消费幂等表。 ```sql 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`: ```go 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 暴露。 ### 用户统计快照 保留查询: ```go GetSelfGameUserStats(ctx, appCode, gameID string, userID int64, nowMS int64) (UserGameStats, error) ``` 字段: - 今日机器人局数。 - 今日净盈亏。 - 近 7 日净盈亏。 - 连赢次数。 - 连输次数。 - 今日新手保护干预次数。 - 最近一次策略干预时间。 统计快照用于新手连输保护、日志解释和后续看板;成熟用户阶段不使用这些统计去强制用户输。 ### 策略输出 成熟用户策略只输出: - `none` - `POOL_RED_NORMAL_RANDOM` - `NORMAL_PHASE_TARGET_BASELINE` - `NORMAL_PHASE_TARGET_MISS` - `NORMAL_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 继续按自然随机机器人手势结算,平局退款。 - 红色水位只取消保护加成,不改写成用户必输。 - 黑色水位在奖池保护层熔断机器人补位。 ## 功能四:奖池水位保护技术实现 ### 水位判断 新增: ```go type PoolLevel string const ( PoolLevelGreen PoolLevel = "green" PoolLevelYellow PoolLevel = "yellow" PoolLevelRed PoolLevel = "red" PoolLevelBlack PoolLevel = "black" ) ``` 判断规则: ```text 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`: ```text required_pool_coin = payout_coin - human_stake_coin ``` 如果当前档位奖池余额小于 `required_pool_coin`,直接进入 `pool_guard`,输出 `none` 并熔断机器人补位。 ### 黑色熔断 黑色状态处理: - 当前档位不再触发机器人补位。 - 已经冻结的机器人局继续按已写入策略完成结算。 - 新进入的用户只等待真人匹配,或提示当前档位匹配繁忙。 - 后台必须展示黑色状态和触发原因。 实现要求: - `attachRobotIfDue` 调用策略引擎前先判断当前档位是否黑色。 - 黑色时不调用 `robot-service` 抽机器人,避免抽到机器人后又无法入座。 - 黑色状态恢复到红色以上后,才允许重新开启该档机器人补位。 ## 功能五:风控准入技术实现 ### 数据表 新增 `game_self_game_risk_rules`。 ```sql 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='自研游戏机器人策略风控规则'; ``` ### 风控接口 新增内部接口: ```go type RiskEvaluator interface { EvaluateSelfGameRobotRisk(ctx context.Context, input RiskInput) (RiskResult, error) } ``` 第一阶段本地判断: - protection state 中补贴收益是否超限。 - strategy logs 中保护命中是否异常。 - 用户是否保护期低档获利后快速进入高档。 第二阶段接 user-service 或风控服务: - 同设备账号数。 - 同 IP 账号数。 - 黑名单。 ### 风控结果 ```go type RiskResult struct { RiskScore int32 RiskTier string BlockProtection bool BlockReason string ReasonJSON string } ``` 只阻断保护,不阻断正常游戏。 ### 风控分层 ```text 0-29 -> normal,完整保护 30-49 -> reduced,降低保护强度,只保留低档位保护 50-69 -> weak,只保留 1000 档弱保护,关闭连输保护 70+ -> blocked,关闭全部新用户保护 ``` 实现要求: - 风控分层结果写入策略日志 `reason_json`。 - 新用户保护策略根据 `risk_tier` 降级,而不是只看 `BlockProtection`。 - 高风险只关闭保护,不阻止用户正常进入游戏。 ## 功能六:全量策略日志技术实现 ### 数据表 新增 `game_self_game_strategy_logs`。 ```sql 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_stage` - `risk_result` - `risk_score` - `risk_tier` - `pool_level` - `pool_balance_coin` - `pool_reserved_coin` - `required_pool_coin` - `effective_bot_game_count` - `lifetime_subsidy_left` - `day1_subsidy_left` - `new_user_policy_snapshot` - `user_stats_snapshot` ## 策略引擎 ### 输入 ```go 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 } ``` ### 输出 ```go type RobotStrategyDecision struct { StrategyCode string Decision string ReasonCode string ReasonJSON string ForceResult string RequiredPoolCoin int64 ReservationID string ShouldConsumeProtection bool } ``` ### 决策顺序 ```text 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 旧链路在机器人入座时直接判断奖池、新用户首胜和固定胜负控制。改造后: ```text 读取 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 当前结算后调用奖池更新。改造后: ```text 按现有逻辑扣款、结算、派奖 按 stake_coin 更新档位奖池 释放 stake pool reservation 如果策略命中新用户保护,结算成功后消费保护状态 MarkDiceMatchSettled ``` 失败处理: - 结算失败时释放冻结额度。 - 钱包扣款失败时释放冻结额度。 - 用户取消或匹配过期时释放冻结额度。 - 释放失败时写补偿任务或人工对账队列。 ### dicePoolDelta 输入不变,输出增加 `stake_coin`。 ```go 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[].stakeCoin` - `items[].balanceCoin` - `items[].reservedCoin` - `items[].availableCoin` - `items[].targetBalanceCoin` - `items[].safeBalanceCoin` - `items[].warningBalanceCoin` - `items[].dangerBalanceCoin` - `items[].poolLevel` - `items[].recentInCoin` - `items[].recentOutCoin` - `items[].recentNetCoin` - `items[].humanWinCapacity` - `items[].botMatchEnabled` - 相关 IM:无 #### 档位奖池配置 - 地址:`PUT /admin/self-games/:game_id/stake-pools/:stake_coin` - 参数: - `targetBalanceCoin` - `safeBalanceCoin` - `warningBalanceCoin` - `dangerBalanceCoin` - `status` - 返回值:更新后的档位奖池 - 相关 IM:无 #### 档位奖池加扣 - 地址:`POST /admin/self-games/:game_id/stake-pools/:stake_coin/adjust` - 参数: - `direction` - `amountCoin` - `reason` - 返回值: - `adjustmentId` - `balanceAfter` - `reservedAfter` - 相关 IM:无 #### 新用户保护配置 - 地址:`GET /v1/admin/game/self-games/:game_id/new-user-policy` - 参数:无 - 返回值:当前配置 - 相关 IM:无 - 地址:`PATCH /v1/admin/game/self-games/:game_id/new-user-policy` - 参数: - `enabled` - `protectionRounds` - `protectionHours` - `maxStakeCoin` - `lifetimeSubsidyQuotaCoin` - `day1SubsidyQuotaCoin` - `singleRoundSubsidyCapCoin` - `strategyLevel` - `loseStreakProtectionEnabled` - `loseStreakTrigger` - 返回值:更新后的配置 - 相关 IM:无 #### 风控规则配置 - 地址:`GET /admin/self-games/:game_id/risk-rules` - 参数:无 - 返回值:当前配置 - 相关 IM:无 - 地址:`PUT /admin/self-games/:game_id/risk-rules` - 参数: - `enabled` - `deviceUserLimit` - `ipUserLimit` - `protectionProfitLimitCoin` - `highStakeAfterProtectionLimitMs` - `fullProtectionMaxScore` - `reducedProtectionMaxScore` - `weakProtectionMaxScore` - `disableProtectionMinScore` - 返回值:更新后的配置 - 相关 IM:无 #### 策略日志查询 - 地址:`GET /admin/self-games/strategy-logs` - 参数: - `gameId` - `stakeCoin` - `userId` - `matchId` - `strategyCode` - `decision` - `riskTier` - `poolLevel` - `startTimeMs` - `endTimeMs` - `pageSize` - `cursor` - 返回值: - `items` - `nextCursor` - 相关 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` 解释。 ## 实施顺序 1. 建表和仓储。 2. 策略引擎 shadow 模式。 3. 策略日志后台查询。 4. 档位奖池后台列表和加扣。 5. 档位奖池切换结算。 6. 新用户保护灰度。 7. 风控准入接入。 8. 成熟用户观察看板。 9. 告警。