Bootstrap

云尚办公笔记(代码注释非常详细)

1 搭建环境

1.1 项目模块

  • 只使用了一个模块,感觉更加方便
    在这里插入图片描述

1.2 配置依赖关系

  • 只有一个模块,因此只使用了一个配置文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

<!--    spingboot的版本-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.atguigu</groupId>
    <artifactId>gui-oa-java</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gui-oa-java</name>
    <description>gui-oa-java</description>

    <properties>
<!--        统一配置版本-->
        <java.version>1.8</java.version>
        <java.version>1.8</java.version>
        <mybatis-plus.version>3.4.1</mybatis-plus.version>
        <mysql.version>8.0.30</mysql.version>
        <knife4j.version>3.0.3</knife4j.version>
        <jwt.version>0.9.1</jwt.version>
        <fastjson.version>2.0.21</fastjson.version>
    </properties>

    <dependencies>
<!--        spingboot启动类-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!--mybatis-plus 持久层-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        
<!--        mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.0</version>
        </dependency>
        
<!--        mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        
<!--        springboot测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!--knife4j-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>
        
        <!--jjwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jwt.version}</version>
        </dependency>
        
        <!--fastjson-->
<!--        可用于将Java对象转换为其JSON表示。它还可用于将JSON字符串转换为等效的Java对象-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        
<!--        lombok用来简化实体类-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

1.3 MyBatis-Plus

1.4 MyBatis-Plus入门

1.4.1 MyBatis-Plus条件构造器

  • 1 设置配置文件 com.atguigu.guioajava.config
package com.atguigu.guioajava.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
// 实际上是要扫描到mapper包
//@MapperScan("com.atguigu.guioajava.serviceoa")
public class MybatisPlusConfig {

    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置
     * MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 注意自己数据库的类型
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> configuration.setUseDeprecatedExecutor(false);
    }
}
  • 2 编写代码
    /**
     * 条件分页查询
     * @param page 当前页
     * @param limit 每页显示记录数
     * @param sysRoleQueryVo 条件对象
     * @return
     */
    @ApiOperation("条件分页查询")
    @GetMapping("{page}/{limit}")
    public Result pageQueryRole(@PathVariable Long page,
                                @PathVariable Long limit,
                                SysRoleQueryVo sysRoleQueryVo) {
        //调用service的方法实现
        //1 创建Page对象,传递分页相关参数
        //page 当前页  limit 每页显示记录数
        Page<SysRole> pageParam = new Page<>(page,limit);

        //2 封装条件,判断条件是否为空,不为空进行封装
        // 用StringUtils判断字符串是否为空  org.springframework.util.StringUtils
        LambdaQueryWrapper<SysRole> wrapper = new LambdaQueryWrapper<>();
        String roleName = sysRoleQueryVo.getRoleName();
        wrapper.like(!StringUtils.isEmpty(roleName),SysRole::getRoleName,roleName);

        //3 调用方法实现
        IPage<SysRole> pageModel = sysRoleService.page(pageParam, wrapper);
        System.out.println(pageModel);
        return Result.ok(SUCCESS,pageModel);
    }
  • 结果的格式
    在这里插入图片描述

1.5 角色管理

1.5.1 定义统一返回结果对象

  • 定义枚举类 com.atguigu.guioajava.common.result
package com.atguigu.guioajava.common.result;

import lombok.Getter;
/**
 * 统一返回结果状态信息类
 */
@Getter
public enum ResultCodeEnum {

    SUCCESS(200,"成功"),
    FAIL(201, "失败"),
    SERVICE_ERROR(2012, "服务异常"),
    DATA_ERROR(204, "数据异常"),
    LOGIN_AUTH(208, "未登陆"),
    PERMISSION(209, "没有权限");

    private Integer code;
    private String message;
    private ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
  • 2 定义结果类(和老师写的有些不同)
  • com.atguigu.guioajava.common.result
package com.atguigu.guioajava.common.result;

import lombok.Data;

@Data
public class Result<T> {

    //返回码
    private Integer code;
    //返回消息
    private String message;
    //返回数据
    private T data;
    // 构造私有化   不能new啦
    private Result(){};

