接了个内部管理系统的定制需求,用的正是若依分离版。业务方提了两件事:一是要加一个"客户管理"模块,二是把原来藏在"系统工具"里的"统计分析"功能挪到一级菜单。听起来就是菜单管理界面里点几下的事,结果从数据库翻到前端路由,从角色配权到缓存刷新,一步步踩下来发现坑还真不少。这篇就把若依分离版二次开发里"创建新菜单"和"移动菜单"的完整套路拆开讲清楚,包括菜单表单里每个字段该怎么填、背后对应的前端路由和数据库逻辑是什么、以及哪些坑是新手最容易踩的。适合刚接触若依分离版、准备搞二次开发的后端同学,也适合前端同学想搞清楚菜单和页面组件的对应关系。
1. 动手之前先把菜单三层结构弄清楚:目录、菜单、按钮各自管什么
1.1 一张菜单表如何撑起整套权限体系
很多人第一次接触若依,会误以为"菜单"就是左边栏那一列导航。实际上在若依分离版里,菜单表 sys_menu 是整套权限系统的核心数据源,它同时承担了三件事:告诉前端侧边栏渲染成什么样、告诉前端路由该怎么生成、告诉后端接口哪些人有权限调用。
整个权限链路是这样的:用户 → 角色 → 菜单。用户登录成功后,若依后端会根据当前用户的角色,查出他能看到的所有菜单数据,组装成树形 JSON 返回给前端;前端拿到这份 JSON 之后,在 store/modules/permission.js 里动态生成 Vue Router 路由表。也就是说,你在数据库里往 sys_menu 插一条数据,不只是多了一个侧边栏入口,而是实实在在多了一条前端可用路由。
sys_menu 表里比较关键的字段有这几个:
sql复制menu_id -- 菜单ID
menu_name -- 菜单名称
parent_id -- 父菜单ID,顶级默认为0
order_num -- 显示排序,数字越小越靠前
path -- 路由地址
component -- 组件路径
query -- 路由参数
is_frame -- 是否外链(0否 1是)
is_cache -- 是否缓存(0缓存 1不缓存)
visible -- 显示状态(0显示 1隐藏)
status -- 菜单状态(0正常 1停用)
perms -- 权限标识
icon -- 菜单图标
前端动态路由的核心逻辑其实很直接,就是把后端返回的 component 字段动态加载成 Vue 组件:
javascript复制// store/modules/permission.js 中的核心逻辑(简化)
export const loadView = (view) => {
return () => import(`@/views/${view}`)
}
function filterAsyncRouter(asyncRouterMap) {
return asyncRouterMap.filter(route => {
if (route.component === 'Layout') {
// 目录类型:挂载到主布局框架
route.component = Layout
} else if (route.component === 'ParentView') {
// 嵌套目录使用 ParentView
route.component = ParentView
} else {
// 菜单类型:动态加载 src/views/ 下的实际组件
route.component = loadView(route.component)
}
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
return true
})
}
这段逻辑决定了 component 字段的取值规则:目录类型通常填 Layout,菜单类型就填相对于 src/views/ 目录的 vue 文件路径。把这个模型记在心里,后面所有菜单配置问题都能串起来。
1.2 目录、菜单、按钮选错会发生什么
sys_menu.type 字段有三个取值,这个字段在菜单管理表单里对应"菜单类型"单选组,分别是:M(目录)、C(菜单)、F(按钮)。
三种类型的分工差异很大,我这里直接给一张对照表:
| 菜单类型 | 英文值 | 作用 | 对应 component | 对应 perms |
|---|---|---|---|---|
| 目录 | M | 侧边栏里的"文件夹",用来分组 | 通常填 Layout | 一般不填 |
| 菜单 | C | 实际页面入口,点击后跳转到一个 vue 页面 | 填具体组件路径,如 customer/index | 建议填,用于后端接口权限校验 |
| 按钮 | F | 页面里的操作权限点,比如新增、删除、导出 | 不填 | 必填,如 customer:add |
这三个选错会出现什么后果?我见过最典型的几个错误:
第一,把按钮类型的菜单挂在了目录下面,导致按钮被当成页面来加载,前端动态路由加载失败,控制台直接报组件找不到。按钮本来就不该出现在路由表里,它只是权限标识的载体。
第二,把菜单类型的父级选成了另一个菜单而不是目录。Vue Router 的路由嵌套里,只有目录(也就是 component: Layout 的节点)才能真正挂载子路由。如果你把子菜单挂到一个普通菜单下面,前端生成路由时会拼出奇怪的层级,展开侧边栏后点父级直接跳到子页面,体验非常怪异。
第三,新建顶级菜单(父级选"主目录")时,路由地址 path 没写 / 开头。比如顶级菜单 path 填了 customer,前端生成路由时它会被拼成站点根路径下的一个相对地址,访问时直接 404 或者路由错乱。这是若依开发文档里明确强调的规则:顶级菜单路由地址必须以 / 开头,子菜单路由地址不能以 / 开头。
把这三层结构搞明白之后,再去操作菜单管理界面,心里就有底了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 创建"客户管理"菜单全过程:组件准备、字段填写、动态路由生效
2.1 前端页面组件放哪、怎么命名
在菜单管理界面里填"组件路径"之前,得先把实际的 vue 页面文件准备好。若依的约定是:所有业务页面统一放在 src/views/ 目录下,组件路径就是相对这个目录的路径,不需要写 .vue 后缀。
我这次要做"客户管理",先按业务模块建立目录结构:
text复制src/views/
└── customer/
├── index.vue # 客户列表页
├── detail.vue # 客户详情页
└── form.vue # 新增/编辑客户弹窗页
文件命名建议统一用小写和驼峰,保持一致性。有人喜欢中文目录名或者大写开头,虽然本地开发没问题,但部署到 Linux 服务器之后一旦大小写不匹配,动态 import 加载不到组件就会 404,排查起来相当抓狂。
组件写完之后,菜单表单里的"组件路径"就填 customer/index,对应关系一目了然。
2.2 菜单表单字段逐个拆解(附实际操作值)
打开"系统管理 → 菜单管理",点新增。这里我建议分成两步走:先建一个顶级目录"业务中心",再在目录下建"客户管理"菜单。
第一步,新增顶级目录"业务中心",关键字段这样填:
| 表单字段 | 填写值 | 背后逻辑与说明 |
|---|---|---|
| 上级菜单 | 主目录 | parent_id = 0 |
| 菜单类型 | 目录 | type = M,只作为侧边栏分组 |
| 菜单名称 | 业务中心 | menu_name,也是侧边栏显示文本 |
| 显示排序 | 1 | order_num,数字越小越靠前 |
| 路由地址 | /business | path,顶级目录必须以 / 开头 |
| 组件路径 | Layout | component,让前端挂到主布局框架 |
| 是否缓存 | 缓存 | is_cache = 0,目录本身无状态,保留默认 |
| 是否可见 | 显示 | visible = 0 |
| 菜单状态 | 正常 | status = 0 |
| 权限标识 | 留空 | 目录不参与按钮级鉴权 |
| 菜单图标 | 选择一个合适的图标 | 侧边栏展示用 |
第二步,在"业务中心"下新增菜单"客户管理":
| 表单字段 | 填写值 | 背后逻辑与说明 |
|---|---|---|
| 上级菜单 | 业务中心 | parent_id = 业务中心的 menu_id |
| 菜单类型 | 菜单 | type = C,点击后加载实际页面 |
| 菜单名称 | 客户管理 | menu_name,侧边栏显示文本 |
| 显示排序 | 1 | order_num,同级菜单内比较 |
| 路由地址 | customer | path,子菜单不能以 / 开头 |
| 组件路径 | customer/index | component,映射到 src/views/customer/index.vue |
| 权限标识 | customer:list | perms,模块:功能:操作 的三段式约定 |
| 是否外链 | 否 | is_frame = 0 |
| 是否缓存 | 缓存 | 组件开启 keep-alive,切换路由时保留页面状态 |
| 是否可见 | 显示 | visible = 0 |
| 菜单状态 | 正常 | status = 0 |
这里有几个字段值得单独解释。
路由地址与组件路径的区别:路由地址是 URL 上的那个路径,组件路径是前端动态 import 时真正加载的文件地址。两者可以不相同,但强烈建议保持一致的语义,否则以后维护链路的时候人脑容易短路。
权限标识的格式:若依权限标识约定俗成是 模块:功能:操作,比如 customer:list(查询客户列表)、customer:add(新增客户)、customer:remove(删除客户)。这个值将来会出现在前端指令和后端注解里,必须完全一致,差一个字母后端就会拒绝放行。
是否外链:如果选择"是",那么路由地址填的是完整的外部 URL,前端不会去加载组件,而是直接在浏览器里打开新地址。日常业务菜单基本不会触发这个字段,但如果碰上了,一定要知道它的存在。
2.3 保存后为何没有立刻生效
新增菜单这一操作本身是纯数据库操作,保存成功之后,前端侧边栏不会立刻出现新菜单。原因在于若依前端在用户登录成功时,把菜单数据一次性拉到前端并存在 Vuex 里了。菜单表变了,但前端内存里的数据还是登录那一刻的。
所以保存菜单之后,最稳妥的验证方式是直接退出登录,重新登录。如果不想退出,强制刷新浏览器页面让前端重新加载也能生效。
这里牵扯到若依的权限缓存机制:用户信息、角色、权限集合都存在 Redis 里,token 对应一份 LoginUser 对象。修改菜单、修改角色权限后,不清除这个缓存的话,老用户拿到的还是旧的权限集合。
在开发阶段偷懒省事的方式,是直接用 Redis 工具把该用户的缓存删掉,或者干脆重新登录。生产环境不建议这么做,走正常的权限分配流程就行。
3. 按钮权限的下半场:权限标识如何被后端和前端双重拦截
3.1 页面菜单只能控制"能进",控制不了"能点"
第一次体验若依的人,很容易陷入一个误区:以为给角色勾了一个菜单权限,整个页面就放开了。实际上,菜单权限只是控制"能不能进入这个页面",页面内部那些按钮,比如新增客户、删除客户、导出 Excel,是另一套独立控制体系——按钮权限。
拿"客户管理"页面来说,如果只给角色分配了"客户管理"这个菜单,那么页面能正常打开,但页面里的操作按钮如果不做任何控制,全公司凡是进了这个页面的人都能点新增、删除,这显然不行。
若依的做法是:在菜单管理里,把按钮也当成一条"菜单记录"来管理,只不过类型选"按钮",挂在对应页面的菜单下。比如"客户管理"下面是按钮"客户新增":
| 表单字段 | 填写值 |
|---|---|
| 上级菜单 | 客户管理 |
| 菜单类型 | 按钮 |
| 菜单名称 | 客户新增 |
| 权限标识 | customer:add |
| 显示排序 | 1 |
注意按钮类型不需要填组件路径,也不会出现在侧边栏,它纯粹是权限标识的一个载体。
前端配合 v-hasPermi 指令控制按钮显隐:
html复制<el-button v-hasPermi="['customer:add']" type="primary" @click="openForm">
新增客户
</el-button>
后端配合 @PreAuthorize 注解控制接口访问:
java复制@PreAuthorize("@ss.hasPermi('customer:add')")
@PostMapping
public AjaxResult add(@Validated @RequestBody Customer customer) {
// 新增客户逻辑
}
前端指令只负责"隐藏按钮",后端注解才是真正的安全边界。这两层必须同时存在,否则绕过前端直接调接口,敏感操作就裸奔了。
3.2 给角色分配权限的正确姿势
菜单和按钮都建好之后,还需要把权限分配给角色。操作路径是"系统管理 → 角色管理 → 找到对应角色 → 修改 → 菜单权限树勾选"。
这里有一个常见的操作盲区:菜单树是父子联动勾选的,勾选"客户管理"父菜单时,下面的按钮权限默认会全部勾上。有些同学图省事,直接勾了父级就保存,结果页面上所有按钮都暴露了。
正确的做法是:该角色需要哪些操作按钮,就只勾哪些。比如客服角色只需要查询和导出,那就只勾"客户查询"、"客户导出",不勾"客户新增"和"客户删除"。
分配完之后,还有最后一关:用户重新登录,让后端重新计算权限集合。判断权限有没有生效,最快的办法是登录后打开浏览器开发者工具,看登录接口返回的数据里,perms 数组里是否包含 customer:add。权限标识对上了,后端拦截自然就放行。
4. 移动"统计分析"菜单的那些细节:排序、换父级、路由地址改法
4.1 同级排序:orderNum 的数字游戏
菜单管理页面的树形表格里,同级菜单的展示顺序由 order_num 字段决定,数字越小越靠前。若依原版菜单管理界面默认不支持拖拽排序,要调整只能编辑菜单、修改"显示排序"这个字段。
比如"统计分析"原来排在"系统工具"目录下的第二位,想让它跑到第一位,把它的显示排序从 2 改成 1 就行。
这里有个容易被忽略的点:order_num 只影响同级菜单之间的顺序。不同层级的菜单,不管 order_num 填多大,都不会跑到别的层级里去。所以调整排序之前,先确认目标菜单的父级是谁,再在同一父级下比较。
4.2 跨层级移动:不是改个上级菜单就完事
把"统计分析"从"系统工具"目录里挪到一级菜单,操作上确实只是编辑这条菜单,把上级菜单改成"主目录",但有几个细节必须同步处理,否则前端路由会出问题。
细节一:路由地址要加 / 前缀。原来"统计分析"是"系统工具"的子菜单,path 大概率是 statistics 这种不带 / 的相对路径。变成顶级菜单后,path 要改成 /statistics。否则前端生成路由时,顶级路由没有绝对的根路径,会导致访问地址变成当前域名下的某个拼接路径,直接找不到页面。
细节二:组件路径不要动。很多人以为菜单移动了,组件路径也要跟着改。完全不用。component 字段映射的是 src/views/ 下的物理文件位置,跟它在菜单树里挂在哪一层没有关系。statistics/index 这个组件路径,不管它是顶级菜单还是五级菜单,都能正确加载到同一个 vue 文件。
移动后的效果对比:
| 项目 | 移动前 | 移动后 |
|---|---|---|
| 层级 | 系统工具 → 统计分析 | 主目录 → 统计分析 |
| 路由地址 | statistics | /statistics |
| 访问 URL | /system-tool/statistics | /statistics |
| 组件路径 | statistics/index | statistics/index(不变) |
移动菜单会改变 URL,这一点要提醒业务方。如果有其他系统收藏了旧地址,或者系统内部有硬编码跳转到旧地址的地方,都会受影响。
4.3 移动后菜单没变化的排查套路
移动菜单保存之后,发现侧边栏还是老样子,通常先按这三步排查:
第一步,确认数据库里 parent_id 和 order_num 真的改了。可以直接查 sys_menu 表,排除表单没保存成功的可能性。
第二步,重新登录,清掉前端内存里的旧菜单树。如果用户是登录状态,前端不会知道菜单结构变了,必须重新登录获取新的路由表。
第三步,检查浏览器路由缓存。如果页面上配置了 keep-alive 缓存,某些页面状态确实会残留,但这不影响菜单树结构。侧边栏结构一般只受前端 store 里路由表的影响,刷新页面即可重置。
我这里还遇到过一种情况:数据库字段类型是 char,你去更新菜单的时候不小心把 visible 或者 status 改掉了,菜单直接隐藏了。前端看起来是菜单没移动,实际上是菜单不见了。查的时候优先看这两个状态字段。
5. 复盘这一轮遇到的三个典型问题及完整定位链路
5.1 问题一:新菜单刷新后根本不显示
我新建完"业务中心"目录和"客户管理"菜单之后,用业务账号登录发现侧边栏什么都没有。第一反应是菜单是不是没建对,回菜单管理界面看数据都正常。
真正的排查链路是这样的:先用系统自带的 admin 账号登录,看能不能看到新菜单。结果 admin 能看到。那就说明问题不在菜单本身,而在角色分配。回角色管理里查看当前角色的菜单权限树,发现新菜单没有勾选。勾选之后让业务账号重新登录,菜单就出来了。
这个问题的根因其实是:若依的菜单展示有双层过滤,第一层是菜单自身状态,第二层是用户角色与菜单的关联关系。新建菜单默认不会自动分配给任何角色,必须手动在角色管理的菜单树里勾选。
5.2 问题二:点开页面 404
菜单显示出来后,点击"客户管理",页面直接跳到了 404 页面。浏览器控制台报错信息大概是 Failed to fetch dynamically imported module,翻译过来就是前端动态 import 的模块没有加载到。
定位过程是在前端代码里检查组件路径。我最初把组件路径填成了 customer/list,但实际文件的文件名是 index.vue,路径不匹配,组件自然加载不出来。
若依的动态路由加载是基于字符串拼路径的,loadView('customer/list') 实际加载的是 src/views/customer/list.vue。如果文件不存在,或者大小写不对,都会 404。这个坑在本地开发和 Linux 服务器上表现还不一样:本地 Windows/Mac 文件系统不区分大小写,有时候能加载;部署到 Linux 上区分大小写,直接暴露。
排查这类问题,记住一条铁律:控制台报错信息永远比页面表现先一步告诉你真相。看到 Failed to fetch dynamically imported module,先确认组件路径拼出来的实际文件路径存不存在。
5.3 问题三:接口直接 403
页面能打开了,列表数据却加载不出来,接口返回 403 Forbidden。后端日志里能看到类似 AccessDeniedException 的报错。
这个问题的产生原因通常是:页面对应菜单权限配了,但按钮权限没配。页面的列表查询接口挂了 @PreAuthorize("@ss.hasPermi('customer:list')"),而角色只勾了"客户管理"菜单,没勾"客户查询"按钮,权限标识 customer:list 根本不在用户的权限集合里。
排查链路分三步:第一步,看后端接口贴的权限标识是什么;第二步,看菜单管理里这条按钮菜单的 perms 字段填的是什么;第三步,看角色管理里有没有把这条按钮权限勾上。
现在把三个问题的根因放到一起看,你会发现一条主线:菜单权限的生效链路,是"菜单记录 → 角色关联 → 用户登录缓存 → 前端路由/后端校验"的一条完整链路。任何一个环节断掉,现象都是"菜单有问题",但根子可能完全不在菜单表里。
做完这两件事,我最大的感受是:若依的菜单不只是菜单,它同时是路由表、权限表、界面配置表的合体。配置任何一条菜单,都要心里挂着前端路由是怎么读它的。最后分享一个小技巧:开发阶段经常改菜单,如果发现权限半天不生效,别怀疑代码,大概率是 Redis 里那份登录缓存还在,退出重新登录一下就好。
