基于 Spring Boot 实现动态路由加载:从数据库到前端菜单的完整方案

在后台管理系统中,不同用户角色往往拥有不同的操作权限,对应的菜单展示也需动态调整。动态路由加载正是解决这一问题的核心方案 —— 根据登录用户的权限,从数据库查询其可访问的菜单,封装成前端所需的路由结构并返回。本文将详细讲解如何基于 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/bicheng/91357.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91357.shtml

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

相关文章

VitePress学习-自定义主题

VitePress-自定义主题 代码仓库 基础了解 初始化项目的时候选择 custom theme 运行后会发现页面挺丑的。 如果想要用默认主题怎么办呢&#xff0c;修改Layout。 使用默认主题的Layout <script setup lang"ts"> import { useData } from vitepress; impo…

【GEO从入门到精通】生成式引擎与其他 AI 技术的关系

2.1.3 生成式引擎与其他 AI 技术的关系生成式引擎作为人工智能领域的创新力量&#xff0c;与其他 AI 技术紧密相连&#xff0c;相互促进&#xff0c;共同推动 生成式引擎优化&#xff08;GEO&#xff09; 的发展。这些技术使生成式引擎能够为消费者提供更加个性化和精准的内容。…

JAVAEE--4.多线程案例

设计模式1.单例模式1.1饿汉模式1.2懒汉模式(单线程版)1.3懒汉模式(多线程版本)1.4懒汉模式(多线程版本进阶版)2.阻塞队列3.定时器4.线程池1.单例模式设计模式是"软性约束",不是强制的,可以遵守也可以不遵守,按照设计模式写代码使代码不会太差框架是"硬性约束&qu…

量化感知训练(QAT)流程

WHAT&#xff1a;量化感知训练&#xff08;Quantization-Aware Training, QAT&#xff09; 是一种在模型训练阶段引入量化误差的技术。它的核心思想是&#xff1a;通过在前向传播时插入“伪量化节点”引入量化误差&#xff0c;将权重和激活模拟为低精度&#xff08;如 int8&…

docker 用于将镜像打包为 tar 文件

docker save 是 Docker 中用于将镜像打包为 tar 文件的命令&#xff0c;常用于镜像的备份、迁移或离线传输。以下是其核心用法和注意事项&#xff1a;一、基本语法bashdocker save [选项] IMAGE [IMAGE...] > 文件名.tar # 或 docker save -o 文件名.tar IMAGE [IMAGE...]IM…

设计模式(六)创建型:单例模式详解

设计模式&#xff08;六&#xff09;创建型&#xff1a;单例模式详解单例模式&#xff08;Singleton Pattern&#xff09;是 GoF 23 种设计模式中最简单却最常被误用的创建型模式。其核心价值在于确保一个类在整个应用程序生命周期中仅存在一个实例&#xff0c;并提供一个全局访…

PostgreSQL AND OR 操作符详解

PostgreSQL AND & OR 操作符详解 在数据库查询中,AND 和 OR 是两种常见的逻辑操作符,用于组合多个查询条件。PostgreSQL 作为一款功能强大的开源关系型数据库管理系统,同样支持这些操作符。本文将详细介绍 PostgreSQL 中的 AND 和 OR 操作符,并探讨它们在查询中的应用…

RabbiteMQ安装-ubuntu

Ubuntu 1.安装Erlang RabbitMQ需要Erlang语言的支持&#xff0c;在安装RabbitMQ之前需要安装Erlang #更新软件包 sudo apt-get update#安装erlang sudo apt-get install erlang查看erlang版本 roothcss-ecs-027f:/# erl Erlang/OTP 24 [erts-12.2.1] [source] [64-bit] [sm…

Linux驱动20 --- FFMPEG视频API

目录 一、FFMPEG 视频 API 的使用 1.1 介绍 1.2 整体编程过程 获取核心上下文指针 打开输入流文件 获取输入流 获取编码器 初始化解码器 申请输出流指针 获取显示数据空间大小 申请输出显示空间 绑定输出流和输出显示空间 申请格式转换上下文 申请输入流指针 读取一帧数据 发…

OpenBayes 一周速览丨Self Forcing 实现亚秒级延迟实时流视频生成;边缘AI新秀,LFM2-1.2B采用创新性架构超越传统模型