    // 1 成功
    // 1.1 自己写code和message
    public static <T> Result<T> ok(Integer code, String message) {
        Result<T> result = new Result<T>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
    // 1.2 ResultCodeEnum设置的code和message
    public static <T> Result<T> ok(ResultCodeEnum resultCodeEnum) {
        Result<T> result = new Result<T>();
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }
    // 1.3 自己写code  message  data
    public static <T> Result<T> ok(Integer code, String message,T data) {
        Result<T> result = new Result<T>();
        result.setCode(code);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
    // 1.4 ResultCodeEnum设置的code和message  自己传入data
    public static <T> Result<T> ok(ResultCodeEnum resultCodeEnum,T data) {
        Result<T> result = new Result<T>();
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        result.setData(data);
        return result;
    }
    // 1.5 直接设置好成功操作
    public static <T> Result<T> ok() {
        Result<T> result = new Result<T>();
        result.setCode(200);
        result.setMessage("成功");
        return result;
    }

    // 2 失败
    // 2.1 ResultCodeEnum设置的code和message
    public static <T> Result<T> fail(ResultCodeEnum resultCodeEnum) {
        Result<T> result = new Result<T>();
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }
    // 2.2 自己写code  message
    public static <T> Result<T> fail(Integer code, String message) {
        Result<T> result = new Result<T>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
    // 1.3 直接设置好失败操作
    public static <T> Result<T> fail() {
        Result<T> result = new Result<T>();
        result.setCode(200);
        result.setMessage("数据异常");
        return result;
    }
}

1.5.2 knife4j

  • 文档地址:https://doc.xiaominfo.com/
  • knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案
  • 添加依赖
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
  • 添加knife4j配置类 com.atguigu.guioajava.config
package com.atguigu.guioajava.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
import java.util.ArrayList;
import java.util.List;
/**
 * knife4j配置信息
 * 直接全部复制 不然有些类的导入会出错
 */
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {
    @Bean
    /**
     * 添加头的信息
     */
    public Docket adminApiConfig(){
        List<Parameter> pars = new ArrayList<>();
        ParameterBuilder tokenPar = new ParameterBuilder();
        tokenPar.name("token")
                .description("用户token")
                .defaultValue("")
                .modelRef(new ModelRef("string"))
                .parameterType("header")
                .required(false)
                .build();
        pars.add(tokenPar.build());
        //添加head参数end
        Docket adminApi = new Docket(DocumentationType.SWAGGER_2)
                .groupName("adminApi")
                .apiInfo(adminApiInfo())
                .select()
                // 想要显示全部 就把这两句话注释掉
                //只作用于com.atguigu包下
                .apis(RequestHandlerSelectors.basePackage("com.atguigu"))
                //只显示admin路径下的页面
                .paths(PathSelectors.regex("/admin/.*"))
                .build()
                .globalOperationParameters(pars);
        return adminApi;
    }

    private ApiInfo adminApiInfo(){
        return new ApiInfoBuilder()
                .title("后台管理系统-API文档")
                .description("本文档描述了后台管理系统微服务接口定义")
                .version("1.0")
                .contact(new Contact("atguigu", "http://atguigu.com", "[email protected]"))
                .build();
    }
}
  • Controller层添加注解
@Api(tags = "角色管理")
@RestController
@RequestMapping("/admin/system/sysRole")
public class SysRoleController {

    @Autowired
    private SysRoleService sysRoleService;

    @ApiOperation(value = "获取全部角色列表")
    @GetMapping("findAll")
    public Result<List<SysRole>> findAll() {
        List<SysRole> roleList = sysRoleService.list();
        return Result.ok(roleList);
    }
}
  • 测试:http://localhost:8800/doc.html

配置日期时间格式
application-dev.yml添加以下内容
jackson在spring下面和datasource是一个级别的

  jackson:
    # 设置日期格式
    date-format: yyyy-MM-dd HH:mm:ss
    # 东八区  要加八个小时
    time-zone: GMT+8

1.5.3 统一异常处理

  • 全局异常处理
  • 特定异常处理
  • 自定义异常处理
package com.atguigu.guioajava.common.exception;
import com.atguigu.guioajava.common.result.ResultCodeEnum;
import lombok.Data;
/**
 * 业务异常:用户行为不规范
 */
@Data
public class BusinessException extends RuntimeException{

    private Integer code;// 状态码
    private String msg;// 描述信息
    // 有code的
    public BusinessException(Integer code) {
        this.code = code;
    }
    public BusinessException(Integer code,String message) {
        super(message);
        this.code = code;
        this.msg = message;
    }
    /**
     * 接收枚举类型对象
     * @param resultCodeEnum
     */
    public BusinessException(ResultCodeEnum resultCodeEnum) {
        super(resultCodeEnum.getMessage());
        this.code = resultCodeEnum.getCode();
        this.msg = resultCodeEnum.getMessage();
    }
}

package com.atguigu.guioajava.common.exception;

import com.atguigu.guioajava.common.result.ResultCodeEnum;
import lombok.Data;

/**
 * 系统异常
 */
@Data
public class SystemException extends RuntimeException{
    private Integer code;// 状态码
    private String msg;// 描述信息
    public SystemException(Integer code) {
        this.code = code;
    }
    public SystemException(Integer code, String message) {
        super(message);
        this.code = code;
        this.msg = message;
    }
    /**
     * 接收枚举类型对象
     * @param resultCodeEnum
     */
    public SystemException(ResultCodeEnum resultCodeEnum) {
        super(resultCodeEnum.getMessage());
        this.code = resultCodeEnum.getCode();
        this.msg = resultCodeEnum.getMessage();
    }
}
  • 创建统一异常处理器 com.atguigu.guioajava.common.exception
package com.atguigu.guioajava.common.exception;
import com.atguigu.guioajava.common.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
 * 全局异常处理类
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    // 执行的异常种类 全局异常处理
    @ExceptionHandler(Exception.class)
    // 返回json数据格式
    @ResponseBody
    public Result error(Exception e){
        // 输出异常信息
        //记录日志
        //发送消息给运维
        //发送邮件给开发人员,ex对象发送给开发人员
        e.printStackTrace();
        return Result.fail(201,e.getMessage());
    }

    // 特定异常处理
    @ExceptionHandler(ArithmeticException.class)
    // 返回json数据格式
    @ResponseBody
    public Result error(ArithmeticException e){
        // 输出异常信息
        //记录日志
        //发送消息给运维
        //发送邮件给开发人员,ex对象发送给开发人员
        e.printStackTrace();
        return Result.fail(201,e.getMessage());
    }

    // 业务异常处理
    @ExceptionHandler(BusinessException.class)
    // 返回json数据格式
    @ResponseBody
    public Result error(BusinessException e){
        // 输出异常信息
        //记录日志
        //发送消息给运维
        //发送邮件给开发人员,ex对象发送给开发人员
        e.printStackTrace();
        return Result.fail(e.getCode(),e.getMsg());
    }

    // 系统异常处理
    @ExceptionHandler(SystemException.class)
    // 返回json数据格式
    @ResponseBody
    public Result error(SystemException e){
        // 输出异常信息
        //记录日志
        //发送消息给运维
        //发送邮件给开发人员,ex对象发送给开发人员
        e.printStackTrace();
        return Result.fail(e.getCode(),e.getMsg());
    }
}

完整的Controller层代码

package com.atguigu.guioajava.serviceoa.controller;
import com.atguigu.guioajava.common.exception.BusinessException;
import com.atguigu.guioajava.common.result.Result;
import com.atguigu.guioajava.enity.model.system.SysRole;
import com.atguigu.guioajava.enity.vo.system.SysRoleQueryVo;
import com.atguigu.guioajava.serviceoa.service.SysRoleService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.atguigu.guioajava.common.result.ResultCodeEnum.FAIL;
import static com.atguigu.guioajava.common.result.ResultCodeEnum.SUCCESS;
@Api(tags = "角色管理")
@RestController
@RequestMapping("/admin/system/sysRole")
public class SysRoleController {
    @Autowired
    private SysRoleService sysRoleService;
    
    @ApiOperation(value = "获取全部角色列表")
    @GetMapping("findAll")
    public Result<List<SysRole>> findAll() {
        // java.lang.ArithmeticException lang后面才是异常的class
        // int a=10/0;
//        try {
//            int a = 10/0;
//        }catch(Exception e) {
//            throw new BusinessException(201,"出现自定义异常");
//        }
        List<SysRole> roleList = sysRoleService.list();
        return Result.ok(SUCCESS,roleList);
    }

    /**
     * 条件分页查询
     * @param page 当前页
     * @param limit 每页显示记录数
     * @param sysRoleQueryVo 条件对象
     * @return
     */
    @ApiOperation("条件分页查询")
    @GetMapping("{page}/{limit}")
    public Result pageQueryRole(@PathVariable Long page,
                                @PathVariable Long limit,
                                SysRoleQueryVo sysRoleQueryVo) {
        //调用service的方法实现
        //1 创建Page对象,传递分页相关参数
        //page 当前页  limit 每页显示记录数
        Page<SysRole> pageParam = new Page<>(page,limit);

        //2 封装条件,判断条件是否为空,不为空进行封装
        // 用StringUtils判断字符串是否为空  org.springframework.util.StringUtils
        LambdaQueryWrapper<SysRole> wrapper = new LambdaQueryWrapper<>();
        String roleName = sysRoleQueryVo.getRoleName();
        wrapper.like(!StringUtils.isEmpty(roleName),SysRole::getRoleName,roleName);

        //3 调用方法实现
        IPage<SysRole> pageModel = sysRoleService.page(pageParam, wrapper);
        System.out.println(pageModel);
        return Result.ok(SUCCESS,pageModel);
    }

    @ApiOperation(value = "根据id获取角色")
    @GetMapping("get/{id}")
    public Result get(@PathVariable Long id) {
        SysRole role = sysRoleService.getById(id);
        // 三元运算符 更加优雅
        return null == role ? Result.fail(FAIL) : Result.ok(SUCCESS,role);
    }

    @ApiOperation(value = "新增角色")
    @PostMapping("save")
    public Result save(@RequestBody @Validated SysRole role) {
        sysRoleService.save(role);
        return Result.ok();
    }

    @ApiOperation(value = "修改角色")
    @PutMapping("update")
    public Result updateById(@RequestBody SysRole role) {
        sysRoleService.updateById(role);
        return Result.ok();
    }

    @ApiOperation(value = "根据id删除角色")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        boolean b = sysRoleService.removeById(id);
        return b ? Result.ok() : Result.fail(FAIL);
    }

    @ApiOperation(value = "根据id列表删除")
    @DeleteMapping("batchRemove")
    // 前端数组[1,2,3](前端list最终会转化为数组格式)  @RequestBody:后端将数组变为List
    public Result batchRemove(@RequestBody List<Long> idList) {
        boolean b = sysRoleService.removeByIds(idList);
        return b ? Result.ok() : Result.fail(FAIL);
    }
}

2 前端基础知识(已学过)

3 角色管理前端

3.1 前端框架

3.1.1 vue-element-admin

3.1.2 vue-admin-template

3.1.2.1 简介
  • vue-admin-template是基于vue-element-admin的一套后台管理系统基础模板(最少精简版)
  • GitHub地址
  • vue-admin-template 的基础上进行二次开发,把 vue-element-admin当做工具箱
3.1.2.2 使用

在这里插入图片描述

3.1.2.3 源码目录结构
|-dist 生产环境打包生成的打包项目
|-mock 使用mockjs来mock接口
|-public 包含会被自动打包到项目根路径的文件夹
	|-index.html 唯一的页面
|-src
	|-api 包含接口请求函数模块
		|-table.js  表格列表mock数据接口的请求函数
		|-user.js  用户登陆相关mock数据接口的请求函数
	|-assets 组件中需要使用的公用资源
		|-404_images 404页面的图片
	|-components 非路由组件
		|-SvgIcon svg图标组件
		|-Breadcrumb 面包屑组件(头部水平方向的层级组件)
		|-Hamburger 用来点击切换左侧菜单导航的图标组件
	|-icons
		|-svg 包含一些svg图片文件
		|-index.js 全局注册SvgIcon组件,加载所有svg图片并暴露所有svg文件名的数组
	|-layout
		|-components 组成整体布局的一些子组件
		|-mixin 组件中可复用的代码
		|-index.vue 后台管理的整体界面布局组件
	|-router
		|-index.js 路由器
	|-store
		|-modules
			|-app.js 管理应用相关数据
			|-settings.js 管理设置相关数据
			|-user.js 管理后台登陆用户相关数据
		|-getters.js 提供子模块相关数据的getters计算属性
		|-index.js vuex的store
	|-styles
		|-xxx.scss 项目组件需要使用的一些样式(使用scss)
	|-utils 一些工具函数
		|-auth.js 操作登陆用户的token cookie
		|-get-page-title.js 得到要显示的网页title
		|-request.js axios二次封装的模块
		|-validate.js 检验相关工具函数
		|-index.js 日期和请求参数处理相关工具函数
	|-views 路由组件文件夹
		|-dashboard 首页
		|-login 登陆
	|-App.vue 应用根组件
	|-main.js 入口js
	|-permission.js 使用全局守卫实现路由权限控制的模块
	|-settings.js 包含应用设置信息的模块
|-.env.development 指定了开发环境的代理服务器前缀路径
|-.env.production 指定了生产环境的代理服务器前缀路径
|-.eslintignore eslint的忽略配置
|-.eslintrc.js eslint的检查配置
|-.gitignore git的忽略配置
|-.npmrc 指定npm的淘宝镜像和sass的下载地址
|-babel.config.js babel的配置
|-jsconfig.json 用于vscode引入路径提示的配置
|-package.json 当前项目包信息
|-package-lock.json 当前项目依赖的第三方包的精确信息
|-vue.config.js webpack相关配置(如: 代理服务器)

3.1.3 实现登录&退出登录

在这里插入图片描述

3.1.3.1 改变登陆的访问路径
  • 方法一:.env.development文件
  • 本地IP+端口号
# base api
# VUE_APP_BASE_API = '/dev-api'
VUE_APP_BASE_API = 'http://localhost/8800'
  • 方法二:vue.config.js文件 第四十行
  • 注释掉mock接口配置
  • 配置代理转发请求到目标接口
    // 把这一段话注释掉
    // before: require('./mock/mock-server.js')
    proxy: {
      '/dev-api': { // 匹配所有以 '/dev-api'开头的请求路径
        // 把所有以/dev-api开头的请求路径转为http://localhost:8080
        target: 'http://localhost:8080',
        changeOrigin: true, // 支持跨域
        pathRewrite: { // 重写路径: 去掉路径中开头的'/dev-api'
          '^/dev-api': ''
        }
      }
    }

在这里插入图片描述

3.1.3.2 后端接口
  • 在后端创建相关接口返回与Mock相同的数据
package com.atguigu.guioajava.serviceoa.controller;

import com.atguigu.guioajava.common.result.Result;
import com.atguigu.guioajava.common.result.ResultCodeEnum;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 后台登录登出
 * </p>
 */
@Api(tags = "后台登录管理")
@RestController
@RequestMapping("/admin/system/index")
public class IndexController {
    /**
     * 登录
     * @return
     */
    @PostMapping("login")
    public Result login() {
        Map<String, Object> map = new HashMap<>();
        // {code: 200, message: "成功", data: {token: "admin"}}
        map.put("token","admin");
        return Result.ok(ResultCodeEnum.SUCCESS,map);
    }
    /**
     * 获取用户信息
     * @return
     */
    @GetMapping("info")
    public Result info() {
        Map<String, Object> map = new HashMap<>();
        // avatar: "https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg"
        // name: "admin"
        // roles: "[admin]"
        map.put("roles","[admin]");
        map.put("name","admin");
        map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
        return Result.ok(ResultCodeEnum.SUCCESS,map);
    }
    /**
     * 退出
     * @return
     */
    @PostMapping("logout")
    public Result logout(){
        return Result.ok();
    }
}
3.1.3.3 前端部分
  • 修改登录退出相关接口的路径
  • api的user.js文件(三个都要改)
export function login(data) {
  return request({
    // url: '/vue-admin-template/user/login',
    url: '/vue-admin-template/user/login',
    method: 'post',
    data
  })
}
  • 修改返回状态码,返回成功默认为20000,修改为200
  • src\utils\request.js(48行)
    // if the custom code is not 20000, it is judged as an error.
    // if (res.code !== 20000) {
    if (res.code !== 200) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
    // if (store.getters.token) {
    //   // let each request carry token
    //   // ['X-Token'] is a custom headers key
    //   // please modify it according to the actual situation
    //   config.headers['X-Token'] = getToken()
    // }
    // 修改的地方:请求自动携带token, 且请求头名称为 token
    const token = store.getters.token
    if (token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      // config.headers['X-Token'] = getToken()
      config.headers['token'] = token
    }

4 用户管理

  • 需求分析
    在这里插入图片描述

4.1 代码生成器

  • 1 引入依赖
	<dependency>
		<groupId>com.baomidou</groupId>
		<artifactId>mybatis-plus-generator</artifactId>
		<version>3.4.1</version>
	</dependency>

