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

发布于:2025-07-31 ⋅ 阅读:(27) ⋅ 点赞:(0)

在后台管理系统中,不同用户角色往往拥有不同的操作权限,对应的菜单展示也需动态调整。动态路由加载正是解决这一问题的核心方案 —— 根据登录用户的权限,从数据库查询其可访问的菜单,封装成前端所需的路由结构并返回。本文将详细讲解如何基于 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 {
    @Autowired
    private RoleMapper roleMapper;
    @Autowired
    private 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 {
    @Autowired
    private MenuMapper menuMapper;

    @Override
    public 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 = 0
            if (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 = 一级菜单ID
                    if (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"
          }
        }
      ]
    }
  ]
}


网站公告

今日签到

点亮在社区的每一天
去签到