博客后台模块续更(三)

四、后台模块-动态路由

实现了这个动态路由功能之后,就能在浏览器web页面登录进博客管理后台了

1. 接口分析

后台系统需要能实现不同的用户权限可以看到不同的功能,即左侧的导航栏

请求方式

请求地址

请求头

GET

/getRouters

需要token请求头

响应格式如下: 前端为了实现动态路由的效果,需要后端有接口能返回用户所能访问的菜单数据。注意: 返回的菜单数据需要体现父子菜单的层级关系

如果用户id为1代表管理员,menus中需要有所有菜单类型为C或者M的,C表示菜单,M表示目录,状态为正常的,未被删除的权限

注意这里不返回F的原因是按钮不属于菜单管理,所以接口不用返回

响应体如下:

{
	"code":200,
	"data":{
		"menus":[
			{
				"children":[],
				"component":"content/article/write/index",
				"createTime":"2022-01-08 11:39:58",
				"icon":"build",
				"id":2023,
				"menuName":"写博文",
				"menuType":"C",
				"orderNum":"0",
				"parentId":0,
				"path":"write",
				"perms":"content:article:writer",
				"status":"0",
				"visible":"0"
			},
			{
				"children":[
					{
						"children":[],
						"component":"system/user/index",
						"createTime":"2021-11-12 18:46:19",
						"icon":"user",
						"id":100,
						"menuName":"用户管理",
						"menuType":"C",
						"orderNum":"1",
						"parentId":1,
						"path":"user",
						"perms":"system:user:list",
						"status":"0",
						"visible":"0"
					},
					{
						"children":[],
						"component":"system/role/index",
						"createTime":"2021-11-12 18:46:19",
						"icon":"peoples",
						"id":101,
						"menuName":"角色管理",
						"menuType":"C",
						"orderNum":"2",
						"parentId":1,
						"path":"role",
						"perms":"system:role:list",
						"status":"0",
						"visible":"0"
					},
					{
						"children":[],
						"component":"system/menu/index",
						"createTime":"2021-11-12 18:46:19",
						"icon":"tree-table",
						"id":102,
						"menuName":"菜单管理",
						"menuType":"C",
						"orderNum":"3",
						"parentId":1,
						"path":"menu",
						"perms":"system:menu:list",
						"status":"0",
						"visible":"0"
					}
				],
				"createTime":"2021-11-12 18:46:19",
				"icon":"system",
				"id":1,
				"menuName":"系统管理",
				"menuType":"M",
				"orderNum":"1",
				"parentId":0,
				"path":"system",
				"perms":"",
				"status":"0",
				"visible":"0"
			}
		]
	},
	"msg":"操作成功"
}

2. 代码实现

第一步: 在huanf-framework工程的vo目录新建RoutersVo类,写入如下,负责把指定字段返回给前端

package com.keke.domain.vo;

import com.keke.domain.entity.Menu;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoutersVo {
     List menus;
}

第二步: 把keke-framework工程的Menu类修改为如下,增加了children字段(成员变量)、增加了

@Accessors(chain = true)注解

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;
import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.experimental.Accessors;

/**
 * 菜单权限表(Menu)表实体类
 *
 * @author makejava
 * @since 2023-10-18 20:55:24
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@TableName("sys_menu")
public class Menu {
    //菜单ID
    private Long id;
    //菜单名称
    private String menuName;
    //父菜单ID
    private Long parentId;
    //显示顺序
    private Integer orderNum;
    //路由地址
    private String path;
    //组件路径
    private String component;
    //是否为外链(0是 1否)
    private Integer isFrame;
    //菜单类型(M目录 C菜单 F按钮)
    private String menuType;
    //菜单状态(0显示 1隐藏)
    private String visible;
    //菜单状态(0正常 1停用)
    private String status;
    //权限标识
    private String perms;
    //菜单图标
    private String icon;
    //创建者
    private Long createBy;
    //创建时间
    private Date createTime;
    //更新者
    private Long updateBy;
    //更新时间
    private Date updateTime;
    //备注
    private String remark;
    
    private String delFlag;

    private List children;

}

第三步: 把keke-framework工程的MenuService接口修改为如下,增加了查询用户的路由信息(权限菜单)的接口

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Menu;

import java.util.List;


/**
 * 菜单权限表(Menu)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
public interface MenuService extends IService {


     //查询用户权限信息
     List selectPermsByUserId(Long userId);

     //查询用户的路由信息,也就是权限菜单
     List selectRouterMenuTreeByUserId(Long userId);
}

第四步: 把keke-framework工程的MenuServiceImpl类修改为如下,增加了查询用户的路由信息(权限菜单)的具体代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.entity.Menu;
import com.keke.mapper.MenuMapper;
import com.keke.service.MenuService;
import com.keke.utils.SecurityUtils;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 菜单权限表(Menu)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
@Service("menuService")
public class MenuServiceImpl extends ServiceImpl implements MenuService {



     //根据用户id查询权限关键字
     @Override
     public List selectPermsByUserId(Long userId) {
          //如果用户id为1代表管理员,roles 中只需要有admin,
          // permissions中需要有所有菜单类型为C(菜单)或者F(按钮)的,状态为正常的,未被删除的权限
          if(SecurityUtils.isAdmin()) {
               LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
               lambdaQueryWrapper.in(Menu::getMenuType, SystemConstants.MENU, SystemConstants.BUTTON);
               lambdaQueryWrapper.eq(Menu::getStatus, SystemConstants.STATUS_NORMAL);
               //由于我们的逻辑删除字段已经配置了,所以无需封装lambdaQueryWrapper
               List menuList = list(lambdaQueryWrapper);
               //我们需要的是String类型的集合,这里我们要进行数据的处理,这里采用流的方式
               List permissions = menuList.stream()
                       .map(new Function() {
                            @Override
                            public String apply(Menu menu) {
                                 String perms = menu.getPerms();
                                 return perms;
                            }
                       })
                       .collect(Collectors.toList());
               return permissions;
          }
          //否则返回这个用户所具有的权限
          //这里我们需要进行连表查询,因为我们的用户先和角色关联,然后角色才跟权限关联
          MenuMapper menuMapper = getBaseMapper();
          //我们期望menuMapper中有一个方法可以直接帮我们去实现这个复杂的逻辑,这里直接返回
          return menuMapper.selectPermsByUserId(userId);
     }

     @Override
     public List selectRouterMenuTreeByUserId(Long userId) {
          MenuMapper menuMapper = getBaseMapper();
          List menus = null;
          //如果是管理员,返回所有
          if(SecurityUtils.isAdmin()){
               menus = menuMapper.selectAllRoutersMenu();
          }else {
               //如果不是管理员,返回对应用户的菜单
               menus = menuMapper.selectRoutersMenuTreeByUserId(userId);
          }
          //因为上面的查询都是从数据库进行查询,所以无法封装children,这里构建Tree

          List menuTree = buildMenuTree(menus,0L);
          return menuTree;
     }

     /**
      * 构建MenuTree
      * 思路先找第一层级的菜单,就是找到id于parentId的对应关系,然后把parentId设置为Id的children
      * @param menus
      * @return
      */
     private List buildMenuTree(List menus,Long parentId) {
          //转化流处理
          List menuTree = menus.stream()
                  //过滤掉除一级菜单之外的菜单
                  .filter(menu -> menu.getParentId().equals(parentId))
                  //然后将获取其子菜单设置到children字段,并返回
                  .map(m -> m.setChildren(gerChildren(m, menus)))
                  .collect(Collectors.toList());
          return menuTree;
     }

     //获取当前菜单的子菜单
     private List gerChildren(Menu menu, List menus) {
          //流处理,遍历每一个流对象,筛选出流对象的parentId=menu的id,即过滤
          List children = menus.stream()
                  .filter(m -> m.getParentId().equals(menu.getId()))
                  //这里其实不必要写,这一步的逻辑是如果有三级,
                  //可以把流对象中再过筛选出子菜单设置给对应的children并返回
                  .map(m -> m.setChildren(gerChildren(m,menus)))
                  .collect(Collectors.toList());
          return children;
     }
}