	<dependency>
		<groupId>org.apache.velocity</groupId>
		<artifactId>velocity-engine-core</artifactId>
		<version>2.0</version>
	</dependency>
  • 2 代码
package com.atguigu.common.codeGenerator;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
public class CodeGet {
    public static void main(String[] args) {

        // 1、创建代码生成器
        AutoGenerator mpg = new AutoGenerator();
        // 2、全局配置
        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        // 前面是项目部工程的绝对路径  后面是到java
        gc.setOutputDir("D:\\Code_Java\\guigu-oa\\guigu-oa-java"+"/src/main/java");

        gc.setServiceName("%sService");	//去掉Service接口的首字母I
        gc.setAuthor("atguigu");
        gc.setOpen(false);
        mpg.setGlobalConfig(gc);

        // 3、数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/guigu-oa?serverTimezone=GMT%2B8&useSSL=false");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("123456");
        dsc.setDbType(DbType.MYSQL);
        mpg.setDataSource(dsc);

        // 4、包配置
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.atguigu");
        pc.setModuleName("serviceoa"); //模块名
        pc.setController("controller");
        pc.setService("service");
        pc.setMapper("mapper");
        mpg.setPackageInfo(pc);

        // 5、策略配置
        StrategyConfig strategy = new StrategyConfig();

        // 指定表的名称
        strategy.setInclude("sys_user");

        strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
        strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作
        strategy.setRestControllerStyle(true); //restful api风格控制器
        strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符
        mpg.setStrategy(strategy);

        // 6、执行
        mpg.execute();
    }
}

4.2 CRUD(复制粘贴即可)

  • 后端
package com.atguigu.serviceoa.controller;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.enity.model.system.SysUser;
import com.atguigu.enity.vo.system.SysUserQueryVo;
import com.atguigu.serviceoa.service.SysUserService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 * 用户表 前端控制器
 * </p>
 *  * @author atguigu
 * @since 2023-04-21
 */
@RestController
@RequestMapping("admin/system/sysUser")
public class SysUserController {

    @Autowired
    private SysUserService sysUserService;

    //用户条件分页查询
    @ApiOperation("用户条件分页查询")
    @GetMapping("{page}/{limit}")
    public Result index(@PathVariable Long page,
                        @PathVariable Long limit,
                        SysUserQueryVo sysUserQueryVo) {
        //创建page对象
        Page<SysUser> pageParam = new Page<>(page,limit);

        //封装条件,判断条件值不为空
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        //获取条件值
        String username = sysUserQueryVo.getKeyword();
        String createTimeBegin = sysUserQueryVo.getCreateTimeBegin();
        String createTimeEnd = sysUserQueryVo.getCreateTimeEnd();
        //判断条件值不为空
        //like 模糊查询
        wrapper.like(!StringUtils.isEmpty(username),SysUser::getUsername,username);
        // 用户的创建时间在createTimeBegin和createTimeEnd之间
        //ge 大于等于
        wrapper.ge(!StringUtils.isEmpty(createTimeBegin),SysUser::getCreateTime,createTimeBegin);
        //le 小于等于
        wrapper.le(!StringUtils.isEmpty(createTimeEnd),SysUser::getCreateTime,createTimeEnd);

        //调用mp的方法实现条件分页查询
        IPage<SysUser> pageModel = sysUserService.page(pageParam, wrapper);
        return Result.ok(ResultCodeEnum.SUCCESS,pageModel);
    }

    @ApiOperation(value = "获取用户")
    @GetMapping("get/{id}")
    public Result get(@PathVariable Long id) {
        SysUser user = sysUserService.getById(id);
        return Result.ok(ResultCodeEnum.SUCCESS,user);
    }

    @ApiOperation(value = "保存用户")
    @PostMapping("save")
    public Result save(@RequestBody SysUser user) {
        sysUserService.save(user);
        return Result.ok();
    }

    @ApiOperation(value = "更新用户")
    @PutMapping("update")
    public Result updateById(@RequestBody SysUser user) {
        sysUserService.updateById(user);
        return Result.ok();
    }

    @ApiOperation(value = "删除用户")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        sysUserService.removeById(id);
        return Result.ok();
    }
}
  • 前端

  • 修改 src/router/index.js 文件

{
  path: '/system',
  component: Layout,
  meta: {
    title: '系统管理',
    icon: 'el-icon-s-tools'
  },
  alwaysShow: true,
  children: [
    {
      name: 'sysUser',
      path: 'sysUser',
      component: () => import('@/views/system/sysUser/list'),
      meta: {
        title: '用户管理',
        icon: 'el-icon-s-custom'
      },
    },
    {
      path: 'sysRole',
      component: () => import('@/views/system/sysRole/list'),
      meta: {
        title: '角色管理',
        icon: 'el-icon-s-help'
      },
    }
  ]
},
  • 定义基础api
  • 创建文件 src/api/system/sysUser.js(复制粘贴即可)
  • 实现页面功能
  • 创建src/views/system/sysUser/list.vue

4.3 给用户分配角色

  • 需求分析

在这里插入图片描述

  • 操作类:SysRoleController
    @ApiOperation(value = "根据用户ID获取角色数据")
    @GetMapping("/toAssign/{userId}")
    public Result toAssign(@PathVariable Long userId) {
        Map<String, Object> roleMap = sysRoleService.findRoleByUserId(userId);
        return Result.ok(roleMap);
    }

    @ApiOperation(value = "为一个用户分配多个角色")
    @PostMapping("/doAssign")
    public Result doAssign(@RequestBody AssginRoleVo assginRoleVo) {
        sysRoleService.doAssign(assginRoleVo);
        return Result.ok();
    }
  • service接口 && service接口实现
  • 操作类:SysRoleService
    // 根据用户ID获取角色数据
    Map<String, Object> findRoleByUserId(Long userId);

    // 为一个用户分配多个角色
    void doAssign(AssginRoleVo assginRoleVo);
  • 操作类:SysRoleServiceImpl
package com.atguigu.serviceoa.service.impl;
import com.atguigu.enity.model.system.SysRole;
import com.atguigu.enity.model.system.SysUserRole;
import com.atguigu.enity.vo.system.AssginRoleVo;
import com.atguigu.serviceoa.mapper.SysRoleMapper;
import com.atguigu.serviceoa.service.SysRoleService;
import com.atguigu.serviceoa.service.SysUserRoleService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {

    @Autowired
    private SysUserRoleService sysUserRoleService;
    // 根据用户ID获取角色数据
    @Override
    public Map<String, Object> findRoleByUserId(Long userId) {
        // 1 查询所有的角色 返回List集合
        List<SysRole> allSysRoles = baseMapper.selectList(null);

        // 2 根据用户ID获取所属角色ID
        // 从查询出的所有角色中获取角色ID
        LambdaQueryWrapper<SysUserRole> lqwUserRole = new LambdaQueryWrapper<>();
        lqwUserRole.eq(SysUserRole::getUserId,userId);
        List<SysUserRole> sysUserRoleList = sysUserRoleService.list(lqwUserRole);
        List<Long> roleIdList = sysUserRoleList.stream()
                .map(item -> item.getRoleId()).collect(Collectors.toList());

        // 3 根据所属角色ID查询出相应的角色对象
        // 从查询出的所有角色中获取角色对象
        List<SysRole> sysRoleList = new ArrayList<>();
        allSysRoles.stream().forEach( item ->{
            if (roleIdList.contains(item.getId())){
                sysRoleList.add(item);
            }
        });


        // 4 进行数据封装
        Map<String, Object> roleMap = new HashMap<>();
        roleMap.put("assginRoleList", sysRoleList);
        roleMap.put("allRolesList",allSysRoles);
        return roleMap;
    }

    // 为一个用户分配多个角色
    @Override
    public void doAssign(AssginRoleVo assginRoleVo) {

        // 1 根据userId删除sys_role_user表中的数据
        LambdaQueryWrapper<SysUserRole> lqw = new LambdaQueryWrapper<>();
        lqw.eq(SysUserRole::getUserId,assginRoleVo.getUserId());
        sysUserRoleService.remove(lqw);

        // 2 根据assginRoleVo中的RoleIdList重新分配
        List<Long> roleIdList = assginRoleVo.getRoleIdList();
        // 定义一个集合 一次性存入数据库
        List<SysUserRole> sysUserRoleList = new ArrayList<>();
        roleIdList.stream().forEach( item ->{
            SysUserRole sysUserRole = new SysUserRole();
            sysUserRole.setUserId(assginRoleVo.getUserId());
            sysUserRole.setRoleId(item);
            sysUserRoleList.add(sysUserRole);
        });
        sysUserRoleService.saveBatch(sysUserRoleList);
    }
}

4.4 更改用户状态&&整合前端

  • 需求分析

用户状态:状态(1:正常 0:停用),当用户状态为正常时,可以访问后台系统
当用户状态停用后,不可以登录后台系统

  • 操作类:SysUserController
    @ApiOperation(value = "更新状态")
    @GetMapping("updateStatus/{id}/{status}")
    public Result updateStatus(@PathVariable Long id, @PathVariable Integer status) {
        sysUserService.updateStatus(id, status);
        return Result.ok();
    }
  • 操作类:SysUserService
    // 更新用户状态
    void updateStatus(Long id, Integer status);
  • 操作类:SysUserServiceImpl
    // 更新用户状态
    @Override
    public void updateStatus(Long id, Integer status) {

        // 1 根据用户ID获取用户对象
        SysUser sysUser = baseMapper.selectById(id);
        // 2 设置状态修改值
        sysUser.setStatus(status);
        // 3 调用方法进行修改
        baseMapper.updateById(sysUser);

    }
  • 整合前端(复制粘贴即可)

5 菜单管理

  • 需求分析
    在这里插入图片描述
  • 代码生成器(连续生成两个)
        // 指定表的名称
        strategy.setInclude("sys_menu","sys_role_menu");

5.1 CRUD

  • SysMenuController
package com.atguigu.serviceoa.controller;
import com.atguigu.common.result.Result;
import com.atguigu.enity.model.system.SysMenu;
import com.atguigu.serviceoa.service.SysMenuService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
 * <p>
 * 菜单表 前端控制器
 * </p>
 *
 * @author atguigu
 * @since 2023-04-21
 */
@Api(tags = "菜单管理")
@RestController
@RequestMapping("/admin/system/sysMenu")
public class SysMenuController {

    @Autowired
    private SysMenuService sysMenuService;

    @ApiOperation(value = "获取菜单")
    @GetMapping("findNodes")
    public Result findNodes() {
        List<SysMenu> list = sysMenuService.findNodes();
        return Result.ok(list);
    }

    @ApiOperation(value = "新增菜单")
    @PostMapping("save")
    public Result save(@RequestBody SysMenu sysMenu) {
        sysMenuService.save(sysMenu);
        return Result.ok();
    }