公共资源速递 This Weekly Snapshots &#xff01; 5 个公共数据集&#xff1a; * AF-Chat 音频对话文本数据集 * ArtVIP 机器交互式图像数据集 * Updesh 印度语合成文本数据集 * Medical Information 药品信息数据集 * Nemotron-Math-HumanReasoning 数学推理数据集…

[NOIP2002 提高组] 均分纸牌

题目描述有N堆纸牌&#xff0c;编号分别为 1,2,…,N。每堆上有若干张&#xff0c;但纸牌总数必为N的倍数。可以在任一堆上取若干张纸牌&#xff0c;然后移动。移牌规则为&#xff1a;在编号为1堆上取的纸牌&#xff0c;只能移到编号为2的堆上&#xff1b;在编号为N的堆上取的纸…

【音视频】WebRTC-Web 音视频采集与播放

一、打开摄像头 打开摄像头首先需要有一个html的video标签&#xff1a; id "local-video"&#xff0c;是为了后续的js脚本调用这个对象autoplay是设置打开后自动播放&#xff0c;playsinline则是为了兼容移动端 <video id "local-video" autoplay p…

数据治理平台如何选?深度解析国产化全栈方案与行业落地实践

“数据治理平台厂商有哪些&#xff1f;”国内主流厂商包括阿里云、华为、百分点科技等&#xff0c;各有所长。其中&#xff0c;百分点科技凭借在应急管理、智慧公安及央国企数字化领域的深度实践&#xff0c;打造了行业特色鲜明的数据治理解决方案。百分点科技的数据治理解决方…

限流算法详解:固定窗口、滑动窗口、令牌桶与漏桶算法全面对比

限流&#xff08;Rate Limiting&#xff09;是保障系统稳定性和服务质量的关键机制&#xff0c;尤其在高并发、突发流量、攻击防护等场景中至关重要。本文将详细介绍四种主流限流算法&#xff1a;固定窗口&#xff08;Fixed Window&#xff09;滑动窗口&#xff08;Sliding Win…

Sentinel 搭建应用层面与网关层面的流控保护

源码&#xff1a;妖精的尾巴/spring-cloud-alibaba Nacos 和 Sentinel Dashboard 我这里全是使用window 本地运行的&#xff0c;需要自行下载运行 服务层面&#xff1a; 当你在某个具体的服务上使用Sentinel时&#xff0c;更多的是关注该服务内部资源的保护。例如&#xff0c…

纯血鸿蒙 AudioRenderer+AudioCapturer+RingBuffer 实现麦克风采集+发声

总共两个类&#xff0c;放到代码里&#xff0c;就可以快速完成K歌的效果&#xff0c;但应用层这么做延迟是比较高的&#xff0c;只是做一个分享。 类代码 import { audio } from kit.AudioKit; import { BusinessError } from kit.BasicServicesKit; import { AudioBufferFlow,…

洛谷 P1601 A+B Problem(高精)普及-

题目描述 高精度加法&#xff0c;相当于 ab problem&#xff0c;不用考虑负数。 输入格式 分两行输入。a,b≤10500a,b \leq 10^{500}a,b≤10500。 输出格式 输出只有一行&#xff0c;代表 ababab 的值。 输入输出样例 #1 输入 #1 1 1输出 #1 2输入输出样例 #2 输入 #2 1001 909…

Matrix Theory study notes[6]

文章目录linear spacereferenceslinear space a basis of linear space VkV^kVk,which is x1,x2,...xkx_1,x_2,...x_kx1​,x2​,...xk​,can be called as a coordinate system.let vector v∈Vkv \in V^kv∈Vk and it can be linear expressed on this basis as va1x1a2x2...…

专线与专线之间的区别

下面我们从定义、技术特点、适用场景、优缺点等多个维度来详细对比&#xff1a;✅ 一、四种方案简要定义技术方案定义MPLS 专线运营商基于 MPLS 技术提供的私有虚拟网络&#xff0c;逻辑隔离、安全可靠VPN over Internet利用公网加密通道&#xff08;如IPSec&#xff09;构建虚…

Git工作流:团队协作的最佳实践

目录 一、什么是 Git 工作流&#xff1f;为什么需要它&#xff1f; 二、基础&#xff1a;Git 分支核心概念 三、主流 Git 工作流实战指南 1. 集中式工作流&#xff08;Centralized Workflow&#xff09;&#xff1a;适合小团队 / 新手 操作步骤&#xff1a; 优缺点&#…