菜单归属与保护
问题
应用装的菜单是 sys_menu 里的普通行。管理员在「系统管理 → 菜单管理」里 可以直接改掉 path / component / perms,或直接删掉:
- 改一个字段 → 页面打不开、权限失效;
- 删掉之后 → 除非重装 / 升级应用,菜单再也回不来。
归属从哪来:不需要改表
菜单同步时已经把 (version_id, menu_id, menu_key) 写进了 app_version_menu, 这就是权威记录。反向查询就得到"菜单 → 应用":
app_install(本租户装了的应用) → app_info(编码 / 名称)
→ app_version(这些应用的版本) → app_version_menu(版本 → 菜单)2
为什么不在 sys_menu 上加一列 app_code
看着更直接,但要动框架的实体 / VO / Mapper XML / 菜单页面,代价大得多; 而且会和 app_version_menu 形成两份真相。
契约放在哪
| 角色 | 位置 |
|---|---|
| 契约 | ruoyi-common-core 的菜单归属提供者 |
| 实现 | 应用市场模块 |
| 拦截 | 菜单服务的 updateMenu / deleteMenuById(用 ObjectProvider 注入,缺席即"没有受保护菜单") |
| 标注 | GET /app/console/owned-menus + 菜单管理页 |
契约必须放在 ruoyi-common-core —— 宿主通用模块不能反向依赖应用市场。
保护策略:只挡会坏的,不挡想调的
| 操作 | 允许? | 理由 |
|---|---|---|
改 path / component / perms / menuType / parentId | ❌ 拒绝 | 与插件代码、前端路由、权限标识绑死 |
| 改名称 / 图标 / 排序 / 显隐 | ✅ 放行 | 纯展示属性,管理员按习惯调没有风险 |
| 删除 | ❌ 拒绝 | 删了只能靠重装/升级恢复。想隐藏请「停用」 |
两个容易搞错的点
1. 保护必须在服务端
菜单管理页会禁用按钮,但接口是同权限可调的(其它客户端、直接 curl)。 只在页面禁用等于没做保护。真正的收口点是服务端的菜单服务。
2. 框架自带的检查会先拦住大多数删除
存在子菜单,不允许删除 与 菜单已分配,不允许删除 都发生在进入服务之前。 所以实测时必须先摘掉角色分配才能触达归属检查 —— 归属检查是最后一道,专治"菜单成了孤儿"这种情况。
归属要连后代一起算
映射表里只有清单声明过的菜单,而按钮(menu_type='F')不在里面 —— 它历史上来自平台初始化 SQL。
不收进来的话,管理员能随手删掉应用按钮,恢复只能靠重装。
所以清单里必须写按钮(type: "F" + 唯一的 menu_key)。
重装后菜单没了是怎么回事
一个真实踩到过的问题:「安装成功、状态 enabled,但一个菜单都没有」。
根因是残留的死映射 —— 某条归属映射挂在软删除的旧版本上 (version_id 已不在目录里),清理时按"本次删掉的菜单"和"清单声明的键" 两条线索都找不到它。
而菜单同步是按 appId 跨版本解析 key → menu_id 的,于是下次安装/升级 复用它指向的已删除菜单 ID:updateById 影响 0 行、映射照样写回。
修法有两处:
- 清理归属映射时按三条线索并行(
menu_id∪menu_key∪version_id); - 菜单同步自愈 —— 同步前把"映射指向已不存在的菜单行"的项丢掉、按新菜单重建, 并 WARN 出被丢的键。