    @ApiOperation(value = "修改菜单")
    @PutMapping("update")
    public Result updateById(@RequestBody SysMenu sysMenu) {
        sysMenuService.updateById(sysMenu);
        return Result.ok();
    }

    @ApiOperation(value = "删除菜单")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        sysMenuService.removeById(id);
        return Result.ok();
    }
}


5.2 获取菜单

  • 需求分析
    在这里插入图片描述

  • sysMenu实体类

package com.atguigu.enity.model.system;
import com.atguigu.enity.model.base.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;

@Data
@ApiModel(description = "菜单")
@TableName("sys_menu")
public class SysMenu extends BaseEntity {
	
	private static final long serialVersionUID = 1L;

	@ApiModelProperty(value = "所属上级")
	@TableField("parent_id")
	private Long parentId;

	@ApiModelProperty(value = "名称")
	@TableField("name")
	private String name;

	@ApiModelProperty(value = "类型(1:菜单,2:按钮)")
	@TableField("type")
	private Integer type;

	@ApiModelProperty(value = "路由地址")
	@TableField("path")
	private String path;

	@ApiModelProperty(value = "组件路径")
	@TableField("component")
	private String component;

	@ApiModelProperty(value = "权限标识")
	@TableField("perms")
	private String perms;

	@ApiModelProperty(value = "图标")
	@TableField("icon")
	private String icon;

	@ApiModelProperty(value = "排序")
	@TableField("sort_value")
	private Integer sortValue;

	@ApiModelProperty(value = "状态(0:禁止,1:正常)")
	@TableField("status")
	private Integer status;

	// 下级列表
	@TableField(exist = false)
	private List<SysMenu> children;
	//是否选中
	@TableField(exist = false)
	private boolean isSelect;
}
  • SysUserService接口
    // 获取菜单
    List<SysMenu> findNodes();
  • SysUserServiceImpl实现
  • 构建树形结构
package com.atguigu.serviceoa.service.impl;
import com.atguigu.common.util.MenuHelper;
import com.atguigu.enity.model.system.SysMenu;
import com.atguigu.serviceoa.mapper.SysMenuMapper;
import com.atguigu.serviceoa.service.SysMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * 菜单表 服务实现类
 */
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {

    @Override
    public List<SysMenu> findNodes() {
        // 1 查询所有菜单数据
        List<SysMenu> allSysMenuList = baseMapper.selectList(null);
        // 2 构建树形结构
//        第一层
//                children:[
//                        第二层
//                                ...
//                        ]
        List<SysMenu> resultList = MenuHelper.buildTree(allSysMenuList);
        return resultList;
    }
}
  • 添加帮助类
  • com.atguigu.common.util.MenuHelper
package com.atguigu.common.util;
import com.atguigu.enity.model.system.SysMenu;
import java.util.ArrayList;
import java.util.List;
public class MenuHelper {
    /**
     * 使用递归方法建菜单
     * @param allSysMenuList
     * @return
     */
    public static List<SysMenu> buildTree(List<SysMenu> allSysMenuList) {

        // 1 找到递归的入口  2 找到递归结束的条件
        // 1 创建List集合 用于存放最终数据
        List<SysMenu> trees = new ArrayList<>();
        // 2 把所有菜单那进行遍历
        allSysMenuList.forEach( sysMenu ->{
            // 递归入口进入 parentID = 0
            if (sysMenu.getParentId().longValue() == 0){
                // 定义一个方法 传入入口和所有菜单的数据
                trees.add(getChildrens(sysMenu,allSysMenuList));
            }
        });
        // 返回数据
        return trees;
    }

    // 定义一个方法 传入入口和所有菜单的数据
    // 按照入口查下一层数据  一层一层往下查
    private static SysMenu getChildrens(SysMenu sysMenu, List<SysMenu> allSysMenuList) {

        // 当前节点的ID与下一层节点的ID是否相同
        // 相同就封装起来  不一样 就没有
        // 遍历所有菜单数据  判断ID和allSysMenuList中的每一项的parentId对应关系
        allSysMenuList.stream().forEach( item ->{
            if (sysMenu.getId().longValue() == item.getParentId().longValue()){
                // 如果sysMenu.getChildren()为空 就要加一个空的集合
                if (sysMenu.getChildren()==null){
                    sysMenu.setChildren(new ArrayList<>());
                }
                // 相同 封装起来
                // 这个是得到sysMenu的属性
                // 也可能还有下一层
                sysMenu.getChildren().add(getChildrens(item,allSysMenuList));
            }
        });
        return sysMenu;
    }
}

5.3 删除菜单方法的完善

  • 需求分析:当主菜单下面有子菜单的时候 不能删除 另外写个方法
  • com.atguigu.serviceoa.controller.SysMenuController
    @ApiOperation(value = "删除菜单")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        sysMenuService.removeMenuById(id);
        return Result.ok();
    }
  • com.atguigu.serviceoa.service.impl.SysMenuServiceImpl
    // 删除菜单
    @Override
    public void removeMenuById(Long id) {

        // 判断当前菜单是否有子菜单
        LambdaQueryWrapper<SysMenu> lqw = new LambdaQueryWrapper<>();

        // 判断方式是当前菜单的id与其他菜单的parentID相同
        lqw.eq(SysMenu::getParentId,id);
        // 查询数量
        Integer integer = baseMapper.selectCount(lqw);
        if (integer>0){
            throw  new BusinessException(201,"当前菜单含有子菜单,不能删除");
        }else {
            baseMapper.deleteById(id);
        }

    }

5.4 用户管理前端实现(复制粘贴)

  • 添加路由:修改 src/router/index.js 文件
  • 定义基础api 创建文件 src/api/system/sysMenu.js
  • 实现页面功能 创建src/views/system/sysMenu/list.vue

5.5 给角色分配菜单

  • 需求分析
    在这里插入图片描述
  • 操作类:SysMenuController
    @ApiOperation(value = "查询所有菜单和根据角色ID获取分配给角色的菜单")
    @GetMapping("toAssign/{roleId}")
    public Result toAssign(@PathVariable Long roleId) {
        List<SysMenu> list = sysMenuService.findSysMenuByRoleId(roleId);
        return Result.ok(list);
    }

    @ApiOperation(value = "给角色分配菜单")
    @PostMapping("/doAssign")
    public Result doAssign(@RequestBody AssginMenuVo assginMenuVo) {
        sysMenuService.doAssign(assginMenuVo);
        return Result.ok();
    }
  • 操作类:SysMenuService
    // 查询所有菜单和根据角色ID获取分配给角色的菜单
    List<SysMenu> findSysMenuByRoleId(Long roleId);

    // 给角色分配菜单
    void doAssign(AssginMenuVo assginMenuVo);
  • 操作类:SysMenuServiceImpl
    // 查询所有菜单和根据角色ID获取分配给角色的菜单(菜单表中的isSelect要变为true)
    @Override
    public List<SysMenu> findSysMenuByRoleId(Long roleId) {
        // 1 查询所有菜单  status==1 菜单才可以用
        LambdaQueryWrapper<SysMenu> lqwMenu = new LambdaQueryWrapper<>();
        lqwMenu.eq(SysMenu::getStatus,1);
        List<SysMenu> allSysMenus = baseMapper.selectList(lqwMenu);

        // 2 根据角色ID查询角色对应的所有菜单ID(SysRoleMenu)
        LambdaQueryWrapper<SysRoleMenu> lqwRoleMenu = new LambdaQueryWrapper<>();
        lqwRoleMenu.eq(SysRoleMenu::getRoleId,roleId);
        List<SysRoleMenu> sysRoleMenus = sysRoleMenuService.list(lqwRoleMenu);
        List<Long> roleIds = sysRoleMenus.stream().map(item -> item.getMenuId()).collect(Collectors.toList());

        // 3 拿菜单ID和所有菜单进行比较 有对应ID的 菜单表中的isSelect要变为true
        allSysMenus.stream().forEach( item ->{
            if (roleIds.contains(item.getId())){
                item.setSelect(true);
            } else {
                item.setSelect(false);
            }
        });

        // 4 返回规定格式的菜单树形结构
        List<SysMenu> sysMenuTrees = MenuHelper.buildTree(allSysMenus);
        return sysMenuTrees;
    }

    // 给角色分配菜单
    @Override
    public void doAssign(AssginMenuVo assginMenuVo) {

        // 1 根据角色ID删除角色菜单表中的数据
        LambdaQueryWrapper<SysRoleMenu> lqw = new LambdaQueryWrapper<>();
        lqw.eq(SysRoleMenu::getRoleId,assginMenuVo.getRoleId());
        sysRoleMenuService.remove(lqw);

        // 2 从参数中获取角色新分配的菜单ID 进行遍历 然后将数据添加入角色菜单表中
        List<Long> menuIdList = assginMenuVo.getMenuIdList();
        List<SysRoleMenu> sysRoleMenus =new ArrayList<>();
        // foreach似乎不能使用continue
        for (Long item : menuIdList) {
            // 如果为空的话 就直接跳出本次循环
            if (StringUtils.isEmpty(item)) {
                continue;
            }
            SysRoleMenu sysRoleMenu = new SysRoleMenu();
            sysRoleMenu.setMenuId(item);
            sysRoleMenu.setRoleId(assginMenuVo.getRoleId());
            sysRoleMenus.add(sysRoleMenu);

        }
        // 将数据存入数据库
        sysRoleMenuService.saveBatch(sysRoleMenus);

    }
  • AssginMenuVo实体类
  • 给角色分配菜单方法的参数
package com.atguigu.enity.vo.system;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@ApiModel(description = "分配菜单")
@Data
public class AssginMenuVo {

    @ApiModelProperty(value = "角色id")
    // 当前的角色ID
    private Long roleId;
    // 从前端返回的角色所选择的菜单ID的列表
    @ApiModelProperty(value = "菜单id列表")
    private List<Long> menuIdList;

}

6 权限管理

6.1 权限管理介绍

  • 大致可归为三种:页面权限(菜单级)、操作权限(按钮级)、数据权限。当前系统只是讲解:菜单权限与按钮权限的控制。
    在这里插入图片描述

6.1.1 权限管理设计思路

  • 接下来需要实现这两个接口:

1、用户登录

2、登录成功根据token获取用户相关信息(菜单权限及按钮权限数据等)
在这 里插入  图片描述

6.2 JWT

  • 一个JWT由三个部分组成:JWT头、有效载荷、签名哈希,最后由这三者组合进行base64url编码得到JWT

6.2.1 项目集成JWT

  • 操作模块:common-util
  • 引入依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>
  • 添加JWT帮助类
  • com.atguigu.common.util.JwtHelper
package com.atguigu.common.util;
import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;
import java.util.Date;
public class JwtHelper {

