Files
henan-backend/docs/修改记录.md
T

1022 lines
84 KiB
Markdown
Raw 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.
# 修改记录
> **只记大改动**:一个需求 / 一个模块算一条;同一件事的多轮微调合并成一条,不逐轮记。
> **排序:最新在最上面。**
> 展开细节看 `docs/updata_sql.md`(数据库改动,含完整可执行 SQL)与 `.workbuddy/memory/YYYY-MM-DD.md`(过程与踩坑)。
> **维护**:完成一个大改动后,在顶部按日期加一条;同一天追加在该日期块末尾。
---
## 2026-09-27
- **管理后台按钮级权限补全:`sys_menu` 补 24 行 `F` + 防漂移校验脚本**
**起因**:用户问「管理后台会不会有只有超管能看到、非超管看不见的页面内按钮」。核查结论:**会,而且比按钮更严重**。
**机制**:按钮显隐走前端 `v-hasPermi`、接口放行走后端 `@PreAuthorize("@ss.hasPermi(...)")`,
两边比对的是**同一份 `sys_menu.perms`**;超管(`user_id==1`)拿 `*:*:*` 所以永远看得见。
⇒ **某权限串只要在 `sys_menu` 里没有对应行,就永远授权不到任何非超管角色**:按钮看不见、部分列表/详情接口直接 403。
**盘出的问题**(代码在用、库里未登记,并集 43 串):
- 4 个孤儿页面(开箱概率 / 首充奖励 / 月度充值 / 换装记录)占 23 串 —— 已核实无菜单、无组件引用、无路由、无接口复用方,
**不可达,本期不动**(手输 URL 也 404,因 RuoYi 路由由 `/getRouters` 动态注册)。
- 其余 20 串 + `admin:box:*` 复用组补 4 行 = **24 行**,本次补齐。
- 结构性漂移:**从 `menu_id` 31xx 段起,新增页面只建 `C` 行、不建 `F` 行**(F 行分布:1000 段 61、2000 段 18、3100 段 9、3200+ 段 0)。
**改动清单**
1. `sys_menu` 新增 24 行 `F`(`menu_id 3209~3232`,挂 2035/2038/2086/2092/2123/3118 六个 C 行下)。
2. `sys_role_menu` 动态补齐:把新 F 行补给「已授权其父 C 行」的角色(本次实写 24 行,全给 `role_id=104`)。
3. `sys_menu` 修正 3 行(`3160~3162`)`parent_id`:目录 `3156` → 页面 `3157`(F 行挂错层级,父子联动带不到)。
4. **代码侧配套**:新增 `.workbuddy/verify-perms.cjs` —— 扫前端 `v-hasPermi` + 后端 `@PreAuthorize`,
与 `sys_menu.perms` 做双向差集 + 校验全库 F 行 parent 必须指向 C 行。非 0 退出码,建议挂 `publish.sh` 前防复发。
**验证**:库层 5 项全对(24 行 / 0 重复 / 0 挂错父节点 / 0 未授权 / `role_menu` 222→246);
`node .workbuddy/verify-perms.cjs` ⇒ **ALL_OK 6 passed / 0 failed**。
**注意**:① 权限生效需用户**重新登录**(`permissions` 登录时从 `/getInfo` 拉取,不热更新)。
② 执行时库内授权已被重排 —— 只有 `role_id=104`(测试人员)持有这些页面,`三恒` 等角色**0 菜单**,
且多个启用用户(三恒/国荣/s1/zzyzzy123/Fdily)**未分配任何角色**,这是业务配置问题,本次未动。
细节 SQL 见 `docs/updata_sql.md`「管理后台按钮级权限补全」,过程与踩坑见 `.workbuddy/memory/2026-09-27.md`。
- **推广后台「佣金明细记录」等 3 张报表右侧留白 → 全列 `width` 改 `min-width`**
**起因**:用户截图 `promo-ui2/src/views/CommissionReport/index.vue` 的「佣金明细记录」表,说「这里表格填充不满」。
**根因**(读 Element Plus 源码定论,不是猜):`element-plus/es/components/table/src/table-layout.mjs:100-171`
的 `updateColumnsWidth()` 只在 **存在弹性列** 时才把表格撑满容器
(`flexColumns = flattenColumns.filter(c => !isNumber(c.width))`);
全列都是数字 `width` 时走 else 分支 ⇒ `bodyWidth = Σ realWidth` ⇒ **表格宽恒等于列宽之和,容器再宽右侧也留白**。
⚠️ 关键陷阱:`width="170"` 这种**静态字符串**也会被 `table-column/render-helper.mjs:39` 的
`parseWidth()`(= `Number.parseInt`)转成数字 170 ⇒ 那是**固定列**;
反过来只写 `min-width` 时 `setColumnWidth()` 会执行 `column.width = void 0` ⇒ 那才是**弹性列**。
**改动清单**(`promo-ui2/src/views/CommissionReport/index.vue`,3 张表共 16 列;另有 6 张表见本节末):
1. 「佣金明细记录」6 列(本地时间170 / 手机号130 / 该次消费120 / 佣金比例90 / 佣金金额100 / 来源120)
2. 「佣金日报」5 列(手机号130 / 今日消费120 / 来源120 / 佣金金额120 / 日期130)
3. 「佣金汇总」5 列(手机号130 / 来源120 / 消费金额120 / 本次佣金120 / 参加游戏次数140)
—— **一律 `width` → `min-width`,数值不变**。选「全列都变弹性」而不是「只挑一列变弹性」:
弹性的那一列会独吞全部余量(1280 视口下 170px 的时间列会涨到 ≈590px),全列按 min-width 等比摊才均匀。
模板顶部加了注释说明为什么不能改回 `width`。脚本/接口/分页/过滤逻辑一行未动。
**验证**(3 件,全部 ALL_OK):
- SFC 编译:`node .workbuddy/verify-promo-vue-compile.cjs src/views/CommissionReport/index.vue` ⇒ **ALL_OK 4/0**
(连带跑 Home ⇒ 11/0,未受影响)。
- 算法级多视口回归:`node .workbuddy/verify-ep-table-stretch.cjs src/views/CommissionReport/index.vue`
⇒ **ALL_OK 15/0**。1280/1440/1600/1920/2560 五个视口下 3 张表**留白全为 0px、`scrollX` 全 false**。
- 真产物:`node .workbuddy/check-promo-modules.cjs src/views/CommissionReport/index.vue --has=min-width --prop=min-width:16`
⇒ **ALL_OK**(用户常驻的 `:4000` dev server 上主模块 200、`"min-width"` prop 恰好 **16** 次、0 处编译错误串)。
- 全站审计:`node .workbuddy/audit-table-fill-ep.cjs` ⇒ CommissionReport 3 张表从「✗ 不能撑满」转为有弹性列。
**新增脚本(3 个,都是 promo-ui2 / Element Plus 侧原先缺的)**:
`.workbuddy/audit-table-fill-ep.cjs`(全站扫「全固定列 ⇒ 留白」)、
`.workbuddy/verify-ep-table-stretch.cjs`(复现 `updateColumnsWidth` 做多视口定量回归)、
`.workbuddy/check-promo-modules.cjs`(从 `:4000` 拉真实编译产物做断言)。
原有的 `sim-table-width.cjs` / `verify-table-fill.cjs` 只适用 ruoyi-ui(Element UI 2.x),**不要混用**。
**注意 / 一并修的另外 6 张**:审计出全站共 **9 张**同样会留白的表,用户拍板「6 张全改」,已一并完成:
`BusinessManage/index.vue` 3 张(招商 Σ1128 / 主播 Σ828 / 普通用户 Σ920,后两张在展开行内)、
`CardMerchant/index.vue:44`(Σ1210)、`subFlow/index.vue:20`(Σ1570)、`SubordinateRecharge/index.vue:23`(Σ760)。
- 改法一致:**数据列 `width` → `min-width`**;**「操作」列保持 `width`**(CardMerchant 100、subFlow 120)——
操作列固定宽是设计意图,不该被拉宽。BusinessManage 的 `type="expand"` 列也不动(`cellStarts` 强制 48px 固定)。
- **嵌套子表(BusinessManage 二/三级)**:容器宽度由父级展开行决定,**不能用「视口 − 288」推算**。
`verify-ep-table-stretch.cjs` 已加 `depth` 判定,嵌套表只做「有没有弹性列」的定性断言,不套用视口断言。
- **两张宽表仍会横向滚动**(改 min-width 治不了,属列宽总量问题):`CardMerchant` Σ1210 ⇒ 视口 ≤1440 横滚(差 58px);
`subFlow` Σ1570 ⇒ 视口 ≤1600 横滚。**这两个视口下改前改后都是横滚**,未新增;≥1600 / ≥1920 起改为撑满(改前是留白)。
若要 1440 也不横滚,得再压列宽(`使用时间/创建时间` 180→165、`卡密` 200→190 之类),本次未做,等确认。
- 复验:全站审计 **不能撑满 0 / 34**;SFC 编译 **19/0 ALL_OK**;列宽回归 **22/0 ALL_OK**(6 张表 × 视口);
真产物(`:4000`)4 个文件主模块全 200、`"min-width"` prop 计数 **22 / 8 / 6 / 13** 逐一吻合、0 错误串。
⚠️ **未经浏览器实测**(本机 Chrome 起不来,见 `.workbuddy/memory/theme-skin.md`)⇒ 效果请自己刷 `:4000` 确认。
- **修复:幸运升级「水位驱动」因 mapper 里 `SUM(x, 0)` 语法错而整条不可用**
**现象**:启动日志 `TtUpgradeRewardLogMapper.xml` → `sumRecordTotals` 报
`java.sql.SQLSyntaxErrorException: ... right syntax to use near '' at line 4`,
外层 WARN「幸运升级水位驱动初始化失败(不阻断启动,首次拉取时会自动重试)」。
**根因**:`TtUpgradeRewardLogMapper.xml` 的 `blankBet` / `blankWin` 两行把外层 `COALESCE(..., 0)` 的写法抄进了 `SUM()` ——
`SUM(CASE WHEN is_blank THEN r.amount_consumed END, 0)`。**`SUM()` 只接受一个参数**,第二个 `, 0` 直接语法错。
(同文件 `totalBet` / `totalWin` 两行写的是 `CASE WHEN is_blank THEN 0 ELSE ... END`,是对的,可作对照。)
**改动**:删掉 `SUM(...)` 里多余的 `, 0`(外层 `COALESCE(SUM(...), 0)` 保留,仍是 null-safe)。全仓库仅此一处。
**影响面(不是"只是个 WARN")**:
- `ensureInit(false)`(`@PostConstruct` 预热)异常 → Redis 累计量 `KEY_BET/KEY_WIN/KEY_BLANK_*` **从未写入**,
`KEY_INIT_AT` 也没落 ⇒ 每次调用都重新全表扫并再次失败,**水位恒为 0**,水位驱动实际等于停摆。
- 后台「奖池水位 → 按历史重算」`/recalc` 调 `ensureInit(true)` 且**没有 try/catch** ⇒ 该按钮**必 500**。
- `calibrationFactor()` 有 try/catch → 失败返回 0 → 回落兜底系数 1.19(这条不受影响)。
**验证**:库里实测 `SELECT SUM(CASE WHEN 1 THEN 2 END, 0)` 复现同一 `ERROR 1064`;
改成 `SUM(CASE WHEN ... END)` 后整条 `sumRecordTotals` 跑通出数。XML 重新解析通过。
**上线动作**:`./ry.sh stop` → `mvn clean install`(见 `USER.md` 的「必须 clean install,否则嵌套旧 jar」)→ `./ry.sh start`。
- **推广后台登录页新增「开发环境账号密码登录」**
**起因**:推广后台唯一登录入口是 `/promo/smsLogin`(手机号 + 4 位短信验证码 + 滑块),本地联调要连短信通道、拖滑块;
后端此前已明确下线账号密码登录(`PromoLoginServiceImpl` 注释:「账号密码登录已下线……避免用密码绕过短信校验被撞库」)。
**改动清单**(前后端配合,**默认关闭**):
1. **后端新增 `POST /promo/devLogin`**(`@Anonymous`):账号解析直接复用 `createUserToken` 的 `promo_user_` 入口
⇒ 推广账号(`tt_user` 的主播/招商/卡商,登录名=手机号)与超管(`promo_admin` 的 `username` 或 `phone_number`)
**都能登录**,区别只是「短信验证码」换成了 `SecurityUtils.matchesPassword` 的 BCrypt 密码校验。
账号不存在 / 停用 / 密码错统一返回「账号或密码错误」(防账号枚举,真实原因走 `log.warn`)。
2. **开关走配置文件而非数据库**:新增 `promo.dev-login.enabled` —— `application.yml` 默认 `false`,`application-dev.yml` 置 `true`,
生产 profile 不覆盖 ⇒ 接口恒返回「账号密码登录未开启」,且后台管理员无法误开。
3. `PromoWebConfig` 放行 `/promo/devLogin`(维护拦截器不拦登录类接口,与 smsLogin/register 同口径)。
4. **前端** `LoginForm.vue` 只在 `import.meta.env.DEV`(即 `pnpm run dev`)下渲染「短信验证码 / 账号密码」切换与密码表单;
登录成功后复用原有的 `completeLogin` + `enterSystem` 落地流程。`api/login` 增加 `devLoginApi` 与 `DevLoginType`。
**验证**:`vue-tsc --noEmit` 改动文件 0 报错(仓库其余 30+ 条为既存错误);`mvn -B -DskipTests compile` ⇒ `BUILD SUCCESS`。
**无需数据库变更**:开关是配置文件项,不是 `sys_config`(相对 `promoMaintenance` 的取舍见 `.workbuddy/memory/2026-09-27.md`)。
**使用注意**:密码按 BCrypt 校验,测试账号若不知明文,查 `tt_user.remark`(格式 `明文密码:xxx`)或 `promo_admin` 表。
- **管理后台首页改排版:11 张等宽统计卡 → 「左窄轨 + 右主区」两栏**
**起因**:用户反馈首页「排版还是和原来一样,只是改了 UI 的风格」——换肤把配色圆角阴影改了,但骨架仍是
「页头 → 卡片网格 → 区块 → 区块」的垂直堆叠模板,要求**排版差异化、风格照旧**。
**改动清单**(`ruoyi-ui/src/views/index.vue`,单文件)
1. **骨架拆两栏**:`grid-template-columns: 300px minmax(0,1fr)` + `align-items:start`。
- **左窄轨**(`position:sticky; top:16px`,跟随滚动):3 个核心大数字(在线用户 / 总充值 / 总流水,字号 28px)
+ 待办区(待处理提货申请 / 提货余额)。待处理 >0 时雏菊金描边 + 数值变色。
- **右侧主区**:实时 RTP → 「经营数据」紧凑指标带(6 项 `mini-stat`,小图标 + 标签 + 数值)→ 流水统计图
(顺序按用户看完后的要求定:图表 400px 最高,放最底部滚到底正好)。
2. **指标一项不丢**:原 `statCards` 11 项拆成 `railMetrics`(3) + `miniStats`(6) + 待办区硬编码 2 项 = 11。
原「图标 + 大数值 + 标签」的等权卡片形态取消,改为「大数字 / 紧凑条」两级,主次拉开。
3. **RTP 网格** `repeat(5,1fr)` → `repeat(auto-fit, minmax(160px,1fr))`:右区宽度随屏幕浮动(左轨占了 300px),
写死 5 列会在 1440 以下挤出 4+1。
4. 旧骨架 CSS(`.stat-grid` / `.stat-card` / `.stat-icon` / `.stat-info` / `.stat-arrow`)与 `statCards` computed **一并删除**。
**风格**:色板 / 圆角 / 阴影 / 图标浅底规则全部沿用,**未新增任何色值** —— cottagecore 配色纪律不动。
**验证**:`node .workbuddy/verify-home-layout.cjs`(新增,cwd=ruoyi-ui)⇒ **ALL_OK 28 条 0 失败**
(含 vue-template-compiler 编译模板、dart-sass 编译样式、新旧骨架核对、区块顺序、指标总数 11、交互绑定未丢);
`node .workbuddy/audit-palette.cjs` ⇒ **离版文件 0/185、离版色值 0 次**;
dev server(`:80`)`/static/js/app.js` ⇒ 200 / 7.1MB / 0 处 `Module build failed`。
⚠️ 本机助手拉不起 Chrome(安全策略)⇒ **渲染后实际观感未经实测**,需用户刷新页面肉眼确认。
**后续补丁(用户看完截图提的)**:左轨「待处理提货申请 / **提货余额**」那格里,提货余额写的是 `$9259.64`
(**美元符号**),而页面其余金额(总充值 `¥1800`、总流水 `¥0.00`、RTP 投注/奖励 `¥0.00`、经营数据 6 项 `¥…`)
一律 `¥` ⇒ 统一为 `¥{{ balance }}`(`ruoyi-ui/src/views/index.vue:55`,全站唯一一处 `$` 货币写法,已 grep 确认)。
`verify-home-layout.cjs` 补 2 条断言(模板内不得出现 `$` 紧跟插值;提货余额那行前缀必须是 `¥`)⇒ 现 **30 条 ALL_OK**。
真产物复核(新增 `.workbuddy/check-ruoyi-bundle.cjs`,从 `:80` 拉 `app.js`):编译后为
`_vm._v("¥" + _vm._s(_vm.balance))`,`$` 版本 0 处。
- **推广后台首页改排版:「基本信息 / 5 张返利卡 / 汇总表」三段堆叠 → 「推广工具置顶」**
**起因**:与上一轮管理端首页同类问题(换肤只改配色,骨架没动)。用户在 3 轮高保真稿里选定 **版本 3 · 推广工具置顶**。
**改动清单**(`promo-ui2/src/views/Home/index.vue`,单文件)
1. **① 我的推广(置顶通栏)**:`hero` 行放**推广链接**(15px 加粗、超长省略、整条可复制)+ 主色「复制链接」按钮(无链接时 `disabled`);
下面一条虚线分隔的三格:**我的邀请码**(复制 / 修改 / 已修改)、**手机号码**(绑 `phoneNumber`)、**上级邀请码**。
2. **② 返利 KPI 横带**(4 格,19px 数字):**累计返利 / 今日返利 / 本周返利 / 累计下级** ——
全部从已有的 `getGameCommissionData()` 的 `periods` 里取(`总`/`今日`/`本周` 行),**不新增任何接口**。
3. **③ 底部两栏** `260px minmax(0,1fr)`:左 = **返利比例**(5 行「玩法名 ⋯⋯ 百分比」点线清单,替掉原来 5 张等宽卡);
右 = **周期返利汇总**表格(列一项没删,仍是 10 列)。`@media(max-width:1200px)` 收成一栏让表格拿满宽度。
4. **④ 账号信息降级到最底部**:用户账号 / 用户类型 / 金币余额 / 积分余额 / 注册时间 / Steam交易链接(6 项)。
样式上**做成非卡片**(`background/border` 透明、去阴影),避免和上面的数据抢注意力。
5. 旧骨架(`.section` / `.section-title` / `.game-rate-cards` / `.rate-card*`)与那两条**从未命中过**的
`.el-table__row tr:nth-child(odd)` 伪条纹规则一并删除(Element 的 `stripe` 已负责隔行)。
6. 颜色从残留的旧 Element 灰(`#f5f7fa` / `#e4e7ed` / `#303133` / `#67c23a` / `#606266` / `#909399` / `#fafafa` / `#e6a23c`)
收敛到田园核白名单 —— 该页 scoped 样式是换肤时漏掉的一份。
**一字未动的**:`username: userInfo.userName`(改邀请码的**入参**,后端拿它做「只能改自己」校验,与展示用的 `phoneNumber` 语义不同);
返利比例出处仍是 `getInfo()` 的三个 `*CommissionRate`;表格 10 列 / 弹窗 / 复制 / `onMounted` 双并发加载全部保留。
**验证**(新增 2 个脚本)
- `node .workbuddy/verify-promo-home.cjs` ⇒ **ALL_OK 54 条 0 失败**:新骨架 12 项、旧骨架 4 项须绝迹、区块顺序、
表格 10 列 / 账号 6 项 / KPI 4 项 / 玩法 5 项齐全、交互绑定 7 项、旧 Element 色 0 处、色值全在白名单、8 组对比度 ≥4.5:1。
- `node .workbuddy/verify-promo-vue-compile.cjs`(新增,用 promo 自带的 `vue/compiler-sfc` 走
parse → compileScript → compileTemplate → compileStyle)⇒ **ALL_OK 7 条 0 失败**。
- `node .workbuddy/verify-home-page.cjs` ⇒ **ALL_OK 5 条 0 失败**(该脚本①的结构性正则随「手机号码」移位已改写为与结构无关的定位)。
- **真机 dev 产物**(用户的 vite dev server 正在 `:4000` 跑,热更新):`/src/views/Home/index.vue` 的
style / template / script-setup / main 四个模块全 200,0 处 `Transform failed` / `Internal server error`;
编译后 15/15 个新骨架选择器都带上了 `[data-v-a763a6ae]` 前缀,旧类名 0 残留。
⚠️ 两点未做实测:**渲染后观感**(本机 Chrome 拉不起来);**窄屏横向滚动** —— 底部两栏时表格可用宽度 ≈ 屏宽-200(侧栏)-80,
10 列 `min-width` 合计 1090px,**视口 < 约 1600px 时右侧表格会出现横向滚动**(≥1600 正常),已在 `<1200px` 断点收成一栏规避。
**数据全是 0 是账号本身无下级/无消费所致**,不是渲染问题(`periodData` 的 4 行都在,只是值都为 0.00)。
过程与踩坑见 `.workbuddy/memory/2026-09-27.md`。
- **管理后台站点名「河南项目」→「凯科佰娱」(ruoyi-ui)**
**起因**:用户截图(`localhost/#/index`)圈出左下角 Logo 旁标题与浏览器 tab 标题,要求改成「凯科佰娱」。
**定位**:截图端口 80 + hash 路由 ⇒ 是 **`ruoyi-ui`(Vue2 管理后台,`vue.config.js` 默认 port 80)**,不是 promo-ui2(:4000)。
站点名只有一个源:`VUE_APP_TITLE`(`.env.*`)。消费方两处 ——
`src/layout/components/Sidebar/Logo.vue:23`(侧栏标题)、`src/App.vue:20-29` 的 `metaInfo.titleTemplate`
(`${路由名} - ${VUE_APP_TITLE}`);`public/index.html` 的 `<title>` 由 `vue.config.js:10` 的 `name` 注入。
⇒ **改一处 env 即可同时改掉侧栏 + tab**。另有 login/register 两页是硬编码同名,一并改。
**改动清单(6 处,全是字符串替换)**
1. `.env.development` / `.env.production` / `.env.staging`:`VUE_APP_TITLE = 河南项目` → `凯科佰娱`。
2. `vue.config.js:10` 兜底值 `|| '河南项目'` → `|| '凯科佰娱'`。
3. `src/views/login.vue:5`、`src/views/register.vue:4` 的 `<h3 class="title">`。
**注意**:① `.env` 在**进程启动时**注入 `process.env`,**改完必须重启 dev server**(webpack 不会热更 env)。
② `ruoyi-ui/dist/` 是构建产物,仍含旧串,需 `npm run build:prod` 重新生成才会更新(未手动改 dist)。
③ 后端 `application-*.yml` 里的「河南」是注释(控制台账号备注),与站点名无关,未动;
`docs/修改记录.md:910` 等处的「河南项目」是历史叙述,保留。
过程见 `.workbuddy/memory/2026-09-27.md`。
- **推广后台顶栏「分享链接」删除(与首页「我的推广链接」重复)**
**起因**:用户截图 `promo-ui2` 首页(`localhost:4000/#/home/index`),圈出**顶栏**的 `分享链接:https://…?code=…`
与**页面里**的「我的推广链接」hero 卡是同一份内容,要求「把上面的去掉」。
**定位**:两处不同来源 ——
① **顶栏**:`src/layout/components/ToolHeader.vue:45-47` 的 `<span>分享链接:{userStore.getUserInfo?.shareLink}</span>`
(**布局层组件,所有页面都有**,`role !== '99' && role !== '05'` 才显示);
② **页面**:`src/views/Home/index.vue:4-21` 的 hero 卡 —— 本次改版(版本 3「推广工具置顶」)**新加的**。
⇒ hero 一上线,顶栏那份就成了重复。按用户口径删**顶栏**(上面的)。
**改动清单**
1. `ToolHeader.vue`:删掉该 `<span>`(原地留注释说明为何删)。
2. 同文件连带清理:`useUserStore` 的 import 与 `const userStore = useUserStore()` —— 全文件只有这一处在用,
不删会变成未使用变量(`vue-tsc` / eslint 会报)。
3. `Home/index.vue`:删掉「改邀请码成功后同步顶部 header 分享链接」的 `userStore.setUserInfo({...})` 块 + 其 import/实例——
该块**存在的唯一目的**就是喂被删掉的顶栏,属同一改动的死代码,不是重构。
**验证**:给 `.workbuddy/check-promo-modules.cjs` **新增 `--absent=<子串>`**(原先只有 `--has` 用于"必须有",
删元素没法验),从用户常驻的 `:4000` 拉 **vite 真产物**断言:
- `layout/components/ToolHeader.vue` → 主模块 200、style 子模块 200、`useUserStore` / `shareLink` / `getUserInfo` **全部已消失**,**ALL_OK 6/0**。
- `views/Home/index.vue` → 主模块 200、`useUserStore` / `setUserInfo` **已消失**,**ALL_OK 4/0**。
⚠️ `--absent` 的坑:dev 构建**保留模板注释** ⇒ 注释里**别写被删的那个词**,否则 `_createCommentVNode("…")` 会命中假 FAIL。
**影响面**:改的是**布局层** ⇒ 所有页面顶栏都不再显示分享链接(不只首页)。
`userStore.userInfo.shareLink` 仍由 `Login/completeLogin.ts` 与 `views/User/index.vue` 写入,只是**不再有展示方**(无其他读取处,已 grep 确认);
改邀请码后 store 里那份不再自动刷新,因无消费方,无影响。
过程见 `.workbuddy/memory/2026-09-27.md`。
---
## 2026-09-24
- **头像管理:「是否默认」可同时多条为「是」的缺陷修复(唯一性约束 + 存量数据清洗 + 开关交互)**
**起因**:用户在「网站设置 → 头像管理」截图反馈,「是否默认」那一列有 7 条都显示绿色「是」,
而按语义**全站默认头像只应该有一条**。
**排查结论(逐层验证,不是猜的)**:
| 层 | 实测 | 结论 |
|---|---|---|
| 数据库 | `SELECT is_default, COUNT(*) ... GROUP BY is_default` → `0→1`、`1→7`(id 63~69) | **数据本身就错**,界面是忠实反映 |
| 前端 | `v-model="scope.row.isDefault"` 直读接口字段 | 无显示层问题,不需要改渲染 |
| 后端 | `TtUserAvatarServiceImpl.updateUserAvatarById` 只做 `updateById` | **根因**:设为默认时不互斥清空其他记录 |
⇒ 定位为**后端写接口缺约束 + 存量脏数据**,不是 UI bug。
**二、连带发现(比表面问题更严重)**:C 端有 4 处消费「默认头像」,其中
`ApiRegisterServiceImpl`、`PromoLoginServiceImpl` 用的是
`list(is_default='1')` + `RandomUtil.choice(...)` **随机挑一条** —— 这是在「可能有多条默认」的
既成事实上打的补丁。后果是**每次注册拿到的初始头像都不一样**,默认头像形同虚设。
(`TtUserServiceImpl` 白号那边已有 `defaultAvatarList.isEmpty()` 的降级,逻辑本身没问题。)
**三、改动清单**
1. **后端互斥(核心)** `skins-service/service-admin`:
- `TtUserAvatarService` 新增 `saveAvatar(TtUserAvatar)`、`getDefaultAvatar()`;
- `TtUserAvatarServiceImpl` 新增私有方法 `clearOtherDefaultsIfNeeded(...)`:
凡是把某条写成 `is_default='1'` 的写入,**先 `UPDATE ... SET is_default='0' WHERE is_default='1' AND id<>当前id`**,
再落当前这条,两步同一事务(`@Transactional`)。只在确实要置默认时才执行 UPDATE,普通编辑不白扫全表。
- `updateUserAvatarById` / `saveAvatar` 均接入;顺手删掉原方法里无意义的 `setAvatar(getAvatar())`。
2. **控制器** `TtUserAvatarController.add` 由 `userAvatarService.save(...)` 改走 `saveAvatar(...)`,
使**新增**也受同一约束(原来新增一条默认就会叠加出第二个)。
3. **存量数据清洗**:`UPDATE tt_user_avatar SET is_default='0' WHERE is_default='1' AND id <> (最小 id 的默认)`,
实测影响 6 行,清洗后 `0→7 / 1→1`(保留 id=63)。SQL 全文见 `docs/updata_sql.md`。
4. **前端交互** `ruoyi-ui/src/views/skins/ttSetting/avatar.vue`:「是否默认」由 `updateUserStatus`
改走新的 `updateDefault(...)` —— 开启时先 `$confirm` 告知「原默认头像「xx」将自动取消默认」,
取消则回滚开关视觉并重新拉列表。关闭时直接保存。
**四、验证**:后端 `mvn -q -pl skins-service/service-admin -am install -DskipTests` 通过,
字节搜 `clearOtherDefaultsIfNeeded`/`saveAvatar`/`getDefaultAvatar` 均在 `.class` 里;
前端 `vue-cli-service build` 通过,新文案与新方法名均落在 `dist/static/js/chunk-cf2f9538.*.js`。
**五、踩坑**:MySQL **不允许 `UPDATE` 的子查询直接查同一张表**(报 1093),
解法是包一层派生表 `(SELECT MIN(id) ... ) AS t` 绕过 —— 后续同类 SQL 照此办理。
⚠️ **待办**:后端 jar 未重打包、服务未重启,互斥逻辑**尚未在运行中的服务里生效**;
前端 `dist` 已就绪但线上资源需按部署流程发布。存量数据已清洗,界面当下已正确。
- **幸运升级:水位配置从「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,暂未在后台暴露),或者直接人工注水/抽水。
- **后台开关(el-switch)统一为「文字 = 当前值」口径,修掉反向文字露底色的误导**
**起因**:用户反馈「头像管理」的**状态**与**是否默认**两列看得费解。查下来是两个叠加问题:
1. **两边文字显示规则相反**。element-ui 的 `el-switch` 里 `active-text` 渲染在**右**侧、
且只在**开启时**显示;`inactive-text` 渲染在**左**侧、只在**关闭时**显示。
本页「状态」用的是 `inactive-text="启用"`(把当前值放在左侧),
「是否默认」用的是 `active-text="是"`(把当前值放在右侧)—— 同一张表里两套相反的读法。
2. **更糟的是反向文字会露出底色**。「状态」列开启时左侧露出绿色底 + 白字「启用」,
点一下滑块滑到左边、绿底被盖住,可读文字变成「停用」。两列的滑块方向可以完全相反,
同一行的绿白视觉还不一样(`is-checked` 与背景色是两套独立配色)。
⇒ **定的新口径(全后台统一):`inactive-text` = 关闭时的值,`active-text` = 开启时的值,
也就是「文字显示的就是当前值」,滑块停在哪边、哪边的文字就是现在的结果。**
颜色同步收敛为与 `active-value`/`inactive-value` 同侧:**绿 = 该项生效、红 = 该项未生效**。
**改动清单(7 个文件,15 处开关)**
| 文件 | 列 / 字段 | 调整 |
|---|---|---|
| `skins/ttSetting/avatar.vue` | 状态 `status` | 文字翻正(启用/停用),配色与 `active-value` 同侧 |
| `skins/ttSetting/avatar.vue` | 是否默认 `isDefault` | `active/inactive-value` 翻正为 `1/0`(= 与 `initData`、弹窗单选按钮一致),文字归右侧 |
| `skins/ttuser/index.vue` | 帐号状态 `status` | `active/inactive-value` 翻正为 `0/1`(原为 `1/0`,是全局唯一的反向异类) |
| `skins/ttuser/index.vue` | 提货状态 `deliveryStatus` | 同上翻正(`1` = 已禁用提货) |
| `skins/ttuser/index.vue` | 赠送状态 `giftStatus` | 同上翻正(`1` = 已禁用赠送) |
| `skins/ttBonus/index.vue` | 启用状态 `status` | 文字从「是/否」改为「启用/禁用」并翻正 |
| `skins/ttBox/boxType.vue` | 是否对战类型 `isFightType`、是否显示 `isShow` | label 归位;`isShow` 的 `active/inactive-value` 翻正为 `1/0` |
| `skins/ttredPacket/index.vue` | 是否启用 `useStatus` | `active/inactive-value` 翻正为 `1/0`,配色同侧 |
| `skins/ttUpgrade/index.vue` | 状态 `status` | `active/inactive-value` 翻正为 `0/1`(原为 `1/0`)+ 文字翻正 |
| `skins/ttBox/index.vue` | 弹窗内 是否对战宝箱 `isFight`、宝箱状态 `status`、是否首页推荐 `isHome` | 文字归位、`isFight`/`status` 的 `active-value` 翻正 |
**关键约束:以上全部只是「值 ↔ 显示」的映射调整,`active-value` 与 `inactive-value` 成对同翻,
提交给后端的字段值逐字节不变,数据库语义与接口契约零改动**(各页 `@change` 都是把整个 row/form
原样 `PUT` 出去,不做值转换)。
**语义校对依据**(改前逐个确认过,避免把含义改反):
- `tt_user_avatar.status` 与 `tt_user.status`:`UserStatus` 枚举 = `OK("0","正常")` / `DISABLE("1","停用")`,
`ApiUserDetailsServiceImpl` 对 `DISABLE` 直接拒登并踢线 ⇒ **`0` = 启用**。
- `tt_user_avatar.is_default`:`TtUserAvatar` 的 `@Excel(readConverterExp = "0=否,1=是")`,
且注册 / 白号 / 登录三处均查 `is_default = '1'` 取默认头像 ⇒ **`1` = 是**。
- `tt_user.delivery_status` / `gift_status`:实体注释 + 业务代码均为 **`1` = 已禁用**(`"1".equals(...)` 返回"已被禁用"提示)。
- 可对照的既有正确写法:`ttSetting/banner.vue`、`ttRechargeList/index.vue`、`ttuser/userPack.vue`。
⚠️ **踩坑**:用户截图里「是否默认」列显示为**红底白字「否」**容易被误读成"开着"。
那是 `active-color`/`inactive-color` 与 `active-text`/`inactive-text` 配成反的典型症状 ——
改完须确认**颜色所在的半边和文字所在的半边是同一半**。
⚠️ **未改(故意留着)**:`ttRoll/officialRollConfig.vue` 的状态开关用的是
`:value="scope.row.status === '0'"` + 无文字标注的正向布尔(绿=开),本身自洽;
只建议后续补一个文字标注。
- **管理后台:3 个页面表格右侧留白(「表格没有平铺完整」)**
**起因**:用户反馈「管理后台有些页面的表格没有平铺完整」。追问确认现象是
**表格右侧留一条空白**(不是列错位、也不是横向滚动条)。
**根因**(读 Element 源码定论):`element-ui/packages/table/src/table-layout.js:132-195`
的 `updateColumnsWidth()` ——
```
flexColumns = 所有「width 不是数字」的列(含显式 min-width 列、以及两者都没写的列)
if (flexColumns.length > 0 && fit) → 自动分配,表格撑满 bodyWidth (第 145 行)
else → bodyWidth = Σ(width || minWidth || 80)(第 182-194 行)
```
⇒ **只要有一列写了 `min-width`(或宽度两者都不写),表格就会自动撑满容器;
但当所有列都是固定 `width` 时,表格宽度恒等于列宽之和 —— 容器再宽也不拉伸,右侧就空一条。**
同一份源码里还有个易漏的点:`type="selection" / "expand" / "index"` 在未写 width 时,
由 `config.js:3-26` 的 `cellStarts` 给死 **48px 固定宽**,**不参与弹性分配** ——
所以「展开列 + 其余全固定宽」的表格同样会留白。
**扫描结论**(115 个顶层表格,1920 视口下容器可用 1584px):
- **仅 6 个**是全固定列(会留白);**109 个**含弹性列、会自行撑满。
- **0 个**会出横向滚动条(判据:`Σwidth + Σmin-width > 容器`)。
- 留白量**随屏幕变宽而增大**(2560 视口下最大 1854px)—— 这正是"没平铺完整"的典型特征。
**改动清单(3 个页面主表格,各把「内容最长的固定列」改为 `min-width`)**
| 文件 | 列 | 调整 | 表格宽 → 结果 |
|---|---|---|---|
| `skins/blankCardKey/index.vue` | 卡密编码 `cardCode` | `width="200"` → `min-width="200"` | 1000 → 撑满(原留白 584px) |
| `system/role/index.vue` | 权限字符 `roleKey` | `width="150"` → `min-width="150"` | 1075 → 撑满(原留白 509px) |
| `game/wheel/records/index.vue` | 投注符号 `betSymbols` | `width="140"` → `min-width="140"` | 1368 → 撑满(原留白 216px) |
选列理由:卡密编码是 32 位等宽字符 + 复制按钮、权限字符内容最长且**已带 `show-overflow-tooltip`**
(加宽无副作用)、投注符号是拼接字符串、长度波动最大 —— 都是"多给宽度只有好处"的列。
三处均加注释说明「为什么必须是 min-width」,防止后来人按"列宽统一用 width"的惯例改回去。
**未改(用户明确不动)**:3 个展开行内嵌明细表(`wheel/records` 下注明细、
`delivery/transferApply` 与 `transferApplyProcessed` 的饰品明细)与 1 个弹窗内倍率表
(`wheel/config`,固定 `width:640px`)—— 它们宽度受容器约束是设计意图。
其中 `transferApply` / `transferApplyProcessed` 声明了 `style="width: auto"` 却只有 2 列固定宽
(300+120=420),**在展开行里只占 420px**;若后续要铺满,改其中一列为 `min-width` 即可。
**验证**:
- 编译产物断言:`vue-template-compiler` 编译 3 个页面,✓ 全部通过,
`min-width` 各 1 个(200 / 150 / 140),其余列仍为固定 width。
- 算法级回归:复现 `updateColumnsWidth` 后,
**1280 / 1440 / 1600 / 1920 / 2560 五个视口下 3 个主表格留白全部归零**。
- dev server 起来后 `curl` 返回 200,确认改动未破坏编译。
- ⚠️ **本机无法真机截图核对**:助手拉起 Chrome 被安全策略拦(连带报 `wsl.exe` 在程序黑名单),
故本次验证口径是「读源码 + 复现算法 + 编译产物断言」,**未经浏览器实测**。
**纯前端改动,不涉及数据库与接口**;`dist` 未发布。
---
## 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_` 前缀)**
**起因**:`docker/` 下的编排文件里到处是 `changzhou`,是**从同源项目拷贝模板时的命名残留**。查实本机 `docker volume ls` / `docker ps -a`:**从未跑过 changzhou 这套**(只存在 hunan 项目的容器与卷 `hunan_mysql_db` / `hunan_redis_cache` / `hunan_rabbitmq_server`,3 个月前创建、均已 Exited)⇒ 改名**无数据迁移包袱**。统一为 `henan` 全套前缀,与实跑的 hunan 命名风格对齐。
**改动 7 个文件**:
1. `docker/compose/docker-compose.yml` —— `MYSQL_DATABASE` / `MYSQL_USER` → `henan`;三个 `container_name` → `henan_mysql_db` / `henan_redis_cache` / `henan_rabbitmq_server`;网络 → `henan_app_network`;external 卷 → `henan_mysql_data`
2. `docker/compose/deploy.sh` —— 自动建卷的卷名
3. `docker/compose/migrate-mysql-volume.sh` —— `NEW_VOL` 与旧卷候选列表;`docker stop henan_mysql_db`
4. `docker/WINDOWS_QUICKSTART.md` —— 卷名 / 库名 / 容器名;顺带修正 3 处**本身就是错的**路径 `d:\changzhou_app\changzhou_backend\...` → `D:\henan_background\henan-backend\docker\compose`;第 4 节「和 chengdu 项目切换」→ hunan
5. `ruoyi-admin/src/main/resources/application-dev.yml` —— jdbc url 库名 + `username`
6. `ruoyi-admin/src/main/resources/application-pro.yml` —— 同上
7. `bin/logtool.js` —— `-uhenan`、`-D henan`、容器名 `henan_mysql_db`
**验证**:`docker compose -f docker-compose.yml config` 解析通过、零报错,`name` 段全部已生效(库 `henan`、三个 `henan_*` 容器名、网络 `henan_app_network`、external 卷 `henan_mysql_data`)。**零 SQL 变更、零后端代码改动。**
**刻意没改**:① `chengduhipay` —— 第三方**支付通道名**不是地名,`application-dev/pro.yml` 与 `updata_sql.md` 里几十处,动了支付会挂;② `docs/updata_sql.md` 里历史条目的 `USE changzhou;` —— 台账回改会失真,需要留痕就**新加一条**;③ 各 `target/` `dist*/` `.lst` `stats.html` —— 里面是 `D:/changzhou_app/...` 的**编译期绝对路径**,属产物,重新构建自然消失;④ `docs/DEPLOYMENT.md`、`docs/README.md`、`docs/麻将胡了设计方案.md`(`D:\changzhou-cocos\...` 是客户端仓库路径)—— 旧项目文档。
**上线前必做两件事**:① 卷是 `external: true`,**必须先 `docker volume create henan_mysql_data`**,否则报 `external volume not found` 直接起不来(`deploy.sh up -d` 会自动建,手敲 `docker-compose up` 不会);② 新卷首次初始化会建出**空**的 `henan` 库 —— `MYSQL_DATABASE` / `MYSQL_USER` / `MYSQL_PASSWORD` **只在数据卷首次初始化时生效**,原有数据需另行导入,否则应用连上的是空库。
**遗留待定**:网络没固定 `name:`,实际名会随执行目录变(在 `docker/compose` 下执行时是 `compose_henan_app_network`)。卷已用 `external` + 固定 `name` 规避「换目录丢数据」,网络是否同样钉死成 `henan_app_network` 待定。
- **下架三个玩法:删除「奥林匹斯 / 荒野争锋 / 极速糖果」的后台菜单与页面**
**起因**:这三个玩法不再需要,要求从后台左侧菜单移除。
**关键前提**:RuoYi 的左侧菜单是**数据库驱动**的(`sys_menu` → `/getRouters` → 前端 `sidebarRouters`),**代码里没有菜单清单**,所以必须"先删数据、再删页面文件",只删前端文件的话菜单依然在。
**改动清单**(完整 SQL 见 `docs/updata_sql.md`):
1. **数据库**(库 `henan`,已执行):`sys_menu` 删 **22 行**(3 个顶级 M + 19 个子页面 C);`sys_role_menu` 删 **22 行**授权(全属 `role_id=103`)。**无 `'F'` 按钮权限行**。
- 3130 奥林匹斯 `olympus` → 子菜单 3131~3137
- 2105 荒野争锋 `bullet` → 子菜单 2106~2110、2112
- 2125 极速糖果 `sweet` → 子菜单 2126~2131
2. **前端文件**(38 个,删前已确认全部被 git 跟踪):`ruoyi-ui/src/views/game/{olympus,bullet,sweet}/`(7+6+6)与 `ruoyi-ui/src/api/game/{olympus,bullet,sweet}/`(7+6+6)。
3. **文档**:本条 + `docs/updata_sql.md` 新条目。
**验证**:
- 删除后 `sys_menu` / `sys_role_menu` 命中行数**均为 0**(`printf` 实测);`SELECT ... WHERE parent_id = 0` 复查剩余 20 个一级菜单,三个玩法已消失。
- 全量搜索 `game/olympus|game/bullet|game/sweet|api/game/...` 残留引用 **= 0**。
- `ruoyi-ui` 生产构建 **`exitcode=0` / `DONE Build complete`**(删掉页面后仍能编译,说明没有别处静态引用它们)。
**刻意没动**:
1. **后端 Java 代码**。这些玩法名在后端出现在**跨模块共享**的文件里 —— `TtAccountRecordSource`(流水来源码 60=奥林匹斯)、`TtboxRecordSource`、`RedisConstants`、`MessageCategoryEnum`、`PromoTurnover*`、`ApiUserPackSackServiceImpl`、`WebsiteSetupServiceImpl`。删它们会波及**其他玩法与推广/发货链路**,不在「删页面」范围内。
2. **数据库业务表**:`game_olympus_*` / `game_sweet_*` 等原样保留,未 TRUNCATE、未 DROP。数据还在,随时可回。
3. **`ruoyi-ui/src/views/game/sugar/`**(5 api + 5 页面)—— **没有任何菜单引用**。菜单「极速糖果」指向的是 `game/sweet/*`,而 `docs/cleanup_records.sql` 又把 `game_sugar_*` 标注为「极速糖果」,两套并存;后端还存在 `GameSugarWin/User/StepInfo/Spin` 实体。**疑似遗留或未接入的开发分支,未确认前不删。**
**遗留待定**:① 仪表盘「实时 RTP」卡片与「流水统计」折线里仍有 **玫瑰女郎(=olympus)** 一条 —— **已于下一轮处理**;② `game/sugar/*` 是否清理待定。③ 是否需要把 `game_olympus_*` / `game_sweet_*` 业务表也清掉(现在只是入口没了,数据仍在库里占空间)——实查确认**不存在 `%olympus%` 业务表**,无需处理。
- **彻底清理「玫瑰女郎」(olympus)的后端:枚举、返利统计链路与前端列**
**起因**:上一轮只下架了菜单与页面,后端仍保留整套 olympus 常量、统计列与返利计算。本轮要求「和后端相关的都删掉,包括返利的统计」。
**顺手修掉一个构建阻塞**:`skins-service/pom.xml` 仍声明 `<module>game-olympus-service</module>`、`ruoyi-admin/pom.xml` 仍依赖该 artifact,而**该模块目录早已不存在** ⇒ 这是**当时 Maven 构建必挂**的隐藏故障(本次删掉引用后构建才通过)。全仓已无任何文件名含 olympus。
**改动面:2 个 pom + 21 个后端文件 + 13 个前端文件**
| 层 | 文件 | 改动 |
|---|---|---|
| pom | `skins-service/pom.xml`、`ruoyi-admin/pom.xml` | 删 `game-olympus-service` 的 module / dependency |
| 常量 | `TtAccountRecordSource.java` | 删 `OLYMPUS_SPIN(60)` / `OLYMPUS_REWARD(61)` 枚举项,并从 **6 处**集合与判断中移除(推广福利流水、返利游戏类型、今日投注、今日赢奖、流水抽奖条件);删 `isSpecialGameConsume` / `isNewGameConsumeForCommission`,`isGameConsumeForCommission` 简化为只判经典 |
| 常量 | `TtboxRecordSource.java` | 删 `OLYMPUS_BOX(13)` |
| 常量 | `MessageCategoryEnum.java` | 删 `GAME_OLYMPUS` |
| 常量 | `RedisConstants.java` | 删 3 个 `OLYMPUS_*` 缓存键 |
| 配置 | `ParameterSettingBody.java`、`WebsiteSetupServiceImpl.java` | 删 `olympusMaintenance` 字段与读写(数据库侧见 `updata_sql.md`) |
| 仪表盘 | `TtRechargeRecordController.java` | 折线 `chartData` 整条 `roseladyList` 链删除;实时 RTP 的 `gameConfig` / `gameNames` 去掉第 8 项(**保留索引 5 = Roll 房**,`if (i == 5)` 依赖它) |
| 用户报表 | `TtUserBlendErcashMapper.xml`、`TtUserSourceReportMapper.xml`、`TtUserAmountRecordsController.java` | 删 `olympusGame` 列 ×2 + 排序分支;总流水 source 列表去 60 |
| 开箱统计 | `TtBoxRecordsMapper.xml` | 返奖总额 `source IN (...)` 去 13 |
| 返利 | `PromoTurnoverMapper.xml` | 删 `todayOlympus` / `yesterdayOlympus` / `weekOlympus` / `totalOlympus` 四列、`todayNewGame` / `yesterdayNewGame`、`newGameConsume` ×2,source 列表去 60 |
| 返利 | `PromoTurnoverMapper.java` | 删 `getSubNewGameConsumeByDate` / `getSpecialGameConsumeByUserId` 两个专属查询 |
| 返利 | `PromoTurnoverServiceImpl.java` | 看板 `buildPeriod` 去掉 olympus 周期项与 4 个调用点参数;下级佣金去掉新游戏分项 |
| 返利 | `PromoTurnoverController.java` | 三处 `olympusConsume` 统计链删除;`businessGameRate` 统一取经典费率 |
| 返利 | `PromoAdminManageController.java` | 删 `specialGameTurnover` 输出与 `sumNewGameConsume` |
| 返利 | `CommissionSettlementTask.java` | **周结佣金**去掉新游戏分项 |
| 其他 | `TtUserServiceImpl.java` | 实时返佣分支去掉「新游戏」,只判经典 |
| 其他 | `ApiUserPackSackServiceImpl.java`、`ApiProfileRecordsServiceImpl.java`、`TaskUpdate.java`、`TtBoxServiceImpl.java` | 映射 / 筛选集合 / 发奖公式 / 注释 |
| 前端 ruoyi-ui | `utils/accountSource.js`、`ttSetting/index.vue`、`ttuser/{userTurnover,userPack,creditsRecords,amountRecords,blendErcashArchive}.vue` | 删流水来源码 60/61 与 13 的字典与筛选项、删「玫瑰女郎维护」开关、删用户流水表的 `olympusGame` 列与排序标签 |
| 前端 promo-ui2 | `Home`、`FlowRecord`、`BusinessManage`、`AnchorManage`、`subFlow/transaction`、`CommissionReport` | 删费率卡 / 周期列 / 玫瑰女郎列 ×3 / 特殊游戏流水列 ×2 / 日特殊游戏消费列 / 来源码字典 |
**验证**(产物级断言,不是看代码):
- 后端 `mvn -B clean install -DskipTests` → **`BUILD SUCCESS`**,`Total time: 04:13 min`,无 `BUILD FAILURE` / `COMPILATION ERROR`;`ruoyi-admin.jar` 正常产出(6 个模块全链打通)。
- `target/classes` 里 5 个关键 class(`TtAccountRecordSource` / `TtboxRecordSource` / `MessageCategoryEnum` / `RedisConstants` / `PromoTurnoverServiceImpl`)搜 `OLYMPUS` **命中 = 0**。
- `target` 中 3 个 mapper XML 搜 `olympus` / `newGame` / `source 60` **命中全 0**。
- 数据库:`sys_config` 里 `%olympus%` 残留 **= 0**;一级菜单 20 个(与上轮一致)。
- `ruoyi-ui` 生产构建通过;`promo-ui2` 因 `node_modules` 缺 `esbuild` 无法构建(环境问题,非本次改动),改做静态校验。
**刻意保留**:
1. **历史数据**(`tt_user_blend_ercash` 47 行、`tt_box_records` 293 行、`tt_user_source_report` 14 行)—— 属账目,未删。副作用:流水来源码字典删了,这些历史流水在后台显示不出中文名。
2. `ruoyi-ui/src/views/index.vue` 的 `hidden` 数组仍留「玫瑰女郎」、`.rtp-theme-4` 样式保留 —— 后端已不再返回,留作**二层防护**。
3. **`newGameCommissionRate`(新游戏返佣比例)字段保留** —— 它挂在 `tt_user` 表上,属**用户配置字段**而非统计,删除涉及 DDL 与用户管理页,且语义上不点名玫瑰女郎。现状:无任何消费来源指向它,相关分项恒为 0。
- **新增「麻将胡了」玩法:以「新游戏」类型接入,复用原玫瑰女郎的整套统计口径**
**起因**:玫瑰女郎下架后,用户引入新游戏「麻将胡了」(后端 `mahjong` 玩法),要求「以新游戏的类型把相应的统计都加上」。
本轮是**上一轮删除动作的完整镜像** —— 把上一轮删掉的 olympus 分支,按 mahjong 重新接回全部统计链路。
**口径映射**(沿用原语义,未改结构):
| 概念 | olympus 旧值 | mahjong 新值 |
|---|---|---|
| 消费来源码 | `OLYMPUS_SPIN = 60` | `MAHJONG_SPIN = 68` |
| 奖励来源码 | `OLYMPUS_REWARD = 61` | `MAHJONG_REWARD = 69` |
| 开箱产出码 | `TtboxRecordSource` 13 | `MAHJONG_BOX = 15` |
| 新游戏返佣费率 | `tt_user.newGameCommissionRate` | **同名沿用**(未动 DDL) |
| 返佣流水来源 | `GAME_CONSUME_COMMISSION(47)`「特殊游戏返利」 | 同 |
**改动清单**(后端 21 个文件 + 前端 7 个 + DB 3 张表):
| 层 | 文件 | 改了什么 |
|---|---|---|
| 枚举 | `TtAccountRecordSource` / `TtboxRecordSource` / `MessageCategoryEnum` / `RedisConstants` | 新增 68 / 69 / 15 及消息分类、缓存键 |
| 仪表盘 | `TtRechargeRecordController` | `chartData` 加 `mahjongList`;`realTimeRtp` 加第 8 项「麻将胡了」(**追加在末尾,不动 `i == 5` 的 Roll 房下标**) |
| 开箱统计 | `TtBoxRecordsMapper.xml` | 3 处返奖口径 `source IN` 加 `15` |
| 用户报表 | `TtUserBlendErcashMapper.xml` / `TtUserSourceReportMapper.xml` / `TtUserAmountRecordsController` | 加 `mahjongGame` 列 ×2 + 排序分支 + `AGG_SORT_FIELDS` |
| 返利 | `PromoTurnoverMapper.xml` | `getBatchedCommissionData` / `getGameCommissionData` / `getWeeklyCommissionDataWithSnapshot` / `getWeeklyDailyCommissionData` / `businessGameConsumeRows` 全部加 68 与 newGame/mahjong 列 |
| 返利 | `PromoTurnoverServiceImpl` / `PromoTurnoverController` / `PromoAdminManageController` / `CommissionSettlementTask` | 看板费率卡与周期项、三处 `*Consume` 统计链、`businessGameRate` 新游戏分支、`specialGameTurnover`、`sumNewGameConsume`、周结分项 |
| 其他 | `TtUserServiceImpl`(实时返佣新游戏分支)、`ApiProfileRecordsServiceImpl`(69 / 68)、`ApiUserPackSackServiceImpl`(15 → 69 映射)、`TaskUpdate`(特殊游戏消费 + 0.01 系数)、`TtBoxServiceImpl`(注释) | |
| 配置 | `ParameterSettingBody` + `WebsiteSetupServiceImpl` | `mahjongMaintenance` 读写(键 `mahjongMaintenance`,兜底 `"0"`) |
| 前端 ruoyi-ui | `views/index.vue`(折线加「麻将胡了流水」)、`utils/accountSource.js`(68 / 69)、`skins/ttSetting/index.vue`(麻将胡了维护)、`skins/ttuser/{amountRecords,creditsRecords,blendErcashArchive,userPack,userTurnover}` | 来源码字典与筛选 68 / 69、开箱码 15、`mahjongGame` 列与排序标签 |
| 前端 promo-ui2 | `Home`(费率卡 + 周期列)、`FlowRecord`(麻将胡了列 ×2)、`BusinessManage` / `AnchorManage`(特殊游戏流水列)、`subFlow/transaction`(日特殊游戏消费)、`CommissionReport`(68 → 麻将胡了) | |
**数据库**:`sys_menu` +8(麻将胡了目录 `3200` + 7 子页)、`sys_role_menu` +8(role 103)、
`sys_config` +1(`mahjongMaintenance`)。完整 SQL 见 `docs/updata_sql.md`。
**验证**(产物级断言):
- 后端 `mvn -B clean install -DskipTests` → **`BUILD SUCCESS`**,19 个模块全过,`Total time: 03:55 min`。
- `ruoyi-ui` 生产构建 → `DONE Build complete`。
- `promo-ui2` 生产构建 → **本轮把环境修好了**,构建通过(修法见「踩坑」)。
- 数据库实测:`sys_menu` 3200~3207 = 8 行、`sys_role_menu` = 8 行、`sys_config.mahjongMaintenance` = 1 行。
**踩坑(记一次,下次别重踩)**:`promo-ui2/node_modules` 是**残缺的扁平布局** —— 绝大多数包只在
`node_modules/.pnpm/node_modules/` 里有,顶层没有软链 ⇒ vite 一启动就 `ERR_MODULE_NOT_FOUND`
(先 `esbuild`、再 `@rollup/rollup-win32-x64-msvc`、再 `@babel/core`…… 逐个补是打地鼠)。
**一次性修法**:把 `.pnpm/node_modules` 下**所有**包(含 `@scope` 子目录)在顶层批量建 junction
(本次补了 800+ 个链接)后构建即通过。根因是当初 `pnpm install` 没跑完/被裁过,属**环境问题、非代码问题**。
**刻意未做**:麻将胡了的 **11 张 `game_mahjong_*` 游戏表尚未落库**(DDL + 种子在 `mahjong-py/schema.sql`)。
这 7 个新菜单页面依赖它们,未建表前打开会报「表不存在」。属「游戏本体」前置依赖、且 generator 还没写完
(见 `mahjong-py/README.md`),**待用户确认后再执行**。
- **后台管理端(`ruoyi-ui`)整站换肤:Cottagecore 田园核风 + 逐页 UI 细节收敛**
**起因**:管理后台长期沿用它默认的企业蓝(Element 默认色板 + vue-element-admin / AdminLTE 遗留色),与河南项目的
田园调性不符;用户要求按 Cottagecore 规范整站重做,并**逐页检查小问题、把 UI 细节调到位**。
**做法:不引 Tailwind,走「Element 变量 + 皮肤覆盖层」**
(`ruoyi-ui` 是 Vue2 + Element UI 2.15 + vue-cli4/webpack4/postcss7,Tailwind 的 preflight 会打乱 Element 基础样式)。
新建 **`src/assets/styles/cottagecore.scss` 作为全站唯一色源**,其余文件一律引用变量、不写死十六进制;
再通过重编译 `element-ui` 的 theme-chalk 源码(`element-variables.scss`)换掉 Element 自己的底色。
| 文件 | 角色 |
|---|---|
| `assets/styles/cottagecore.scss` | **唯一色源**:品牌色 + 组件槽位色 + 图标色相 + 对比度实测注释 |
| `assets/styles/element-variables.scss` | 覆盖 `$--color-*` / 圆角 / 阴影 / `$--color-white` → 暖白 `#fffdfa`,再 `@import` theme-chalk 源码整包编译 |
| `assets/styles/skin.scss` | 皮肤覆盖层(按钮 / 表单 / 表格 / 分页 / 弹层 / Element 写死的冷色) |
| `assets/styles/index.scss` | 总入口,**最后一行 `@import './skin.scss'`**(整体回滚 = 注释这一行) |
| `assets/styles/sidebar.scss` / `variables.scss` | 侧栏(4 色图标轮换)/ 菜单深浅两套色 + `:export` 给 js |
**六轮递进,要点**:
1. **建色源 + 换 Element 底色**:primary 定陶土 `#9a5c35`(**不用草绿**,否则「新增 / 修改」同色系分不清;
想切回只改色源一处),success 草绿 `#457450`,warning 深雏菊金 `#8f6418`,danger 莓红 `#a4485a`,info 暖褐 `#6b6152`。
正文 `#5f4f3d`(**`#8b7355` 只有 4.17:1,禁止做正文**);草绿 `#5a8f5a` 白字仅 3.81:1 ⇒ **只做装饰与大色块**,
绿色文字一律 `#3f6b45`。
2. **摘掉隐藏的 ThemePicker**:`App.vue` 曾无条件挂 `<theme-picker>`(只用 CSS `display:none` 藏,组件照样 mount),
它在 `created()` 里会去 `unpkg.com` 拉整包 Element 默认主题并注入 `<style>` **把配色改回企业蓝**。已移除,别再挂回去。
3. **全局色板机械清扫**(45 个文件):把散在业务页里的旧 Element 色(`#409eff` / `#67c23a` / `#e6a23c` / `#f56c6c` / `#909399` …)
按语义映射到色源;同时改掉 `Logo.vue` / `Navbar.vue` 的白色毛玻璃 + 蓝阴影、全局滚动条的冷蓝。
4. **顶栏与标签页合并成一条**,去掉重复显示的面包屑。
5. **Element 内部写死的冷色**(hex 搜索抓不到、只能用更高权重规则盖):白遮罩、蓝色 info toast、
冷绿上传成功勾 `#13ce66`(**它的选择器是带父级的三段式 0,2,0,单类选择器会被压住**)、冷灰滚动条 / 标签 / 日期面板。
根因是 `$--color-white` → 把**暖白 `#fffdfa`** 提为「白」,一次性统一所有白色面。
6. **操作列按钮折行**(用户截图里那类问题)——根因不是列宽不够,而是 `element-ui.scss` 里一条全局规则
`.fixed-width .el-button--mini { width: 60px }` 把按钮钉死 60px,`图标 + 2~4 个中文` 直接被截断换行。
删掉该规则 + 给 19 个操作列补显式宽度(≥3 个按钮的收进「更多」下拉)+ 补下拉与前钮间距。
**验证(三层,脚本留在 `.workbuddy/`,可重跑)**:
- `node .workbuddy/audit-palette.cjs` —— 源码级色板审计(`.vue` + `.scss` + `.css`,**先剥注释再扫**):
当前 **0 / 184 文件离版**。色板白名单里另有一类**刻意保留的域内语义色**:
CS2 稀有度、麻将/饰品品质色谱(金/红/紫)、仪表盘折线色 —— **不是主题色,不许改**。
- `node .workbuddy/verify-dist.cjs` —— 构建产物断言,**34 项 ALL_OK**:① 本项目源码的冷色必须为 0;
② 新增规则必须存在;③ 中性色(`rgba(0,0,0,*)`)设「冻结基线」上限防复发。
**扫描范围是 app.css + 全部 `chunk-*.css`**(views/* 是懒加载,只看 app.css 会整片漏掉)。
- `node .workbuddy/verify-computed.cjs` —— **真机 Chrome 计算样式断言,20 项 ALL_OK**。
专治 element-ui 内部字面量色:这些色值**在样式表文本里去不掉**,`grep = 0` 不成立,必须看计算值。
- 构建:`DONE Build complete`,零 Sass 错误;仅剩既有的 `delLevel` 缺失导出 + 体积警告,与本改动无关。
**踩坑**:并行对**同一文件**发多个 Edit 会撞 `EBUSY`;更阴的是**偶发不报错、但两次写入只剩一次**(本次 `sidebar.scss` 丢过两个替换)
⇒ **机械替换一律串行,改完用 grep 逐条复验**,别信 Edit 的 success 回执。
**刻意未做**:① `variables.scss` 里 `sideTheme: theme-dark` 的深色侧栏回退色(冷蓝灰)保留 ——
设置抽屉里的「深色」按钮现在点下去视觉上是空操作(皮肤层已把侧栏壳设成透明 + 文字 `!important`),
**没有删按钮也没改回退色**,要不要彻底下掉这个开关待定;② `btn.scss` 的 `.blue-btn` 等 7 个类全项目 0 引用,
但其颜色已随 `variables.scss` 指向新色板,**不构成视觉问题,未清理**。