hyapp-server/docs/自研游戏机器人策略与档位奖池技术方案.md
2026-06-12 19:40:21 +08:00

26 KiB
Raw Permalink Blame History

自研游戏机器人策略与档位奖池技术方案

本文只描述《自研游戏机器人策略与档位奖池产品文档》里功能设计的技术实现。产品目标、功能原因和运营策略以产品文档为准。

技术结论

  • 策略、奖池、结算和 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% 基础体感时,兜底奖池长期期望接近不流失。

技术评估项:

  • 当前 SettleByHighestScoreWithPoolSettleRPSByGestureWithPool 是否把机器人赢局也按 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 日净盈亏。
  • 连赢次数。
  • 连输次数。
  • 今日新手保护干预次数。
  • 最近一次策略干预时间。

统计快照用于新手连输保护、日志解释和后续看板;成熟用户阶段不使用这些统计去强制用户输。

策略输出

成熟用户策略只输出:

  • 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 继续按自然随机机器人手势结算,平局退款。
  • 红色水位只取消保护加成,不改写成用户必输。
  • 黑色水位在奖池保护层熔断机器人补位。

功能四:奖池水位保护技术实现

水位判断

新增:

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_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

策略引擎

输入

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_codereason_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[].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_winnone 输出稳定。
  • 策略日志 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. 告警。