在后台管理系统中,不同用户角色往往拥有不同的操作权限,对应的菜单展示也需动态调整。动态路由加载正是解决这一问题的核心方案 —— 根据登录用户的权限,从数据库查询其可访问的菜单,封装成前端所需的路由结构并返回。本文将详细讲解如何基于 Spring Boot + MyBatis-Plus 实现这一功能,包含完整代码与实现思路。

一、需求与实现思路

动态路由加载的核心目标是:根据登录用户的权限,动态生成其可访问的菜单路由,最终返回给前端用于渲染侧边栏。整体实现思路分为四步:

  1. 获取当前登录用户信息:通过 Session 获取已登录用户的 ID(userId);
  2. 查询用户角色名称:基于 userId,通过user_role表(用户 - 角色关联)和role表(角色表)联查,获取用户的角色名称(如 “超级管理员”);
  3. 查询用户权限菜单:基于 userId,通过user_rolerole_menu(角色 - 菜单关联)、menu(菜单表)三表联查,获取用户可访问的所有菜单;
  4. 封装路由结构:将数据库查询的菜单列表,转换为前端所需的路由格式(包含一级菜单、二级菜单、路由元信息等)。

二、核心表结构设计

实现动态路由的前提是合理的表结构设计,需包含 3 张核心表(用户 - 角色 - 菜单的关联关系):

  • user:用户表(存储用户 ID、用户名等);
  • role:角色表(存储角色 ID、角色名称,如 “超级管理员”);
  • menu:菜单表(存储菜单 ID、父级 ID、路径、组件路径等路由信息);
  • user_role:用户 - 角色关联表(多对多关系);
  • role_menu:角色 - 菜单关联表(多对多关系)。

其中,menu表的核心字段如下(与代码对应):

字段名含义说明示例值
menu_id菜单 ID(主键)1
parent_id父级菜单 ID(0 表示一级菜单)0
name菜单名称(用于前端显示)"系统管理"
path路由路径"/sys"
component前端组件路径"Layout"
icon菜单图标(前端显示)"system"
hidden是否隐藏("true"/"false")"false"
sort排序号(控制菜单展示顺序)1

三、VO 类设计(适配前端路由格式)

前端路由通常需要包含菜单名称、路径、组件、图标等信息,且需区分一级菜单和子菜单。因此,我们设计以下 VO(View Object)类封装路由数据:

1. MenuRouterVO(一级菜单路由)

@Data
public class MenuRouterVO {private String name;       // 菜单名称private String path;       // 路由路径private String component;  // 前端组件路径private String hidden;     // 是否隐藏("true"/"false")private String redirect = "noRedirect";  // 重定向路径(默认无)private Boolean alwaysShow = true;       // 是否总是显示(一级菜单通常为true)private MetaVO meta;       // 路由元信息(包含标题、图标)private List<ChildMenuRouterVO> children;  // 子菜单列表
}

2. ChildMenuRouterVO(二级菜单路由)

@Data
public class ChildMenuRouterVO {private String name;       // 子菜单名称private String path;       // 子菜单路径private String component;  // 子菜单组件路径private String hidden;     // 是否隐藏private MetaVO meta;       // 子菜单元信息
}

3. MetaVO(路由元信息)

用于存储前端渲染所需的标题和图标:

@Data
public class MetaVO {private String title;  // 菜单标题(显示在侧边栏)private String icon;   // 菜单图标(如"system")
}

四、核心代码实现

1. 控制器:处理动态路由请求(Controller)

控制器的作用是接收前端请求,协调获取用户信息、角色、菜单,并封装返回结果。

