在后臺管理系統中,不同用戶角色往往擁有不同的操作權限,對應的菜單展示也需動態調整。動態路由加載正是解決這一問題的核心方案 —— 根據登錄用戶的權限,從數據庫查詢其可訪問的菜單,封裝成前端所需的路由結構并返回。本文將詳細講解如何基于 Spring Boot + MyBatis-Plus 實現這一功能,包含完整代碼與實現思路。
一、需求與實現思路
動態路由加載的核心目標是:根據登錄用戶的權限,動態生成其可訪問的菜單路由,最終返回給前端用于渲染側邊欄。整體實現思路分為四步:
- 獲取當前登錄用戶信息:通過 Session 獲取已登錄用戶的 ID(userId);
- 查詢用戶角色名稱:基于 userId,通過
user_role
表(用戶 - 角色關聯)和role
表(角色表)聯查,獲取用戶的角色名稱(如 “超級管理員”); - 查詢用戶權限菜單:基于 userId,通過
user_role
、role_menu
(角色 - 菜單關聯)、menu
(菜單表)三表聯查,獲取用戶可訪問的所有菜單; - 封裝路由結構:將數據庫查詢的菜單列表,轉換為前端所需的路由格式(包含一級菜單、二級菜單、路由元信息等)。
二、核心表結構設計
實現動態路由的前提是合理的表結構設計,需包含 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_role
、role_menu
、menu
三表聯查,獲取用戶可訪問的所有菜單:
@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
列表,實現步驟:
- 從數據庫查詢用戶可訪問的所有菜單(
menuList
); - 篩選一級菜單(
parent_id = 0
); - 為每個一級菜單封裝
MenuRouterVO
屬性(名稱、路徑、組件等); - 為每個一級菜單匹配子菜單(
parent_id = 一級菜單ID
),封裝為ChildMenuRouterVO
; - 組合一級菜單與子菜單,返回最終路由列表。
@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"}}]}]
}