diff --git a/docs/updata_sql.md b/docs/updata_sql.md index 85c788a..6f93e39 100644 --- a/docs/updata_sql.md +++ b/docs/updata_sql.md @@ -6,6 +6,128 @@ --- +## 幸运升级·水位策略简化为「单一目标水位」:`sys_config` 配置结构变更 + +**背景**:上一版把水位策略做成了 **4 档波段表**(每档 = 水位下界/上界 + 随机数下界/上界), +后台要维护 16 个数、还要保证档间单调且留有重叠,运营难维护也容易配错。本次简化为**只填一个「目标水位(%)」**: +随机数的**中心**由目标水位自动反推,**取值范围**仍由每个饰品自己的「随机数区间」限定 +(水位高于目标 → 落点靠近区间下限)。**口径推导、换算表、仿真数据与算例见 `docs/修改记录.md` 的 2026-09-24 条。** + +**本次数据库变更 1 处**(2026-09-24 已在库 `henan` 执行):`sys_config.config_key = 'luckyUpgradeWatermark'` 的 +`config_value` 由「波段表」改为「目标水位 + 灵敏度」,`bands` 字段废弃(后端已不再读取,留着不报错)。 + +> ⚠️ **前置依赖**:本条的 `UPDATE` 依赖下一节「幸运升级·水位驱动改造」插入的那行 `sys_config` +> —— 全新库必须先执行那一条,再执行本条。 +> +> ⚠️ **同时恢复**(代码侧,非 SQL):饰品列表页的「随机数区间」编辑入口**重新启用**, +> `tt_upgrade_ornaments.luck_section` / `anchor_luck_section` / `blank_luck_section` 现在**必须配置**, +> 且要**包含按目标水位推算出的中心值**,否则中心被夹在边界上、水位停不到目标值。 +> ⇒ 下一节里「上线前清空三个区间列」的说法**已作废**,不要再执行那两条清空 SQL。 + +**变更 SQL**: + +```sql +UPDATE `sys_config` + SET `config_value` = '{"enabled":true,"targetWatermark":30,"gain":2,"luckyValueMode":"raw"}', + `remark` = '幸运升级水位策略:目标水位%(平台希望长期维持的净收益比例)、灵敏度 gain、幸运值展示口径 raw/eff。随机数中心 = 100/((1-目标水位/100)*实测标定系数),再由每个饰品的随机数区间限定取值范围;中奖判定恒用 min(概率,75)。enabled=false 逐字节回退固定区间', + `update_time` = NOW() + WHERE `config_key` = 'luckyUpgradeWatermark'; +``` + +**字段口径**: + +| 字段 | 含义 | +|---|---| +| `enabled` | `true` = 按水位动态出随机数;`false` = 逐字节回退「每个饰品位固定 `luck_section`」 | +| `targetWatermark` | **目标水位(%)**:平台希望长期维持的净收益比例。30 ⇒ 长期平均 RTP ≈ 70% | +| `gain` | 响应灵敏度:水位每偏离目标 1 个点,随机数中心反向调整 `gain`%。默认 2,后台不展示(调参用) | +| `luckyValueMode` | 幸运值**展示**口径:`raw`(默认,累加前端原始概率)/ `eff`(累加 `min(概率,75)`);**不影响判定** | + +--- + +## 幸运升级·水位驱动改造:水位日志表、拉取记录快照列、水位策略配置与「奖池水位」菜单 + +**背景**:幸运升级原来每个饰品位配一套固定幸运区间,随机数永远落在区间内(如 `[100,160)`), +玩家能看出「每次都要投到 1 倍原价以上才出」。本次改为**按全局水位动态决定本轮的随机数区间**: +`水位 = (Σ下注 − Σ返奖 + 人工调整) / Σ下注`(排除白号),水位越高 → 可出的随机数越低。 +同时新增后台人工注水/抽水。**方案、换算表、口径推导与算例全部汇总在 `docs/修改记录.md` 的 2026-09-23 条**(不另建单独文档)。 + +**本次数据库变更 4 处**(2026-09-23 已在库 `henan` 执行): + +| 对象 | 变更 | 说明 | +|---|---|---| +| `tt_upgrade_reward_log` | **新建表** | 注水/抽水审计日志,`delta` 为带符号影响,水位调整额 = `SUM(delta)`(DB 为真相,Redis 只是短缓存) | +| `tt_upgrade_record` | 新增 **2 列** | `watermark`(本次拉取时的水位%)、`round_target`(本轮抽到的随机数快照)| +| `sys_config` | 新增 **1 行** | `config_id=155`、`config_key='luckyUpgradeWatermark'`,水位波段表 + 幸运值展示口径;`enabled=false` 可逐字节回退旧行为 | +| `sys_menu` / `sys_role_menu` | 各新增 **1 / 2 行** | `menu_id=3208`「幸运升级 → 奖池水位」,授权给 `role_id` 101(三恒)、103(Fdily)| + +> **为什么 `menu_id` 用 `3208`**:`3200~3207` 是麻将胡了的号段,按顺序往后取,不复用已下架玩法的旧号段。 + +**变更 SQL**: + +```sql +CREATE TABLE IF NOT EXISTS `tt_upgrade_reward_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'PK', + `type` tinyint NOT NULL COMMENT '1注水 2抽水 3重置', + `amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额(绝对值,展示用)', + `delta` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '对水位的带符号影响(注水正/抽水负/重置为抵消值)', + `watermark_before` decimal(8,2) DEFAULT NULL COMMENT '调整前水位%', + `watermark_after` decimal(8,2) DEFAULT NULL COMMENT '调整后水位%', + `operator` varchar(64) DEFAULT NULL COMMENT '操作人', + `remark` varchar(200) DEFAULT NULL COMMENT '调整原因(必填,前端保证)', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_create` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='幸运升级奖池水位调整日志'; + +ALTER TABLE `tt_upgrade_record` + ADD COLUMN `watermark` decimal(8,2) NULL COMMENT '本次拉取时的水位%(1-RTP,排除白号,含人工调整)', + ADD COLUMN `round_target` int NULL COMMENT '本轮抽到的随机数 fullV 快照'; + +INSERT INTO `sys_config` + (`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) +VALUES + (155, '幸运升级水位策略', 'luckyUpgradeWatermark', + '{"enabled":true,"luckyValueMode":"raw","bands":[{"from":-999,"to":10,"vMin":95,"vMax":130},{"from":10,"to":26,"vMin":115,"vMax":150},{"from":26,"to":36,"vMin":75,"vMax":175},{"from":36,"to":999,"vMin":85,"vMax":128}]}', + 'Y', 'admin', NOW(), '', NULL, '幸运升级水位策略:波段表(水位%→随机数区间) + 幸运值展示口径 raw(原始概率)/eff(与判定同口径);中奖判定恒用 min(概率,75)。enabled=false 逐字节回退固定区间'); + +INSERT INTO `sys_menu` + (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) +VALUES + (3208, '奖池水位', 2075, 3, 'rewardPool', 'skins/luckyUpgrade/rewardPool/index', NULL, 1, 0, 'C', '0', '0', 'skins:upgrade:pool', 'money', 'admin', NOW(), '', NULL, '幸运升级·奖池水位(全局水位/人工注水抽水/调整历史)'); + +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (101, 3208), (103, 3208); +``` + +**验证**(2026-09-23 实测结果): + +```sql +SELECT COUNT(*) FROM information_schema.tables + WHERE table_schema='henan' AND table_name='tt_upgrade_reward_log'; -- 期望 1,实测 1 +SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema='henan' AND table_name='tt_upgrade_record' + AND column_name IN ('watermark','round_target'); -- 期望 2,实测 2 +SELECT COUNT(*) FROM sys_config WHERE config_key='luckyUpgradeWatermark'; -- 期望 1,实测 1 +SELECT COUNT(*) FROM sys_menu WHERE menu_id=3208; -- 期望 1,实测 1 +SELECT COUNT(*) FROM sys_role_menu WHERE menu_id=3208; -- 期望 2,实测 2 +``` + +后台左侧「幸运升级」目录下出现「奖池水位」页(`/luckyUpgrade/rewardPool`)。 + +**注意:水位策略上线后的运营动作** + +> ⚠️ **本节第 1 条已被本文件顶部「水位策略简化为单一目标水位」那条取代**: +> `luck_section` / `anchor_luck_section` / `blank_luck_section` **重新成为必填项** +> (语义 = 随机数的允许取值范围),**不要清空**;而且要配成**包含「按目标水位推算出的中心值」**的区间。 +> 清空后后端会用兜底区间 `[80,160]`,能用但不受控。 + +1. ~~两个已存在的饰品位(`tt_upgrade_ornaments.id` 18 / 25)的 `luck_section` 要清空,才能完全跟随全局波段~~ + —— **已作废**,见上方提示。 +2. 水位从 0 起步。`enabled=true` 时前几件货会落在收紧档(水位低 → 随机数高),属预期行为; + 需要立刻放水用后台「奖池水位 → 注水」。 + +--- + ## 新增「麻将胡了」(mahjong 玩法)的后台菜单、角色授权与维护开关 **背景**:麻将胡了以「**新游戏**」类型接入,复用原玫瑰女郎(olympus 玩法)的整套统计口径 diff --git a/docs/修改记录.md b/docs/修改记录.md index 9ca4500..084be9d 100644 --- a/docs/修改记录.md +++ b/docs/修改记录.md @@ -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_` 前缀)** diff --git a/docs/幸运升级接口文档.md b/docs/幸运升级接口文档.md index 4e83b42..366a9b6 100644 --- a/docs/幸运升级接口文档.md +++ b/docs/幸运升级接口文档.md @@ -1,6 +1,12 @@ # 幸运升级接口文档(Cocos 客户端对接) -> 适用版本:当前 master。所有接口路径前缀 `/api/luckyUpgrade`(另有 1 个关联接口在 `/api/userPackSack`)。 +> 适用版本:当前 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`。 --- @@ -15,8 +21,8 @@ ↓ 校验(身份 / 封禁 / 删除 / 未充值 703 / 余额 702) ↓ 加锁(饰品位 + 奖池类型)→ 扣款(source=40 幸运升级消费) ↓ 判定(见 1.2) - ├─ 成功:获得该饰品位对应的整件饰品,进度与幸运值清 0 - └─ 失败:获得后台配置的「失败奖励」饰品(安慰奖,可多件),进度与幸运值累加 + ├─ 成功:获得该饰品位对应的整件饰品,**该身份**的进度与随机数清 0 / 重抽(其他身份不受影响) + └─ 失败:获得后台配置的「失败奖励」饰品(安慰奖,可多件),**该身份**的进度累加 ↓ 写 tt_upgrade_record 记录(含各项快照)+ 背包 tt_box_records(source=4 幸运升级)+ 站内消息 ↓ 返回升级记录(含 gainOrnamentList,可用于结果页立即分解) ``` @@ -27,23 +33,72 @@ |---|---| | 单次消耗 | `price = 饰品实时价(tt_ornament.use_price) × probability / 100`,保留 2 位小数(**前端不传价格**,由后端算,防 0 元 100% 中奖) | | 判定概率 | `eff = min(probability, 75)`(**封顶 75**:选 90% 也只按 75% 判定,但钱按 90% 收) | -| 本轮空间 | `fullV`:从该饰品位对应奖池的幸运区间 `[min, max)` 里随机取 `nextInt(max-min) + min` | -| 判定 | 每拉一次 `进度 += eff`;`进度 + eff >= fullV` 即中奖,中奖后 `fullV` 重新随机、进度清 0;否则失败继续累加 | -| 推论 | **玩家为一件饰品付出的总成本 ≈ 饰品价 × fullV / 100**。所以幸运区间 = 该池的"售价百分比区间":`[50,250]` 表示平均要付 50%~250% 原价才出;压到个位数(如 `[9,10]`)等于必中、平台必亏 | +| 本轮空间 | `fullV`:由**当前全局水位**自动推算 —— 先按目标水位反推中心,再在该饰品自己的区间 `[min, max]` 内做**偏斜随机**(支撑集是整个区间,水位高时下限附近的数更频繁)。水位见 1.3 | +| 判定 | 每拉一次 `进度 += eff`;`进度 + eff >= fullV` 即中奖,中奖后**只刷新当前身份**的 `fullV`、该身份进度清 0;**其他身份的进度与随机数一律不受影响**;否则失败继续累加 | +| 推论 | **玩家为一件饰品付出的总成本 ≈ 饰品价 × fullV / 100**,即 `理论 RTP = 100 / fullV`、`水位 = 1 − 100/fullV`。⇒ 想让水位能长期停在某个值,饰品的随机数区间必须**包含**该水位对应的中心随机数 | -### 1.3 幸运值与幸运区间:三套独立奖池 +> ⚠️ **名义 ≠ 实测**:判定是「跨过阈值即出货、最后一拉整段 eff 计入并丢弃溢出」,实测等效需求是名义值的 +> **118%~120%**。所以**不要按 `100/fullV` 公式拍参数**,按后台「奖池水位」页的实测 RTP 与标定系数来调。 -`幸运值` = 该池"累计原始概率",失败累加 `probability`(原始值,不是封顶后的 75)、中奖清 0;界面与记录里展示的就是它。 +### 1.3 幸运值、水位与三个身份池 -| 奖池 | 判定/显示用 userType | 幸运区间字段 | 幸运值字段 | 本轮已投金额字段 | -|---|---|---|---|---| -| 普通用户 | `02` | `tt_upgrade_ornaments.luck_section` | `probability_invested` | `amount_invested` / `effective_amount_invested` | -| 主播 | `01` | `anchor_luck_section` | `anchor_probability_invested` | `anchor_amount_invested` / `anchor_effective_amount_invested` | -| 白号 | `06`(`is_blank_account=1` 优先判定) | `blank_luck_section`(未配置回退 `luck_section`) | `blank_probability_invested` | `blank_amount_invested` / `blank_effective_amount_invested` | +**水位(全局,2026-09-23 起)**: -- 三池**完全隔离**:进度缓存在 Redis `upgrade_range_fixed:{饰品位}:{池类型}` / `upgrade_range:{饰品位}:{池类型}`,互不影响、互不阻塞(锁也是按「饰品位 + 池类型」)。 -- 同一池内是**共享进度**:同一个池子的玩家一起把进度推到 `fullV`,谁那一拉跨过阈值谁中奖(和普通/主播现有行为一致,白号同理,白号之间共享 06 池)。 -- 白号记录里的幸运值/已投金额读的都是白号自己的池,不再是真实玩家池的读数。 +``` +水位% = (Σ下注 − Σ返奖 + 人工调整) / Σ下注 × 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 获得与失败奖励 @@ -132,7 +187,8 @@ | usePrice | BigDecimal | 实时价格(= 计算消耗、展示目标金额的基准) | | imageUrl | String | 饰品图片 | | shortName / typeName / rarityName / exteriorName / levelImg | String | 简称 / 类型 / 稀有度 / 磨损度 / 品质图 | -| luckyValue | Integer | **当前幸运值**(按登录身份取对应池:普通/主播/白号) | +| luckyValue | Integer | **当前幸运值**(三个身份共享的一个累计值,所有人看到的数一致);口径见 1.3。**任一身份中奖即整体归零** | +| myLuckyValue | Integer | 查看者所属身份对共享值的贡献(自上次任一身份中奖以来);排查用,一般不用 | ### 4.2 `POST /api/luckyUpgrade/upgrade` — 拉取一次 @@ -210,7 +266,7 @@ |---|---|---| | userId / avatar / nickName / vipLevel | - | 玩家信息(昵称是升级时快照) | | probability | Integer | 成功率(原始概率,如 89 表示 89%) | -| luckyValue | Integer | 幸运值快照(按该玩家所属池;白号是白号池的值) | +| luckyValue | Integer | 幸运值快照(**中奖记录记的是「中奖那一刻」的值,含本次**),与 `getOrnamentsList.luckyValue` 同口径 | | amountConsumed | BigDecimal | 消耗 | | gainOrnamentsPrice | BigDecimal | 获得饰品总价值 | | isVictory | Boolean | 成败:true=升级成功(前端胜负样式用) | @@ -240,7 +296,7 @@ |---|---|---| | avatar / nickName / vipLevel | - | 拉取人信息 | | probability | Integer | 本次拉取**选择的成功率**(不是中奖率) | -| luckyValue | Integer | 本次拉取时的**幸运值快照**(累计原始概率;成功为 0),与 `recordPage` 同口径 | +| luckyValue | Integer | 本次拉取时的**幸运值快照**(含本次的增量;中奖那一拉记的就是出货那一刻的值),与 `recordPage` 同口径 | | isVictory | Boolean | 成败:true=升级成功 | | name / imageUrl / usePrice / exteriorName / levelImg | - | **目标饰品**("谁在拉这个饰品",不是获得物) | | openTime | String | 拉取时间 | @@ -291,8 +347,8 @@ 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) 三套独立,白号看到的是白号池的值;同一池内进度是共享的,别人垫的进度也算在你头上。 +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` 就是分解入参;不调用 = 饰品留在背包。 @@ -306,16 +362,20 @@ | 后台列 | 字段 | 说明 | |---|---|---| -| 普通用户 / 幸运区间 | `luck_section` | 普通用户池的 `[min, max)`,即"售价百分比区间" | +| 普通用户 / 随机数区间 | `luck_section` | **随机数的允许取值范围**(`[min,max]` JSON),水位推算出的中心必须落在里面 | | 普通用户 / 本轮已投金额 | `amount_invested` | 普通池当前累计投入(运行时值,后台不可编辑回写) | -| 主播用户 / 幸运区间 | `anchor_luck_section` | 主播池区间 | -| 白号用户 / 幸运区间 | `blank_luck_section` | 白号池区间;为空则回退 `luck_section` | -| 白号用户 / 本轮已投金额 | `blank_amount_invested` | 白号池当前累计投入 | +| 主播用户 / 随机数区间 | `anchor_luck_section` | 同上,主播身份独立配置 | +| 白号用户 / 随机数区间 | `blank_luck_section` | 同上,白号身份独立配置;未配则回退 `luck_section` | | 状态 | `status` | 0=启用(=可升级、会出现在客户端列表) 1=禁用 | -- 调难度就调"幸运区间":区间 = 玩家为该饰品付出的**价格百分比**。想更容易出就整体下调(如 `[80,280]`→`[60,200]`);**不要压到个位数**(如 `[9,10]` 等于必中,等于按 9% 价格卖货),也不要配成 `min == max`(会被后端防护强制改成 `[0,160]`)。 -- **改完区间要清缓存才立即生效**:`DEL upgrade_range:{饰品位}:{池}` + `DEL upgrade_range_fixed:{饰品位}:{池}`(池=01/02/06)。不清的话当前这一轮仍按旧空间跑,等本轮中奖后自动按新配置重建。Redis 实测在 db0,密码见 `application-*.yml`。 -- 白号是独立奖池,可与普通/主播配不同的区间;「批量填货」新增饰品位时白号区间默认取普通用户的默认值。 +- **随机数的中心不在这里配**:中心由「目标水位」自动推算(页面顶部水位条显示 当前水位 / 目标水位 / + 实测 RTP / 推算随机数中心),这里只配**取值范围**。调难度请改目标水位(见 6.4)。 +- ⚠️ **区间必须包含推算出的中心值**,且**中心离两端各留约 10% 区间宽度**,否则中心被夹住、水位会停在附近 + 而不是目标值(仿真实测可偏 16~20 个点)。区间还**限定了能维持的水位范围**:`[100,160]` 只能维持约 `[20.7%, 45.4%]`。 +- **整个区间始终都可能出**(偏斜随机),水位只改变概率重心;水位 = 目标时是均匀分布。 + 区间越宽,单轮随机数波动越大、越难被玩家归纳出规律。 +- 区间改完**下一轮立即生效**,不用清 Redis(水位驱动下直接读实体字段,不走区间缓存)。 +- ⚠️ 旧版本文档里「上线前清空三个区间列」的做法**已作废** —— 现在这三列是必填项。 ### 6.2 「幸运升级 → 失败奖励」(`tt_upgrade_fail_ornaments`) @@ -328,13 +388,78 @@ | `websiteMaintenance` | `1` | 全站维护:`upgrade` 与分解接口都拦截 | | `luckyUpgradeMaintenance` | `1` | 只拦幸运升级:`upgrade` 返回「幸运升级维护中......」 | -### 6.4 数据落库 +### 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`) | +| `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_ornaments` | 饰品位配置 + 三个身份的运行进度(各身份累计幸运值/已投金额) | +| `tt_upgrade_reward_log` | 水位人工调整日志(注水/抽水/重置;`delta` 为带符号影响,水位调整额 = `SUM(delta)`) | 字段变更历史见 `docs/updata_sql.md`(含白号三列的 ALTER)。 diff --git a/ruoyi-ui/src/api/skins/luckyUpgrade/rewardPool/api.js b/ruoyi-ui/src/api/skins/luckyUpgrade/rewardPool/api.js new file mode 100644 index 0000000..ad4a127 --- /dev/null +++ b/ruoyi-ui/src/api/skins/luckyUpgrade/rewardPool/api.js @@ -0,0 +1,53 @@ +import request from '@/utils/request' + +// 当前水位快照 + 波段表策略 + 本轮生效的随机数区间 +export function getWatermarkInfo() { + return request({ + url: '/admin/upgradeWatermark/info', + method: 'get' + }) +} + +// 保存水位波段表策略(后端带校验:区间合法 + 水位越高随机数越小) +export function saveWatermarkSetting(data) { + return request({ + url: '/admin/upgradeWatermark/setting', + method: 'post', + data: data + }) +} + +// 注水 / 抽水(type 1注水 2抽水;调整原因必填) +export function adjustWatermark(data) { + return request({ + url: '/admin/upgradeWatermark/adjust', + method: 'post', + data: data + }) +} + +// 重置人工调整(插入抵消日志,SUM(delta) 归零,历史保留) +export function resetWatermarkAdjust(data) { + return request({ + url: '/admin/upgradeWatermark/adjustReset', + method: 'post', + data: data + }) +} + +// 调整历史(分页) +export function getWatermarkLogs(params) { + return request({ + url: '/admin/upgradeWatermark/adjustLog', + method: 'get', + params: params + }) +} + +// 按历史记录全量重算累计下注/返奖(纠正 Redis 计数漂移) +export function recalcWatermark() { + return request({ + url: '/admin/upgradeWatermark/recalc', + method: 'post' + }) +} diff --git a/ruoyi-ui/src/views/skins/luckyUpgrade/rewardPool/index.vue b/ruoyi-ui/src/views/skins/luckyUpgrade/rewardPool/index.vue new file mode 100644 index 0000000..19f72da --- /dev/null +++ b/ruoyi-ui/src/views/skins/luckyUpgrade/rewardPool/index.vue @@ -0,0 +1,530 @@ + + + + + + + diff --git a/ruoyi-ui/src/views/skins/ttUpgrade/index.vue b/ruoyi-ui/src/views/skins/ttUpgrade/index.vue index 017d068..58a1a48 100644 --- a/ruoyi-ui/src/views/skins/ttUpgrade/index.vue +++ b/ruoyi-ui/src/views/skins/ttUpgrade/index.vue @@ -44,6 +44,31 @@ + +
+ + 全局水位 + {{ wm.watermark == null ? '—' : Number(wm.watermark).toFixed(2) + '%' }} + + + 目标水位 + {{ wm.targetWatermark == null ? '—' : Number(wm.targetWatermark).toFixed(1) + '%' }} + + + 实测RTP + {{ wm.rtp == null ? '—' : Number(wm.rtp).toFixed(2) + '%' }} + + + 推算随机数中心 + {{ wm.targetCenter == null ? '—' : Number(wm.targetCenter).toFixed(1) }} + + + {{ wm.enabled ? '水位驱动中' : '已关闭(回退固定区间)' }} + + 随机数在其下方每个饰品自己的区间内、围绕按目标水位推算的中心波动:水位高于目标 → 落点靠近区间下限。 + 奖池水位 / 注水抽水 +
+ @@ -89,7 +114,7 @@ - +