    // 有效时长
    private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;
    // 签名加密的秘钥  设置为固定值
    private static String tokenSignKey = "123456";
    // 根据用户ID和名称生成token字符串(实际中可以设置更多)
    public static String createToken(Long userId, String username) {
        String token = Jwts.builder()
                // 分类
                .setSubject("AUTH-USER")
                // 设置有效时间  当前时间+有效时长
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                // 设置主体部分
                .claim("userId", userId)
                .claim("username", username)
                // 签名部分
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
        return token;
    }

    // 从token字符串中获取用户ID
    public static Long getUserId(String token) {
        try {
            if (StringUtils.isEmpty(token)) return null;
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            Integer userId = (Integer) claims.get("userId");
            return userId.longValue();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    // // 从token字符串中获取用户名称
    public static String getUsername(String token) {
        try {
            if (StringUtils.isEmpty(token)) return "";
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            return (String) claims.get("username");
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    // 测试方法
    public static void main(String[] args) {
        String token = JwtHelper.createToken(1L, "admin");
        // 输出的token包含三部分 用点进行隔开
        System.out.println(token);
        Long userId = JwtHelper.getUserId(token);
        String username = JwtHelper.getUsername(token);
        System.out.println(userId);
        System.out.println(username);
    }
}

6.3 用户登录

  • LoginVo :登录时 前端返回的参数实体类
package com.atguigu.enity.vo.system;
import lombok.Data;
/**
 * 登录对象
 */
@Data
public class LoginVo {

    /**
     * 用户名称
     */
    private String username;

    /**
     * 密码
     */
    private String password;
}
  • 使用MD5对密码进行加密
package com.atguigu.common.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class MD5 {

    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }

    public static void main(String[] args) {
        System.out.println(MD5.encrypt("111111"));
    }
}

  • 操作文件:com.atguigu.serviceoa.controller.IndexController
        @Autowired
    private SysUserService sysUserService;
    /**
     * 登录
     * @return
     */
    @PostMapping("login")
    public Result login(@RequestBody LoginVo loginVo) {
        // 前端接收的数据:{"code":200,"data":{"token":"admin-token"}}
        // 1 获取用户输入名和密码
        String username = loginVo.getUsername();
        String password = loginVo.getPassword();
        // 2 根据用户名查询数据库
        LambdaQueryWrapper<SysUser> lqw = new LambdaQueryWrapper<>();
        lqw.eq(SysUser::getUsername,username);
        // 根据用户名查询只能查出一条数据(getOne)
        SysUser sysUserServiceOne = sysUserService.getOne(lqw);
        // 3 用户信息是否存在
        if(sysUserServiceOne == null){
            throw new BusinessException(201,"用户不存在");
        }
        // 4 判断密码是否正确
        // 密码进行加密 使用MD5
        // 数据库的密码
        String password1 = sysUserServiceOne.getPassword();
        // 输入的密码进行加密
        String password_input = MD5.encrypt(password);
        
        if(!password_input.equals(password1)){
            throw new BusinessException(201,"密码错误");
        }
        // 5 判断用户是否被禁用  1:可用 0:禁用
        if(sysUserServiceOne.getStatus().intValue() == 0){
            throw new BusinessException(201,"用户已被禁用");
        }
        // 6 使用jwt根据用户id 和 用户名生成token字符串
        String token = JwtHelper.createToken(sysUserServiceOne.getId(),sysUserServiceOne.getUsername());
        // 7 结果返回
        Map<String,String> map = new HashMap<>();
        map.put("token",token);
        return Result.ok(map);
    }

6.4 获取用户信息

  • 接口数据:后端返回给前端的数据
Map<String, Object> map = new HashMap<>();
map.put("roles","[admin]");
map.put("name","admin");
map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
// 返回用户可以操作的按钮
map.put("buttons", new ArrayList<>());
// 返回用户可以操作的菜单
map.put("routers", new ArrayList<>());

说明:主要是获取当前登录用户的菜单权限及按钮权限数据

  • 操作文件:com.atguigu.auth.controller.IndexController
    /**
     * 登录之后获取用户信息      1 菜单操作权限 2 按钮操作权限
     * @return
     */
    @GetMapping("info")
    public Result info(HttpServletRequest request) {
        // 需要返回数据格式
        // {"code":200,"data":{"roles":["admin"],"introduction":"I am a super administrator",
        // "avatar":"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif","name":"Super Admin"}}

        // 1 从请求头获取用户信息(获取请求头token字符串)
        // 需要在前端设置(登录时把token放在请求头中)
        String token = request.getHeader("token");

        // 2 从token字符串获取用户ID和用户名称
        Long userId = JwtHelper.getUserId(token);
        String username = JwtHelper.getUsername(token);

        // 3 根据用户ID查询数据库 把对应的用户查询出来
        SysUser sysUser = sysUserService.getById(userId);

        // 4 根据用户ID获取用户可以操作的菜单列表(前端的路由不能写死啦)
        // 用户角色关系表 角色菜单关系表 菜单表
        // 查询数据库动态构造路由
        List<RouterVo> routerVoList = sysMenuService.findUserMenuListByUserId(userId);

        // 5 根据用户ID获取用户可以操作的按钮权限列表  perms(按钮权限) 是 permissions的简写
        // 返回的是 bnt.sysUser.list
        List<String> permsList = sysMenuService.findUserPermsByUserId(userId);

        // 6 返回的数据:t odo:表示需要完善的地方
        Map<String, Object> map = new HashMap<>();
        map.put("roles","[admin]");
        // 返回的是name 不是username
        map.put("name",sysUser.getName());
        map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
        // 6.1 用户可以操作的菜单
        map.put("routers",routerVoList);
        // 6.2 用户可以操作的按钮
        map.put("buttons",permsList);

        return Result.ok(map);
    }

6.4.1 获取用户菜单列表

  • 最终生成的路由格式
    在这里插入图片描述

  • 路由实体类 RouterVo

package com.atguigu.vo.system;
import lombok.Data;
import java.util.List;
/**
 * 路由配置信息
 */
@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;
}
  • 菜单表
    在这里插入图片描述

  • 实现 findUserMenuListByUserId 方法

  • 操作文件:com.atguigu.auth.service.SysMenuService

  • 根据用户ID获取用户可以操作的菜单列表(前端的路由不能写死啦)

    // 根据用户ID获取用户可以操作的菜单列表(前端的路由不能写死啦)
    List<RouterVo> findUserMenuListByUserId(Long userId);
  • 操作文件:com.atguigu.auth.service.impl.SysMenuServiceImpl
  • 一个约定:userId=1 为管理员
    // 根据用户ID获取用户可以操作的菜单列表(前端的路由不能写死啦)
    @Override
    public List<RouterVo> findUserMenuListByUserId(Long userId) {

        // 集合最终都要构建  放在外面同意构建
        List<SysMenu> sysMenuList = new ArrayList<>();

        // 1 判断当前用户是否是管理员  userID = 1
        // 1.1 如果是管理员 查询所有菜单列表
        if(userId.longValue()==1){
            // 查询所有菜单列表
            LambdaQueryWrapper<SysMenu> lqw = new LambdaQueryWrapper<>();
            // Status必须为1
            lqw.eq(SysMenu::getStatus,1);
            // 按照SortValue升序排列
            lqw.orderByAsc(SysMenu::getSortValue);
            sysMenuList = baseMapper.selectList(lqw);
        }else {
            // 1.2 如果不是管理员 根据userId查询可以操作的菜单列表
            // 多表关联查询 :用户角色关系表 角色菜单关系表 菜单表
            // 编写sql语句进行实现
            sysMenuList = baseMapper.findMenuListByUserId(userId);
        }

        // 2 把查询出来的数据构建成框架需要的路由结构
        // 2.1 使用菜单操作工具类构建成树形结构
        List<SysMenu> sysMenuListTree = MenuHelper.buildTree(sysMenuList);
        // 2.1 构建成框架要求的路由结构  this指的就是SysMenuService
        List<RouterVo> routerList = this.buildRouter(sysMenuListTree);
        return routerList;
    }
  • 把树形结构转变为所需要的路由结构
  • 实现 buildRouter 方法
  • 操作文件:com.atguigu.auth.service.impl.SysMenuServiceImpl
  • menu表中有一个type字段 0:一级菜单, 1:二级菜单, 2 :按钮
    // 构建成框架要求的路由结构  this指的就是SysMenuService
    private List<RouterVo> buildRouter(List<SysMenu> sysMenuListTree) {

        // 创建List集合 存储最终数据
        List<RouterVo> routers = new ArrayList<>();

        // 遍历sysMenuListTree 树形结构但是并不完整 放入一些必要的数据
        // 主菜单 比如:系统管理 日志管理 审批设置
        sysMenuListTree.stream().forEach( sysMenu -> {

            RouterVo router = new RouterVo();
            router.setHidden(false);
            router.setAlwaysShow(false);
            // 使用了一个自定义的方法 getRouterPath(sysMenu)
            router.setPath(getRouterPath(sysMenu));
            router.setComponent(sysMenu.getComponent());
            // 一个新的metaVo实体类
            router.setMeta(new MetaVo(sysMenu.getName(), sysMenu.getIcon()));

            // 下一层数据部分
            List<SysMenu> children = sysMenu.getChildren();
            // 分配权限的type也是2 但是他有path component  隐藏路由
            // type=1说明是二级菜单,在系统管理下面,比如用户管理 角色管理 菜单管理
            if(sysMenu.getType().intValue()==1){
                // 首先加载出隐藏路由
                // type = 2 component不为空
                List<SysMenu> hiddenMenuList = children.stream()
                        .filter(item -> !StringUtils.isEmpty(item.getComponent()))
                        .collect(Collectors.toList());
                // 隐藏路由可能不止一个 还是要遍历
                // 隐藏路由放入一些必要的数据  setHidden(true)
                hiddenMenuList.stream().forEach( item ->{

                    RouterVo router1 = new RouterVo();
                    // Hidden是true 说明是隐藏路由
                    router1.setHidden(true);
                    router1.setAlwaysShow(false);
                    router1.setPath(getRouterPath(sysMenu));
                    router1.setComponent(sysMenu.getComponent());
                    router1.setMeta(new MetaVo(sysMenu.getName(), sysMenu.getIcon()));

                    routers.add(router1);
                });
                // else 说明sysMenu不是二级菜单  应该是一级菜单
            } else {
                // children不为空
                // 集合不为空  说明为一级菜单
                if(!CollectionUtils.isEmpty(children)){
                    if(children.size()>0){
                        // 总是会被显示
                        router.setAlwaysShow(true);
                    }
                    // 进行递归操作
                    router.setChildren(buildRouter(children));
                }
            }
            routers.add(router);
        } );
        return routers;
    }
  • 隐藏路由
    在这里插入图片描述

隐藏路由的特点:
type = 2
component不为空

  • 实现 getRouterPath(sysMenu) 方法
  • 操作文件:com.atguigu.auth.service.impl.SysMenuServiceImpl
    /**
     * 获取路由地址
     * @param menu 菜单信息
     * @return 路由地址
     */
    public String getRouterPath(SysMenu menu) {
        // 主路由要加 "/"
        String routerPath = "/" + menu.getPath();
        // 是子路由 不用加“/”
        if(menu.getParentId().intValue() != 0) {
            routerPath = menu.getPath();
        }
        return routerPath;
    }

6.4.2 获取用户按钮权限

  • 对于按钮权限标识不为空才能被用户使用
    在这里插入图片描述

  • 实现 findUserPermsByUserId 方法

  • 操作文件:com.atguigu.auth.service.SysMenuService

  • 根据用户ID获取用户可以操作的按钮权限列表

    // 根据用户ID获取用户可以操作的按钮权限列表  perms 是 permissions的简写
    List<String> findUserPermsByUserId(Long userId);
  • 实现 findUserPermsByUserId 方法
  • 操作文件:com.atguigu.auth.service.SysMenuServiceImpl
  • findMenuListByUserId 多表关联查询 方法共用
    // 根据用户ID获取用户可以操作的按钮权限列表  perms 是 permissions的简写
    @Override
    public List<String> findUserPermsByUserId(Long userId) {

        // 集合最终都要构建  放在外面统一构建
        List<SysMenu> sysMenuList = new ArrayList<>();
        // 1 判断当前用户是否是管理员  userID = 1
        if(userId.longValue() == 1){
            // 1.1 如果是管理员 查询所有按钮列表
            // 查询所有按钮列表
            LambdaQueryWrapper<SysMenu> lqw = new LambdaQueryWrapper<>();
            // Status必须为1
            lqw.eq(SysMenu::getStatus,1);
            sysMenuList = baseMapper.selectList(lqw);
        }else {
            // 1.2 如果不是管理员 根据userId查询可以操作的按钮列表
            // 多表关联查询 :用户角色关系表 角色菜单关系表 菜单表
            sysMenuList = baseMapper.findMenuListByUserId(userId);
        }

        // 2 从查询出来的数据里面,获取可以操作的按钮的list集合(perms)
        List<String> permsList = sysMenuList.stream()
                // 先过滤 type= 2 为按钮
                .filter(item -> item.getType() == 2)
                // 取出pems = bnt.sysUser.update 的数据
                .map(item -> item.getPerms())
                .collect(Collectors.toList());
        return permsList;

    }

6.4.3 多表关联查询

  • 操作文件:com.atguigu.auth.mapper.SysMenuMapper以及对应的xml文件
  • 实现 baseMapper.findMenuListByUserId(userId) 方法
    // 1.2 如果不是管理员 根据userId查询可以操作的菜单列表
    // 多表关联查询 :用户角色关系表 角色菜单关系表 菜单表
    List<SysMenu> findMenuListByUserId(@Param("userId") Long userId);
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.serviceoa.mapper.SysMenuMapper">

<!--    namespace根据自己需要创建的的mapper的路径和名称填写-->

<!--    自定义返回结果-->
    <resultMap id="sysMenuMap" type="com.atguigu.enity.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>-->
<!--    # #{}接收传过来的参数值-->
<!--    # 状态为1 而且没有被删除的才能被查询出来-->
    <select id="findMenuListByUserId" resultMap="sysMenuMap">
        select
        distinct *
        from sys_menu m
        inner join sys_role_menu rm on rm.menu_id = m.id
        inner join sys_user_role ur on ur.role_id = rm.role_id
        where
        ur.user_id = #{userId}
        and m.status = 1
        and rm.is_deleted = 0
        and ur.is_deleted = 0
        and m.is_deleted = 0
    </select>
</mapper>
  • 出现报错
    在这里插入图片描述
  • 报错原因
    在这里插入图片描述
  • 方式二
  • 1、在pom.xml添加
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.yml</include>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes> <include>**/*.yml</include>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
  • 2、application-dev.yml添加
mybatis-plus:
  mapper-locations: classpath:com/atguigu/auth/mapper/xml/*.xml

7 Spring Security

7.1 Spring Security介绍

  • Web 应用的安全性包括**用户认证(Authentication)和用户授权(Authorization)**两个部分,这两点也是 SpringSecurity 重要核心功能
  • 用户认证指的是:系统认为用户是否能登录
  • 用户授权指的是:系统判断用户是否有权限去做某些事情

7.2 Spring Security实现权限

  • 要对Web资源进行保护,最好的办法莫过于Filter
  • 要想对方法调用进行保护,最好的办法莫过于AOP。(不改变原代码 增加功能)

7.2.1 Spring Security入门

  • 创建springSecurity包
  • 引入依赖
        <!-- Spring Security依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<scope>provided </scope>
		</dependency>
  • 在config文件夹中创建配置类
package com.atguigu.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
  • 启动项目测试

7.2.2 用户认证

  • 需求分析
    在这里插入图片描述
    在这里插入图片描述
7.2.2.1 加密器PasswordEncoder
package com.atguigu.common.springSecurity.custom;
import com.atguigu.common.util.MD5;
import org.springframework.stereotype.Component;

/**
 * 密码处理
 */
@Component
public class CustomMd5PasswordEncoder {

    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }
}
7.2.2.2 添加CustomUser对象
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class CustomUser extends User {

    /**
     * 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了)
     */
    private SysUser sysUser;
    public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
        super(sysUser.getUsername(), sysUser.getPassword(), authorities);
        this.sysUser = sysUser;
    }
    public SysUser getSysUser() {
        return sysUser;
    }
    public void setSysUser(SysUser sysUser) {
        this.sysUser = sysUser;
    }
}
7.2.2.3 业务对象UserDetailsService
  • com.atguigu.common.springSecurity.custom
