Files
henan-backend/docs/updata_sql.md
T

305 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据库变更台账
> **数据库改动只写这里**:中文标题 + 完整可执行 SQL。不再单独建 `docs/*.sql`(全量基线除外)。
> 只记**变更 SQL**;**不写回滚 SQL** —— 已执行的条目即为既成事实,要撤销请从本文件的 git 历史取旧版本。
> 若某条变更依赖**尚未执行**的前置对象(建表、种子数据等),必须在条目标注清楚,别让后来者踩空。
---
## 头像库「是否默认」唯一化:`tt_user_avatar` 存量数据清洗
**背景**:`tt_user_avatar.is_default` 的语义是「新用户 / 白号注册时分配的初始头像」,
**全表必须最多只有一条 `is_default='1'`**。但后台「设置 → 头像管理」的开关写成默认时,
后端只做了 `updateById`,没有把其他记录的默认标记清掉 ⇒ 库中一度累积出 **7 条默认**(id 63~69),
`id=62` 反而是「否」。后果:后台看到一排「是」容易误解(用户 2026-09-24 反馈),
且 C 端注册处(`ApiRegisterServiceImpl` / `PromoLoginServiceImpl`)用的是
`list(is_default='1')` + `RandomUtil.choice(...)` **随机挑一条**,导致默认头像实际不唯一、每次注册拿到的都不同。
**代码侧修复**(非 SQL,见 `docs/修改记录.md` 同日条目):`TtUserAvatarServiceImpl`
在 `updateUserAvatarById` / `saveAvatar` 中增加互斥逻辑 —— 任何把某条置为 `is_default='1'` 的写入,
都先把其余记录清成 `'0'`,两步同一事务。
**本次数据库变更 1 处**(2026-09-24 已在库 `henan` 执行):把多余的默认标记收敛为**只保留 id 最小的一条**(id=63)。
**变更 SQL**:
```sql
UPDATE `tt_user_avatar`
SET `is_default` = '0'
WHERE `is_default` = '1'
AND `id` <> (SELECT `min_id` FROM (
SELECT MIN(`id`) AS `min_id` FROM `tt_user_avatar` WHERE `is_default` = '1'
) AS `t`);
```
> ⚠️ MySQL 不允许在 `UPDATE` 的子查询里直接查同一张表,故此处包了一层派生表 `t` 绕过;
> 该写法的坑已记在 `.workbuddy/memory/` 当日日志里,后续同类 SQL 照此办理。
**验证口径**:执行后 `SELECT is_default, COUNT(*) FROM tt_user_avatar GROUP BY is_default;`
应当只有两行 —— `0 → N`(N = 总数-1)、`1 → 1`。
---
## 幸运升级·水位策略简化为「单一目标水位」:`sys_config` 配置结构变更
**背景**:上一版把水位策略做成了 **4 档波段表**(每档 = 水位下界/上界 + 随机数下界/上界),
后台要维护 16 个数、还要保证档间单调且留有重叠,运营难维护也容易配错。本次简化为**只填一个「目标水位(%)」**:
随机数的**中心**由目标水位自动反推,**取值范围**仍由每个饰品自己的「随机数区间」限定
(水位高于目标 → 落点靠近区间下限)。**口径推导、换算表、仿真数据与算例见 `docs/修改记录.md` 的 2026-09-24 条。**
**本次数据库变更 1 处**(2026-09-24 已在库 `henan` 执行):`sys_config.config_key = 'luckyUpgradeWatermark'` 的
`config_value` 由「波段表」改为「目标水位 + 灵敏度」,`bands` 字段废弃(后端已不再读取,留着不报错)。
> ⚠️ **前置依赖**:本条的 `UPDATE` 依赖下一节「幸运升级·水位驱动改造」插入的那行 `sys_config`
> —— 全新库必须先执行那一条,再执行本条。
>
> ⚠️ **同时恢复**(代码侧,非 SQL):饰品列表页的「随机数区间」编辑入口**重新启用**,
> `tt_upgrade_ornaments.luck_section` / `anchor_luck_section` / `blank_luck_section` 现在**必须配置**,
> 且要**包含按目标水位推算出的中心值**,否则中心被夹在边界上、水位停不到目标值。
> ⇒ 下一节里「上线前清空三个区间列」的说法**已作废**,不要再执行那两条清空 SQL。
**变更 SQL**:
```sql
UPDATE `sys_config`
SET `config_value` = '{"enabled":true,"targetWatermark":30,"gain":2,"luckyValueMode":"raw"}',
`remark` = '幸运升级水位策略:目标水位%(平台希望长期维持的净收益比例)、灵敏度 gain、幸运值展示口径 raw/eff。随机数中心 = 100/((1-目标水位/100)*实测标定系数),再由每个饰品的随机数区间限定取值范围;中奖判定恒用 min(概率,75)。enabled=false 逐字节回退固定区间',
`update_time` = NOW()
WHERE `config_key` = 'luckyUpgradeWatermark';
```
**字段口径**:
| 字段 | 含义 |
|---|---|
| `enabled` | `true` = 按水位动态出随机数;`false` = 逐字节回退「每个饰品位固定 `luck_section`」 |
| `targetWatermark` | **目标水位(%)**:平台希望长期维持的净收益比例。30 ⇒ 长期平均 RTP ≈ 70% |
| `gain` | 响应灵敏度:水位每偏离目标 1 个点,随机数中心反向调整 `gain`%。默认 2,后台不展示(调参用) |
| `luckyValueMode` | 幸运值**展示**口径:`raw`(默认,累加前端原始概率)/ `eff`(累加 `min(概率,75)`);**不影响判定** |
---
## 幸运升级·水位驱动改造:水位日志表、拉取记录快照列、水位策略配置与「奖池水位」菜单
**背景**:幸运升级原来每个饰品位配一套固定幸运区间,随机数永远落在区间内(如 `[100,160)`),
玩家能看出「每次都要投到 1 倍原价以上才出」。本次改为**按全局水位动态决定本轮的随机数区间**:
`水位 = (Σ下注 − Σ返奖 + 人工调整) / Σ下注`(排除白号),水位越高 → 可出的随机数越低。
同时新增后台人工注水/抽水。**方案、换算表、口径推导与算例全部汇总在 `docs/修改记录.md` 的 2026-09-23 条**(不另建单独文档)。
**本次数据库变更 4 处**(2026-09-23 已在库 `henan` 执行):
| 对象 | 变更 | 说明 |
|---|---|---|
| `tt_upgrade_reward_log` | **新建表** | 注水/抽水审计日志,`delta` 为带符号影响,水位调整额 = `SUM(delta)`(DB 为真相,Redis 只是短缓存) |
| `tt_upgrade_record` | 新增 **2 列** | `watermark`(本次拉取时的水位%)、`round_target`(本轮抽到的随机数快照)|
| `sys_config` | 新增 **1 行** | `config_id=155`、`config_key='luckyUpgradeWatermark'`,水位波段表 + 幸运值展示口径;`enabled=false` 可逐字节回退旧行为 |
| `sys_menu` / `sys_role_menu` | 各新增 **1 / 2 行** | `menu_id=3208`「幸运升级 → 奖池水位」,授权给 `role_id` 101(三恒)、103(Fdily)|
> **为什么 `menu_id` 用 `3208`**:`3200~3207` 是麻将胡了的号段,按顺序往后取,不复用已下架玩法的旧号段。
**变更 SQL**:
```sql
CREATE TABLE IF NOT EXISTS `tt_upgrade_reward_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'PK',
`type` tinyint NOT NULL COMMENT '1注水 2抽水 3重置',
`amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额(绝对值,展示用)',
`delta` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '对水位的带符号影响(注水正/抽水负/重置为抵消值)',
`watermark_before` decimal(8,2) DEFAULT NULL COMMENT '调整前水位%',
`watermark_after` decimal(8,2) DEFAULT NULL COMMENT '调整后水位%',
`operator` varchar(64) DEFAULT NULL COMMENT '操作人',
`remark` varchar(200) DEFAULT NULL COMMENT '调整原因(必填,前端保证)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_create` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='幸运升级奖池水位调整日志';
ALTER TABLE `tt_upgrade_record`
ADD COLUMN `watermark` decimal(8,2) NULL COMMENT '本次拉取时的水位%(1-RTP,排除白号,含人工调整)',
ADD COLUMN `round_target` int NULL COMMENT '本轮抽到的随机数 fullV 快照';
INSERT INTO `sys_config`
(`config_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES
(155, '幸运升级水位策略', 'luckyUpgradeWatermark',
'{"enabled":true,"luckyValueMode":"raw","bands":[{"from":-999,"to":10,"vMin":95,"vMax":130},{"from":10,"to":26,"vMin":115,"vMax":150},{"from":26,"to":36,"vMin":75,"vMax":175},{"from":36,"to":999,"vMin":85,"vMax":128}]}',
'Y', 'admin', NOW(), '', NULL, '幸运升级水位策略:波段表(水位%→随机数区间) + 幸运值展示口径 raw(原始概率)/eff(与判定同口径);中奖判定恒用 min(概率,75)。enabled=false 逐字节回退固定区间');
INSERT INTO `sys_menu`
(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES
(3208, '奖池水位', 2075, 3, 'rewardPool', 'skins/luckyUpgrade/rewardPool/index', NULL, 1, 0, 'C', '0', '0', 'skins:upgrade:pool', 'money', 'admin', NOW(), '', NULL, '幸运升级·奖池水位(全局水位/人工注水抽水/调整历史)');
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (101, 3208), (103, 3208);
```
**验证**(2026-09-23 实测结果):
```sql
SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema='henan' AND table_name='tt_upgrade_reward_log'; -- 期望 1,实测 1
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema='henan' AND table_name='tt_upgrade_record'
AND column_name IN ('watermark','round_target'); -- 期望 2,实测 2
SELECT COUNT(*) FROM sys_config WHERE config_key='luckyUpgradeWatermark'; -- 期望 1,实测 1
SELECT COUNT(*) FROM sys_menu WHERE menu_id=3208; -- 期望 1,实测 1
SELECT COUNT(*) FROM sys_role_menu WHERE menu_id=3208; -- 期望 2,实测 2
```
后台左侧「幸运升级」目录下出现「奖池水位」页(`/luckyUpgrade/rewardPool`)。
**注意:水位策略上线后的运营动作**
> ⚠️ **本节第 1 条已被本文件顶部「水位策略简化为单一目标水位」那条取代**:
> `luck_section` / `anchor_luck_section` / `blank_luck_section` **重新成为必填项**
> (语义 = 随机数的允许取值范围),**不要清空**;而且要配成**包含「按目标水位推算出的中心值」**的区间。
> 清空后后端会用兜底区间 `[80,160]`,能用但不受控。
1. ~~两个已存在的饰品位(`tt_upgrade_ornaments.id` 18 / 25)的 `luck_section` 要清空,才能完全跟随全局波段~~
—— **已作废**,见上方提示。
2. 水位从 0 起步。`enabled=true` 时前几件货会落在收紧档(水位低 → 随机数高),属预期行为;
需要立刻放水用后台「奖池水位 → 注水」。
## 管理后台按钮级权限补全:`sys_menu` 补 24 行 `F`(按钮)记录
**背景**:后台按钮的显隐走前端 `v-hasPermi`(`ruoyi-ui/src/directive/permission/hasPermi.js`),
接口放行走后端 `@PreAuthorize("@ss.hasPermi('...')")`,**两边比对的是同一个 `sys_menu.perms`**
(超管 `user_id=1` 拿 `*:*:*`,其他人拿「已授权菜单行的 `perms` 去重集合」,见
`ruoyi-framework/.../SysPermissionService.java:55`)。
历史遗留:**从 `menu_id` 31xx 段开始,新增页面只建了 `C` 行、没建 `F` 行**
(`F` 行分布:1000 段 61 行、2000 段 18 行、3100 段仅 9 行、3200+ 段 0 行)。
后果是这些页面的按钮在角色授权树里**根本勾不到** ⇒ 除超管外谁都看不见;
更严重的是部分 **列表/查询接口也会 403**,页面进去是空的。
盘点结果:代码在用、但 `sys_menu` 未登记的权限串共 **43 个**。其中 **23 个属于 4 个无菜单入口的孤儿页面**
(`ttOrnaments/boxOpenChance`、`ttBonus/firstRecharge`、`ttWelfare/monthlyRecharges`、`ttReplacementRecord/index`
—— 已核实无菜单、无组件引用、无路由、无接口复用方,**不可达,本期不动**);
**其余 20 个 + 复用组补 4 行 = 24 行**,本次补齐。
> 为什么 `menu_id` 用 `3209~3232`:`MAX(menu_id)=3208`(麻将胡了 3200~3207、幸运升级 3208),按顺序往后取。
**本次数据库变更 3 处**(2026-09-27 已在库 `henan` 执行):
| 对象 | 变更 | 说明 |
|---|---|---|
| `sys_menu` | 新增 **24 行** | `menu_id=3209~3232`,`menu_type='F'`,挂到 6 个 C 行下(2035/2038/2086/2092/2123/3118)|
| `sys_menu` | 修正 **3 行** | `menu_id=3160~3162` 的 `parent_id`:`3156`(目录 `M`「流水抽奖」)→ `3157`(页面 `C`「抽奖活动」)。**同属 F 行挂载错误**,不改的话勾「抽奖活动」联动带不到这 3 个按钮 |
| `sys_role_menu` | 新增 **24 行** | 把上述 F 行补授权给「**已授权其父 C 行**」的角色(动态 SQL,执行时按实际授权现状生成)。执行时这些父 C 行**仅 `role_id=104`(测试人员)**已授权 ⇒ 24 行全落给 104(`sys_role_menu` 总行数 222 → 246)|
> 关于 `3160~3162`:这三个串(`system:turnoverLottery:add/edit/remove`)**当前代码里无人使用**
> (`turnoverLottery/list.vue` 的新增/修改/删除按钮没写 `v-hasPermi`,后端也没有对应 `@PreAuthorize`),
> 所以本次修正只是把菜单树层级摆正、消除「F 行挂在目录下」的隐患,不影响线上行为。
> 若将来给该页面补按钮权限,直接复用这三个串即可。
**变更 SQL**:
```sql
/* ---- 1. 24 行按钮权限(F)---- */
INSERT INTO `sys_menu`
(`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`,
`menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
VALUES
/* 奖池详情(ttRoll/prizedetail)—— 该页「删除」按钮历史上绑的就是 admin:prize:edit */
(3209, '修改奖池', 2038, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:prize:edit', '#', 'admin', NOW(), '', NULL, '奖池详情·修改'),
/* CDK管理(ttRedPack/index)—— 整套新增,此前 0 行 F */
(3210, '新增红包', 2086, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:add', '#', 'admin', NOW(), '', NULL, 'CDK管理·新增'),
(3211, '删除红包', 2086, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:remove', '#', 'admin', NOW(), '', NULL, 'CDK管理·删除'),
(3212, '修改红包', 2086, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:edit', '#', 'admin', NOW(), '', NULL, 'CDK管理·修改'),
(3213, '导出红包', 2086, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:export', '#', 'admin', NOW(), '', NULL, 'CDK管理·导出'),
(3214, '红包查询', 2086, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:query', '#', 'admin', NOW(), '', NULL, 'CDK管理·详情'),
(3215, '红包列表', 2086, 6, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:list', '#', 'admin', NOW(), '', NULL, 'CDK管理·列表接口'),
(3216, '红包全部列表', 2086, 7, '', NULL, NULL, 1, 0, 'F', '0', '0', 'skinsback:ttRedPack:listAll', '#', 'admin', NOW(), '', NULL, 'CDK管理·不分页列表接口'),
/* 对战排行榜奖励(ttFight/fightRankingReward)—— 此前 0 行 F */
(3217, '新增奖励', 2092, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:add', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·生成'),
(3218, '删除奖励', 2092, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:remove', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·清空'),
(3219, '修改奖励', 2092, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:edit', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·修改'),
(3220, '奖励查询', 2092, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:query', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·详情'),
(3221, '奖励列表', 2092, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:reward:list', '#', 'admin', NOW(), '', NULL, '对战排行榜奖励·列表接口'),
/* 机器人对战配置(ttFight/robotFightConfig)—— 该 C 行下原有 4 行(3119~3122),价格配置这套是漏的,故 order 从 5 起 */
(3222, '新增价格配置', 3118, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:add', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置新增'),
(3223, '删除价格配置', 3118, 6, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:remove', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置删除'),
(3224, '修改价格配置', 3118, 7, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:edit', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置修改'),
(3225, '价格配置查询', 3118, 8, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:query', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置详情'),
(3226, '价格配置列表', 3118, 9, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:robotFightPriceConfig:list', '#', 'admin', NOW(), '', NULL, '机器人对战配置·价格配置列表接口'),
/* 货币列表(ttRechargeList/index)—— 该页只有详情接口校验;列表接口本身无 @PreAuthorize */
(3227, '货币查询', 2035, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:rechargeList:query', '#', 'admin', NOW(), '', NULL, '货币列表·详情'),
/* 机器人对战分组宝箱(ttFight/robotFightGroupBox)
⚠️ 复用组:该页前端按钮用 admin:box:{add,edit,remove}、后端接口用 admin:box:{list,query},
而这 4 个串原先只挂在 2019「宝箱列表」下 —— 父子联动带不过来,
所以必须在 2123 下再挂一遍,才能做到「勾这一个页面 = 该页完整可用」。*/
(3228, '新增分组宝箱', 2123, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:add', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·新增(与 2020 同 perms,刻意重复)'),
(3229, '删除分组宝箱', 2123, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:remove', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·删除(与 2021 同 perms,刻意重复)'),
(3230, '修改分组宝箱', 2123, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:edit', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·修改(与 2022 同 perms,刻意重复)'),
(3231, '分组宝箱查询', 2123, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:query', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·详情接口'),
(3232, '分组宝箱列表', 2123, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'admin:box:list', '#', 'admin', NOW(), '', NULL, '机器人对战分组宝箱·列表接口(与 2019/3139 同 perms,刻意重复)');
/* ---- 2. 授权补齐:把新 F 行补给「已授权其父 C 行」的角色 ----
动态写法的好处:不依赖执行时刻的角色授权现状,也不用在角色界面逐个重存。
注意:只补 F 行;C 行(页面本身)是否可见仍由业务在「角色管理」里决定。*/
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT DISTINCT `rm`.`role_id`, `f`.`menu_id`
FROM `sys_menu` `f`
JOIN `sys_role_menu` `rm` ON `rm`.`menu_id` = `f`.`parent_id`
WHERE `f`.`menu_id` BETWEEN 3209 AND 3232
AND NOT EXISTS (SELECT 1 FROM `sys_role_menu` `x`
WHERE `x`.`role_id` = `rm`.`role_id` AND `x`.`menu_id` = `f`.`menu_id`);
/* ---- 3. 修正 3 行挂错父节点的 F(3160~3162:目录 3156 → 页面 3157)---- */
UPDATE `sys_menu`
SET `parent_id` = 3157
WHERE `menu_id` BETWEEN 3160 AND 3162
AND `menu_type` = 'F'
AND `parent_id` = 3156;
```
**验证**:
```sql
-- ① 24 行 F 都进去了、且 perms 无重复录入
SELECT COUNT(*) FROM sys_menu WHERE menu_id BETWEEN 3209 AND 3232 AND menu_type='F'; -- 期望 24
SELECT perms, COUNT(*) FROM sys_menu WHERE menu_id BETWEEN 3209 AND 3232
GROUP BY perms HAVING COUNT(*) > 1; -- 期望 0 行(2123 组与 2019 组不同 id,不算重复)
-- ② 全库 F 行的 parent 必须是指向 C 行的 menu_id(防挂错;本次修掉了 3160~3162 这组)
SELECT f.menu_id, f.menu_name, f.perms, f.parent_id, p.menu_type
FROM sys_menu f LEFT JOIN sys_menu p ON p.menu_id = f.parent_id
WHERE f.menu_type = 'F' AND (p.menu_id IS NULL OR p.menu_type <> 'C'); -- 期望 0 行
-- ③ 授权补齐后:每个新 F 行,其父 C 行的每个授权角色都应能查到
SELECT f.menu_id, f.perms, rm.role_id
FROM sys_menu f
JOIN sys_role_menu rm ON rm.menu_id = f.parent_id
WHERE f.menu_id BETWEEN 3209 AND 3232
AND NOT EXISTS (SELECT 1 FROM sys_role_menu x
WHERE x.role_id = rm.role_id AND x.menu_id = f.menu_id); -- 期望 0 行
-- ④ 端到端:模拟某角色的 perms 集合,确认缺串已消失
SELECT DISTINCT m.perms FROM sys_menu m
JOIN sys_role_menu rm ON m.menu_id = rm.menu_id
WHERE rm.role_id = <角色ID> AND m.perms IS NOT NULL AND m.perms <> '';
```
**代码侧配套(非 SQL)**:新增 `.workbuddy/verify-perms.cjs`,做「代码在用的权限串 ↔ `sys_menu.perms`」双向差集,
防止后续新增页面再次只建 C 行、漏建 F 行。跑法见脚本头部注释。
**注意:权限生效需要用户重新登录**(`permissions` 是登录时从 `/getInfo` 拉取的,存在前端 store 里,不会热更新)。
---
-----------------------对战接口优化--------------------------
SHOW INDEX FROM tt_fight;
ALTER TABLE tt_fight ADD INDEX idx_ttf_status_create (status, create_time);
ALTER TABLE tt_fight
ADD COLUMN total_ornaments_price DECIMAL(18,2) NULL COMMENT '本场开箱饰品总价值(百佳榜排序用)',
ADD INDEX idx_ttf_status_total (status, total_ornaments_price);
ALTER TABLE tt_fight_user ADD INDEX idx_tfu_fight_user (fight_id, user_id);
ALTER TABLE tt_fight ADD INDEX idx_ttf_create_time (create_time);
-----------------------------------------------------------