Files

84 KiB
Raw Permalink Blame History

修改记录

只记大改动:一个需求 / 一个模块算一条;同一件事的多轮微调合并成一条,不逐轮记。 排序:最新在最上面。 展开细节看 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 指向新色板,不构成视觉问题,未清理。