public interface UserDetailsService {
    /**
     * 根据用户名获取用户对象(获取不到直接抛异常)
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • com.atguigu.serviceoa.service.impl
package com.atguigu.serviceoa.service.impl;
import com.atguigu.common.springSecurity.custom.CustomUser;
import com.atguigu.enity.model.system.SysUser;
import com.atguigu.serviceoa.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.Collections;
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 根据用户名进行查询
        SysUser sysUser = sysUserService.getUserByUsername(username);
        if(null == sysUser) {
            throw new UsernameNotFoundException("用户名不存在!");
        }

        if(sysUser.getStatus().intValue() == 0) {
            throw new RuntimeException("账号已停用");
        }
        return new CustomUser(sysUser, Collections.emptyList());
    }
}

  • 在SysUserService中定义getUserByUsername方法
    // 根据用户名进行查询
    SysUser getUserByUsername(String username);
  • SysUserServiceImpl实现getUserByUsername方法
    @Override
    public SysUser getUserByUsername(String username) {

        LambdaQueryWrapper<SysUser> lqw = new LambdaQueryWrapper<>();
        lqw.eq(SysUser::getUsername,username);
        SysUser sysUser = baseMapper.selectOne(lqw);
        return sysUser;
    }
7.2.2.4 自定义用户认证接口TokenLoginFilter
  • com.atguigu.common.springSecurity.filter
package com.atguigu.common.springSecurity.filter;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.springSecurity.custom.CustomUser;
import com.atguigu.common.util.JwtHelper;
import com.atguigu.common.util.ResponseUtil;
import com.atguigu.enity.vo.system.LoginVo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {


    // 1 构造方法
    public TokenLoginFilter(AuthenticationManager authenticationManager) {
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径(需要自己修改)
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
    }

    // 2 登录认证
    // 获取输入的用户名和密码 调用方法进行认证
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            // 需要自己修改(登录封装的实体类LoginVo)
            // 获取用户信息
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);
            // 封装对象
            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            // 调用spring-security中的方法完成认证
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    // 3 认证成功调用方法
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        // 获取当前用户对象
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        // 生成token
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
        // 返回数据
        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        // 使用原生方式返回 使用了工具类
        // 不要导错包 使我们自己定义的工具类
        ResponseUtil.out(response, Result.ok(map));
    }

    // 4 认证失败调用方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
            ResponseUtil.out(response, Result.fail(ResultCodeEnum.LOGIN_ERROR));
    }
}

  • 认证成功调用方法,使用原生方式返回 使用了工具类
  • com.atguigu.common.util
package com.atguigu.common.util;

import com.atguigu.common.result.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ResponseUtil {

    public static void out(HttpServletResponse response, Result r) {
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            mapper.writeValue(response.getWriter(), r);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

7.2.2.5 认证解析token
  • com.atguigu.common.springSecurity.filter
package com.atguigu.common.springSecurity.filter;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.util.JwtHelper;
import com.atguigu.common.util.ResponseUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

public class TokenAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        //如果是登录接口,直接放行 不需要做认证判定
        if("/admin/system/index/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        // 得到当前用户  得到请求头中的token
        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(null != authentication) {
            // 如果返回值不为空 就放在上下文对象中
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } else {
            // 为空  返回失败
            ResponseUtil.out(response, Result.fail(ResultCodeEnum.PERMISSION));
        }

    }

    // 从请求头中获取对象
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置于header里 获取token
        String token = request.getHeader("token");
        // token不为空
        if (!StringUtils.isEmpty(token)) {
            String useruame = JwtHelper.getUsername(token);
            // usename不为空 返回返回封装的对象
            if (!StringUtils.isEmpty(useruame)) {
                return new UsernamePasswordAuthenticationToken(useruame, null, Collections.emptyList());
            }
        }
        return null;
    }
}

7.2.2.6 配置用户认证
  • 修改WebSecurityConfig配置类
  • com.atguigu.config
package com.atguigu.config;
import com.atguigu.common.springSecurity.custom.CustomMd5PasswordEncoder;
import com.atguigu.common.springSecurity.custom.UserDetailsService;
import com.atguigu.common.springSecurity.filter.TokenAuthenticationFilter;
import com.atguigu.common.springSecurity.filter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private CustomMd5PasswordEncoder customMd5PasswordEncoder;


    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
        http
                //关闭csrf跨站请求伪造
                .csrf().disable()
                // 开启跨域以便前端调用接口
                .cors().and()
                .authorizeRequests()
                // 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的

                // 需要自己修改
                .antMatchers("/admin/system/index/login").permitAll()

                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
                .addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager()));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定UserDetailService和加密器
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(customMd5PasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     * 排除swagger相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
    }
}

  • 这里会出现一个报错
    在这里插入图片描述
  • 报错原因:这行代码注入的必须是spring-security自带的类
    在这里插入图片描述
  • 解决方法:往我们自己定义的UserDetailsService中加入继承
  • extends org.springframework.security.core.userdetails.UserDetailsService
@Service
public interface UserDetailsService extends org.springframework.security.core.userdetails.UserDetailsService{

    /**
     * 根据用户名获取用户对象(获取不到直接抛异常)
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}

7.2.3 用户授权

  • 需求分析
    在这里插入图片描述

  • 使用默认的FilterSecurityInterceptor来进行权限校验

  • 在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication

  • 获取其中的权限信息,判断当前用户是否拥有访问当前资源所需的权限

7.2.3.1修改loadUserByUsername接口方法
  • com.atguigu.serviceoa.service.impl.UserDetailsServiceImpl
package com.atguigu.serviceoa.service.impl;
import com.atguigu.common.springSecurity.custom.CustomUser;
import com.atguigu.common.springSecurity.custom.UserDetailsService;
import com.atguigu.enity.model.system.SysUser;
import com.atguigu.serviceoa.service.SysMenuService;
import com.atguigu.serviceoa.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysMenuService sysMenuService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 根据用户名进行查询用户名
        SysUser sysUser = sysUserService.getUserByUsername(username);
        if(null == sysUser) {
            throw new UsernameNotFoundException("用户名不存在!");
        }

        if(sysUser.getStatus().intValue() == 0) {
            throw new RuntimeException("账号已停用");
        }

        // 根据用户ID得到用户可以操作的按钮权限数据
        List<String> userPermsList = sysMenuService.findUserPermsByUserId(sysUser.getId());
        // 不能直接封装 创建一个新的集合
        List<SimpleGrantedAuthority> authList = new ArrayList<>();
        // 把查询出来的数据放进这个SimpleGrantedAuthority的集合
        for (String perm : userPermsList) {
            authList.add(new SimpleGrantedAuthority(perm.trim()));
        }

        // 返回数据
        return new CustomUser(sysUser, authList);
    }
}

7.2.3.2修改successfulAuthentication接口方法
  • 登陆成功之后需要将用户名和权限数据放到redis
  • spring-security模块配置redis,添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 登录成功我们将权限数据保单到reids
  • 这里会有一个报错,后面解决
    // 注入redis
    private RedisTemplate redisTemplate;

    // 1 构造方法
    public TokenLoginFilter(AuthenticationManager authenticationManager,
                            RedisTemplate redisTemplate) {
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径(需要自己修改)
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));

        // 传递redis  构造函数参数不要忘了引用
        this.redisTemplate = redisTemplate;
    }
  • 登录成功我们将权限数据保单到reids
    // 3 认证成功调用方法
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        // 获取当前用户对象
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        // 生成token
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
        // 获取当前用户权限数据 放到redis里面     key:username   value:权限数据
        // 权限数据是集合数据  转化为JSON格式
        redisTemplate.opsForValue().set(customUser.getUsername(), JSON.toJSONString(customUser.getAuthorities()));
        // 返回数据
        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        // 使用原生方式返回 使用了工具类
        // 不要导错包 使我们自己定义的工具类
        ResponseUtil.out(response, Result.ok(map));
    }
7.2.3.3修改TokenAuthenticationFilter类
  • getAuthentication方法
  • 从请求头中获取用户名称和权限数据
// 从请求头中获取对象
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置于header里 获取token
        String token = request.getHeader("token");
        // token不为空
        if (!StringUtils.isEmpty(token)) {
            String username = JwtHelper.getUsername(token);
            // usename不为空 返回返回封装的对象
            if (!StringUtils.isEmpty(username)) {

                // 通过用户名称从redis中获取权限数据
                String authorString = (String) redisTemplate.opsForValue().get(username);

                // 把从redis获取的权限数据的字符串转化为要求的集合类型
                if (!StringUtils.isEmpty(authorString)){
                    // 权限数据 authorString不为空
                    // JSON转化为Map格式
                    List<Map> mapList = JSON.parseArray(authorString, Map.class);
                    System.out.println(mapList);
                    // 定义一个空集合接收所需类型的权限数据
                    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                    for (Map map : mapList) {
                        authorities.add(new SimpleGrantedAuthority((String)map.get("authority")));
                    }
                    return new UsernamePasswordAuthenticationToken(username, null, authorities);
                } else {
                    return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
                }
            }
        }
        return null;
    }
7.2.3.4 修改配置类
  • com.atguigu.config.WebSecurityConfig
// 开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
@EnableGlobalMethodSecurity(prePostEnabled = true)
  • 两个fillter添加redisTemplate参数
  • 注意查看完整代码
    在这里插入图片描述
7.2.3.5 在controller层加权限判断注解
  • 添加redis相关配置
spring:
  redis:
    host: 192.168.199.129
    port: 6379
    database: 0
    timeout: 1800000
    password: 123456
    jedis:
      pool:
        max-active: 20 #最大连接数
        max-wait: -1   #最大阻塞等待时间(负数表示没限制)
        max-idle: 5    #最大空闲
        min-idle: 0    #最小空闲
  • 在controller层加权限判断注解
7.2.3.5 异常处理
  • 分配了权限的能够成功返回接口数据
  • 没有分配权限的会抛出异常:org.springframework.security.access.AccessDeniedException: 不允许访问
  • 全局异常添加处理
  • AccessDeniedException需要引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <scope>provided</scope>
</dependency>
  • com.atguigu.common.exception.GlobalExceptionHandler
    /**
     * spring security异常
     * @param e
     * @return
     */
    // org.springframework.security.access.AccessDeniedException;
    // 这个包才对
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseBody
    public Result error(AccessDeniedException e) throws AccessDeniedException {
        return Result.fail(ResultCodeEnum.PERMISSION);
    }

8 Activiti入门

8.1 了解工作流

