# 修改记录
> **只记大改动**:一个需求 / 一个模块算一条;同一件事的多轮微调合并成一条,不逐轮记。
> **排序:最新在最上面。**
> 展开细节看 `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` 的 `
` 由 `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` 的 ``。
**注意**:① `.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` 的 `分享链接:{userStore.getUserInfo?.shareLink}`
(**布局层组件,所有页面都有**,`role !== '99' && role !== '05'` 才显示);
② **页面**:`src/views/Home/index.vue:4-21` 的 hero 卡 —— 本次改版(版本 3「推广工具置顶」)**新加的**。
⇒ hero 一上线,顶栏那份就成了重复。按用户口径删**顶栏**(上面的)。
**改动清单**
1. `ToolHeader.vue`:删掉该 ``(原地留注释说明为何删)。
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` 仍声明 `game-olympus-service`、`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` 曾无条件挂 ``(只用 CSS `display:none` 藏,组件照样 mount),
它在 `created()` 里会去 `unpkg.com` 拉整包 Element 默认主题并注入 `