本文基于云尚办公项目展开叙述。
一、概述
不同的角色对应着不同的角色,每个角色又有着各自的权限,那么在登录的时候,在侧边栏就需要根据其角色获得权限,生成对应的管理菜单。
换句通俗点的话来说,也就是张三有董事长的身份,李四有总经理的身份,那么他们对应的角色自然有着各自的权限,董事长可以管理整个公司,总经理却不能管理董事会的事务,那么登录的时候,他们所展示的页面自然是不相同的。
二、数据库建表分析
2.1数据库字段分析
2.2 表关系分析
用户与角色,角色与权限菜单,这两个关系都是多对多的关系,所以都使用了第三张表。表之间的关系从图上可以轻松的看出。
三、后端数据构建分析
3.1获取Router和perms(按钮权限)并按照特定格式返回
3.1.1Controller层分析
@ApiOperation("获取用户信息")
@GetMapping("/info")
public Result info(HttpServletRequest request) {
//从请求头获取字符串
String token = request.getHeader("token");
//解析token 获取用户id
Long userId = JwtHelper.getUserId(token);
//根据用户id查询数据库,取出用户信息
SysUser sysUser = sysUserService.getById(userId);
//根据用户id查询用户可操作的菜单列表
//查询数据库动态构建路由结构,进行显示
List<RouterVo> routerList = sysMenuService.findUserMenuByUserId(userId);
//根据用户id获取用户可以操作菜单按钮
List<String> PermsList = sysMenuService.findUserPermsByUserId(userId);
//返回相应的数据
Map<String, Object> map = new HashMap<>();
map.put("roles","[admin]");
map.put("name",sysUser.getName());
map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
//用户可操作的菜单
map.put("router",routerList);
//用户可操作的路由地址
map.put("buttons",PermsList);
return Result.ok(map);
}
根据代码中给出的注释我们可以很清晰得理清在用户权限获取过程中需要事情。
那么对于router表和权限按钮得封装,也是就是两个方法:
-
sysMenuService.findUserMenuByUserId(userId)
-
sysMenuService.findUserPermsByUserId(userId)
需要重点理解
3.1.2Service层分析
3.2.1 findUserMenuByUserId(userId)获取路由结构方法分析
@Override
//查询数据库动态构建路由结构,进行显示
public List<RouterVo> findUserMenuByUserId(Long userId) {
List<SysMenu> sysMenuList = null;
//判断当前用户是否是管理员 userId=1 那么就是管理员
if (userId == 1) {
//如果是管理员 可以查询所有的菜单
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getStatus,1);
wrapper.orderByAsc(SysMenu::getSortValue);
sysMenuList = baseMapper.selectList(wrapper);
}else {
//如果不是管理员,根据用户Id查询可以操作的菜单列表
//多表关联查询 sys_user_role sys_role_menu menu 三表联查
sysMenuList = baseMapper.findMenuListByUserId(userId);
}
//将查出的数据列表封装为路由数据结构
//构建为树形结构
List<SysMenu> sysMenuTreeList = MenuHelper.buildTree(sysMenuList);
//构建为路由结构
return this.buildRouter(sysMenuTreeList);
}
对于路由权限的判断,有两种情况,一种是系统管理员,其拥有所有的权限,那只需要判断管理员账号使用情况,并对所有权限做一个升序排序即可;另一种是非管理员用户,那么就需要对其菜单进行判断和构建。
个人认为上述代码逻辑分析并无难度,那么我们分析非管理员用户可以操作的菜单列表。
3.2.1.1findMenuListByUserId(userId)
三表之间的关系已经在数据库分析中详细描述,在理清三表之间的关系后,那么对于当前用户如何获取其角色对应的菜单已经呼之欲出:
根据用户id查询sys_user_role表获取user对应的所有role_id,在根据所有的role_id查询sys_role_menu表获取对应的所有menu信息
<resultMap id="sysMenuMap" type="com.atguigu.model.system.SysMenu" autoMapping="true">
</resultMap>
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
m.id,m.parent_id,m.name,m.type,m.path,m.component,m.perms,m.icon,m.sort_value,m.status,m.create_time,m.update_time,m.is_deleted
</sql>
<!--List<SysMenu> findMenuListByUserId(@Param("userId") Long userId);-->
<select id="findMenuListByUserId" resultMap="sysMenuMap">
SELECT DISTINCT
<include refid="columns"/>
FROM
sys_menu m
LEFT JOIN sys_role_menu rm ON rm.menu_id = m.id
LEFT JOIN sys_user_role ur ON ur.role_id = rm.role_id
WHERE
m.`status` = 1
AND rm.is_deleted = 0
AND ur.is_deleted = 0
AND m.is_deleted = 0
AND ur.user_id = #{userId}
</select>
那么在这个方法中,主要是进行对三表的联合查询,咱们将重点可以放在sql语句的分析上。对于mybatis-plus我们减少了不少的开发的sql语句编写,但是对于多表查询,我们往往需要自己手动实现。
根据多表联查,我们很轻松的可以获取对应当前userId对应的菜单,并将其存储在List<SysMenu> sysMenuList,这个变量当中。
接下来,我们使用MenuHelper.buildTree()静态方法,将查询出来的用户菜单构建为一个树形结构(对于树形结构遗忘或者需要恶补一下的可以查看上一篇博客
2.2.1.2this.buildRouter(sysMenuTreeList)
好的,我们已经将当前用户对应的权限菜单全部取出,并且已经封装为一个树形菜单,那么接下来我们如何将这个树形菜单再进一步封装为我们需要的路由集合呢?
private List<RouterVo> buildRouter(List<SysMenu> menus) {
//创建一个list集合用于存储最终的数据
List<RouterVo> routers = new ArrayList<>();
//对menus集合进行遍历
for (SysMenu menu : menus) {
RouterVo router = new RouterVo();
router.setHidden(false);
router.setAlwaysShow(false);
router.setPath(getRouterPath(menu));
router.setComponent(menu.getComponent());
router.setMeta(new MetaVo(menu.getName(), menu.getIcon()));
//对下一层进行封装
List<SysMenu> children = menu.getChildren();
if (menu.getType() == 1) {
//是菜单 加载下面的隐藏路由 (item.getComponent())为空 则说明是隐藏路由
List<SysMenu> hiddenMenuList = children.stream().filter(item -> !StringUtils.isEmpty(item.getComponent())).collect(Collectors.toList());
for (SysMenu hiddenMenu : hiddenMenuList) {
RouterVo hiddenRouter = new RouterVo();
hiddenRouter.setHidden(true);//隐藏路由
hiddenRouter.setAlwaysShow(false);
hiddenRouter.setPath(getRouterPath(hiddenMenu));
hiddenRouter.setComponent(hiddenMenu.getComponent());
hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon()));
routers.add(hiddenRouter);
}
} else {
if (!CollectionUtils.isEmpty(children)){
if (children.size() > 0) router.setAlwaysShow(true);
//如果children不为空 就进行递归
router.setChildren(buildRouter(children));
}
}
routers.add(router);
}
return routers;
}
接下来我们分析如何对树形结构进行进一步的封装:
-
使用List<SysMenu> menus作为参数接收当前角色的树形菜单
-
RouterVo对象的声明,作用是封装为需要的Router菜单
@Data
public class RouterVo
{
/**
* 路由名字
*/
//private String name;
/**
* 路由地址
*/
private String path;
/**
* 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现
*/
private boolean hidden;
/**
* 组件地址
*/
private String component;
/**
* 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
*/
private Boolean alwaysShow;
/**
* 其他元素
*/
private MetaVo meta;
/**
* 子路由
*/
private List<RouterVo> children;
}
-
对当前的路由菜单进行遍历封装
-
对于部分简单属性,这边可以直接从树形菜单menus中取值,赋值到RouterVo对象中
-
难点在于对其子菜单的进一步封装,下面分析
我们在构建树形菜单的时候,使用了递归的方法进行构建子菜单,也就是children属性,相似的道理,在获取(读取)children属性的时候,我们也可以使用递归的方式进行读取,按序赋值。
首先对于数据库sys_menu表中type字段应有如下说明 type=0代表布局,type=1代表菜单,type=2代表按钮。
对树形菜单menus集合进行循环,逐个取值(也就是最外层的大router,而大router中的children字段包含了其中的子路由),先将大router的信息封装到routerVo对象中,然后对其children进行处理。
在chilren中又包含两类菜单,一种是隐藏路由,一种是显示路由,隐藏路由是指不在侧边栏进行显示展示,但是某个功能可能需要这个路由地址,所以其不存在component,显式路由则相反。那么对children遍历的时候需要注意这个情况。
如果menu_type = 1(大router的type是菜单) 并且 menu_component.isEmpty()(大router的children),满足条件组说明是一个隐式路由,需要额外处理,即setHidden(true)其余的与上面一致。
那么如果当前不是的大router不是菜单,(type=0说明是一个大Layout)就需要继续将children进行递归处理,完成对children路由的构建。
3.2.2findUserPermsByUserId(userId)获取按钮结构方法分析
@Override
//根据用户id获取用户可以操作菜单路由 (type为2,perms属性就存在)
public List<String> findUserPermsByUserId(Long userId) {
//判断是否是管理员,如果是管理员查询所有按钮列表
List<SysMenu> sysMenuList = null;
if (userId == 1) {
//如果是管理员 可以查询所有的菜单
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getStatus,1);
sysMenuList = baseMapper.selectList(wrapper);
}else {
//如果不是管理员,根据userId查询可以操作按钮列表
//多表关联查询 sys_user_role sys_role_menu menu 三表联查
sysMenuList = baseMapper.findMenuListByUserId(userId);
}
//从查询出来的数据里面,获取按钮值的list集合,返回
return sysMenuList.stream().filter(item -> item.getType() == 2).map(SysMenu::getPerms).collect(Collectors.toList());
}
有了上面对角色菜单的分析之后,那么对角色对应的按钮获取就可以更加炉火纯青。同样需要判断身份,如果是管理员,则不做限制,直接返回所有;而不是管理员需要根据userId进行多表查询获取所有的权限(与上面的多表查询完全一致),那么只需要判断type==2,就可以说明是一个按钮,然后获取其perms封装为一个List集合返回即可。