Files
henan-backend/docs/幸运升级接口文档.md

466 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 幸运升级接口文档(Cocos 客户端对接)
> 适用版本:当前 master。
> **2026-09-23 起随机数改由「全局水位」动态决定**(不再是每个饰品位的固定幸运区间);三身份的进度与随机数**完全隔离**;
> 幸运值展示值改为三身份合计(累加口径仍按玩家在前端拉的值,**中奖判定恒用 `min(概率,75)`**)。
> **2026-09-24 起水位配置简化为「只填一个目标水位(%)」** —— 随机数中心由它自动推算,
> 取值范围仍由每个饰品自己的区间限定(见 §1.2 / §1.3 / §6.4)。
> 方案、换算表、口径推导与算例见 `docs/修改记录.md` 的 2026-09-24 条(前情见 2026-09-23 条);数据库变更见 `docs/updata_sql.md`。
> 所有接口路径前缀 `/api/luckyUpgrade`(另有 1 个关联接口在 `/api/userPackSack`)。
> 代码位置:`skins-service/service-playingmethod/.../ApiLuckyUpgradeController.java`、`ApiLuckyUpgradeServiceImpl.java`、`ApiUpgradeRecordServiceImpl.java`。
---
## 一、玩法机制(实现口径)
### 1.1 一局流程
```
选饰品位(upgradeId) + 选成功率(probability)
↓ 后端按实时价算钱:price = 饰品价 × probability / 100
↓ 校验(身份 / 封禁 / 删除 / 未充值 703 / 余额 702)
↓ 加锁(饰品位 + 奖池类型)→ 扣款(source=40 幸运升级消费)
↓ 判定(见 1.2)
├─ 成功:获得该饰品位对应的整件饰品,**该身份**的进度与随机数清 0 / 重抽(其他身份不受影响)
└─ 失败:获得后台配置的「失败奖励」饰品(安慰奖,可多件),**该身份**的进度累加
↓ 写 tt_upgrade_record 记录(含各项快照)+ 背包 tt_box_records(source=4 幸运升级)+ 站内消息
↓ 返回升级记录(含 gainOrnamentList,可用于结果页立即分解)
```
### 1.2 消耗与判定公式
| 项 | 公式 / 取值 |
|---|---|
| 单次消耗 | `price = 饰品实时价(tt_ornament.use_price) × probability / 100`,保留 2 位小数(**前端不传价格**,由后端算,防 0 元 100% 中奖) |
| 判定概率 | `eff = min(probability, 75)`(**封顶 75**:选 90% 也只按 75% 判定,但钱按 90% 收) |
| 本轮空间 | `fullV`:由**当前全局水位**自动推算 —— 先按目标水位反推中心,再在该饰品自己的区间 `[min, max]` 内做**偏斜随机**(支撑集是整个区间,水位高时下限附近的数更频繁)。水位见 1.3 |
| 判定 | 每拉一次 `进度 += eff`;`进度 + eff >= fullV` 即中奖,中奖后**只刷新当前身份**的 `fullV`、该身份进度清 0;**其他身份的进度与随机数一律不受影响**;否则失败继续累加 |
| 推论 | **玩家为一件饰品付出的总成本 ≈ 饰品价 × fullV / 100**,即 `理论 RTP = 100 / fullV`、`水位 = 1 − 100/fullV`。⇒ 想让水位能长期停在某个值,饰品的随机数区间必须**包含**该水位对应的中心随机数 |
> ⚠️ **名义 ≠ 实测**:判定是「跨过阈值即出货、最后一拉整段 eff 计入并丢弃溢出」,实测等效需求是名义值的
> **118%~120%**。所以**不要按 `100/fullV` 公式拍参数**,按后台「奖池水位」页的实测 RTP 与标定系数来调。
### 1.3 幸运值、水位与三个身份池
**水位(全局,2026-09-23 起)**:
```
水位% = (Σ下注 − Σ返奖 + 人工调整) / Σ下注 × 100 # 统计范围 = 整个幸运升级,排除白号
```
水位越高说明平台已经净赚越多 → 本轮可出的随机数越低(放水);水位不足 → 随机数越高(收紧)。
后台只配一个**目标水位 T(%)**,随机数中心由它自动反推:
```
V* = 100 / ((1 − T/100) × k) k = 实测标定系数(1.18~1.20,样本不足按 1.19)
中心 = V* × (1 − (当前水位 − T)/100 × gain) gain 默认 2
t = (中心 − min) / (max − min) 中心在区间内的相对位置,夹到 [0.10, 0.90]
V = min + (max − min) × u^p, p = 1/t − 1 u ~ U(0,1)
```
**整个区间始终都可能出**,水位只改变概率重心:水位 = 目标时 `p = 1`(区间内均匀分布);
水位高于目标 ⇒ `p > 1` ⇒ 下限附近更频繁;低于目标则相反。
`E[V] = 中心` ⇒ 水位会**长期自动收敛回目标值**(仿真实测偏差 < 0.05 个点)。
⚠️ `t` 的夹取范围意味着**中心的可表达范围 = min + [10%, 90%] × 区间宽度**:
区间太窄或下限太高时,高水位目标就实现不了(例:`[100,160]` 只能维持约 `[20.7%, 45.4%]` 的水位)。
**虚拟水位(抽随机数实际用的是它,不是上面那个真实水位)**:真实水位只反映**已经结算**的下注/返奖,
一批饰品同时开新轮时若都用同一个真实水位,会连着出好几个同侧偏斜的随机数。所以每抽一个随机数,
就把这一轮的预估交易预留进 pending:
```
虚拟水位 = (真实bet + pending_bet − 真实win − pending_win + 调整) / (真实bet + pending_bet)
pending_bet += 饰品价 × k × 随机数 / 100 pending_win += 饰品价
```
出货过程中产生的实际流水会**逐步把预留扣回去**(两边口径一致 ⇒ 真正出货时归零,不会长期挂账)。
白号轮次不进水位口径,不参与预留;未抽过数时 pending = 0,退化为真实水位。
水位从 0 起步,可用后台「幸运升级 → 奖池水位」人工注水/抽水。策略与人工调整见 §6.4。
**三个身份池**:
| 池 | userType | 幸运值字段(累计进度) | 本轮已投金额字段 |
|---|---|---|---|
| 普通用户 | `02` | `probability_invested` | `amount_invested` / `effective_amount_invested` |
| 主播 | `01` | `anchor_probability_invested` | `anchor_amount_invested` / `anchor_effective_amount_invested` |
| 白号 | `06`(`is_blank_account=1` 优先判定) | `blank_probability_invested` | `blank_amount_invested` / `blank_effective_amount_invested` |
- 三池**完全隔离**:进度与随机数缓存在 Redis `upgrade_range_fixed:{饰品位}:{池类型}`,锁也是按「饰品位 + 池类型」。
**白号中奖只刷新白号自己的随机数**;普通玩家的进度和随机数原样保留 —— 一个奖池但不会互相污染。
- **随机数的中心由全局水位决定,取值范围由每个饰品自己的区间限定**:`luck_section` /
`anchor_luck_section` / `blank_luck_section` 三列仍按身份分别生效,但语义从改造前的「固定随机数区间」
变为「**允许的取值范围**」—— 区间要**包含水位推算出的中心值**,否则中心被夹在边界上、水位会停在别处。
策略关闭(`enabled=false`)时这三列恢复成改造前的「固定随机数区间」用法。
- **幸运值的累加口径**:默认按「玩家在前端拉的那个值」累加(原始 `probability`,选 90% 就 `+90`),与历史记录口径一致;
**中奖判定固定用 `min(probability, 75)`** —— 两者刻意分开(75% 封顶是风控手段,不改展示数字)。
后台开关 `luckyValueMode` 可切成 `eff`(按 `min(概率,75)` 累加),**只影响展示值与落库快照**。
- **幸运值 = 三个身份共享的一个累计器**:`getOrnamentsList.luckyValue` 就是这个值,所以所有人看到的数完全一致;
**任一身份(含白号)中奖都把它整体归零**,且**中奖那一拉的增量仍计入**(出货那一刻看到的是含本次的数,下一拉才从 0 重新累计)。
⚠️ 这与「判定进度」不是一回事:**判定进度与随机数按身份完全独立**,白号中奖不会动普通玩家的进度。
另有 `myLuckyValue` = 查看者所属身份的贡献(自上次任一身份中奖以来),排查用。
### 1.4 获得与失败奖励
- **成功**:获得 `tt_upgrade_ornaments.ornaments_id` 对应的整件饰品,`gain_ornaments_price` = 该饰品实时价。
- **失败**:获得后台「幸运升级 → 失败奖励」为该饰品位配置的饰品(`tt_upgrade_fail_ornaments`,可多件,按 `ornament_number` 计数),`gain_ornaments_price` = 安慰奖总价值;该饰品位**没配失败奖励时啥都不给**(`gainOrnamentList` 为 `[]`、金额 0)。
- 所有获得物都写入背包 `tt_box_records`(`source = 4` 幸运升级、状态 = 在背包)。
### 1.5 流水与返佣口径
| 场景 | 流水 source |
|---|---|
| 升级消费 | `40`(GOLD_DEVICE,「幸运升级消费」,输出) |
| 游戏内分解(结果页立即分解) | `41`(GOLD_DEVICE_REWARD,「幸运升级奖励」,输入) |
| 背包里分解(事后在背包页分解) | `10`(DECOMPOSE_ORNAMENT,输入) |
- 消费 source 40 计入:今日/历史投注流水、综合消费排行榜、流水抽奖资格、**上级返佣**(按 `classicGameCommissionRate` 口径)、**VIP 流水返利**。
- **白号例外**:白号消费不触发上级返佣、不触发 VIP 流水返利;后台充值/流水/记录类页面普遍按 `is_blank_account=1` 排除白号。
---
## 二、通用约定
- **域名**:`{host}`(例:`http://103.108.67.60:28081`)。
- **Content-Type**:`application/json`;**鉴权**:除 `getOrnamentsList` 外都需要请求头带 token,401 时按项目统一逻辑弹登录。
- **统一响应**:
```json
{ "code": 200, "msg": "操作成功", "data": { } }
```
| code | 含义 |
|---|---|
| 200 | 成功(客户端以 `code === 200` 判定) |
| 401 | 未登录 / 登录过期,msg「登录过期,请重新登录。」 |
| 702 | 账户余额不足 |
| 703 | 玩家未充值不能游玩任何游戏 |
| 500 | 业务失败,msg 直接给玩家看 |
- **维护开关**(`sys_config`,后台「网站设置」):`websiteMaintenance=1` → 「网站维护中......」;`luckyUpgradeMaintenance=1` → 「幸运升级维护中......」。两个开关都**先于登录校验**返回。
- **通用拦截**(`upgrade` 接口):非 主播(01)/普通(02)/白号 → 「非法的用户类型」;`status=1` → 「账号已被封禁,无法参与游戏」;`del_flag != 0` → 「账号状态异常,无法参与游戏」。
- 幸运升级相关也有 `/api/luckyUpgrade/**` 其他接口,本文件只覆盖下表这些。
---
## 三、接口清单
| 接口 | 说明 | 鉴权 | 分页 |
|---|---|---|---|
| `POST /api/luckyUpgrade/getOrnamentsList` | 可升级饰品列表(含当前幸运值) | 否(未登录按普通用户口径) | 无 |
| `POST /api/luckyUpgrade/upgrade` | 拉取一次(扣款 + 判定 + 发奖) | 是 | - |
| `POST /api/luckyUpgrade/recordPage` | 记录页:全服/我的 × 当前饰品/全部饰品 | 是 | 真分页 |
| `POST /api/luckyUpgrade/recentPulls` | 某饰品位最近 50 次拉取流水 | 是 | 无(固定 50) |
| `POST /api/luckyUpgrade/myHistoryRecord` | 我的记录(全部饰品,成败都返回) | 是 | 真分页 |
| `POST /api/luckyUpgrade/getUpgradeRecord` | 通用条件查询(内部/后台复用) | 是 | 真分页 |
| `POST /api/luckyUpgrade/tenTopUpgradeRecord` | 十佳掉落榜(按实际获得价值 Top10) | 是 | 无(固定 10) |
| `POST /api/userPackSack/decompose` | 分解获得的饰品(结果页或背包) | 是 | - |
---
## 四、接口详情
### 4.1 `POST /api/luckyUpgrade/getOrnamentsList` — 可升级饰品列表
**请求**(全部可选,无分页、无排序参数,返回按 `use_price` 升序)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer | 可直接取某一个饰品位(`tt_upgrade_ornaments.id`) |
| type | String | 饰品类型数字字典码(1匕首 2手枪 3步枪 4微型冲锋枪 5重型武器 6手套 7印花 8其它);空串按不传处理 |
| itemName | String | 饰品名称模糊匹配 |
| priceMin / priceMax | BigDecimal | 按价格区间过滤(**只在传了 `priceMax` 时生效**;不传 `priceMin` 默认 0) |
| userType | String | **后端注入,客户端不要传**(用登录用户身份决定返回哪个池的幸运值) |
```json
{ "type": "3", "itemName": "", "priceMax": 5000 }
```
**响应**:`data` 为数组,仅返回 `status='0'`(启用)的饰品位:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer | **可升级饰品ID**(后续 `upgrade.upgradeId`、`recordPage.upgradeId`、`recentPulls.upgradeRecordId` 都传它) |
| ornamentId | Long | 饰品ID(`tt_ornament.id`) |
| name | String | 饰品全名(优先展示) |
| itemName | String | 名称(与 name 同源字段) |
| usePrice | BigDecimal | 实时价格(= 计算消耗、展示目标金额的基准) |
| imageUrl | String | 饰品图片 |
| shortName / typeName / rarityName / exteriorName / levelImg | String | 简称 / 类型 / 稀有度 / 磨损度 / 品质图 |
| luckyValue | Integer | **当前幸运值**(三个身份共享的一个累计值,所有人看到的数一致);口径见 1.3。**任一身份中奖即整体归零** |
| myLuckyValue | Integer | 查看者所属身份对共享值的贡献(自上次任一身份中奖以来);排查用,一般不用 |
### 4.2 `POST /api/luckyUpgrade/upgrade` — 拉取一次
**请求**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| upgradeId | Integer | 是 | 可升级饰品ID(`getOrnamentsList` 的 `id`) |
| probability | Integer | 是 | 本次选择的成功率,1~100;**判定按 min(概率,75),扣款按传入值** |
```json
{ "upgradeId": 25, "probability": 10 }
```
**响应**:`data` = 本条升级记录(`tt_upgrade_record`),字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 升级记录ID |
| userId / nickName / userType / userAvatar | - | 玩家信息(昵称是升级时快照) |
| probability | Integer | 本次选择的成功率(原始概率快照) |
| luckyValue | Integer | 本次升级时的幸运值快照(中奖为 0) |
| amountConsumed | BigDecimal | 本次实际消耗 |
| amountRequired | BigDecimal | 本轮目标金额快照 = 饰品价 × fullV% |
| amountInvested / effectiveAmountInvested | BigDecimal | 本轮已投金额(实际扣款口径 / 真实计算口径)快照 |
| effectiveAmount | BigDecimal | 真实计算金额 = 饰品价 × min(概率,75)% |
| targetUpgradeId | Integer | 可升级饰品ID |
| targetOrnamentId | Long | 目标饰品ID |
| targetOrnamentPrice | BigDecimal | 目标饰品价格 |
| isVictory | Boolean | 成败:true=升级成功 |
| gainOrnamentsPrice | BigDecimal | 本次获得饰品总价值(成功=目标饰品价,失败=安慰奖总价,可能为 0) |
| gainOrnamentList | Array | **本次实际获得物**(JSON 数组,不是字符串),每项:`id`=背包记录ID(`tt_box_records.id`)、`ornamentId`、`ornamentName`、`ornamentHashName`、`ornamentPrice`、`ornamentImgUrl`、`ornamentLevelId`、`ornamentLevelImg`、`ornamentNumber`;无获得时为 `[]` |
| openTime | String | 升级时间 `yyyy-MM-dd HH:mm:ss` |
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"id": 93, "userId": 34052018, "probability": 10, "luckyValue": 0,
"amountConsumed": 181.95, "amountRequired": 1819.52, "isVictory": true,
"gainOrnamentsPrice": 1819.52,
"gainOrnamentList": [
{ "id": 11530, "ornamentId": 1312995578, "ornamentName": "AK-47 | 表面淬火 (略有磨损)",
"ornamentPrice": 1819.52, "ornamentImgUrl": "/uploads/xxx.png", "ornamentLevelImg": "/profile/xxx.png" }
],
"openTime": "2026-09-11 18:24:01"
}
}
```
**失败返回示例**:`{"code":500,"msg":"系统繁忙,请稍后重试。"}`(同池并发抢锁失败,客户端可直接重试);`{"code":703,"msg":"玩家未充值不能游玩任何游戏"}`;`{"code":702,"msg":"账户余额不足"}`;`{"code":500,"msg":"该饰品未在幸运升级开放。"}`。
**结果页立即分解**:取 `gainOrnamentList[i].id` 作为 `packSackIds`,调 4.8 接口(`decomposeType="game"`)。不分解就等于饰品留在背包里。
### 4.3 `POST /api/luckyUpgrade/recordPage` — 记录页(主力接口)
**请求**(二维组合,共 4 种场景,均需登录)
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| scope | Integer | 否 | 0 | 0=全服(不过滤用户) 1=我的 |
| itemScope | Integer | 否 | 0 | 0=全部饰品 1=当前饰品 |
| upgradeId | Long | 条件必填 | - | 可升级饰品ID;`itemScope=1` 时必填,为空返回 500;`itemScope=0` 时传了也忽略 |
| page | Integer | 否 | 1 | 页码(≥1) |
| size | Integer | 否 | 10 | 每页条数(1~20,超限参数错误) |
```json
{ "scope": 1, "itemScope": 1, "upgradeId": 25, "page": 1, "size": 10 }
```
**响应**:`data` 为分页对象 `{ records, total, current, size, pages }`,`records` 按 `openTime` 倒序:
| 字段 | 类型 | 说明 |
|---|---|---|
| userId / avatar / nickName / vipLevel | - | 玩家信息(昵称是升级时快照) |
| probability | Integer | 成功率(原始概率,如 89 表示 89%) |
| luckyValue | Integer | 幸运值快照(**中奖记录记的是「中奖那一刻」的值,含本次**),与 `getOrnamentsList.luckyValue` 同口径 |
| amountConsumed | BigDecimal | 消耗 |
| gainOrnamentsPrice | BigDecimal | 获得饰品总价值 |
| isVictory | Boolean | 成败:true=升级成功(前端胜负样式用) |
| openTime | String | 升级时间 |
| targetOrnamentId | Long | 目标饰品ID |
| name / imageUrl / usePrice / exteriorName / levelImg | - | **本次实际获得的饰品**:成功=目标饰品,失败=安慰奖饰品(`gainOrnamentList` 首件);未配安慰奖的失败记录会回退成目标饰品且金额为 0,**胜负请用 isVictory 判断,不要看图片** |
- 全服(scope=0)不过滤白号、机器人、封禁用户,白号可正常上榜。
- 旧的 `recentPulls` / `myHistoryRecord` 场景都被本接口覆盖,可继续用但建议逐步切到本接口。
### 4.4 `POST /api/luckyUpgrade/recentPulls` — 某饰品位最近 50 次拉取流水
**请求**:`{ "upgradeRecordId": 25 }`
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| upgradeRecordId | Long | 是 | ⚠️ 字段名有历史误导性:**实际语义是「可升级饰品ID」`tt_upgrade_ornaments.id`**(`getOrnamentsList` 的 `id`)。为空返回 500「upgradeRecordId(升级饰品ID)不能为空」 |
| 其他字段 | - | 否 | `ornamentId`/`userType`/`userId`/`page`/`size`/`limit` 全部忽略 |
- **无分页**:固定最近 **50** 条(SQL `LIMIT 50`),没有 `page`/`total`。
- **不过滤**:全服所有人(含白号、机器人、封禁/删除用户),成功与失败都返回。
- `nickName`/`avatar`/`vipLevel` 可能为 `null`(该记录的用户已不在 `tt_user`),客户端要能容错。
**响应**:`data` 直接是数组(不是分页对象):
| 字段 | 类型 | 说明 |
|---|---|---|
| avatar / nickName / vipLevel | - | 拉取人信息 |
| probability | Integer | 本次拉取**选择的成功率**(不是中奖率) |
| luckyValue | Integer | 本次拉取时的**幸运值快照**(含本次的增量;中奖那一拉记的就是出货那一刻的值),与 `recordPage` 同口径 |
| isVictory | Boolean | 成败:true=升级成功 |
| name / imageUrl / usePrice / exteriorName / levelImg | - | **目标饰品**("谁在拉这个饰品",不是获得物) |
| openTime | String | 拉取时间 |
### 4.5 `POST /api/luckyUpgrade/myHistoryRecord` — 我的记录(全部饰品)
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| page | Integer | 否 | 1 | 页码 |
| size | Integer | 否 | 10 | 1~20 |
**响应**:`data` 为分页对象,`records` 是 `tt_upgrade_record`(字段同 4.2 的响应,含 `gainOrnamentList`),仅当前登录用户、不含他人记录、成败都返回、按时间倒序;昵称/头像会补齐。
### 4.6 `POST /api/luckyUpgrade/getUpgradeRecord` — 通用条件查询
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page / size | Integer | **必传** | 不传会 500(内部直接用于构造分页对象) |
| upgradeRecordId | Long | 否 | 按记录ID精确查 |
| ornamentId | Long | 否 | 按目标饰品ID过滤 |
| userType | String | 否 | 按用户类型过滤(01/02/06…) |
| userId | Integer | 否 | 按用户过滤 |
**响应**:`data` 为分页对象(`records` 为 `tt_upgrade_record`,`userAvatar` 已补齐,`gainOrnamentList` 空值兜底为 `[]`)。主要用于内部/后台复用,客户端记录页请用 `recordPage`。
### 4.7 `POST /api/luckyUpgrade/tenTopUpgradeRecord` — 十佳掉落榜
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| isVictory | Boolean | 否 | `true` 只看成功、`false` 只看失败;不传则全部记录一起排 |
**响应**:`data` 为分页对象(固定第 1 页、10 条),按 **`gainOrnamentsPrice`(实际获得价值)倒序**取 Top10 —— 即"实际掉落榜",不是目标饰品价值榜。
### 4.8 `POST /api/userPackSack/decompose` — 分解饰品(结果页/背包)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| packSackIds | Long[] | 条件必填 | 背包记录ID(`tt_box_records.id`,即 `gainOrnamentList[i].id`);`isAll=true` 时可不传 |
| isAll | Boolean | 是 | 是否全选 |
| decomposeType | String | 否 | `game`=游戏结果页立即分解(按饰品来源记 `GOLD_DEVICE_REWARD(41)`);`backpack`=背包分解(记 `DECOMPOSE_ORNAMENT(10)`) |
返回 `{"code":200,"msg":"成功分解N件饰品"}`。受 `websiteMaintenance` 开关拦截。
---
## 五、前端对接要点
1. **upgradeId 语义**:所有接口里的 `upgradeId` / `recentPulls.upgradeRecordId` 都是**可升级饰品ID**(`tt_upgrade_ornaments.id`),来自 `getOrnamentsList` 的 `id`,不是 `tt_ornament.id`,也不是升级记录ID。
2. **扣款以服务端为准**:不要传价格;展示价格用 `usePrice × probability / 100`(与后端同公式,后端保留 2 位小数)。
3. **概率封顶 75**:UI 上若提供 >75% 的档位,要提示"更高概率只加钱、不提升判定"。
4. **幸运值怎么用**:主界面实时幸运值取 `getOrnamentsList.luckyValue`(**三个身份合计的全服累计值,所有人一致**);拉取成功后用 `upgrade` 响应里的 `luckyValue` 立即覆盖,不必等列表刷新。记录里的幸运值是历史快照。
5. **分池与水位**:进度与随机数按 普通(02) / 主播(01) / 白号(06) 三套**完全独立** —— 别人(含白号)中奖**不会**动你的进度和随机数。随机数的**中心**由全局水位统一决定(水位越高越容易出货),**取值范围**仍由该饰品自己的随机数区间限定。
6. **分页**:`recordPage`/`myHistoryRecord` 的 `size` 上限 20;切换 tab(我的/全服、当前饰品/全部饰品)时 `page` 重置为 1。
7. **失败记录展示**:以 `isVictory` 判断胜负,`name/imageUrl/...` 是"实际获得物"(失败=安慰奖);无安慰奖时金额为 0。
8. **结果页分解**:`gainOrnamentList` 里的 `id` 就是分解入参;不调用 = 饰品留在背包。
9. **并发**:同一饰品位 + 同一奖池同时拉取会串行,抢不到锁返回 500「系统繁忙,请稍后重试。」,可直接提示重试。
---
## 六、后台配置与调参
### 6.1 「幸运升级 → 饰品列表」(`tt_upgrade_ornaments`)
| 后台列 | 字段 | 说明 |
|---|---|---|
| 普通用户 / 随机数区间 | `luck_section` | **随机数的允许取值范围**(`[min,max]` JSON),水位推算出的中心必须落在里面 |
| 普通用户 / 本轮已投金额 | `amount_invested` | 普通池当前累计投入(运行时值,后台不可编辑回写) |
| 主播用户 / 随机数区间 | `anchor_luck_section` | 同上,主播身份独立配置 |
| 白号用户 / 随机数区间 | `blank_luck_section` | 同上,白号身份独立配置;未配则回退 `luck_section` |
| 状态 | `status` | 0=启用(=可升级、会出现在客户端列表) 1=禁用 |
- **随机数的中心不在这里配**:中心由「目标水位」自动推算(页面顶部水位条显示 当前水位 / 目标水位 /
实测 RTP / 推算随机数中心),这里只配**取值范围**。调难度请改目标水位(见 6.4)。
- ⚠️ **区间必须包含推算出的中心值**,且**中心离两端各留约 10% 区间宽度**,否则中心被夹住、水位会停在附近
而不是目标值(仿真实测可偏 16~20 个点)。区间还**限定了能维持的水位范围**:`[100,160]` 只能维持约 `[20.7%, 45.4%]`。
- **整个区间始终都可能出**(偏斜随机),水位只改变概率重心;水位 = 目标时是均匀分布。
区间越宽,单轮随机数波动越大、越难被玩家归纳出规律。
- 区间改完**下一轮立即生效**,不用清 Redis(水位驱动下直接读实体字段,不走区间缓存)。
- ⚠️ 旧版本文档里「上线前清空三个区间列」的做法**已作废** —— 现在这三列是必填项。
### 6.2 「幸运升级 → 失败奖励」(`tt_upgrade_fail_ornaments`)
每个饰品位可配多件失败奖励(`ornament_name` / `ornament_price` / `ornament_number` / `ornament_level_id`),玩家升级失败即发放这些饰品;**没配就不发**(记录里 `gainOrnamentList=[]`、`gainOrnamentsPrice=0`)。
### 6.3 维护开关(后台「网站设置」,`sys_config`)
| key | 值 | 效果 |
|---|---|---|
| `websiteMaintenance` | `1` | 全站维护:`upgrade` 与分解接口都拦截 |
| `luckyUpgradeMaintenance` | `1` | 只拦幸运升级:`upgrade` 返回「幸运升级维护中......」 |
### 6.4 「幸运升级 → 奖池水位」(目标水位 / 注水抽水)—— 后台接口
Base = `/admin/upgradeWatermark`,均需后台登录。
| 接口 | 说明 |
|---|---|
| `GET /info` | 当前水位快照 + 目标水位策略 + 按目标水位推算的随机数中心 |
| `POST /setting` | 保存策略:`targetWatermark`(必填)+ `gain`(可选,默认 2)+ `enabled` + `luckyValueMode` |
| `POST /adjust` | 注水 / 抽水(`type` 1注水 2抽水;调整原因必填) |
| `POST /adjustReset` | 重置人工调整(插入抵消日志,历史保留) |
| `GET /adjustLog` | 调整历史分页 |
| `POST /recalc` | 按历史流水全量重算累计量 + 刷新标定系数(纠正 Redis 计数漂移) |
`/info` 返回(节选):
| 字段 | 说明 |
|---|---|
| `enabled` | 策略开关;`false` = 回退到每个饰品位的固定随机数区间 |
| `targetWatermark` | **目标水位(%)** —— 后台唯一要配的运营参数 |
| `gain` | 响应灵敏度(水位每偏离目标 1 个点,随机数中心反向调整 gain%),默认 2 |
| `watermark` / `rtp` | 当前(真实)水位% / 实测 RTP%(均排除白号) |
| `watermarkWithPending` | **虚拟水位** —— 抽随机数实际用的那个;与 `watermark` 的差 = 已抽走未出货轮次的影响 |
| `pendingBet` / `pendingWin` | 已抽走随机数、还没出货的那些轮次的剩余预估下注 / 返奖 |
| `targetGap` | 当前水位 − 目标水位(正 = 赚得比目标多 ⇒ 下一轮放水) |
| `targetCenter` | 按目标水位推算的随机数中心 `V*`(**饰品区间要包含它**) |
| `centerNow` | 下一轮新开会用到的随机数中心(按**虚拟水位**算,与实际抽数口径一致) |
| `calibration` | 实测标定系数 k;样本不足时为 `null` |
| `effectiveCalibration` | 实际参与推算的系数(样本不足时 = 1.19 兜底) |
| `totalBet` / `totalWin` / `blankBet` / `blankWin` / `adjust` | 水位口径的累计量(白号不进水位,仅并列展示) |
**策略配置(`sys_config: luckyUpgradeWatermark`)**:
```json
{
"enabled": true,
"targetWatermark": 30,
"gain": 2,
"luckyValueMode": "raw"
}
```
| 字段 | 说明 |
|---|---|
| `enabled` | `false` = 逐字节回退改造前的固定幸运区间行为,不发版即可回滚 |
| `targetWatermark` | **目标水位(%)**:平台希望长期维持的净收益比例。30 ⇒ 长期平均 RTP ≈ 70%。合法范围 −50 ~ 95 |
| `gain` | 响应灵敏度,默认 2:水位每高出目标 1 个点,随机数中心降 2%(放水);低于目标则反之 |
| `luckyValueMode` | 幸运值**展示**口径:`raw`(默认,累加原始 `probability`)/ `eff`(累加 `min(概率,75)`)。**不影响中奖判定** |
**目标水位 → 随机数中心换算表**(按 `k = 1.19` 兜底系数算):
| 目标水位 | 0% | 10% | 20% | **30%** | 40% | 45% | 50% | 60% |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| 中心 `V*` | 84 | 93 | 105 | **120** | 140 | 153 | 168 | 210 |
保存时强校验:`targetWatermark` 必填且在 −50% ~ 95% 之间;`gain` 若填写需在 0.5 ~ 10 之间。
⚠️ **标定**:判定溢出让「实测等效需求」是名义值的 118%~120%,所以中心公式必须带上**实测标定系数**
(`V* = 100 / ((1 − 目标水位/100) × k)`),不能按 `100/(1 − 目标水位/100)` 直接算。
`k` 由系统按真实流水滚动计算(5 分钟内存缓存),样本不足时按 1.19 兜底。
⚠️ **`V*` 必须落在饰品的随机数区间内**,且**离两端各留约 10% 区间宽度**,否则中心被夹住、水位停不到目标值。
⚠️ **区间同时限定了能维持的水位范围**(中心可表达范围 = 区间下界 + `[10%, 90%]` 宽度):
例 `[100,160]` 对应水位约 `[20.7%, 45.4%]`;想把水位开到 60%,区间上限得提到 210 以上。
完整推导、仿真数据与算例见 `docs/修改记录.md` 的 2026-09-24 条。
### 6.5 数据落库
| 表 | 说明 |
|---|---|
| `tt_upgrade_record` | 每条拉取记录(成败、消耗、获得总额、各项快照、`gain_ornament_list` JSON、`lucky_value`、`watermark` 水位快照、`round_target` 本轮随机数快照) |
| `tt_box_records` | 获得物入背包(`source=4` 幸运升级),`id` 即结果页分解入参 |
| `tt_user_blend_ercash` | 余额流水:消费 40、游戏内分解 41、背包分解 10 |
| `tt_upgrade_ornaments` | 饰品位配置 + 三个身份的运行进度(各身份累计幸运值/已投金额) |
| `tt_upgrade_reward_log` | 水位人工调整日志(注水/抽水/重置;`delta` 为带符号影响,水位调整额 = `SUM(delta)`) |
字段变更历史见 `docs/updata_sql.md`(含白号三列的 ALTER)。