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. 扩展优化:缓存、权限验证等生产级特性

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

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《SpringBoot+MySQL+Vue实现文件共享系统》

《这20条SQL优化方案,让你的数据库查询速度提升10倍》

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

《Vue3和Vue2的核心区别?很多开发者都没完全搞懂的10个细节》

相关推荐
S***q3771 小时前
Java进阶-在Ubuntu上部署SpringBoot应用
java·spring boot·ubuntu
棋啊_Rachel1 小时前
Spring Boot深度解析:从零开始构建企业级应用
java·spring boot·后端
小王不爱笑1321 小时前
代码生成器
java·mybatis
计算机毕设小月哥1 小时前
【Hadoop+Spark+python毕设】中式早餐店订单数据分析与可视化系统、计算机毕业设计、包括数据爬取、数据分析、数据可视化
后端·python
Slow菜鸟1 小时前
Java开发规范(五)| 接口设计规范—前后端/跨服务协作的“架构级契约”
java·状态模式·设计规范
Slow菜鸟1 小时前
SpringBoot教程(三十五)| SpringBoot集成TraceId(追踪ID)
java·spring boot·后端
__万波__2 小时前
二十三种设计模式(二)--工厂方法模式
java·设计模式·工厂方法模式
汤姆yu2 小时前
基于SpringBoot的餐饮财务管理系统的设计与实现
java·spring boot·后端