修改河南项目的管理后台ui

This commit is contained in:
2026-09-24 11:18:30 +08:00
parent 23fe1f3995
commit 4be9ce4e85
19 changed files with 2513 additions and 178 deletions
+405
View File
@@ -7,6 +7,411 @@
---
## 2026-09-24
- **幸运升级:水位配置从「4 档波段表」简化为「一个目标水位百分比」+ 饰品随机数区间恢复可编辑**
**起因**:上一版后台要维护一张 4 档波段表(水位下界/上界 × 随机数下界/上界 = 16 个数),
还要保证档间单调且留有重叠 —— 运营看不懂,也容易配出互相矛盾的表。
用户要的模型其实简单得多:**「每个饰品设一个区间,水位高的时候随机数靠近区间下限,
但水位要维持在我设定的那个值」** ⇒ 后台只该填**一个水位百分比**。
⚠️ **本条取代 `2026-09-23` 条里的「三、波段表」与「十、上线前清空旧区间」两处**,
其余口径(三身份判定隔离、幸运值共享展示、标定系数、注水抽水、水位统计口径)**全部仍然有效**。
**一、新口径(一句话)**
目标水位 → 自动反推随机数中心 → 在该饰品自己的区间内围绕中心抖动。
后台不配随机数,随机数由目标水位算出来:
```
V* = 100 / ((1 − 目标水位/100) × k) k = 实测标定系数(见第四节,样本不足按 1.19)
中心 = V* × (1 − (当前水位 − 目标水位)/100 × gain) gain 默认 2
t = (中心 − 下限) / (上限 − 下限) 中心在区间内的相对位置,夹到 [0.10, 0.90]
V = 下限 + (上限 − 下限) × u^p, p = 1/t − 1 u ~ U(0,1)
```
- **偏斜随机**:支撑集始终是**整个区间**(任何值都可能出),水位只改变**概率密度**。
水位 = 目标 ⇒ `t = 0.5`、`p = 1` ⇒ **均匀分布**(区间内等概率);
水位高于目标(放水)⇒ 中心下移、`p > 1` ⇒ 概率向**下限**堆积;低于目标则向**上限**堆积。
- `E[u^p] = 1/(p+1) = t` ⇒ `E[V] = 中心` ⇒ **水位精确收敛回目标值**(这就是「保证维持在我设定的值」)。
- ⚠️ 曾先写过「中心平移 + 固定宽度抖动,再夹取到区间边界」,**已废弃**:中心贴边时落点会被压成贴着边界的一条窄带,
且**水位偏离越大窄带越窄**,玩家一眼看出是"卡边界"。改成偏斜随机后,任何水位下支撑集都是整个区间。
- ⚠️ `t` 夹到 `[0.10, 0.90]` 决定了**中心的可表达范围 = 区间下界 + [10%, 90%] × 区间宽度**。
中心超出这个范围(水位远离目标,或区间配得太窄)⇒ 水位只能停在附近。
例:区间 `[100,160]` 的中心可表达范围是 `[106, 154]`,对应水位约 **`[20.7%, 45.4%]`**;
想把水位开到 60%,区间上限得提到 **210 以上**。
**二、目标水位 ↔ 随机数中心换算表**(`k = 1.19`,即实测系数兜底值)
| 目标水位 | 目标 RTP | 随机数中心 `V*` |
|---:|---:|---:|
| 0% | 100% | 84 |
| 10% | 90% | 93 |
| 20% | 80% | 105 |
| **30%** | **70%** | **120** |
| 40% | 60% | 140 |
| 45% | 55% | 153 |
| 50% | 50% | 168 |
| 60% | 40% | 210 |
⚠️ **`V*` 必须落在饰品的随机数区间内**(且离边界 > 10),否则中心被夹住、水位停在别处。
例:目标 60% 需要中心 210 ⇒ 饰品区间上界必须 ≥ 210。
**三、饰品随机数区间的作用(恢复了编辑入口)**
`tt_upgrade_ornaments` 的三个区间列(`luck_section` / `anchor_luck_section` / `blank_luck_section`)
重新成为**必填项**,语义从「固定随机数区间」变成「**随机数的取值范围**」:
**整个区间始终都可能出**,中心由水位决定、只控制概率往哪一端堆。区间越宽,单轮波动越大、越难被玩家归纳出规律。
⇒ **`2026-09-23` 条里「上线前清空三个区间列」的做法已作废**,不要再执行。
⇒ 区间还**限定了这台机器能维持的水位范围**(见第一节末条):区间太窄或下限太高,高水位目标就实现不了。
**四、实测标定系数 k:改为 5 分钟内存缓存**
k = 「改造后累计下注(排除白号) / Σ(`round_target/100` × 目标饰品价)」,含义与算法不变。
但它是一次**全表聚合 + `tt_user` JOIN**,而抽随机数的热路径现在也要用它
⇒ 加 **5 分钟内存缓存**(启动预热 + 后台「按历史重算」主动刷新),不能每次拉取都查一次 DB。
**五、仿真验证**(脚本 `.workbuddy/upgrade-target-watermark-sim.mjs`,每种配置 60 万次拉取,可复跑)
| 配置 | 目标水位 | 实测稳态水位 | 偏差 | 实测 RTP | p10~p90 随机数 | 随机数实际范围 | 低于 100 占比 |
|---|---:|---:|---:|---:|---:|---|---:|
| A 区间 `[100,160]` | 30% | **29.97%** | −0.03 | 70.03% | 100~148 | **`[100,160]` 全覆盖** | 0.0% |
| B 区间 `[75,175]`(宽) | 30% | **29.98%** | −0.02 | 69.99% | 81~163 | **`[75,175]` 全覆盖** | **32.8%** |
| C 区间 `[100,160]` | 45% | 43.80% | −1.20 | 56.21% | 146~159 | `[111,160]` | 0.0% |
| D 区间 `[100,160]` | 60% | 43.79% | **−16.21** | 56.21% | 146~159 | `[117,160]` | 0.0% |
| E 区间 `[80,90]`(窄) | 30% | 9.77% | **−20.23** | 90.25% | 88~90 | `[83,90]` | 100.0% |
- **A/B**:区间包含 `V*` ⇒ 稳态水位与目标值差 < 0.05 个点,**「维持在我设定的值」成立**;
且随机数的**实际出现范围就是整个区间**(不是贴着中心的一条窄带)。
- **C**:`V* = 157.4` 距区间上限 160 只差 2.6,小于 10% 区间宽度(6)⇒ 被 `t` 上限夹住,偏 1.2 个点。
- **D/E**:区间不包含 `V*` ⇒ 中心被夹死在边界,水位**到不了**目标值(偏 16~20 个点)—— 这是唯一的坑。
- **B 的 32.8%** 说明:想把「每次都要 100 以上才出」的观感彻底打掉,区间下限要低于 100(宽区间 `[75,175]`)。
**六、改动清单**
1. `UpgradeWatermarkSettingBody`:删 `bands` / `Band` 类,改为 `targetWatermark` + `gain`(后台只填目标水位)。
2. `UpgradeWatermarkService`:删波段表查表逻辑,新增 `targetCenter()` / `centerAt()` / `drawTarget(lo, hi)`;
`drawTarget` 用**偏斜随机**(`V = 下限 + 宽度 × u^p`,`p = 1/t − 1`,`t` 由中心在区间内的相对位置给出);
标定系数加 5 分钟缓存;`info()` 返回 `targetWatermark` / `targetGap` / `targetCenter` / `centerNow` /
`effectiveCalibration`(原 `bands` / `bandRows` / `bandMin` / `bandMax` 全部移除)。
3. `ApiLuckyUpgradeServiceImpl`:新增 `resolveRange()`(读该饰品位 × 本身份的区间,**直接读实体字段**,
不再走 Redis 区间缓存 ⇒ 后台改完下一轮立即生效,不用清缓存);`drawRoundTarget` 把区间交给水位服务。
未配区间时兜底 `[80,160]`。回退分支 `legacyFixedTarget()` 保持旧的 Redis 缓存写法不动。
4. 前端 `views/skins/luckyUpgrade/rewardPool/index.vue`:**删掉整张波段表**,改为一个「目标水位」输入框
+ 策略开关,**改完立即生效(页面没有保存按钮)**;实时条显示「当前水位 / 目标水位 / 离目标多少 /
下一轮随机数 / 人工调整 / 实测标定系数 / 白号 / 策略开关」。
策略卡里另有一个**「随机数区间 → 可维持水位范围」换算器**:填两个数即时反推
(`中心可表达范围 = 下限 + [10%, 90%] × 宽度`,再代回 `水位 = 1 − 100/(k × 中心)`),
目标水位不在算出的范围内会标红提示;右侧 `?` 悬浮说明「区间是什么 / 为什么它限定了水位范围 / 怎么用」。
⇒ 正文只讲**区间硬约束**,不展示「随机数中心」这类内部推算量。
5. 前端 `views/skins/ttUpgrade/index.vue`:**恢复**普通/主播/白号三列「随机数区间」编辑(列名由「幸运区间」改为
「随机数区间」);顶部水位条改为显示「目标水位 / 推算随机数中心」(原「本轮随机数区间」两栏随波段表一起删)。
**七、验证**
- `mvn -B clean install -DskipTests` 通过;`ruoyi-ui` 生产构建通过。
- 仿真 E 组反证有效:区间 `[80,90]` + 目标 30% ⇒ 稳态 9.84%、RTP 90.2%,说明夹取逻辑确实按预期生效(不是"看起来对")。
- 全仓扫 `bands` / `bandRows` / `bandMin` / `bandMax` 无残留引用。
**八、上线前必做**
1. **给每个在售饰品配随机数区间**,且区间要包含「按目标水位推算的中心值」,**中心离两端各留约 10% 区间宽度**
(后台水位页会显示该中心)。区间缺失时后端兜底 `[80,160]`,能用但不受控。
区间同时**限定了水位能调到的范围**(例:`[100,160]` 只能维持约 `[20.7%, 45.4%]` 的水位)。
2. 目标水位**默认 30%**(≈ 长期 RTP 70%);上线初期水位从 0 起步,会有一段收紧期,想立刻拉到目标值就在水位页注水。
3. 后端需**重启**才生效(策略与抽数逻辑都在 Java 侧)。
**九、虚拟水位:一批饰品同时开轮时,不会都按同一个水位放水**(2026-09-24 补充)
**问题**:真实水位只反映**已经结算**的下注/返奖。一批饰品同时开新轮时(同时上架、或几个玩家同时首次拉取),
它们会全部用同一个真实水位去算中心 —— 水位高时连着出好几个低随机数,而这批货真出完后水位其实已经变了。
**做法**:每抽一个随机数就把这一轮的**预估交易**预留进 `upgrade_pool:pending:bet` / `:pending:win`:
`预估下注 = 饰品价 × k × 随机数 / 100`(乘 k 是因为判定溢出)、`预估返奖 = 饰品价`。
抽下一个时用的是**虚拟水位**:
```
虚拟水位 = (真实bet + pending_bet − 真实win − pending_win + 调整) / (真实bet + pending_bet)
```
这一轮出货过程中产生的实际流水会在 `recordPull` 里**逐步把预留扣回去** —— 两边口径一致
(`k` 就是「实际 / 名义」的实测系数),所以真正出货时预留基本归零、不会长期挂账。
白号轮次不进水位口径,不参与预留;`enabled=false` 或没抽过数时 pending = 0,退化为真实水位。
后台「按历史重算」会把 pending 一起归零,兜住「某个轮次永远没出货、pending 长期挂账」的漂移。
**效果**(同一仿真脚本的「预留机制」段;8 轮连开后的 `E[V]`):
| 累计下注体量 | 不用预留 | 用预留(第 8 轮) | 收紧 |
|---:|---:|---:|---:|
| 2 万 | 88.0 | **108.6** | **+23.4%** |
| 10 万 | 88.0 | 90.8 | +3.2% |
| 100 万 | 88.0 | 88.0 | 0% |
⚠️ **效果与水位体量成反比,这是数学必然、不是实现问题** —— 水位是「累计比例」,
一件货(玩家成本约 1.2 倍饰品价)只占累计下注的极小一份。刚上线体量小时很灵敏,体量大后几乎可忽略。
⇒ 想让大水位下收敛更灵敏,要调的是 **`gain`**(水位每偏离 1 个点、中心移动的幅度,默认 2),不是靠预留。
⇒ 另外:中心一旦撞到区间边界(`t` 被夹到 `[0.10, 0.90]`),`E[V]` 就不再随水位变化 ——
上面 100 万那组水位明明降了 0.32 个点,`E[V]` 仍是 88,就是这个原因。
**★ 生效边界:能稳定处理的量级**(判据 `|Δ虚拟水位| ≥ 1 个点` ⇒ `E[V]` 变化 ≥ 2.4 点、约 2.7%,线上看得出来):
| 累计下注 \ 连开个数 | 3 个 | 5 个 | 10 个 | 20 个 | 50 个 |
|---:|---:|---:|---:|---:|---:|
| 1 万 | −10.0 \* | −10.8 \* | −16.1 \* | −12.6 \* | −14.0 \* |
| 3 万 | −4.1 \* | −6.6 \* | −6.4 \* | −13.1 \* | −13.2 \* |
| 10 万 | −1.4 \* | −2.2 \* | −4.0 \* | −6.3 \* | −10.6 \* |
| 30 万 | −0.5 | −0.7 | −1.4 \* | −2.7 \* | −6.0 \* |
| 100 万 | −0.1 | −0.2 | −0.3 | −0.9 | −2.0 \* |
| 500 万 | −0.03 | −0.04 | −0.1 | −0.2 | −0.4 |
(\* = 有效;饰品价按 1000、水位 45%、目标 30% 算。因撞区间边界后随机数饱和,1 万那行不再单调。)
**门槛公式**:`Δ水位 ≈ x·[q(1−r) − 1] / (1 + q·x)`,其中 `x = N·p/B`、`q = k·V/100 ≈ 1.2~1.43`、`r` = 当前水位。
⇒ **`r = 目标水位` 时分子为 0 —— 稳态下预留不起作用(本该如此)**,水位偏离目标越多才越明显。
⇒ 判据的本质是「**连开饰品的总面值 `Np` 占累计下注 `B` 的比例**」:**`Np/B ≳ 3%~5%` 才看得出来**。
**为什么和累计下注相关(而不是只看水位)** —— 水位是**比值**、流水是**绝对额**:
`水位 = 利润 / 累计下注`,一件货(饰品价 1000、随机数 120)带来「下注 +1200、返奖 +1000、净赚 +200」,
但这 200 元对水位的推动完全取决于分母:
| 累计下注 B | 加一件货后 | 水位变化 |
|---:|---|---:|
| 10 万 | 45.00% → 44.80% | **−0.20 点** |
| 100 万 | 45.00% → 44.98% | **−0.02 点** |
⇒ 同一笔钱落进 10 倍大的池子,效果就是 1/10。
⚠️ **预留不会凭空制造推动** —— 它预估的就是这批货真实产生的交易,所以效果上限 = **这批货真出完后本来就会推动的量**,
机制只是把这份推动**提前**给第 2、第 3 个随机数看到。⇒ 「连开 N 个才有效」的门槛,本质不是预留不灵,
而是**这 N 件货本来也推不动水位**。
⇒ 所以「按水位判断」和「体量影响效果」是两件事:**水位→随机数**(判断,只看水位与 `gain`)与
**流水→水位**(记账,比例反馈天然有惯性,强依赖体量)。
| 累计下注(p = 1000) | 需要连开多少个才有效 |
|---:|---|
| 1 万 ~ 3 万 | 2 ~ 3 个 |
| 10 万 | 3 个(面值占 3%) |
| 30 万 | 10 个 |
| 100 万 | 50 个(现实中不存在) |
| 500 万以上 | 不现实 |
⇒ **一句话结论:刚上线、累计下注在十万量级以内能稳定处理;滚到百万以上基本失效。**
这是「水位是累计比例」的必然结果 —— 想在大水位下也能快速纠偏,要调 **`gain`**(水位每偏离 1 个点、
中心移动的幅度;现在默认 2,暂未在后台暴露),或者直接人工注水/抽水。
---
## 2026-09-23
- **幸运升级:随机数改由「全局水位」动态决定 + 判定层三身份隔离 / 展示层共享幸运值 + 后台注水抽水页**
> ⚠️ **后续修订(2026-09-24)**:本条的「三、水位 → 随机数的映射(波段表)」与「十、上线前必做」第 1 条
> (清空三个区间列)**已被 2026-09-24 条取代** —— 水位从「4 档波段表」简化为「单一目标水位」,
> 饰品区间从"建议清空"变为"必须配置且要包含推算中心"。其余内容(水位口径、三身份判定隔离、
> 幸运值共享展示、标定系数、注水抽水、换算关系)**全部仍然有效**。
**起因**:原来每个饰品位配一套**固定**幸运区间(如 `[100,160)`),每轮从区间里均匀抽一个随机数当作出货阈值,
于是随机数永远 ≥ 区间下限 —— 玩家能看出「每次都要投到 1 倍原价以上才出」这条硬底。
同时三个身份(普通/主播/白号)各有各的区间与进度,前端展示的「幸运值」各不相同,
看主播或白号表演的观众会发现数值对不上账。
**一、换算关系(改这个玩法的参数前必看)**
幸运区间的单位是**「饰品价的百分比」**:
| 量 | 关系 |
|---|---|
| 每拉一次消耗 | `饰品价 × probability / 100` |
| 每拉一次推进 | `eff = min(probability, 75)` 点进度 |
| 每 1 点进度的成本 | `饰品价 / 100`(`probability ≤ 75` 时严格成立) |
| 出一件货需要 | 约 `fullV` 点进度 |
| **理论 RTP** | `100 / fullV` |
| **水位** | `1 − RTP = 1 − 100/fullV` |
| 目标水位 | 目标 RTP | 名义中心随机数 |
|---:|---:|---:|
| −41% | 141% | 71 |
| −20% | 120% | 83 |
| 0% | 100% | 100 |
| 10% | 90% | 111 |
| 20% | 80% | 125 |
| **30%** | **70%** | **143** |
| 40% | 60% | 167 |
| 47% | 53% | 189 |
⚠️ **水位能到多少由随机数区间的上下界决定**:想让水位能长期停在 40% 以上,某一档的随机数上界必须 > 160。
**二、最终口径(三条,都是拍过板的)**
1. **随机数 + 判定进度:按身份完全隔离**。每个身份有自己的一份「本轮进度 + 本轮随机数」,
白号中奖**只刷新白号自己的随机数**,普通玩家的进度与随机数原样不动。
2. **水位:全局**(整个幸运升级一个水位,不按饰品位拆),且**排除白号**。
3. **幸运值:判定层隔离、展示层共享** —— 见下面第 4 条改动。
**三、水位 → 随机数的映射(波段表)**
`sys_config: luckyUpgradeWatermark` 存一张波段表,命中档后在该档区间内**均匀随机**(有 10s 内存缓存):
| 水位区间 | 可出的随机数区间 |
|---|---|
| < 10% | `[95, 130]` |
| 10% ~ 26% | `[115, 150]` |
| **26% ~ 36%** | **`[75, 175]`** ← 稳态档 |
| ≥ 36% | `[85, 128]` |
保存时后端强校验:水位档升序不重叠、`0 < vMin < vMax`、**水位越高随机数中心值必须越小**(配反直接拒绝)。
**四、名义 ≠ 实测:标定系数 k**
判定是「跨过阈值即出货、最后一拉整段 eff 计入并丢弃溢出」,所以**实测等效需求是名义 `fullV` 的 118%~120%**,
波段表的随机数值**不能按 `100/fullV` 公式拍**。后台自动算标定系数:
```
k = 改造后累计下注(排除白号) / Σ(round_target/100 × 目标饰品价,仅中奖记录)
预计落点水位 = (1 − 100/(k × 中心随机数)) × 100
```
数据不足时 `k = null`,后台显示「样本不足」而不是用公式瞎估;「预计落点水位」落在该档自己的水位区间内 ⇒ 该档是**稳态档**(后台标绿)。
**五、改动清单**
(SQL 见 `docs/updata_sql.md`,接口口径见 `docs/幸运升级接口文档.md`)
1. **水位**:`水位% = (Σ下注 − Σ返奖 + 人工调整) / Σ下注 × 100`,统计范围**整个幸运升级**且**排除白号**。
累计量走 Redis 增量计数(`upgrade_pool:*`),DB 只用于启动初始化与后台「按历史重算」——
不在拉取热路径上对 `tt_upgrade_record` 做全表聚合。
2. **随机数**:按当前水位查波段表,命中档后在档内均匀随机;水位越高随机数越低(放水)。
`enabled=false` ⇒ 逐字节回退到「每个饰品位固定 `luck_section`」,不发版可回滚。
新增 `UpgradeWatermarkService`(水位/波段表/标定/注水抽水)+ `TtUpgradeWatermarkController`。
3. **判定层三身份完全隔离**:进度与随机数各存 Redis `upgrade_range_fixed:{饰品位}:{身份}`
(结构与改造前一致,老缓存不用迁移),白号中奖不影响普通玩家的进度与随机数。
4. **幸运值(展示)**:累加口径**保持不变** —— 按「玩家在前端拉的那个值」累加原始 `probability`;
**中奖判定恒用 `min(概率,75)`**,两者刻意分开(75% 封顶是风控手段,不改展示数字)。
**展示值是三个身份共享的一个累计器:任一身份(含白号)中奖即整体归零,且中奖那一拉的增量仍计入**
(出货那一刻显示的是含本次的值,下一拉才从 0 重新累计);表里那三列只用来**分别记录各身份的贡献**(后台排查),展示时相加。
`getOrnamentsList.luckyValue` 返回这个共享值(所以所有人看到的数一致),另给 `myLuckyValue`。
另留 `luckyValueMode` 开关(默认 `raw`,可切 `eff`),**只影响展示值与落库快照**。
5. **后台新增「幸运升级 → 奖池水位」页**(`menu_id=3208`):水位/实测RTP 统计卡、波段表编辑器(含名义RTP 与
**按实测标定的「预计落点水位」**)、注水抽水(原因必填)、调整历史、「按历史重算」。
6. 拉取记录新增 `watermark` / `round_target` 快照列,便于事后复盘「这批货是在什么水位下出的」。
**六、仿真结论(脚本 `.workbuddy/upgrade-watermark-sim.mjs`,40 万次拉取,可复跑)**
| 方案 | 名义 target | 实测 RTP | 实测水位 | 等效需求/名义 | 随机数<100 占比 |
|---|---:|---:|---:|---:|---:|
| **A** 现状:固定 `[100,160)` | 129 | 65.1% | 34.9% | 119% | **0.0%** |
| **C** 曾设想的「共享池 + 只清中奖方」(**已否决**) | 108 | **131.8%** | **−31.9%** | 71% | 19.2% |
| **F** 水位波段表(已实现) | 125 | **66.9%** | **33.1%** | 120% | **24.6%** |
三个关键结论:
- ⚠️ **「三身份共享一个池、中奖只清中奖方」会亏钱**:残留进度会垫薄下一轮阈值,非主力身份中奖时垫得最狠还会连锁
⇒ 仿真实测 **RTP 从 62% 飙到 131.8%、水位掉到 −32%**。所以**判定层必须按身份隔离**,
「一个奖池」只体现在**幸运值的展示**上。
- ⚠️ **名义 ≠ 实测**(见第四节):波段表的随机数必须按实测标定,按 `100/fullV` 公式拍一定偏。
- **稳态随机数的均值由目标水位唯一决定**(水位 30% ⟺ 平均随机数 ≈ 121),这是数学、改不掉;
能做的只是把档内区间**做宽**(推荐跨度 ≥ 80)。改造后随机数覆盖 75~175、**24.6% 的轮次低于 100**,
而 RTP 基本不变(65.1% → 66.9%)—— 玩家不再看到「必须 100 以上才出」的硬底。
**七、一个完整算例(对着数字走一遍)**
**前提**:饰品价 1000 元;波段表如上(当前落在 26~36% 档 ⇒ 随机数取自 `[75,175]`);
起始累计(排除白号)下注 100,000 / 返奖 65,000 / 调整 0 ⇒ **水位 = 35.00%**。
| 步 | 谁 | 拉取 | 扣款 | 该池随机数 | 判定(进度 + min(概率,75)) | 结果 | 该池进度 |
|---|---|---|---|---|---|---|---|
| 1 | 普通 A | 10% | 100 | 120(本池新抽) | 0 + 10 = 10 < 120 | 失败 | 10 |
| 2 | 普通 B | 50% | 500 | 120(沿用) | 10 + 50 = 60 < 120 | 失败 | 60 |
| 3 | 主播 C | 20% | 200 | **95**(主播池自己抽) | 0 + 20 = 20 < 95 | 失败 | 20 |
| 4 | 白号 D | 90% | 900 | **88**(白号池自己抽) | 0 + **75** = 75 < 88 | 失败 | 75 |
| 5 | 白号 D | 20% | 200 | 88(沿用) | 75 + 20 = 95 ≥ 88 | **中奖** | 0 → 重抽 130 |
| 6 | 普通 A | 75% | 750 | 120(沿用) | 60 + 75 = 135 ≥ 120 | **中奖** | 0 → 重抽 105 |
**第 5 步后三个池的状态**(判定层隔离:白号中奖,普通/主播一点没动)
| 池 | 本轮随机数 | 进度 |
|---|---|---|
| 普通 | 120 | 60 |
| 主播 | 95 | 20 |
| 白号 | 130(重抽) | 0 |
**第 4 步为什么要用 75 而不是 90**:白号拉 90% 时扣款按 90% 收(900 元)、判定只按 `min(90,75)=75` 加进度,
而界面幸运值按**原始概率**加(白号那份 +90)。⇒ 那一刻白号显示「幸运值 90」、本池随机数 88,看着早该中了,
但判定进度只有 75(还差 13)—— **实际没中**;下一步再拉 20%(75+20=95 ≥ 88)才中。这就是那个分叉的实际后果。
**幸运值展示 = 三个身份共享的一个累计值**(所有人看到同一个数;任一身份中奖即整体归零)
| 时点 | 普通 | 主播 | 白号 | 展示 luckyValue | 说明 |
|---|---|---|---|---|---|
| 起始 | 0 | 0 | 0 | **0** | |
| 步 1 后 | 10 | 0 | 0 | **10** | 普通 +10 |
| 步 2 后 | 60 | 0 | 0 | **60** | 普通 +50 |
| 步 3 后 | 60 | 20 | 0 | **80** | 主播**也加进同一个数** |
| 步 4 后 | 60 | 20 | 90 | **170** | 白号 +90 |
| 步 5 后 | 0 | 0 | 0 | **190** | 白号 +20 → **中奖 → 整体归零**;显示的是中奖那一刻的 190 |
| 步 6 后 | 0 | 0 | 0 | **75** | 0 + 75 → **中奖 → 整体归零** |
**水位的变化(白号的量不算)**
| 步 | 累计下注 | 累计返奖 | 水位 |
|---|---|---|---|
| 起始 | 100,000 | 65,000 | 35.00% |
| 1(普通 +100) | 100,100 | 65,000 | 35.06% |
| 2(普通 +500) | 100,600 | 65,000 | 35.39% |
| 3(主播 +200) | 100,800 | 65,000 | 35.52% |
| 4(白号 +900) | 100,800 | 65,000 | 35.52%(**不变**) |
| 5(白号 +200 且中奖 +1000) | 100,800 | 65,000 | 35.52%(**不变**) |
| 6(普通 +750 且中奖 +1000) | 101,550 | 66,000 | 35.01% |
白号的下注/返奖进另一组计数(后台「白号」那一栏),只展示、不影响水位。
**水位什么时候生效**:只在「新的一轮开始」读一次(该饰品位该身份首次拉取 / 刚中奖)。
本轮中途水位变化**不改手里的随机数**;若水位涨到 ≥ 36%,下一次**新开的一轮**才会从 `[85,128]` 里抽。
**落库记录(`tt_upgrade_record`,节选)**
| 步 | user_type | amount_consumed | amount_required | round_target | watermark | is_victory | lucky_value |
|---|---|---|---|---|---|---|---|
| 1 | 02 | 100 | 1200 | 120 | 35.00 | 0 | 10 |
| 4 | 06 | 900 | 880 | 88 | 35.52 | 0 | 170 |
| 5 | 06 | 200 | 880 | 88 | 35.52 | 1 | **190** |
| 6 | 02 | 750 | 1200 | 120 | 35.52 | 1 | **75** |
- `amount_required = 饰品价 × round_target / 100`。
- **中奖记录的 `lucky_value` 记的是「中奖那一刻的累计值」(含本次),不是 0** —— 与玩家当下看到的一致。
- **`watermark` 记的是「本次拉取之前」的水位** —— 代码先读水位、判定出货成功后才做增量记账,顺序不能反。
**八、改动文件**
后端 `ApiLuckyUpgradeServiceImpl`、`UpgradeWatermarkService`(新)、`TtUpgradeWatermarkController`(新)、
`TtUpgradeRewardLogMapper`(新) + XML、`RedisConstants`、`ApiLuckyUpgradeMapper.xml`、`TtUpgradeOrnamentsMapper.xml`、
实体 `TtUpgradeRecord` / `TtUpgradeRewardLog`(新) / `UpgradeWatermarkSettingBody`(新) / 两个 VO;
前端 `views/skins/luckyUpgrade/rewardPool/index.vue`(新) + api、`views/skins/ttUpgrade/index.vue`。
**九、验证**
`mvn -B clean install -DskipTests` 通过,且 **javap 产物确认 `doUpgrade` 只剩新签名、无旧 `(Integer[], UpgradeBodyA)` 残留**;
`ruoyi-ui` 构建 `DONE Build complete`;`audit-palette` 离版文件/色值 **0 / 0**;`verify-dist` **ALL_OK(34 项)**;
`audit-ops-fit` 需加宽 **0 条**。新页面确认已进 `dist` chunk。
算例里的 6 个幸运值(10/60/80/170/190/75)用脚本复刻改后逻辑跑过,全部一致。
**踩坑**:① 本项目是 **Spring Boot 4(jakarta)**,`@PostConstruct` 写成 `javax.` 会编译失败;
② **并行对同一文件发多个 Edit 会静默丢改动**(回执 success 但没落盘,仿真脚本主循环那次白排查一轮)⇒ 改完必须 grep 复验;
③ 15 万元素数组用 `Math.min(...arr)` 直接 `RangeError: Maximum call stack size exceeded`,大数组统计先 sort 再取分位;
④ **口径类需求别用文字对齐**:「幸运值」到底是「三身份各自累计、各清各的」还是「一个共享累计器、谁中奖都整体归零」,
文字上两句话都解释得通 —— 最后靠「第 5 步应该显示 190、第 6 步应该显示 75」这组数字才把模型逼出来
(若是各清各的,第 5 步应为 80;若不清零,第 6 步应为 265)。
**十、上线前必做**
1. 清空两个已存在饰品位的旧固定区间
(`UPDATE tt_upgrade_ornaments SET luck_section = NULL, anchor_luck_section = NULL, blank_luck_section = NULL;`)
—— 不清的话它们仍会被旧区间夹住,仍看得见「100 以上才出」的硬底;
2. 水位从 0 起步,`enabled=true` 时前几件货会落在收紧档,属预期,需要立刻放水就在新页面注水。
## 2026-09-22
- **docker 配置命名统一:`changzhou` → `henan`(全套 `henan_` 前缀)**