SpringBoot 动态菜单权限系统设计的企业级解决方案

SpringBoot动态菜单权限设计

今天我们来基于Spring Boot实现后端的功能。

为什么需要动态菜单?

场景:公司里有三种员工:

  • 管理员:需要管理用户、角色、系统设置等所有功能
  • 内容编辑:只需要管理文章、分类等内容相关功能
  • 普通员工:只能查看数据报表,不能进行任何修改

如果为每个角色开发不同的系统界面,工作量巨大且难以维护。动态菜单就是为了解决这个问题——一套系统,根据用户角色显示不同的菜单

一、数据库设计

核心表结构设计

我们的权限系统需要5张核心表:

sql

-- 用户表:存储系统用户信息 CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码', `nickname` varchar(50) NOT NULL COMMENT '昵称', `avatar` varchar(200) DEFAULT NULL COMMENT '头像', `status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-启用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='用户表'; -- 角色表:定义系统中的角色 CREATE TABLE `sys_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID', `role_code` varchar(50) NOT NULL COMMENT '角色编码', `role_name` varchar(50) NOT NULL COMMENT '角色名称', `description` varchar(200) DEFAULT NULL COMMENT '角色描述', `status` tinyint(1) DEFAULT '1' COMMENT '状态', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='角色表'; -- 菜单表:系统的所有菜单项 CREATE TABLE `sys_menu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', `parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单ID,0表示根菜单', `menu_name` varchar(50) NOT NULL COMMENT '菜单名称', `menu_icon` varchar(50) DEFAULT NULL COMMENT '菜单图标', `menu_type` tinyint(1) NOT NULL COMMENT '菜单类型:1-目录,2-菜单', `route_path` varchar(200) DEFAULT NULL COMMENT '路由路径', `sort_order` int(11) DEFAULT '0' COMMENT '排序号', `status` tinyint(1) DEFAULT '1' COMMENT '状态', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='菜单表'; -- 角色菜单关联表:角色拥有哪些菜单权限 CREATE TABLE `sys_role_menu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `role_id` bigint(20) NOT NULL COMMENT '角色ID', `menu_id` bigint(20) NOT NULL COMMENT '菜单ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='角色菜单关联表'; -- 用户角色关联表:用户属于哪些角色 CREATE TABLE `sys_user_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `role_id` bigint(20) NOT NULL COMMENT '角色ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB COMMENT='用户角色关联表';

表关系图解

scss

用户表 (sys_user) ↑ 用户角色表 (sys_user_role) -- 多对多关系 ↑ 角色表 (sys_role) ↑ 角色菜单表 (sys_role_menu) -- 多对多关系 ↑ 菜单表 (sys_menu) -- 树形结构

初始化测试数据

sql

-- 1. 添加三种角色 INSERT INTO `sys_role` (`role_code`, `role_name`, `description`) VALUES ('admin', '管理员', '系统管理员,拥有所有权限'), ('editor', '编辑者', '内容编辑人员,可以管理内容'), ('viewer', '查看者', '数据查看人员,只能浏览数据'); -- 2. 添加菜单数据 INSERT INTO `sys_menu` (`parent_id`, `menu_name`, `menu_icon`, `menu_type`, `route_path`, `sort_order`) VALUES (0, '仪表板', 'DataBoard', 2, '/dashboard', 1), (0, '系统管理', 'Setting', 1, '/system', 100), (2, '用户管理', 'User', 2, '/system/user', 1), (2, '角色管理', 'Lock', 2, '/system/role', 2), (2, '菜单管理', 'Menu', 2, '/system/menu', 3), (0, '内容管理', 'Document', 1, '/content', 2), (6, '文章管理', 'Document', 2, '/content/article', 1), (6, '分类管理', 'Collection', 2, '/content/category', 2), (0, '数据统计', 'DataAnalysis', 1, '/statistics', 3), (9, '访问统计', 'TrendCharts', 2, '/statistics/visit', 1); -- 3. 创建管理员用户 INSERT INTO `sys_user` (`username`, `password`, `nickname`) VALUES ('admin', '123456', '系统管理员'); -- 4. 设置权限关系 -- 管理员拥有所有菜单权限 INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (1, 1),(1, 2),(1, 3),(1, 4),(1, 5),(1, 6),(1, 7),(1, 8),(1, 9),(1, 10); -- 编辑者拥有部分权限 INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (2, 1),(2, 6),(2, 7),(2, 8); -- 查看者只有仪表板权限 INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (3, 1); -- 设置用户角色 INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1);

二、Spring Boot后端实现

项目结构规划

bash

src/main/java/com/example/dynamicmenu/ ├── config/ # 配置类 ├── controller/ # 控制器 - 处理HTTP请求 ├── entity/ # 实体类 - 数据库表映射 ├── dto/ # 数据传输对象 - 接收前端数据 ├── vo/ # 视图对象 - 返回给前端的数据 ├── mapper/ # 数据访问层 ├── service/ # 业务逻辑层 │ └── impl/ # 服务实现类 └── common/ # 通用工具类

核心实体类设计

菜单实体类 - Menu.java

java

@Data @TableName("sys_menu") public class Menu { @TableId(type = IdType.AUTO) private Long id; private Long parentId; // 父菜单ID private String menuName; // 菜单名称 private String menuIcon; // 菜单图标 private Integer menuType; // 菜单类型 private String routePath; // 路由路径 private Integer sortOrder; // 排序号 private Integer status; // 状态 @TableField(exist = false) // 非数据库字段 private List<Menu> children; // 子菜单列表 }

角色实体类 - Role.java

java

@Data @TableName("sys_role") public class Role { @TableId(type = IdType.AUTO) private Long id; private String roleCode; // 角色编码 private String roleName; // 角色名称 private String description; // 角色描述 private Integer status; }

数据传输对象设计

登录请求DTO - LoginDTO.java

java

@Data public class LoginDTO { @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; }

菜单视图对象 - MenuVO.java

java

@Data public class MenuVO { private Long id; private Long parentId; private String name; // 菜单名称 private String icon; // 菜单图标 private String route; // 路由路径 private List<String> roles; // 可访问的角色 private List<MenuVO> children; // 子菜单 }

核心业务逻辑实现

菜单服务类 - MenuServiceImpl.java

java

@Service public class MenuServiceImpl implements MenuService { @Autowired private MenuMapper menuMapper; /** * 根据角色获取菜单树 */ @Override public List<MenuVO> getMenuTreeByRole(String roleCode) { // 1. 查询该角色有权限的菜单列表 List<Menu> menuList = menuMapper.selectMenusByRoleCode(roleCode); // 2. 构建菜单树形结构 return buildMenuTree(menuList); } /** * 构建菜单树 - 核心算法 */ private List<MenuVO> buildMenuTree(List<Menu> menuList) { if (menuList == null || menuList.isEmpty()) { return new ArrayList<>(); } // 找出所有根菜单(parentId = 0) List<MenuVO> rootMenus = menuList.stream() .filter(menu -> menu.getParentId() == 0) .sorted(Comparator.comparing(Menu::getSortOrder)) .map(this::convertToVO) .collect(Collectors.toList()); // 为每个根菜单递归查找子菜单 for (MenuVO rootMenu : rootMenus) { findChildren(rootMenu, menuList); } return rootMenus; } /** * 递归查找子菜单 */ private void findChildren(MenuVO parentMenu, List<Menu> allMenus) { List<MenuVO> children = allMenus.stream() .filter(menu -> parentMenu.getId().equals(menu.getParentId())) .sorted(Comparator.comparing(Menu::getSortOrder)) .map(this::convertToVO) .collect(Collectors.toList()); if (!children.isEmpty()) { parentMenu.setChildren(children); // 递归处理子菜单的子菜单 for (MenuVO child : children) { findChildren(child, allMenus); } } } /** * 将Menu实体转换为MenuVO视图对象 */ private MenuVO convertToVO(Menu menu) { MenuVO vo = new MenuVO(); vo.setId(menu.getId()); vo.setParentId(menu.getParentId()); vo.setName(menu.getMenuName()); vo.setIcon(menu.getMenuIcon()); vo.setRoute(menu.getRoutePath()); // 设置可访问角色(实际应该从数据库查询) List<String> roles = new ArrayList<>(); roles.add("admin"); roles.add("editor"); roles.add("viewer"); vo.setRoles(roles); return vo; } }

控制器层的实现

认证控制器 - AuthController.java

java

@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private UserService userService; /** * 用户登录接口 */ @PostMapping("/login") public Result<LoginResult> login(@RequestBody LoginDTO loginDTO) { // 1. 验证用户名密码 if (!"admin".equals(loginDTO.getUsername()) || !"123456".equals(loginDTO.getPassword())) { return Result.error("用户名或密码错误"); } // 2. 获取用户信息(包含菜单权限) UserInfoVO userInfo = userService.getUserInfo(loginDTO.getUsername()); // 3. 返回登录结果 LoginResult result = new LoginResult(); result.setUserInfo(userInfo); result.setToken("mock-jwt-token-" + System.currentTimeMillis()); return Result.success(result); } /** * 切换角色接口 */ @PostMapping("/switchRole") public Result<UserInfoVO> switchRole(@RequestParam String roleCode) { // 模拟用户角色切换 UserInfoVO userInfo = userService.getUserInfo("admin"); userInfo.setRole(roleCode); // 重新获取该角色的菜单 userInfo.setMenus(userService.getMenuTreeByRole(roleCode)); return Result.success(userInfo); } }

菜单控制器 - MenuController.java

java

@RestController @RequestMapping("/api/menu") public class MenuController { @Autowired private MenuService menuService; /** * 获取菜单树接口 */ @GetMapping("/tree") public Result<List<MenuVO>> getMenuTree(@RequestParam String roleCode) { List<MenuVO> menuTree = menuService.getMenuTreeByRole(roleCode); return Result.success(menuTree); } }

数据访问层

菜单Mapper接口 - MenuMapper.java

java

@Mapper public interface MenuMapper extends BaseMapper<Menu> { /** * 根据角色编码查询有权限的菜单 */ @Select("SELECT m.* FROM sys_menu m " + "INNER JOIN sys_role_menu rm ON m.id = rm.menu_id " + "INNER JOIN sys_role r ON rm.role_id = r.id " + "WHERE r.role_code = #{roleCode} AND m.status = 1 " + "ORDER BY m.sort_order ASC") List<Menu> selectMenusByRoleCode(@Param("roleCode") String roleCode); }

统一返回结果封装

Result.java - 统一响应格式

java

@Data public class Result<T> { private Integer code; // 状态码 private String message; // 提示信息 private T data; // 返回数据 private Long timestamp; // 时间戳 // 成功响应 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); result.setTimestamp(System.currentTimeMillis()); return result; } // 错误响应 public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); result.setTimestamp(System.currentTimeMillis()); return result; } }

三、前端Vue3对接改造

菜单数据获取改造

javascript

// 从后端API获取菜单数据 const fetchUserMenu = async () => { try { const response = await axios.get('/api/menu/tree', { params: { roleCode: currentUser.value.role } }); if (response.data.code === 200) { // 转换后端数据为前端需要的格式 menuData.value = transformMenuData(response.data.data); } } catch (error) { console.error('获取菜单失败:', error); ElMessage.error('菜单加载失败'); } }; // 数据格式转换函数 const transformMenuData = (backendMenus) => { return backendMenus.map(menu => ({ id: menu.id.toString(), name: menu.name, icon: menu.icon, route: menu.route, roles: menu.roles || ['admin', 'editor', 'viewer'], children: menu.children ? transformMenuData(menu.children) : undefined })); };

角色切换功能增强

javascript

// 角色切换处理 const handleRoleChange = async (role) => { try { // 调用后端角色切换接口 const response = await axios.post('/api/auth/switchRole', null, { params: { roleCode: role } }); if (response.data.code === 200) { const userInfo = response.data.data; // 更新用户信息和菜单 currentUser.value.role = userInfo.role; menuData.value = transformMenuData(userInfo.menus); // 更新当前激活菜单 updateActiveMenu(userInfo.menus); ElMessage.success(`角色已切换为: ${getRoleName(role)}`); } } catch (error) { console.error('角色切换失败:', error); ElMessage.error('角色切换失败'); } };

四、核心技术与实现原理

1. 菜单树构建算法

菜单树构建的核心是递归算法

java

// 伪代码说明 function buildMenuTree(所有菜单列表) { 1. 找出所有根节点(parentId=0的菜单) 2. 对每个根节点: - 找出其直接子节点(parentId=当前节点ID) - 递归处理每个子节点 3. 返回构建好的树形结构 }

2. 权限过滤流程

用户登录 → 获取用户角色 → 查询角色权限菜单 → 构建菜单树 → 返回前端

3. 前后端数据格式转换

后端返回的菜单数据需要转换为前端Element Plus菜单组件需要的格式:

后端字段

前端字段

说明

menuName

name

菜单名称

menuIcon

icon

菜单图标

routePath

route

路由路径

children

children

子菜单

五、可扩展方案

1. 添加权限验证中间件

java

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 验证token和权限 String token = request.getHeader("Authorization"); // 权限验证逻辑... return true; } }

2. 添加菜单缓存

java

@Service public class MenuService { @Autowired private RedisTemplate redisTemplate; public List<MenuVO> getMenuTreeByRole(String roleCode) { String cacheKey = "menu:tree:" + roleCode; // 先查缓存 List<MenuVO> cachedMenu = (List<MenuVO>) redisTemplate.opsForValue().get(cacheKey); if (cachedMenu != null) { return cachedMenu; } // 缓存不存在,查询数据库 List<MenuVO> menuTree = buildMenuTreeFromDB(roleCode); // 存入缓存 redisTemplate.opsForValue().set(cacheKey, menuTree, 30, TimeUnit.MINUTES); return menuTree; } }

3. 前端路由权限控制

javascript

// 路由守卫 router.beforeEach((to, from, next) => { const userRole = store.state.user.role; const requiredRole = to.meta.role; if (requiredRole && !requiredRole.includes(userRole)) { next('/403'); // 无权限页面 } else { next(); } });

总结

通过本文的完整实现,我们构建了一个功能完善的动态菜单权限管理系统:

  1. 数据库设计:合理的表结构是权限系统的基础
  2. 后端实现:Spring Boot + MyBatis Plus提供RESTful API
  3. 核心算法:递归构建菜单树,实现权限过滤
  4. 前端对接:Vue3 + Element Plus渲染动态菜单
  5. 扩展优化:缓存、权限验证等生产级特性

这种设计模式具有很好的扩展性,可以轻松应对更复杂的权限需求。希望这篇文章能帮助你深入理解动态菜单的实现原理,并在实际项目中灵活应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值