  • 工作流(Workflow):就是业务上一个完整的审批流程。例如员工的请假,出差,外出采购,合同审核等等

8.1.1 Activiti介绍

  • activiti是一个工作流引擎,可以将业务系统中复杂的业务流程抽取出来
  • 官方网站:https://www.activiti.org

8.1.2 建模语言BPM

  • 事件 Event

开始:表示一个流程的开始
中间:发生的开始和结束事件之间,影响处理的流程
结束:表示该过程结束

在这里插入图片描述

  • 活动 Activities
  • 一个活动可以是一个任务,还可以是一个当前流程的子处理流程
    在这里插入图片描述

8.2 Activiti7

9 审批管理模块

9.1 审批设置需求

在这里插入图片描述

9.2 审批类型管理

9.2.1 审批类型CRUD

  • 代码生成器生成(mapper、service、controller)
  • 在serviceoa下面新建包:process
    在这里插入图片描述
  • controller层代码直接复制粘贴
package com.atguigu.serviceoa.process.controller;
import com.atguigu.common.result.Result;
import com.atguigu.enity.model.process.ProcessType;
import com.atguigu.serviceoa.process.service.ProcessTypeService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 * 审批类型 前端控制器
 * </p>
 *
 * @author atguigu
 * @since 2023-04-27
 */
@Api(value = "审批类型管理", tags = "审批管理")
@RestController
@RequestMapping("/admin/process/processType")
public class ProcessTypeController {

    @Autowired
    private ProcessTypeService processTypeService;

    @ApiOperation(value = "获取分页列表")
    @GetMapping("{page}/{limit}")
    public Result index(@PathVariable Long page,
                        @PathVariable Long limit) {
        Page<ProcessType> pageParam = new Page<>(page,limit);
        IPage<ProcessType> pageModel = processTypeService.page(pageParam);
        return Result.ok(pageModel);
    }

    @ApiOperation(value = "根据获取审批类型")
    @GetMapping("get/{id}")
    public Result get(@PathVariable Long id) {
        ProcessType processType = processTypeService.getById(id);
        return Result.ok(processType);
    }

    @ApiOperation(value = "新增")
    @PostMapping("save")
    public Result save(@RequestBody ProcessType processType) {
        processTypeService.save(processType);
        return Result.ok();
    }

    @ApiOperation(value = "修改")
    @PutMapping("update")
    public Result updateById(@RequestBody ProcessType processType) {
        processTypeService.updateById(processType);
        return Result.ok();
    }

    @ApiOperation(value = "删除")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        processTypeService.removeById(id);
        return Result.ok();
    }

    @ApiOperation(value = "获取全部审批分类")
    @GetMapping("findAll")
    public Result findAll() {
        return Result.ok(processTypeService.list());
    }

}


  • 启动项目之后可能会出现一个bug:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘com.atguigu.serviceoa.process.mapper.ProcessTypeMapper’
解决方法:记得在每一个service、mapper上面添加@Service、@Mapper注解

9.2.2 前端页面(复制粘贴即可)

9.3 审批模板

9.3.1 审批模板CRUD

  • 审批模板实体类
  • 这里面加了一个新的属性:类型名称

	// 类型名称:表中没有,但是实体类中有  审批类型名称
	@TableField(exist = false)
	private String processTypeName;
9.3.1.1 分页查询功能
  • 由于查询出来的的数据要包含审批类型名称,需要重写分页查询方法
  • com.atguigu.serviceoa.process.controller.ProcessTemplateController
    @Autowired
    private ProcessTemplateService processTemplateService;

    @ApiOperation(value = "获取分页审批模板列表")
    @GetMapping("{page}/{limit}")
    public Result index(
            @ApiParam(name = "page", value = "当前页码", required = true)
            @PathVariable Long page,
            @ApiParam(name = "limit", value = "每页记录数", required = true)
            @PathVariable Long limit) {
        Page<ProcessTemplate> pageParam = new Page<>(page, limit);
        // 在审批模板中 有一个审批类型
        // 在 process_template 表中有一个字段 process_type_id
        // 根据审批类型ID查询审批类型名称
        IPage<ProcessTemplate> pageModel = processTemplateService.selectPage2(pageParam);
        return Result.ok(pageModel);
    }
  • com.atguigu.serviceoa.process.service.ProcessTemplateService
package com.atguigu.serviceoa.process.service;

import com.atguigu.enity.model.process.ProcessTemplate;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.stereotype.Service;

/**
 * <p>
 * 审批模板 服务类
 * </p>
 *
 * @author atguigu
 * @since 2023-04-27
 */
@Service
public interface ProcessTemplateService extends IService<ProcessTemplate> {

    /**
     * 获取分页审批模板列表
     * @param pageParam
     * @return
     * 在审批模板中 有一个审批类型
     * 在 process_template 表中有一个字段 process_type_id
     * 根据审批类型ID查询审批类型名称
     */
    IPage<ProcessTemplate> selectPage2(Page<ProcessTemplate> pageParam);
}

  • com.atguigu.serviceoa.process.service.impl.ProcessTemplateServiceImpl
package com.atguigu.serviceoa.process.service.impl;

