846 lines
26 KiB
Markdown
846 lines
26 KiB
Markdown
# 自研游戏机器人策略与档位奖池技术方案
|
||
|
||
本文只描述《自研游戏机器人策略与档位奖池产品文档》里功能设计的技术实现。产品目标、功能原因和运营策略以产品文档为准。
|
||
|
||
## 技术结论
|
||
|
||
- 策略、奖池、结算和 `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. 告警。
|