@RestController
@RequestMapping("/sys/user")
public class UserController {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate MenuService menuService;/*** 加载动态路由:返回用户信息、角色、可访问菜单路由*/@GetMapping("/getRouters")public Result getRouters(HttpSession session) {// 1. 从Session获取当前登录用户(登录时已存入Session)User user = (User) session.getAttribute("user");if (user == null) {return Result.error("用户未登录");}// 2. 根据userId查询角色名称(如"超级管理员")String roleName = roleMapper.getRoleNameByUserId(user.getUserId());// 3. 根据userId查询并封装用户可访问的菜单路由List<MenuRouterVO> routers = menuService.getMenuRouterByUserId(user.getUserId());// 4. 封装结果返回(用户信息、角色、路由)return Result.ok().put("data", user)       // 用户基本信息.put("roles", roleName)  // 角色名称.put("routers", routers); // 动态路由列表}
}

2. 角色查询:获取用户角色名称(RoleMapper)

通过user_role表关联role表,根据 userId 查询角色名称:

@Repository
public interface RoleMapper extends BaseMapper<Role> {/*** 根据userId查询角色名称* 联表逻辑:user_role(用户-角色关联) → role(角色表)*/@Select("SELECT role_name FROM role, user_role " +"WHERE user_role.role_id = role.role_id " +"AND user_role.user_id = #{userId}")String getRoleNameByUserId(Integer userId);
}

说明:若用户拥有多个角色,可修改 SQL 为GROUP_CONCAT(role_name)并返回字符串(如 “管理员,编辑”)。

3. 菜单查询:获取用户权限菜单(MenuMapper)

通过user_rolerole_menumenu三表联查,获取用户可访问的所有菜单:

@Repository
public interface MenuMapper extends BaseMapper<Menu> {/*** 根据userId查询可访问的菜单列表* 联表逻辑:user_role → role_menu → menu*/@Select({"SELECT m.menu_id, m.parent_id, m.name, m.path, m.component, " +"m.icon, m.hidden, m.sort " +"FROM user_role ur, role_menu rm, menu m " +"WHERE ur.role_id = rm.role_id " +"AND rm.menu_id = m.menu_id " +"AND ur.user_id = #{userId} " +"ORDER BY m.sort"  // 按sort排序,保证菜单展示顺序})List<Menu> getMenusByUserId(Integer userId);
}

说明:查询结果包含菜单的 ID、父级 ID、路径等核心信息,后续将转换为路由 VO。

4. 菜单服务:封装路由结构(MenuService)

Service 层的核心是将数据库查询的Menu列表转换为前端所需的MenuRouterVO列表,实现步骤:

  1. 从数据库查询用户可访问的所有菜单(menuList);
  2. 筛选一级菜单(parent_id = 0);
  3. 为每个一级菜单封装MenuRouterVO属性(名称、路径、组件等);
  4. 为每个一级菜单匹配子菜单(parent_id = 一级菜单ID),封装为ChildMenuRouterVO
  5. 组合一级菜单与子菜单,返回最终路由列表。
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {@Autowiredprivate MenuMapper menuMapper;@Overridepublic List<MenuRouterVO> getMenuRouterByUserId(Integer userId) {// 1. 查询用户可访问的所有菜单List<Menu> menuList = menuMapper.getMenusByUserId(userId);// 2. 存储最终的路由列表(一级菜单)List<MenuRouterVO> routerList = new ArrayList<>();// 3. 遍历菜单列表,筛选一级菜单并封装for (Menu menu : menuList) {// 一级菜单:parent_id = 0if (menu.getParentId() == 0) {MenuRouterVO parentRouter = new MenuRouterVO();// 封装一级菜单基本属性parentRouter.setName(menu.getName());parentRouter.setPath(menu.getPath());parentRouter.setComponent(menu.getComponent());parentRouter.setHidden(menu.getHidden());parentRouter.setRedirect("noRedirect"); // 固定值(前端要求)parentRouter.setAlwaysShow(true);       // 总是显示一级菜单// 封装元信息(标题、图标,用于前端渲染)MetaVO parentMeta = new MetaVO();parentMeta.setTitle(menu.getName());parentMeta.setIcon(menu.getIcon());parentRouter.setMeta(parentMeta);// 4. 为当前一级菜单匹配子菜单List<ChildMenuRouterVO> children = new ArrayList<>();for (Menu childMenu : menuList) {// 子菜单:parent_id = 一级菜单IDif (childMenu.getParentId().equals(menu.getMenuId())) {ChildMenuRouterVO childRouter = new ChildMenuRouterVO();// 封装子菜单属性childRouter.setName(childMenu.getName());childRouter.setPath(childMenu.getPath());childRouter.setComponent(childMenu.getComponent());childRouter.setHidden(childMenu.getHidden());// 子菜单元信息MetaVO childMeta = new MetaVO();childMeta.setTitle(childMenu.getName());childMeta.setIcon(childMenu.getIcon());childRouter.setMeta(childMeta);children.add(childRouter);}}// 5. 绑定子菜单到一级菜单parentRouter.setChildren(children);routerList.add(parentRouter);}}return routerList;}
}

五、关键逻辑解析

1. 表关联查询的意义

动态路由的核心是 “权限控制”,而权限控制的基础是用户 - 角色 - 菜单的关联关系:

  • 用户(user)通过user_role关联角色(role);
  • 角色(role)通过role_menu关联菜单(menu);
  • 最终实现 “用户→角色→菜单” 的权限传递,确保用户只能访问其角色允许的菜单。

2. 路由封装的核心思路

数据库查询的menuList是扁平的菜单列表(包含一级和二级菜单),需要转换为树形结构(一级菜单包含子菜单列表):

  • 先筛选parent_id = 0的一级菜单;
  • 再遍历所有菜单,为每个一级菜单匹配parent_id等于其menu_id的子菜单;
  • 通过MetaVO封装前端渲染所需的标题和图标,确保与前端路由组件属性对应。

3. 扩展性考虑

若系统需要支持三级及以上菜单,只需修改 Service 层的封装逻辑,将子菜单的筛选改为递归处理:

// 递归获取子菜单(示例伪代码)
private List<ChildMenuRouterVO> getChildRouters(Integer parentId, List<Menu> menuList) {List<ChildMenuRouterVO> children = new ArrayList<>();for (Menu menu : menuList) {if (menu.getParentId().equals(parentId)) {ChildMenuRouterVO child = new ChildMenuRouterVO();// 封装子菜单属性...// 递归查询当前子菜单的子菜单(三级菜单)child.setChildren(getChildRouters(menu.getMenuId(), menuList)); children.add(child);}}return children;
}

六、最终返回结果示例

前端接收的 JSON 格式如下(与 VO 类结构对应),可直接用于渲染动态路由:

{"code": 200,"msg": "操作成功","data": {"userId": 1,"username": "admin","realName": "管理员"// ...其他用户信息},"roles": "超级管理员","routers": [{"name": "系统管理","path": "/sys","component": "Layout","hidden": "false","redirect": "noRedirect","alwaysShow": true,"meta": {"title": "系统管理","icon": "system"},"children": [{"name": "管理员管理","path": "/user","component": "sys/user/index","hidden": "false","meta": {"title": "管理员管理","icon": "user"}}]}]
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/916980.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/916980.shtml
英文地址,请注明出处:http://en.pswp.cn/news/916980.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python在自动化与运维领域的核心角色:工具化、平台化与智能化

&#x1f4dd;个人主页&#x1f339;&#xff1a;慌ZHANG-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 引言 在 IT 系统日益复杂、运维任务持续增长的今天&#xff0c;自动化已成为企业基础设施管理的关键方向。Python 以其简洁的语法、强大…

RAG实战指南 Day 28:RAG系统缓存与性能优化

【RAG实战指南 Day 28】RAG系统缓存与性能优化 开篇 欢迎来到"RAG实战指南"系列的第28天&#xff01;今天我们将深入探讨RAG系统的缓存机制与性能优化策略。在实际生产环境中&#xff0c;RAG系统往往面临高并发、低延迟的需求&#xff0c;而合理的缓存设计和性能优…

swanlab实验优雅起名

init中的参数的作用project&#xff1a;整个实验的名字&#xff1b;experiment_name&#xff1a;在这个实验中&#xff0c;你的名字是什么&#xff1b; 比如说现在我们要进行对比实验&#xff0c;PEAN和Triflownet分别是对比方法的名字&#xff0c;这样的好处是&#xff0c;她们…

Nestjs框架: NestJS 核心机制解析 —— DI(依赖注入)容器与模块化工作原理

理解 NestJS 的 DI 管理机制 我们想要了解依赖注入&#xff08;Dependency Injection, DI&#xff09;最核心的工作逻辑NestJS 拥有自己的一套 DI 管理系统&#xff0c;它通过一个称为 DI 容器 的机制&#xff0c;来统一管理应用中所有类&#xff08;class&#xff09;的依赖关…

日语学习-日语知识点小记-构建基础-JLPT-N3阶段(12):文法+单词

日语学习-日语知识点小记-构建基础-JLPT-N3阶段&#xff08;12&#xff09;&#xff1a;文法单词 1、前言&#xff08;1&#xff09;情况说明&#xff08;2&#xff09;工程师的信仰2、知识点&#xff11;ーたぶん 多分&#xff12;ーV&#xff08;て&#xff09;いく ・ V&…

【赵渝强老师】OceanBase租户的资源管理

OceanBase数据库是多租户的数据库系统&#xff0c;一个集群内可包含多个相互独立的租户&#xff0c;每个租户提供独立的数据库服务。在OceanBase数据库中&#xff0c;使用资源配置&#xff08;Unit Config&#xff09;、资源单元&#xff08;Unit&#xff09;和资源池&#xff…

8K、AI、低空智联,H.266能否撑起下一代视频通路?

一、&#x1f4c8; 爆发式增长的 AI 与视频数据&#xff1a;智能时代的“数据燃料革命” 随着生成式 AI、大模型推理、多模态理解等技术的迅猛发展&#xff0c;视频数据从“记录工具”转变为“感知基础设施”&#xff0c;其在现代智能系统中的战略地位日益凸显。 1️⃣ 视频数…

保姆级别IDEA关联数据库方式、在IDEA中进行数据库的可视化操作(包含图解过程)

本文以mysql为例&#xff0c;学会了Mysql&#xff0c;其它的数据库也是类似的模版~如果您觉得这边文章对你有帮助&#xff0c;可以收藏防止找不到~如果您觉得这篇文章不错&#xff0c;也感谢您的点赞对我创作的支持1.1 打开侧边栏的Database2.2 选择要连接的数据库&#xff08;…

33.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--财务服务--记账

这篇文章我们一起把记账模块从单体应用迁移到微服务架构中。记账模块的功能想必大家都已经了解了&#xff0c;主要是记录用户的收入和支出&#xff0c;以及对这些记录的删除修改和查询等操作。具体的功能可以参考单体应用专栏&#xff0c;在这里就不多讲了。我们现在一起开始迁…

Cursor结合Playwright MCP Server支持自动化

Cursor结合Playwright MCP Server支持自动化 今天分享一下 playwright MCP Server&#xff0c;其提供了浏览器自动化能力&#xff0c;使大型语言模型能够在真实的浏览器环境中与网页交互&#xff0c; 也可以执行任务&#xff0c;例如运行JavaScript、截屏和导航网页元素&…

Python 求梯形面积的程序(Program to find area of a Trapezoid)

梯形的定义&#xff1a; 梯形是凸四边形&#xff0c;至少有一对边平行。平行边称为梯形的底边&#xff0c;另外两条不平行的边称为梯形的腿。梯形也可以有两对底边。在上图中&#xff0c;CD || AB&#xff0c;它们构成底边&#xff0c;而另外两条边&#xff0c;即AD和BC&#…

C语言 —— 指针(4)

动态内存分配动态内存需要手动申请&#xff0c;手动归还&#xff0c;其内存是开辟在堆区。申请的函数为&#xff1a;void *malloc(size_t size) &#xff08;需包含头文件#include<stdlib.h>&#xff09;size&#xff1a;要分配的内存大小&#xff0c;以字节为单位。申请…

常用算法思想及模板

今天继续整理一些关于算法竞赛中C适用的一些模板以及思想。 保留x位小数 保留x位小数在C语言中可以使用printf中的"%.xf"来实现&#xff0c;但是很多C选手由于关闭了同步流&#xff0c;害怕cin、cout与scanf、printf混用容易出错&#xff0c;所以就给大家介绍一个强…

GitLab 仓库 — 常用的 git 命令

在公司的 gitlab 公共仓库中写代码做项目时&#xff0c;主要涉及以下常用 git 命令&#xff1a;一、单个命令讲解1. 拉取代码&#xff08;1&#xff09;git clone [仓库 URL]‌克隆远程仓库到本地&#xff08;需确保 URL 正确&#xff09; ‌&#xff08;‌2&#xff09;git pu…

【28】C# WinForm入门到精通 ——多文档窗体MDI【属性、方法、实例、源码】【多窗口重叠、水平平铺、垂直平铺、窗体传值】

文章目录1多文档窗体MDI2 基本设置3 实例&#xff1a;多窗口重叠、水平平铺、垂直平铺3.1 主窗口属性设置3.2 主窗口3.3 主窗口窗口添加MenuStrip菜单3.4 添加处理函数3.5 测试效果4 利用窗体参数定义进行传值4.1 在Form2、Form3添加相关控件4.2 Form3 定义函数public Form3(st…

【计算机科学与应用】基于Session欺骗攻击的Web应用程序防护

导读&#xff1a; 本文对Web应用程序开发中的Session欺骗攻击进行了阐述&#xff0c;详细讲解了防范Session欺骗攻击的三种传统方法&#xff0c;并给出了防范代码&#xff0c;分析了三种传统防范方法的不足&#xff0c;新设计了一种通过Referer信息验证来加强对Session欺骗的防…

yolo8+阿里千问图片理解(华为简易版小艺看世界)

✅ 实现目标 按下空格键 → 获取摄像头当前画面&#xff1b; 将图片上传给 大模型 接口&#xff0c;让其“看图说话”&#xff1b; 获取返回描述后&#xff0c;以字幕形式展示在图像画面上&#xff1b; 持续显示识别结果&#xff0c;直到下次按空格。 &#x1f9e0; 需要准…

【ee类保研面试】数学类---线性代数

25保研er&#xff0c;希望将自己的面试复习分享出来&#xff0c;供大家参考 part0—英语类 part1—通信类 part2—信号类 part3—高数类 part100—self项目准备 文章目录线性代数知识点大全**1. 余子式与代数余子式****2. 行列式的含义****3. 矩阵的秩&#xff08;Rank&#xf…

在 Scintilla 中为 Squirrel 语言设置语法解析器的方法

Scintilla 作为一个强大的开源文本编辑控件&#xff0c;通过配置语法解析器&#xff0c;能够对多种编程语言实现语法高亮、代码折叠等实用功能。若要为新语言 Squirrel 设置语法解析器&#xff0c;可参考以下步骤&#xff1a;​创建 Lexer 源文件&#xff1a;Scintilla 通过 Le…

Go语言核心知识点补充

Go语言核心知识点补充 make函数、for循环与输入处理详解 在前几章的内容中&#xff0c;我们介绍了Go语言的基础语法、变量声明、切片、循环等核心概念。但在实际开发中&#xff0c;一些细节性的知识点往往决定了代码的健壮性与效率。 本文将针对前几章涉及到的变量声明与初始化…