import com.atguigu.enity.model.process.ProcessTemplate;
import com.atguigu.enity.model.process.ProcessType;
import com.atguigu.serviceoa.process.mapper.ProcessTemplateMapper;
import com.atguigu.serviceoa.process.service.ProcessTemplateService;
import com.atguigu.serviceoa.process.service.ProcessTypeService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

/**
 * <p>
 * 审批模板 服务实现类
 * </p>
 *
 * @author atguigu
 * @since 2023-04-27
 */
@Service
public class ProcessTemplateServiceImpl extends ServiceImpl<ProcessTemplateMapper, ProcessTemplate> implements ProcessTemplateService {

    @Autowired
    private ProcessTypeService processTypeService;
    /**
     * 获取分页审批模板列表
     * @param pageParam
     * @return
     * 在审批模板中 有一个审批类型
     * 在 process_template 表中有一个字段 process_type_id
     * 根据审批类型ID查询审批类型名称
     */
    @Override
    public IPage<ProcessTemplate> selectPage2(Page<ProcessTemplate> pageParam) {

        // 1 调用basemapper实现分页查询,查出来的结果不包含审批类型名称
        Page<ProcessTemplate> processTemplatePage = baseMapper.selectPage(pageParam, null);
        // 2 取得processTemplatePage里面的分页数据records
        List<ProcessTemplate> records = processTemplatePage.getRecords();
        // 3 遍历records,获取里面的类型ID  type_id
        List<Long> typeIdList = records.stream().map(item -> item.getProcessTypeId()).collect(Collectors.toList());
        // 4 根据typeIdList 查询对应的审批类型
        List<ProcessType> processTypes = processTypeService.listByIds(typeIdList);
        // 5 遍历rocords 把processType的类型名称放进去
        for (int i = 0; i < records.size(); i++) {
            records.get(i).setProcessTypeName(processTypes.get(i).getName());
        }
        // 6 将records数据放入page
        processTemplatePage.setRecords(records);
        return processTemplatePage;
    }
}

9.3.1.2 添加审批模板
  • 流程分析
    在这里插入图片描述

基本设置:一些基本信息
表单设置:动态表单
流程设置:本地设计流程定义,上传流程定义文件及流程定义图片(压缩上传)

  • 涉及未实现接口:

获取全部审批分类
上传流程定义压缩文件

  • 获取全部审批分类接口
  • com.atguigu.serviceoa.process.controller.ProcessTypeController
    @ApiOperation(value = "获取全部审批分类")
    @GetMapping("findAll")
    public Result findAll() {
        return Result.ok(processTypeService.list());
    }
  • 上传流程定义接口
  • 文件最终是要上传到resource的process中,编译之后也就是classes的process中
  • com.atguigu.serviceoa.process.controller.ProcessTemplateController
    @ApiOperation(value = "上传流程定义文件")
    @PostMapping("/uploadProcessDefinition")
    public Result uploadProcessDefinition(MultipartFile file) throws FileNotFoundException {
        // 文件最终是要上传到target/classes/process这个文件夹汇中
        // 获取class目录位置
        String path = new File(ResourceUtils.getURL("classpath:").getPath()).getAbsolutePath();
        // 设置上传文件夹
        File tempFile = new File(path + "/processes/");
        // 判断目录是否存在
        if (!tempFile.exists()) {
            tempFile.mkdirs();//创建目录
        }
        // 创建空文件用于写入文件
        // 获取文件名称
        String fileName = file.getOriginalFilename();
        File imageFile = new File(path + "/processes/" + fileName);
        // 保存文件流到本地
        try {
            file.transferTo(imageFile);
        } catch (IOException e) {
            e.printStackTrace();
            return Result.fail(201,"上传失败");
        }

        Map<String, Object> map = new HashMap<>();
        //根据上传地址后续部署流程定义,文件名称为流程定义的默认key
        map.put("processDefinitionPath", "processes/" + fileName);
        map.put("processDefinitionKey", fileName.substring(0, fileName.lastIndexOf(".")));
        return Result.ok(map);
    }
  • 集成form-create

1、添加依赖:在package.json文件添加依赖,注意版本号,更高的版本号可能与本项目不兼容
“@form-create/element-ui”: “^2.5.17”,
“@form-create/designer”: “^1.0.8”,
然后 npm install
2、在 main.js 中写入以下内容:
import formCreate from ‘@form-create/element-ui’
import FcDesigner from ‘@form-create/designer’
Vue.use(formCreate)
Vue.use(FcDesigner)
3、集成表单设计器
创建views/processSet/processTemplate/templateSet.vue

补充笔记:classpath

在这里插入图片描述

  • classpath指向的就是打war包之后的classes的位置
  • classes文件夹下就是我们原项目的java文件和resources文件夹里面的内容
9.3.1.3 查看审批模板(前端复制粘贴)
9.3.1.4 发布
  • 发布后审批模板就不可以修改了,然后部署流程定义
  • com.atguigu.serviceoa.process.controller.ProcessTemplateController
@ApiOperation(value = "发布")
@GetMapping("/publish/{id}")
public Result publish(@PathVariable Long id) {
    processTemplateService.publish(id);
    return Result.ok();
}
  • com.atguigu.serviceoa.process.service.impl.ProcessTemplateServiceImpl
@Transactional
@Override
public void publish(Long id) {
   ProcessTemplate processTemplate = this.getById(id);
   processTemplate.setStatus(1);
   processTemplateMapper.updateById(processTemplate);
    
   //TODO 部署流程定义,后续完善
}

9.4 管理端-审批管理

9.4.1 需求分析

  • 页面效果

在这里插入图片描述

  • 数据库表设计(process)
    在这里插入图片描述

9.4.2 审批管理CRUD

  • 分页查询
  • com.atguigu.serviceoa.process.controller.ProcessController
package com.atguigu.serviceoa.process.controller;
import com.atguigu.common.result.Result;
import com.atguigu.enity.vo.process.ProcessQueryVo;
import com.atguigu.enity.vo.process.ProcessVo;
import com.atguigu.serviceoa.process.service.ProcessService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 审批类型 前端控制器
 */
@Api(tags = "审批流管理")
@RestController
@RequestMapping(value = "/admin/process")
public class ProcessController {

    @Autowired
    private ProcessService processService;

    @ApiOperation(value = "获取审批管理分页列表")
    @GetMapping("{page}/{limit}")
    public Result index(@PathVariable Long page, @PathVariable Long limit, ProcessQueryVo processQueryVo) {
        Page<ProcessVo> pageParam = new Page<>(page, limit);
        IPage<ProcessVo> pageModel = processService.selectPage(pageParam, processQueryVo);
        return Result.ok(pageModel);
    }
}
  • com.atguigu.serviceoa.process.service.ProcessService
package com.atguigu.serviceoa.process.service;
import com.atguigu.enity.vo.process.ProcessQueryVo;
import com.atguigu.enity.vo.process.ProcessVo;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * 审批类型 服务类
 */
public interface ProcessService extends IService<Process> {

    // 获取审批管理分页列表
    IPage<ProcessVo> selectPage(Page<ProcessVo> pageParam, ProcessQueryVo processQueryVo);
}

  • com.atguigu.serviceoa.process.service.impl.ProcessServiceImpl
package com.atguigu.serviceoa.process.service.impl;
import com.atguigu.enity.vo.process.ProcessQueryVo;
import com.atguigu.enity.vo.process.ProcessVo;
import com.atguigu.serviceoa.process.mapper.ProcessMapper;
import com.atguigu.serviceoa.process.service.ProcessService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * 审批类型 服务实现类
 */
@Service
public class ProcessServiceImpl extends ServiceImpl<ProcessMapper, Process> implements ProcessService {
    // 获取审批管理分页列表
    @Override
    public IPage<ProcessVo> selectPage(Page<ProcessVo> pageParam, ProcessQueryVo processQueryVo) {
        IPage<ProcessVo> page = baseMapper.selectPage(pageParam, processQueryVo);
        return page;
    }
}
  • com.atguigu.serviceoa.process.mapper.ProcessMapper
package com.atguigu.serviceoa.process.mapper;
import com.atguigu.enity.vo.process.ProcessQueryVo;
import com.atguigu.enity.vo.process.ProcessVo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;

/**
 * 审批类型 Mapper 接口
 */
public interface ProcessMapper extends BaseMapper<Process> {
    // 获取审批管理分页列表
    // @Param("vo"):在xml文件中processQueryVo参数就叫做vo
    IPage<ProcessVo> selectPage(Page<ProcessVo> page, @Param("vo") ProcessQueryVo processQueryVo);
}

  • com/atguigu/serviceoa/process/mapper/xml/ProcessMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.serviceoa.process.mapper.ProcessMapper">

    <!--    // 获取审批管理分页列表-->
    <select id="selectPage" resultType="com.atguigu.enity.vo.process.ProcessVo">
        select
        a.id,a.process_code,a.user_id,a.process_template_id,a.process_type_id,a.title,a.description,a.form_values,a.process_instance_id,a.current_auditor,a.status,a.create_time,a.update_time,
        b.name as processTemplateName,
        c.name as processTypeName,
        d.name
        from process a
        left join process_template b on b.id = a.process_template_id
        left join process_type c on c.id = a.process_type_id
        left join sys_user d on d.id = a.user_id
        <where>
            <if test="vo.keyword != null and vo.keyword != ''">
                and (a.process_code like CONCAT('%',#{vo.keyword},'%') or  a.title like CONCAT('%',#{vo.keyword},'%') or d.phone like CONCAT('%',#{vo.keyword},'%') or d.name like CONCAT('%',#{vo.keyword},'%'))
            </if>
            <if test="vo.userId != null and vo.userId != ''">
                and a.user_id = #{vo.userId}
            </if>
            <if test="vo.status != null and vo.status != ''">
                and a.status = #{vo.status}
            </if>
            <if test="vo.createTimeBegin != null and vo.createTimeBegin != ''">
                and a.create_time &gt;= #{vo.createTimeBegin}
            </if>
            <if test="vo.createTimeEnd != null and vo.createTimeEnd != ''">
                and a.create_time &lt;= #{vo.createTimeEnd}
            </if>
        </where>
        order by id desc
    </select>
</mapper>

  • 前端复制粘贴即可
  • 运行过程会出现一个bug
    在这里插入图片描述
  • 配置文件中配置了xml文件的路径
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志
  # xml文件的路径
  # mapper-locations: classpath:com/atguigu/serviceoa/auth/mapper/xml/*.xml
  mapper-locations: classpath:com/atguigu/serviceoa/*/mapper/xml/*.xml

9.4.3 部署流程定义

  • com.atguigu.serviceoa.process.service.ProcessService
    // 部署流程定义
    // deployPath:zip文件的路径
    void deployByZip(String deployPath);
;