第五步: 把keke-framework工程的MenuMapper接口修改为如下,增加了2个(一个查超级管理员,另一个查普通用户)查询权限菜单的接口

package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Menu;

import java.util.List;


/**
 * 菜单权限表(Menu)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
public interface MenuMapper extends BaseMapper {

     //Mapper的实现类对应xml映射文件
     List selectPermsByUserId(Long userId);

     List selectAllRoutersMenu();

     List selectRoutersMenuTreeByUserId(Long userId);

}

第六步: 把keke-framework工程的resources/mapper目录下的MenuMapper.xml文件修改为如下,是查询权限菜单的具体代码




    
    

    

第七步: 把keke-admin工程的LoginController类修改为如下,增加了查询路由信息(权限菜单)的接口

package com.keke.controller;


import com.keke.annotation.KekeSystemLog;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.Menu;
import com.keke.domain.entity.User;
import com.keke.domain.vo.AdminUserInfoVo;
import com.keke.domain.vo.RoutersVo;
import com.keke.domain.vo.UserInfoVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.MenuService;
import com.keke.service.RoleService;
import com.keke.service.SystemLoginService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.SecurityUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@Api(tags = "用户登录相关接口")
public class LoginController {

     @Autowired
     private SystemLoginService systemLoginService;

     @Autowired
     private MenuService menuService;

     @Autowired
     private RoleService roleService;

     @PostMapping("/user/login")
     @KekeSystemLog(businessName = "后台用户登录")
     public ResponseResult login(@RequestBody User user){
          if(!StringUtils.hasText(user.getUserName())){
               //提示必须要传用户名
               throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
          }
          return systemLoginService.login(user);
     }

     @GetMapping("/getInfo")
     public ResponseResult getInfo(){
          //获取当前登录用户,用我们封装的SecurityUtils
          LoginUser loginUser = SecurityUtils.getLoginUser();
          //根据用户id查询权限信息
          Long userId = loginUser.getUser().getId();
          List permissions = menuService.selectPermsByUserId(userId);
          //根据用户id查询角色信息
          List roles = roleService.selectRoleKeyByUserId(userId);
          //获取userInfo信息
          User user = loginUser.getUser();
          UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
          //创建Vo,封装返回
          AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(permissions,roles,userInfoVo);
          return ResponseResult.okResult(adminUserInfoVo);
     }



     @GetMapping("/getRouters")
     public ResponseResult getRouters(){
          //获取用户id
          Long userId = SecurityUtils.getUserId();
          //查询menu,结果是tree的形式
          List menus = menuService.selectRouterMenuTreeByUserId(userId);
          //封装返回
          return ResponseResult.okResult(menus);
     }

}

第八步: 测试

打开redis,启动前端工程

这里启动前台工程,测试很大可能是不通过的,可以参见以下问题

AdminUserInfoVo的变量名是否与要求返回的字段一致

前端工程中store目录下的modules目录下的permission.js文件中处理子路由的时候push写成了psuh,导致路由不能渲染。看看你浏览器控制台有没有报psuh的错误,有的话就是这里的问题。

补充如果测试失误,可以把token删除,然后重新登录测试

你可能感兴趣的:(状态模式)