程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

云上办公系统项目

发布于2023-05-20 19:16     阅读(1778)     评论(0)     点赞(5)     收藏(5)


云上办公系统项目

申明: 未经许可,禁止以任何形式转载,若要引用,请标注链接地址。 全文共计13077字,阅读大概需要30分钟
更多学习内容, 欢迎关注我
个人公众号:不懂开发的程序猿
个人网站:https://jerry-jy.co/

【警告】本篇博客较长,若引起阅读不适,建议收藏,稍后再读

1、云上办公系统

1.1、介绍

云上办公系统是一套自动办公系统,系统主要包含:管理端和员工端

管理端包含:权限管理、审批管理、公众号菜单管理

员工端采用微信公众号操作,包含:办公审批、微信授权登录、消息推送等功能

项目服务器端架构:SpringBoot + MyBatisPlus + SpringSecurity + Redis + Activiti+ MySQL

前端架构:vue-admin-template + Node.js + Npm + Vue + ElementUI + Axios

1.2、核心技术

基础框架:SpringBoot
数据缓存:Redis
数据库:MySQL
权限控制:SpringSecurity
工作流引擎:Activiti
前端技术:vue-admin-template + Node.js + Npm + Vue + ElementUI + Axios
微信公众号:公众号菜单 + 微信授权登录 + 消息推送

1.3、开发环境说明

工具版本
后台SpringBoot 2.3.6 + MyBatisPlus 3.4.1
服务器Tomcat 8.5.73
数据库MySQL 8.0.27
Build ToolsMaven 3.8.5
前端Vue + ElementUI + Node.js 14.15.0
开发工具IDEA 2022.3
版本管理工具Git

1.4、产品展示

后台

登录页

在这里插入图片描述

【系统管理】–【用户管理】

在这里插入图片描述

【系统管理】–【角色管理】

在这里插入图片描述

【系统管理】–【菜单管理】

在这里插入图片描述

【审批设置】–【审批类型】

在这里插入图片描述

【审批设置】–【审批模板】

在这里插入图片描述

【审批管理】–【审批列表】

在这里插入图片描述

【公众号菜单】–【菜单列表】

在这里插入图片描述

前台

正常的前台页面是在微信公众号上,我这里没有整合

http://localhost:9090/#/

在这里插入图片描述

审批页面

在这里插入图片描述

测试号的页面展示

在这里插入图片描述

1.5、 个人总结

我认为该项目对我来说主要的帮助有:

1、项目是前后端分离的,符合目前主流业务开发逻辑,作为后端程序员,复习前端Vue + ElementUI框架, 巩固练习使用前端的脚手架工程,学习使用前后端联调开发过程

2、项目中引入JWT加密token,用作用户登录身份校验,用 SpringSecurity 来做权限控制,涉及多表查询,是项目的重难点学习对象,也是对前面学习SpringSecurity的一个巩固

3、前端使用微信公众号来作为前端接入口,以前没有开发过,也是亮点。

4、引入 工作流引擎:Activiti 作为组件,第一次用,学习下

5、集成Swagger,方便进行接口API的统一测试

2、后端环境搭建

2.1、建库建表

db.sql

sql语句太多了,见文末的资料

2.2、创建Maven项目

本项目采用Maven聚合模块来管理工程

在这里插入图片描述

pom文件

guigu-oa-parent

<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 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.6.RELEASE</version>
  </parent>

  <groupId>com.jerry</groupId>
  <artifactId>guigu-oa-parent</artifactId>
  <version>1.0</version>
  <packaging>pom</packaging>


  <modules>
    <module>common</module>
    <module>model</module>
    <module>service-oa</module>
  </modules>

  <properties>
    <java.version>1.8</java.version>
    <mybatis-plus.version>3.4.1</mybatis-plus.version>
    <mysql.version>8.0.27</mysql.version>
    <knife4j.version>3.0.3</knife4j.version>
    <jwt.version>0.9.1</jwt.version>
    <fastjson.version>2.0.21</fastjson.version>
  </properties>

  <!--配置dependencyManagement锁定依赖的版本-->
  <dependencyManagement>
    <dependencies>
      <!--mybatis-plus 持久层-->
      <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>${mybatis-plus.version}</version>
      </dependency>
      <!--mysql-->
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
      </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-->
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <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>

common

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.jerry</groupId>
        <artifactId>guigu-oa-parent</artifactId>
        <version>1.0</version>
    </parent>

    <artifactId>common</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>common-util</module>
        <module>service-util</module>
    </modules>
</project>

common-util

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.jerry</groupId>
        <artifactId>common</artifactId>
        <version>1.0</version>
    </parent>
    <artifactId>common-util</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>provided </scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
    </dependencies>
</project>

service-util

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.jerry</groupId>
        <artifactId>common</artifactId>
        <version>1.0</version>
    </parent>
    <artifactId>service-util</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.jerry</groupId>
            <artifactId>common-util</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

</project>

model

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.jerry</groupId>
        <artifactId>guigu-oa-parent</artifactId>
        <version>1.0</version>
    </parent>
    <artifactId>model</artifactId>
    <dependencies>
        <!--lombok用来简化实体类-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <scope>provided </scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <scope>provided </scope>
        </dependency>
    </dependencies>

</project>

service-oa

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.jerry</groupId>
        <artifactId>guigu-oa-parent</artifactId>
        <version>1.0</version>
    </parent>
    <artifactId>service-oa</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>com.jerry</groupId>
            <artifactId>model</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>com.jerry</groupId>
            <artifactId>service-util</artifactId>
            <version>1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

配置数据源、服务器端口号

application.yml

在这里插入图片描述

spring:
  application:
    name: service-oa
  profiles:
    active: dev

application-dev.yml

server:
  port: 8800
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志
spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/guigu-oa?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8
    username: root
    password: root

导入实体类

在这里插入图片描述

2.3、编写代码

在这里插入图片描述

启动类

package com.jerry.auth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * ClassName: ServiceAuthApplication
 * Package: com.jerry.auth
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-02-28 22:03
 * @Version 1.0
 */

@SpringBootApplication
public class ServiceAuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceAuthApplication.class, args);
    }
}

3、后端角色管理

3.1、查询所有角色

SysRoleMapper

package com.jerry.auth.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jerry.model.system.SysRole;
import org.apache.ibatis.annotations.Mapper;

/**
 * ClassName: SysRoleMapper
 * Package: com.jerry.auth.mapper
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-02-28 22:05
 * @Version 1.0
 */

@Mapper
public interface SysRoleMapper extends BaseMapper<SysRole> {
}

SysRoleService

package com.jerry.auth.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jerry.model.system.SysRole;

/**
 * ClassName: SysRoleService
 * Package: com.jerry.auth.service
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 9:12
 * @Version 1.0
 */


public interface SysRoleService extends IService<SysRole> {
}

SysRoleServiceImpl

package com.jerry.auth.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jerry.auth.mapper.SysRoleMapper;
import com.jerry.auth.service.SysRoleService;
import com.jerry.model.system.SysRole;
import org.springframework.stereotype.Service;

/**
 * ClassName: SysRoleServiceImpl
 * Package: com.jerry.auth.service.impl
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 9:13
 * @Version 1.0
 */

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

编写测试类

目的是:

  • 测试数据源连接
  • 复习下MyBatisPlus对数据库的CRUD
package com.jerry.auth;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jerry.auth.mapper.SysRoleMapper;
import com.jerry.auth.service.SysRoleService;
import com.jerry.model.system.SysRole;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Arrays;
import java.util.List;

/**
 * ClassName: TestMpDemo1
 * Package: com.jerry.auth
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-02-28 22:07
 * @Version 1.0
 */

@SpringBootTest
public class TestMpDemo1 {

    // MyBatisPlus 对 service 层和 dao 层都做了很好的封装,直接调对应的CRUD方法就行
    @Autowired
    private SysRoleMapper sysRoleMapper;

    @Autowired
    private SysRoleService sysRoleService;

    // 使用MP 封装的 service 来操作数据库,查询所有记录
    @Test
    public void getAllByService(){
        List<SysRole> list = sysRoleService.list();
        list.forEach(System.out::println);
    }

    // 使用MP 封装的 mapper查询所有记录
    @Test
    public void getAllByMapper(){
        List<SysRole> sysRoles = sysRoleMapper.selectList(null);
        sysRoles.forEach(System.out::println);
    }

    // 添加操作
    @Test
    public void insert(){
        SysRole sysRole = new SysRole();
        sysRole.setRoleName("角色管理员");
        sysRole.setRoleCode("role");
        sysRole.setDescription("角色管理员");
        int result = sysRoleMapper.insert(sysRole);

        System.out.println(result); //影响的行数
        System.out.println(sysRole.getId()); //id自动回填
    }

    // 修改操作
    @Test
    public void updateById(){
        // 根据id查询
        SysRole sysRole = sysRoleMapper.selectById(9);
        // 设置修改值
        sysRole.setRoleName("角色管理员1");
        // 调用方法实现最终修改
        int update = sysRoleMapper.updateById(sysRole);

        System.out.println("update = " + update); // 受影响的行数
    }

    // 根据id删除
    @Test
    public void deleteById(){
        int delete = sysRoleMapper.deleteById(9);

        System.out.println("delete = " + delete); // 受影响的行数
    }

    // 批量删除
    @Test
    public void deleteBatchByIds(){

//        int delete = sysRoleMapper.delete(null);

        // 或者下面这种写法
        int delete = sysRoleMapper.deleteBatchIds(Arrays.asList(1, 2));

        System.out.println("ids = " + delete); // 受影响的行数
    }

    // 条件查询
    @Test
    public void testQueryWrapper(){

//        QueryWrapper<SysRole> queryWrapper = new QueryWrapper<>();
//        queryWrapper.eq("role_name", "系统管理员");

        // 或者下面这种写法
        LambdaQueryWrapper<SysRole> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysRole::getRoleName,"系统管理员");

        List<SysRole> list = sysRoleMapper.selectList(queryWrapper);
        list.forEach(System.out::println);
    }
}

编写统一结果返回类

目的:定义好统一的返回状态码和返回结果信息

ResultCodeEnum

package com.jerry.common.result;

import lombok.Getter;

/**
 * ClassName: ResultCodeEnum
 * Package: com.jerry.common.result
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 9:50
 * @Version 1.0
 */

@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;
    }
}

Result

package com.jerry.common.result;

import lombok.Data;

/**
 * ClassName: Result
 * Package: com.jerry.common.result
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 9:52
 * @Version 1.0
 */

@Data
public class Result<T> {
    private Integer code; // 状态码
    private String message; // 返回信息
    private T data; // 统一返回的结果数据

    /**
     * 封装返回数据
     * @param body
     * @param resultCodeEnum
     * @return
     * @param <T>
     */
    public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
        Result<T> result = new Result<>();

        // 封装数据
        if (body!=null){
            result.setData(body);
        }

        // 状态码
        result.setCode(resultCodeEnum.getCode());
        //返回信息
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }

    // 构造私有化 外部不能new
    private Result(){}

    // 成功 空结果
    public static <T>Result<T> ok(){
        return build(null,ResultCodeEnum.SUCCESS);
    }

    /**
     * 成功 返回有数据的结果
     * @param data
     * @return
     * @param <T>
     */
    public static <T>Result<T> ok(T data){
        return build(data,ResultCodeEnum.SUCCESS);
    }

    // 失败
    public static <T>Result<T> fail(){
        return build(null,ResultCodeEnum.FAIL);
    }

    /**
     * 失败  返回有数据的结果
     * @param data
     * @return
     * @param <T>
     */
    public static <T>Result<T> fail(T data){
        return build(data,ResultCodeEnum.FAIL);
    }

    public Result<T> message(String msg){
        this.setMessage(msg);
        return this;
    }

    public Result<T> code(Integer code){
        this.setCode(code);
        return this;
    }
}

SysRoleController

package com.jerry.auth.controller;

import com.jerry.auth.service.SysRoleService;
import com.jerry.common.result.Result;
import com.jerry.model.system.SysRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * ClassName: SysRoleController
 * Package: com.jerry.auth.controller
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 9:38
 * @Version 1.0
 */

@RestController
@RequestMapping("/admin/system/sysRole")
public class SysRoleController {
    @Autowired
    private SysRoleService sysRoleService;

    // http://localhost:8800/admin/system/sysRole/getAll
    // 测试查询所有的角色
//    @GetMapping("/getAll")
//    private List<SysRole> getAll(){
//        List<SysRole> list = sysRoleService.list();
//        return list;
//    }

    /**
     * 统一返回数据结果
     * @return
     */
    @GetMapping("/getAll")
    private Result getAll(){
        List<SysRole> list = sysRoleService.list();
        return Result.ok(list);
    }

}

测试

http://localhost:8800/admin/system/sysRole/getAll

在这里插入图片描述

3.2、集成knife4j

文档地址:https://doc.xiaominfo.com/

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。

Swagger介绍

前后端分离开发模式中,api文档是最好的沟通方式。

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。

1、及时性 (接口变更后,能够及时准确地通知相关前后端开发人员)

2、规范性 (并且保证接口的规范性,如接口的地址,请求方式,参数及响应格式和错误信息)

3、一致性 (接口信息一致,不会出现因开发人员拿到的文档版本不一致,而出现分歧)

4、可测性 (直接在接口文档上进行测试,以方便理解业务)

目的

  • 用来生成接口的API文档

  • 方便后端Java程序员进行接口测试

使用步骤

添加依赖

service-uitl.pom

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
添加knife4j配置类
package com.jerry.common.config.knife4j;

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;

/**
 * ClassName: knife4j
 * Package: com.jerry.common.config
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 10:53
 * @Version 1.0
 */

/**
 * 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()
                //只显示admin路径下的页面
                .apis(RequestHandlerSelectors.basePackage("com.jerry"))
                .paths(PathSelectors.regex("/admin/.*"))
                .build()
                .globalOperationParameters(pars);
        return adminApi;
    }

    private ApiInfo adminApiInfo(){

        return new ApiInfoBuilder()
                .title("后台管理系统-API文档")
                .description("本文档描述了后台管理系统微服务接口定义")
                .version("1.0")
                .contact(new Contact("jerry", "https://jerry-jy.co", "jinyang9248@163.com"))
                .build();
    }

}
Controller层添加注解
  • 类上加@Api(tags = "角色管理接口")
  • 方法上加@ApiOperation("查询所有角色")

在这里插入图片描述

测试

http://localhost:8800/doc.html

在这里插入图片描述

3.3、分页查询所有角色

service-util模块下创建 MybatisPlusConfig

MybatisPlusConfig

package com.jerry.common.config.mp;

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;

/**
 * ClassName: MybatisPlusConfig
 * Package: com.jerry.common.config.mp
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 11:17
 * @Version 1.0
 */

@Configuration
@MapperScan("com.jerry.auth.mapper")
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);
    }
}

主启动类上添加包扫描

在这里插入图片描述

SysRoleController

    /**
     * 条件分页查询
     *
     * @param page           当前页
     * @param pageSize       分页大小
     * @param sysRoleQueryVo 条件查询对象
     * @return
     */
    @ApiOperation("条件分页查询")
    @GetMapping("{page}/{pageSize}")
    private Result page(@PathVariable int page, @PathVariable int pageSize, SysRoleQueryVo sysRoleQueryVo) {

        // 1、创建 page 对象, 传递分页查询的参数
        Page<SysRole> sysRolePage = new Page<>(page, pageSize);

        // 2、构造分页查询条件, 判断条件是否为空,不为空进行封装
        LambdaQueryWrapper<SysRole> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        String roleName = sysRoleQueryVo.getRoleName();
        if (!StringUtils.isEmpty(roleName)) {
            // 封装
            lambdaQueryWrapper.like(SysRole::getRoleName,roleName);
        }

        // 3、调用方法实现分页查询
       sysRoleService.page(sysRolePage, lambdaQueryWrapper);
        return Result.ok(sysRolePage);
    }

测试

在这里插入图片描述

3.4、添加/修改/删除角色

    /**
     * 添加角色
     * @param sysRole
     * @return
     */
    @ApiOperation("添加角色")
    @PostMapping("/save")
    public Result save(@RequestBody SysRole sysRole) {
        // 调用 service 方法
        boolean is_success = sysRoleService.save(sysRole);
        if (is_success) {
            return Result.ok();
        } else {
            return Result.fail();
        }
    }

    /**
     * 根据 id 修改角色
     * @param id
     * @return
     */
    @ApiOperation("根据 id 查询角色")
    @GetMapping("/get/{id}")
    public Result get(@PathVariable long id){
        SysRole sysRole = sysRoleService.getById(id);
        return Result.ok(sysRole);
    }

    /**
     * 修改角色
     * @param sysRole
     * @return
     */
    @ApiOperation("修改角色")
    @PutMapping("/update")
    public Result update(@RequestBody SysRole sysRole) {
        // 调用 service 方法
        boolean is_success = sysRoleService.updateById(sysRole);
        if (is_success) {
            return Result.ok();
        } else {
            return Result.fail();
        }
    }

    /**
     * 根据 id 删除
     * @param id
     * @return
     */
    @ApiOperation("根据 id 删除")
    @DeleteMapping("delete/{id}")
    public Result deleteById(@PathVariable long id){
        boolean is_success = sysRoleService.removeById(id);

        if (is_success) {
            return Result.ok();
        } else {
            return Result.fail();
        }
    }

    /**
     * 批量删除
     * 说明:
     *      Java 中的对象会转化为Json对象
     *      Java 中的List集合会转化为数组
     * @param ids
     * @return
     */
    @ApiOperation("批量删除")
    @DeleteMapping("/ids")
    public Result deleteByIds(@RequestBody List<Long> ids){

        boolean is_success = sysRoleService.removeByIds(ids);

        if (is_success) {
            return Result.ok();
        } else {
            return Result.fail();
        }
    }

测试

在这里插入图片描述

配置日期时间格式

application-dev.yml添加以下内容

  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

4、统一异常处理

异常处理的思路流程

在这里插入图片描述

4.1、全局异常处理

4.2、特定异常处理

4.3、自定义异常处理

service-util 模块下

GlobalExceptionHandler

package com.jerry.common.config.exception;

import com.jerry.common.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * ClassName: GlobalExceptionHandler
 * Package: com.jerry.common.config.exception
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 15:48
 * @Version 1.0
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 全局异常处理 执行的方法
     * @return
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result error(Exception e){
        e.printStackTrace();
        return Result.fail().message("执行全局处理异常...");
    }

    /**
     * 特定异常处理
     * @param e
     * @return
     */
    @ExceptionHandler(ArithmeticException.class)
    @ResponseBody
    public Result error(ArithmeticException e){
        e.printStackTrace();
        return Result.fail().message("执行特定处理异常...");
    }

    /**
     * 自定义异常处理
     * @param e
     * @return
     */
    @ExceptionHandler(GuiguException.class)
    @ResponseBody
    public Result error(GuiguException e){
        e.printStackTrace();
        return Result.fail().code(e.getCode()).message(e.getMsg());
    }
}

GuiguException

package com.jerry.common.config.exception;

import com.jerry.common.result.ResultCodeEnum;
import lombok.Data;

/**
 * ClassName: GuiguException
 * Package: com.jerry.common.config.exception
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 15:59
 * @Version 1.0
 */
@Data
public class GuiguException extends RuntimeException {

    private Integer code;

    private String msg;

    /**
     * 通过状态码和错误消息创建异常对象
     * @param code
     * @param msg
     */
    public GuiguException(Integer code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    /**
     * 接收枚举类型对象
     * @param resultCodeEnum
     */
    public GuiguException(ResultCodeEnum resultCodeEnum) {
        super(resultCodeEnum.getMessage());
        this.code = resultCodeEnum.getCode();
        this.msg = resultCodeEnum.getMessage();
    }

    @Override
    public String toString() {
        return "GuiguException{" +
                "code=" + code +
                ", msg='" + msg + '\'' +
                '}';
    }
}

5、前端环境搭建

安装脚手架工程

前端用的脚手架工程是:vue-element-admin

https://panjiachen.github.io/vue-element-admin-site/#/

# clone the project
git clone https://github.com/PanJiaChen/vue-element-admin.git

# install dependency
npm install

# develop
npm run dev

http://localhost:9528/#/dashboard

在这里插入图片描述

前后联调的流程

在这里插入图片描述

在这里插入图片描述

修改前端的IP地址

    // before: require('./mock/mock-server.js')
    proxy: {
      '/dev-api': { // 匹配所有以 '/dev-api'开头的请求路径
        target: 'http://localhost:8800',
        changeOrigin: true, // 支持跨域
        pathRewrite: { // 重写路径: 去掉路径中开头的'/dev-api'
          '^/dev-api': ''
        }
      }
    }

在这里插入图片描述

编写后台登录/登出的请求

IndexController

package com.jerry.auth.controller;

import com.jerry.common.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
import java.util.Objects;

/**
 * ClassName: IndexController
 * Package: com.jerry.auth.controller
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 18:15
 * @Version 1.0
 */
@Api(tags = "后台登录管理")
@RestController
@RequestMapping("/admin/system/index")
public class IndexController {

    /**
     * login
     * @return
     */
    @ApiOperation("登录")
    @PostMapping("/login")
    public Result login(){
        // {"code":200,"data":{"token":"admin-token"}}
        HashMap<String, Object> map = new HashMap<>();
        map.put("token","admin-token");
        return Result.ok(map);
    }

    /**
     * info
     * @return
     */
    @GetMapping("/info")
    public Result info(){
        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");
        return Result.ok(map);
    }

    /**
     * logout
     * @return
     */
    @ApiOperation("登出")
    @PostMapping("/logout")
    public Result logout(){
        return Result.ok();
    }
}

修改前端的跳转地址

在这里插入图片描述

修改响应状态码

在这里插入图片描述

测试

重启前端、后端项目,可以发现请求头信息已经做了跳转、转发

在这里插入图片描述

6、前端角色管理

6.1、角色列表

修改路由

重新定义constantRoutes

在这里插入图片描述

  {
    path: '/system',
    component: Layout,
    meta: {
      title: '系统管理',
      icon: 'el-icon-s-tools'
    },
    alwaysShow: true,
    children: [
      {
        path: 'sysRole',
        component: () => import('@/views/system/sysRole/list'),
        meta: {
          title: '角色管理',
          icon: 'el-icon-s-help'
        },
      }
    ]
  },

创建角色页面

在这里插入图片描述

<template>
<div class="app-container">

    <!--查询表单-->
    <div class="search-div">
      <el-form label-width="70px" size="small">
        <el-row>
          <el-col :span="24">
            <el-form-item label="角色名称">
              <el-input style="width: 100%" v-model="searchObj.roleName" placeholder="角色名称"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row style="display:flex">
          <el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="fetchData()">搜索</el-button>
          <el-button icon="el-icon-refresh" size="mini" @click="resetData">重置</el-button>
        </el-row>
      </el-form>
    </div>

    <!-- 表格 -->
    <el-table
      v-loading="listLoading"
      :data="list"
      stripe
      border
      style="width: 100%;margin-top: 10px;"
      @selection-change="handleSelectionChange">

      <el-table-column type="selection"/>

      <el-table-column
        label="序号"
        width="70"
        align="center">
        <template slot-scope="scope">
          {{ (page - 1) * limit + scope.$index + 1 }}
        </template>
      </el-table-column>

      <el-table-column prop="roleName" label="角色名称" />
      <el-table-column prop="roleCode" label="角色编码" />
      <el-table-column prop="createTime" label="创建时间" width="160"/>
      <el-table-column label="操作" width="200" align="center">
        <template slot-scope="scope">
          <el-button type="primary" icon="el-icon-edit" size="mini" @click="edit(scope.row.id)" title="修改"/>
          <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeDataById(scope.row.id)" title="删除"/>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页组件 -->
    <el-pagination
        :current-page="page"
        :total="total"
        :page-size="limit"
        style="padding: 30px 0; text-align: center;"
        layout="total, prev, pager, next, jumper"
        @current-change="fetchData"
    />
  </div>
</template>

<script>
import api from '@/api/system/sysRole'
export default {
  // 定义数据模型
  // 定义数据模型
  data() {
    return {
      list: [], // 列表
      total: 0, // 总记录数
      page: 1, // 页码
      limit: 2, // 每页记录数
      searchObj: {}, // 查询条件
      multipleSelection: []// 批量删除选中的记录列表
    }
  },
  // 页面渲染成功后获取数据
  created() {
    this.fetchData()
  },
  // 定义方法
  methods: {
    fetchData(current=1) {
      this.page = current
      // 调用api
      api.getPageList(this.page, this.limit, this.searchObj).then(response => {
        this.list = response.data.records
        this.total = response.data.total
      })
    },
  }
}
</script>

定义角色管理相关的API请求函数

在这里插入图片描述

/*
角色管理相关的API请求函数
*/
import request from '@/utils/request'

const api_name = '/admin/system/sysRole'

export default {

  /*
  获取角色分页列表(带搜索)
  */
  getPageList(page, limit, searchObj) {
    return request({
      url: `${api_name}/${page}/${limit}`,
      method: 'get',
      // 如果是普通对象参数写法,params:对象参数名
      // 如果是使用json格式传递,data:对象参数名
      params: searchObj
    })
  }
}

测试

重新启动前端工程

http://localhost:9528/?#/system/sysRole

在这里插入图片描述

6.2、角色删除

sysRole.js

  /**
   * 角色删除
   * @param {*} id 
   * @returns 
   */
  removeById(id) {
    return request({
      url: `${api_name}/delete/${id}`,
      method: 'delete'
    })
  }

list.vue

    // 根据id删除数据
    removeDataById(id) {
        // debugger
        this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
        }).then(() => { // promise
            // 点击确定,远程调用ajax
            return api.removeById(id)
        }).then((response) => {
            // 刷新页面
            this.fetchData(this.page)
            // 提示信息
            this.$message.success(response.message || '删除成功')
        })
    }

6.3、角色添加

6.4、角色修改与数据回显

6.5、批量删除

前端CRUD完整代码

注意点:

前端中的url请求路径要和后端的@DeleteMapping@PutMapping@PostMapping@GetMapping路径一致

在这里插入图片描述

sysRole.js

/*
角色管理相关的API请求函数
*/
import request from '@/utils/request'

const api_name = '/admin/system/sysRole'

export default {

 /**
  * 获取角色分页列表(带搜索)
  * @param {*} page 
  * @param {*} limit 
  * @param {*} searchObj 
  * @returns 
  */
  getPageList(page, limit, searchObj) {
    return request({
      url: `${api_name}/${page}/${limit}`,
      method: 'get',
      // 如果是普通对象参数写法,params:对象参数名
      // 如果是使用json格式传递,data:对象参数名
      params: searchObj
    })
  },

  /**
   * 角色删除
   * @param {*} id 
   * @returns 
   */
  removeById(id) {
    return request({
      url: `${api_name}/delete/${id}`,
      method: 'delete'
    })
  },

  /**
   * 角色添加
   * @param {*} role 
   * @returns 
   */
  save(role) {
    return request({
      url: `${api_name}/save`,
      method: 'post',
      data: role
    })
  },

  // 回显要修改的id信息
  getById(id) {
    return request({
      url: `${api_name}/get/${id}`,
      method: 'get'
    })
  },
  
  // 修改
  updateById(role) {
    return request({
      url: `${api_name}/update`,
      method: 'put',
      data: role
    })
  },

  // 批量删除
  batchRemove(idList) {
    return request({
      url: `${api_name}/ids`,
      method: `delete`,
      data: idList
    })
  }


}

list.vue

<template>
<div class="app-container">

    <!--查询表单-->
    <div class="search-div">
      <el-form label-width="70px" size="small">
        <el-row>
          <el-col :span="24">
            <el-form-item label="角色名称">
              <el-input style="width: 100%" v-model="searchObj.roleName" placeholder="角色名称"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row style="display:flex">
          <el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="fetchData()">搜索</el-button>
          <el-button icon="el-icon-refresh" size="mini" @click="resetData">重置</el-button>
        </el-row>
      </el-form>
    </div>

    <!-- 表格 -->
    <el-table
      v-loading="listLoading"
      :data="list"
      stripe
      border
      style="width: 100%;margin-top: 10px;"
      @selection-change="handleSelectionChange">

      <el-table-column type="selection"/>

      <el-table-column
        label="序号"
        width="70"
        align="center">
        <template slot-scope="scope">
          {{ (page - 1) * limit + scope.$index + 1 }}
        </template>
      </el-table-column>

      <el-table-column prop="roleName" label="角色名称" />
      <el-table-column prop="roleCode" label="角色编码" />
      <el-table-column prop="createTime" label="创建时间" width="160"/>
      <el-table-column label="操作" width="200" align="center">
        <template slot-scope="scope">
          <el-button type="primary" icon="el-icon-edit" size="mini" @click="edit(scope.row.id)" title="修改"/>
          <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeDataById(scope.row.id)" title="删除"/>
        </template>
      </el-table-column>
    </el-table>

    
    <!-- 工具条 -->
    <div class="tools-div">
        <el-button type="success" icon="el-icon-plus" size="mini" @click="add">添 加</el-button>
        <el-button class="btn-add" size="mini" @click="batchRemove()" >批量删除</el-button>
    </div>

    <!-- 分页组件 -->
    <el-pagination
        :current-page="page"
        :total="total"
        :page-size="limit"
        style="padding: 30px 0; text-align: center;"
        layout="total, prev, pager, next, jumper"
        @current-change="fetchData"
    />

    <el-dialog title="添加/修改" :visible.sync="dialogVisible" width="40%" >
      <el-form ref="dataForm" :model="sysRole" label-width="150px" size="small" style="padding-right: 40px;">
        <el-form-item label="角色名称">
          <el-input v-model="sysRole.roleName"/>
        </el-form-item>
        <el-form-item label="角色编码">
          <el-input v-model="sysRole.roleCode"/>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false" size="small" icon="el-icon-refresh-right">取 消</el-button>
        <el-button type="primary" icon="el-icon-check" @click="saveOrUpdate()" size="small">确 定</el-button>
      </span>
    </el-dialog>

  </div>
</template>

<script>
import api from '@/api/system/sysRole'
export default {
  // 定义数据模型
  // 定义数据模型
  data() {
    return {
      list: [], // 列表
      total: 0, // 总记录数
      page: 1, // 页码
      limit: 2, // 每页记录数
      searchObj: {}, // 查询条件
      multipleSelection: [],// 批量删除选中的记录列表

      dialogVisible: false,
      sysRole: {},
      saveBtnDisabled: false
    }
  },
  // 页面渲染成功后获取数据
  created() {
    this.fetchData()
  },
  // 定义方法
  methods: {

    // 当多选选项发生变化的时候调用
    handleSelectionChange(selection) {
    console.log(selection)
    this.multipleSelection = selection
    },
    // 批量删除
    batchRemove() {
    if (this.multipleSelection.length === 0) {
        this.$message.warning('请选择要删除的记录!')
        return
    }
    this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
    }).then(() => {
        // 点击确定,远程调用ajax
        // 遍历selection,将id取出放入id列表
        var idList = []
        this.multipleSelection.forEach(item => {
        idList.push(item.id)
        })
        // 调用api
        return api.batchRemove(idList)
    }).then((response) => {
        this.fetchData()
        this.$message.success(response.message)
    })
    },

    // 点击修改,弹出框,根据id查询数据显示
    edit(id) {
    // 弹出框
    this.dialogVisible = true
    // 根据id查询
    this.fetchDataById(id)
    },

    fetchDataById(id) {
    api.getById(id).then(response => {
        this.sysRole = response.data
    })
    },

    // 点击添加弹框
    add(){
    this.dialogVisible = true
    },

    saveOrUpdate() {
    this.saveBtnDisabled = true // 防止表单重复提交
    // 根据id判断
    if (!this.sysRole.id) { // 添加
        this.saveData()
    } else { // 修改
        this.updateData()
    }
    },

    // 新增
    saveData() {
    api.save(this.sysRole).then(response => {
        // 提示
        this.$message.success(response.message || '操作成功')
        // 关闭弹框
        this.dialogVisible = false
        // 刷新页面
        this.fetchData(this.page)
    })
    },

    // 修改
    updateData() {
    api.updateById(this.sysRole).then(response => {
        // 提示
        this.$message.success(response.message || '操作成功')
        // 关闭弹框
        this.dialogVisible = false
        // 刷新页面
        this.fetchData(this.page)
    })
    },

    // 条件分页查询
    fetchData(current=1) {
      this.page = current
      // 调用api
      api.getPageList(this.page, this.limit, this.searchObj).then(response => {
        this.list = response.data.records
        this.total = response.data.total
      })
    },
    // 根据id删除数据
    removeDataById(id) {
        // debugger
        this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
        }).then(() => { // promise
            // 点击确定,远程调用ajax
            return api.removeById(id)
        }).then((response) => {
            // 刷新页面
            this.fetchData(this.page)
            // 提示信息
            this.$message.success(response.message || '删除成功')
        })
    }
  }
}
</script>

页面展示

在这里插入图片描述

7、用户管理

7.1、用户管理CRUD

需求分析

在这里插入图片描述

代码生成器

  • 可以采用MyBatisPlus提供的代码生成器直接生成 mapperserviceimplcontroller
  • 手动创建的话,也行

service-oa

        <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>

CodeGet.java

在这里插入图片描述

package com.jerry.code;


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();
        gc.setOutputDir("E:\\CodeLife\\IdeaProject\\guigu-oa\\guigu-oa-parent\\service-oa"+"/src/main/java");

        gc.setServiceName("%sService");	//去掉Service接口的首字母I
        gc.setAuthor("jerry");
        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("root");
        dsc.setDbType(DbType.MYSQL);
        mpg.setDataSource(dsc);

        // 4、包配置
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.jerry");
        pc.setModuleName("auth"); //模块名
        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();
    }
}

编写代码

代码是写在service-oa类中的

SysUserMapper

public interface SysUserMapper extends BaseMapper<SysUser> {
}

SysUserService

public interface SysUserService extends IService<SysUser> {
}

SysUserServiceImpl

@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
}

SysUserController

package com.jerry.auth.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jerry.auth.service.SysUserService;
import com.jerry.common.result.Result;
import com.jerry.model.system.SysUser;
import com.jerry.vo.system.SysUserQueryVo;
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.web.bind.annotation.*;

/**
 * <p>
 * 用户表 前端控制器
 * </p>
 *
 * @author jerry
 * @since 2023-03-01
 */
@Api(tags = "用户管理接口")
@RestController
@RequestMapping("/admin/system/sysUser")
public class SysUserController {
    @Autowired
    private SysUserService sysUserService;

    /**
     * 用户条件分页查询
     *
     * @param page
     * @param pageSize
     * @param sysUserQueryVo
     * @return
     */
    @ApiOperation("用户条件分页查询")
    @GetMapping("/{page}/{pageSize}")
    public Result page(@PathVariable int page, @PathVariable int pageSize, SysUserQueryVo sysUserQueryVo) {
        Page<SysUser> sysUserPage = new Page<>(page, pageSize);

        LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        // 获取条件
        String userName = sysUserQueryVo.getKeyword();
        String createTimeBegin = sysUserQueryVo.getCreateTimeBegin();
        String createTimeEnd = sysUserQueryVo.getCreateTimeEnd();

        // 判断条件值不为空
        if (!StringUtils.isEmpty(userName)){
            lambdaQueryWrapper.like(SysUser::getUsername,userName);
        }
        if (!StringUtils.isEmpty(createTimeBegin)){
            lambdaQueryWrapper.ge(SysUser::getCreateTime,createTimeBegin);
        }
        if (!StringUtils.isEmpty(createTimeEnd)){
            lambdaQueryWrapper.le(SysUser::getCreateTime,createTimeEnd);
        }

        sysUserService.page(sysUserPage,lambdaQueryWrapper);

        return Result.ok(sysUserPage);
    }

    /**
     * 获取用户
     * @param id
     * @return
     */
    @ApiOperation("获取用户")
    @GetMapping("/get/{id}")
    public Result get(@PathVariable long id){
        SysUser user = sysUserService.getById(id);
        return Result.ok(user);
    }

    /**
     * 更新用户
     * @param sysUser
     * @return
     */
    @ApiOperation("更新用户")
    @PutMapping("/update")
    public Result update(@RequestBody SysUser sysUser){
        boolean is_success = sysUserService.updateById(sysUser);
        if (is_success) {
            return Result.ok();
        } else {
            return Result.fail();
        }
    }

    /**
     * 保存用户
     * @param sysUser
     * @return
     */
    @ApiOperation("保存用户")
    @PostMapping("/save")
    public Result save(@RequestBody SysUser sysUser){
        boolean is_success = sysUserService.save(sysUser);
        if (is_success) {
            return Result.ok();
        } else {
            return Result.fail();
        }
    }

    /**
     * 删除用户
     * @param id
     * @return
     */
    @ApiOperation("删除用户")
    @DeleteMapping("/remove/{id}")
    public Result remove(@PathVariable long id){
        boolean is_success = sysUserService.removeById(id);
        if (is_success) {
            return Result.ok();
        } else {
            return Result.fail();
        }
    }
}

测试

全部测试通过

在这里插入图片描述

整合前端

前端页面 list.vue

在这里插入图片描述

<template>
    <div class="app-container">
  
      <div class="search-div">
        <el-form label-width="70px" size="small">
          <el-row>
            <el-col :span="8">
              <el-form-item label="关 键 字">
                <el-input style="width: 95%" v-model="searchObj.keyword" placeholder="用户名/姓名/手机号码"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="操作时间">
                <el-date-picker
                  v-model="createTimes"
                  type="datetimerange"
                  range-separator="至"
                  start-placeholder="开始时间"
                  end-placeholder="结束时间"
                  value-format="yyyy-MM-dd HH:mm:ss"
                  style="margin-right: 10px;width: 100%;"
                />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row style="display:flex">
            <el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="fetchData()">搜索</el-button>
            <el-button icon="el-icon-refresh" size="mini" @click="resetData">重置</el-button>
          </el-row>
        </el-form>
      </div>
  
      <!-- 工具条 -->
      <div class="tools-div">
        <el-button type="success" icon="el-icon-plus" size="mini" @click="add">添 加</el-button>
      </div>
  
      <!-- 列表 -->
      <el-table
        v-loading="listLoading"
        :data="list"
        stripe
        border
        style="width: 100%;margin-top: 10px;">
  
        <el-table-column
          label="序号"
          width="70"
          align="center">
          <template slot-scope="scope">
            {{ (page - 1) * limit + scope.$index + 1 }}
          </template>
        </el-table-column>
  
        <el-table-column prop="username" label="用户名" width="100"/>
        <el-table-column prop="name" label="姓名" width="70"/>
        <el-table-column prop="phone" label="手机" width="120"/>
        <el-table-column prop="postName" label="岗位" width="100"/>
        <el-table-column prop="deptName" label="部门" width="100"/>
        <el-table-column label="所属角色" width="130">
          <template slot-scope="scope">
            <span v-for="item in scope.row.roleList" :key="item.id" style="margin-right: 10px;">{{ item.roleName }}</span>
          </template>
        </el-table-column>
        <el-table-column label="状态" width="80">
          <template slot-scope="scope">
            <el-switch
              v-model="scope.row.status === 1"
              @change="switchStatus(scope.row)">
            </el-switch>
          </template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" width="160"/>
  
        <el-table-column label="操作" width="180" align="center" fixed="right">
          <template slot-scope="scope">
            <el-button type="primary" icon="el-icon-edit" size="mini" @click="edit(scope.row.id)" title="修改"/>
            <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeDataById(scope.row.id)" title="删除" />
          </template>
        </el-table-column>
      </el-table>
  
      <!-- 分页组件 -->
      <el-pagination
        :current-page="page"
        :total="total"
        :page-size="limit"
        :page-sizes="[5, 10, 20, 30, 40, 50, 100]"
        style="padding: 30px 0; text-align: center;"
        layout="sizes, prev, pager, next, jumper, ->, total, slot"
        @current-change="fetchData"
        @size-change="changeSize"
      />
  
      <el-dialog title="添加/修改" :visible.sync="dialogVisible" width="40%" >
        <el-form ref="dataForm" :model="sysUser"  label-width="100px" size="small" style="padding-right: 40px;">
          <el-form-item label="用户名" prop="username">
            <el-input v-model="sysUser.username"/>
          </el-form-item>
          <el-form-item v-if="!sysUser.id" label="密码" prop="password">
            <el-input v-model="sysUser.password" type="password"/>
          </el-form-item>
          <el-form-item label="姓名" prop="name">
            <el-input v-model="sysUser.name"/>
          </el-form-item>
          <el-form-item label="手机" prop="phone">
            <el-input v-model="sysUser.phone"/>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="dialogVisible = false" size="small" icon="el-icon-refresh-right">取 消</el-button>
          <el-button type="primary" icon="el-icon-check" @click="saveOrUpdate()" size="small">确 定</el-button>
        </span>
      </el-dialog>
    </div>
  </template>
  
  <script>
  import api from '@/api/system/sysUser'
  const defaultForm = {
    id: '',
    username: '',
    password: '',
    name: '',
    phone: '',
    status: 1
  }
  export default {
    data() {
      return {
        listLoading: true, // 数据是否正在加载
        list: null, // banner列表
        total: 0, // 数据库中的总记录数
        page: 1, // 默认页码
        limit: 5, // 每页记录数
        searchObj: {}, // 查询表单对象
  
        createTimes: [],
  
        dialogVisible: false,
        sysUser: defaultForm,
        saveBtnDisabled: false,
      }
    },
  
    // 生命周期函数:内存准备完毕,页面尚未渲染
    created() {
      console.log('list created......')
      this.fetchData()
    },
  
    // 生命周期函数:内存准备完毕,页面渲染成功
    mounted() {
      console.log('list mounted......')
    },
  
    methods: {
      // 当页码发生改变的时候
      changeSize(size) {
        console.log(size)
        this.limit = size
        this.fetchData(1)
      },
  
      // 加载banner列表数据
      fetchData(page = 1) {
        debugger
        this.page = page
        console.log('翻页。。。' + this.page)
  
        if(this.createTimes && this.createTimes.length ==2) {
          this.searchObj.createTimeBegin = this.createTimes[0]
          this.searchObj.createTimeEnd = this.createTimes[1]
        }
  
        api.getPageList(this.page, this.limit, this.searchObj).then(
          response => {
            //this.list = response.data.list
            this.list = response.data.records
            this.total = response.data.total
  
            // 数据加载并绑定成功
            this.listLoading = false
          }
        )
      },
  
      // 重置查询表单
      resetData() {
        console.log('重置查询表单')
        this.searchObj = {}
        this.createTimes = []
        this.fetchData()
      },
  
      // 根据id删除数据
      removeDataById(id) {
        // debugger
        this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => { // promise
          // 点击确定,远程调用ajax
          return api.removeById(id)
        }).then((response) => {
          this.fetchData()
          this.$message.success(response.message || '删除成功')
        }).catch(() => {
           this.$message.info('取消删除')
        })
      },
  
      // -------------
      add(){
        this.dialogVisible = true
        this.sysUser = Object.assign({}, defaultForm)
      },
  
      edit(id) {
        this.dialogVisible = true
        this.fetchDataById(id)
      },
  
      fetchDataById(id) {
        api.getById(id).then(response => {
          this.sysUser = response.data
        })
      },
  
      saveOrUpdate() {
        this.$refs.dataForm.validate(valid => {
          if (valid) {
            this.saveBtnDisabled = true // 防止表单重复提交
            if (!this.sysUser.id) {
              this.saveData()
            } else {
              this.updateData()
            }
          }
        })
      },
  
      // 新增
      saveData() {
        api.save(this.sysUser).then(response => {
          this.$message.success('操作成功')
          this.dialogVisible = false
          this.fetchData(this.page)
        })
      },
  
      // 根据id更新记录
      updateData() {
        api.updateById(this.sysUser).then(response => {
          this.$message.success(response.message || '操作成功')
          this.dialogVisible = false
          this.fetchData(this.page)
        })
      }
    }
  }
  </script>

添加路由

在这里插入图片描述

      {
        name: 'sysUser',
        path: 'sysUser',
        component: () => import('@/views/system/sysUser/list'),
        meta: {
          title: '用户管理',
          icon: 'el-icon-s-custom'
        },
      },

定义API接口

在这里插入图片描述

import request from '@/utils/request'

const api_name = '/admin/system/sysUser'

export default {

  getPageList(page, limit, searchObj) {
    return request({
      url: `${api_name}/${page}/${limit}`,
      method: 'get',
      params: searchObj // url查询字符串或表单键值对
    })
  },
  getById(id) {
    return request({
      url: `${api_name}/get/${id}`,
      method: 'get'
    })
  },

  save(role) {
    return request({
      url: `${api_name}/save`,
      method: 'post',
      data: role
    })
  },

  updateById(role) {
    return request({
      url: `${api_name}/update`,
      method: 'put',
      data: role
    })
  },
  removeById(id) {
    return request({
      url: `${api_name}/remove/${id}`,
      method: 'delete'
    })
  },

  updateStatus(id, status) {
    return request({
      url: `${api_name}/updateStatus/${id}/${status}`,
      method: 'get'
    })
  }
}

页面展示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oiKS5qR9-1678180003061)(E:/typora/image-20230302122542413.png)]

7.2、用户管理分配角色

需求分析

  • 一个用户对应多个角色

  • 一个角色可以有多个用户

多对多的关系

在这里插入图片描述

接口分析

  • 1、进入分配页面:获取已分配角色与全部角色,进行页面展示

  • 2、保存分配角色:删除之前分配的角色和保存现在分配的角色

编写代码

代码是写在service-oa类中的

SysUserRoleMapper

public interface SysUserRoleMapper extends BaseMapper<SysUserRole> {
}

SysUserRoleService

public interface SysUserRoleService extends IService<SysUserRole> {
}

SysUserRoleServiceImpl

@Service
public class SysUserRoleServiceImpl extends ServiceImpl<SysUserRoleMapper, SysUserRole> implements SysUserRoleService {
}

SysRoleController

    // 1、查询所有角色 和 当前用户所属角色
    @ApiOperation("根据用户获取角色数据")
    @GetMapping("/toAssign/{userId}")
    public Result toAssign(@PathVariable Long userId) {
        Map<String, Object> map = sysRoleService.findRoleDataByUserId(userId);
        return Result.ok(map);
    }

    // 2、为用户分配角色
    @ApiOperation("为用户分配角色")
    @PostMapping("/doAssign")
    public Result doAssign(@RequestBody AssginRoleVo assginRoleVo) {
        sysRoleService.doAssign(assginRoleVo);
        return Result.ok();
    }

SysRoleServiceImpl

/**
 * ClassName: SysRoleServiceImpl
 * Package: com.jerry.auth.service.impl
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 9:13
 * @Version 1.0
 */

@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {
    @Autowired
    private SysUserRoleService sysUserRoleService;

    //1 查询所有角色 和 当前用户所属角色
    @Override
    public Map<String, Object> findRoleDataByUserId(Long userId) {

        //1 查询所有角色,返回list集合,返回
        List<SysRole> allRoleList =
                baseMapper.selectList(null);

        //2 根据userid查询 角色用户关系表,查询userid对应所有角色id
        LambdaQueryWrapper<SysUserRole> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUserRole::getUserId,userId);
        List<SysUserRole> existUserRoleList = sysUserRoleService.list(wrapper);

        //从查询出来的用户id对应角色list集合,获取所有角色id
//        List<Long> list = new ArrayList<>();
//        for (SysUserRole sysUserRole:existUserRoleList) {
//            Long roleId = sysUserRole.getRoleId();
//            list.add(roleId);
//        }
        List<Long> existRoleIdList =
                existUserRoleList.stream().map(c -> c.getRoleId()).collect(Collectors.toList());

        //3 根据查询所有角色id,找到对应角色信息
        //根据角色id到所有的角色的list集合进行比较
        List<SysRole> assignRoleList = new ArrayList<>();
        for(SysRole sysRole : allRoleList) {
            //比较
            if(existRoleIdList.contains(sysRole.getId())) {
                assignRoleList.add(sysRole);
            }
        }

        //4 把得到两部分数据封装map集合,返回
        Map<String, Object> roleMap = new HashMap<>();
        roleMap.put("assginRoleList", assignRoleList);
        roleMap.put("allRolesList", allRoleList);
        return roleMap;
    }

    //2 为用户分配角色
    @Override
    public void doAssign(AssginRoleVo assginRoleVo) {
        //把用户之前分配角色数据删除,用户角色关系表里面,根据userid删除
        LambdaQueryWrapper<SysUserRole> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUserRole::getUserId,assginRoleVo.getUserId());
        sysUserRoleService.remove(wrapper);

        //重新进行分配
        List<Long> roleIdList = assginRoleVo.getRoleIdList();
        for(Long roleId:roleIdList) {
            if(StringUtils.isEmpty(roleId)) {
                continue;
            }
            SysUserRole sysUserRole = new SysUserRole();
            sysUserRole.setUserId(assginRoleVo.getUserId());
            sysUserRole.setRoleId(roleId);
            sysUserRoleService.save(sysUserRole);
        }
    }
}

前端展示

在这里插入图片描述

7.3、修改用户状态

需求分析

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

编写代码

SysRoleController

    @ApiOperation(value = "更新状态")
    @GetMapping("/updateStatus/{id}/{status}")
    public Result updateStatus(@PathVariable Long id, @PathVariable Integer status){
        sysUserService.updateStatus(id, status);
        return Result.ok();
    }

SysUserService

public interface SysUserService extends IService<SysUser> {

    // 更新状态
    void updateStatus(Long id, Integer status);
}

SysUserServiceImpl

@Service
@Slf4j
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

    // 更新状态
    @Override
    @Transactional
    public void updateStatus(Long id, Integer status) {
        // 根据用户 userid 查询用户对象
        SysUser sysUser = baseMapper.selectById(id);

        // 设置修改状态
        if (status == 0 || status == 1) {
            sysUser.setStatus(status);
        } else {
            log.info("数值不合法");
        }

        // 调用方法进行修改
        baseMapper.updateById(sysUser);
    }
}

整合前端

定义前端路由

src/api/system/sysUser.js

在这里插入图片描述

updateStatus(id, status) {
  return request({
    url: `${api_name}/updateStatus/${id}/${status}`,
    method: 'get'
  })
}

src/api/system/sysRole.js

在这里插入图片描述

getRoles(adminId) {
  return request({
    url: `${api_name}/toAssign/${adminId}`,
    method: 'get'
  })
},

assignRoles(assginRoleVo) {
  return request({
    url: `${api_name}/doAssign`,
    method: 'post',
    data: assginRoleVo
  })
}

修改前端页面

list.vue

<template>
    <div class="app-container">
  
      <div class="search-div">
        <el-form label-width="70px" size="small">
          <el-row>
            <el-col :span="8">
              <el-form-item label="关 键 字">
                <el-input style="width: 95%" v-model="searchObj.keyword" placeholder="用户名/姓名/手机号码"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="操作时间">
                <el-date-picker
                  v-model="createTimes"
                  type="datetimerange"
                  range-separator="至"
                  start-placeholder="开始时间"
                  end-placeholder="结束时间"
                  value-format="yyyy-MM-dd HH:mm:ss"
                  style="margin-right: 10px;width: 100%;"
                />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row style="display:flex">
            <el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="fetchData()">搜索</el-button>
            <el-button icon="el-icon-refresh" size="mini" @click="resetData">重置</el-button>
          </el-row>
        </el-form>
      </div>
  
      <!-- 工具条 -->
      <div class="tools-div">
        <el-button type="success" icon="el-icon-plus" size="mini" @click="add">添 加</el-button>
      </div>
  
      <!-- 列表 -->
      <el-table
        v-loading="listLoading"
        :data="list"
        stripe
        border
        style="width: 100%;margin-top: 10px;">
  
        <el-table-column
          label="序号"
          width="70"
          align="center">
          <template slot-scope="scope">
            {{ (page - 1) * limit + scope.$index + 1 }}
          </template>
        </el-table-column>
  
        <el-table-column prop="username" label="用户名" width="100"/>
        <el-table-column prop="name" label="姓名" width="70"/>
        <el-table-column prop="phone" label="手机" width="120"/>
        <el-table-column prop="postName" label="岗位" width="100"/>
        <el-table-column prop="deptName" label="部门" width="100"/>
        <el-table-column label="所属角色" width="130">
          <template slot-scope="scope">
            <span v-for="item in scope.row.roleList" :key="item.id" style="margin-right: 10px;">{{ item.roleName }}</span>
          </template>
        </el-table-column>
        <el-table-column label="状态" width="80">
          <template slot-scope="scope">
            <el-switch
              v-model="scope.row.status === 1"
              @change="switchStatus(scope.row)">
            </el-switch>
          </template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" width="160"/>
  
        <el-table-column label="操作" width="180" align="center" fixed="right">
          <template slot-scope="scope">
            <el-button type="primary" icon="el-icon-edit" size="mini" @click="edit(scope.row.id)" title="修改"/>
            <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeDataById(scope.row.id)" title="删除" />
            <el-button type="warning" icon="el-icon-baseball" size="mini" @click="showAssignRole(scope.row)" title="分配角色"/>
          </template>
        </el-table-column>
      </el-table>
  
      <!-- 分页组件 -->
      <el-pagination
        :current-page="page"
        :total="total"
        :page-size="limit"
        :page-sizes="[5, 10, 20, 30, 40, 50, 100]"
        style="padding: 30px 0; text-align: center;"
        layout="sizes, prev, pager, next, jumper, ->, total, slot"
        @current-change="fetchData"
        @size-change="changeSize"
      />
  
      <el-dialog title="添加/修改" :visible.sync="dialogVisible" width="40%" >
        <el-form ref="dataForm" :model="sysUser"  label-width="100px" size="small" style="padding-right: 40px;">
          <el-form-item label="用户名" prop="username">
            <el-input v-model="sysUser.username"/>
          </el-form-item>
          <el-form-item v-if="!sysUser.id" label="密码" prop="password">
            <el-input v-model="sysUser.password" type="password"/>
          </el-form-item>
          <el-form-item label="姓名" prop="name">
            <el-input v-model="sysUser.name"/>
          </el-form-item>
          <el-form-item label="手机" prop="phone">
            <el-input v-model="sysUser.phone"/>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="dialogVisible = false" size="small" icon="el-icon-refresh-right">取 消</el-button>
          <el-button type="primary" icon="el-icon-check" @click="saveOrUpdate()" size="small">确 定</el-button>
        </span>
      </el-dialog>
  
      <el-dialog title="分配角色" :visible.sync="dialogRoleVisible">
        <el-form label-width="80px">
          <el-form-item label="用户名">
            <el-input disabled :value="sysUser.username"></el-input>
          </el-form-item>
  
          <el-form-item label="角色列表">
            <el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange">全选</el-checkbox>
            <div style="margin: 15px 0;"></div>
            <el-checkbox-group v-model="userRoleIds" @change="handleCheckedChange">
              <el-checkbox v-for="role in allRoles" :key="role.id" :label="role.id">{{role.roleName}}</el-checkbox>
            </el-checkbox-group>
          </el-form-item>
        </el-form>
        <div slot="footer">
          <el-button type="primary" @click="assignRole" size="small">保存</el-button>
          <el-button @click="dialogRoleVisible = false" size="small">取消</el-button>
        </div>
      </el-dialog>
    </div>
  </template>
  
  <script>
  import api from '@/api/system/sysUser'
  import roleApi from '@/api/system/sysRole'
  const defaultForm = {
    id: '',
    username: '',
    password: '',
    name: '',
    phone: '',
    status: 1
  }
  export default {
    data() {
      return {
        listLoading: true, // 数据是否正在加载
        list: null, // banner列表
        total: 0, // 数据库中的总记录数
        page: 1, // 默认页码
        limit: 10, // 每页记录数
        searchObj: {}, // 查询表单对象
  
        createTimes: [],
  
        dialogVisible: false,
        sysUser: defaultForm,
        saveBtnDisabled: false,
  
        dialogRoleVisible: false,
        allRoles: [], // 所有角色列表
        userRoleIds: [], // 用户的角色ID的列表
        isIndeterminate: false, // 是否是不确定的
        checkAll: false // 是否全选
      }
    },
  
    // 生命周期函数:内存准备完毕,页面尚未渲染
    created() {
      console.log('list created......')
      this.fetchData()
  
      roleApi.findAll().then(response => {
        this.roleList = response.data;
      })
    },
  
    // 生命周期函数:内存准备完毕,页面渲染成功
    mounted() {
      console.log('list mounted......')
    },
  
    methods: {
      // 当页码发生改变的时候
      changeSize(size) {
        console.log(size)
        this.limit = size
        this.fetchData(1)
      },
  
      // 加载banner列表数据
      fetchData(page = 1) {
        debugger
        this.page = page
        console.log('翻页。。。' + this.page)
  
        if(this.createTimes && this.createTimes.length ==2) {
          this.searchObj.createTimeBegin = this.createTimes[0]
          this.searchObj.createTimeEnd = this.createTimes[1]
        }
  
        api.getPageList(this.page, this.limit, this.searchObj).then(
          response => {
            //this.list = response.data.list
            this.list = response.data.records
            this.total = response.data.total
  
            // 数据加载并绑定成功
            this.listLoading = false
          }
        )
      },
  
      // 重置查询表单
      resetData() {
        console.log('重置查询表单')
        this.searchObj = {}
        this.createTimes = []
        this.fetchData()
      },
  
      // 根据id删除数据
      removeDataById(id) {
        // debugger
        this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => { // promise
          // 点击确定,远程调用ajax
          return api.removeById(id)
        }).then((response) => {
          this.fetchData(this.page)
          this.$message.success(response.message || '删除成功')
        }).catch(() => {
           this.$message.info('取消删除')
        })
      },
  
      // -------------
      add(){
        this.dialogVisible = true
        this.sysUser = Object.assign({}, defaultForm)
      },
  
      edit(id) {
        this.dialogVisible = true
        this.fetchDataById(id)
      },
  
      fetchDataById(id) {
        api.getById(id).then(response => {
          this.sysUser = response.data
        })
      },
  
      saveOrUpdate() {
        this.$refs.dataForm.validate(valid => {
          if (valid) {
            this.saveBtnDisabled = true // 防止表单重复提交
            if (!this.sysUser.id) {
              this.saveData()
            } else {
              this.updateData()
            }
          }
        })
      },
  
      // 新增
      saveData() {
        api.save(this.sysUser).then(response => {
          this.$message.success('操作成功')
          this.dialogVisible = false
          this.fetchData(this.page)
        })
      },
  
      // 根据id更新记录
      updateData() {
        api.updateById(this.sysUser).then(response => {
          this.$message.success(response.message || '操作成功')
          this.dialogVisible = false
          this.fetchData(this.page)
        })
      },
  
      showAssignRole (row) {
        this.sysUser = row
        this.dialogRoleVisible = true
        this.getRoles()
      },
  
      getRoles () {
        roleApi.getRoles(this.sysUser.id).then(response => {
          const {allRolesList, assginRoleList} = response.data
          this.allRoles = allRolesList
          this.userRoleIds = assginRoleList.map(item => item.id)
          this.checkAll = allRolesList.length===assginRoleList.length
          this.isIndeterminate = assginRoleList.length>0 && assginRoleList.length<allRolesList.length
        })
      },
  
      /*
      全选勾选状态发生改变的监听
      */
      handleCheckAllChange (value) {// value 当前勾选状态true/false
        // 如果当前全选, userRoleIds就是所有角色id的数组, 否则是空数组
        this.userRoleIds = value ? this.allRoles.map(item => item.id) : []
        // 如果当前不是全选也不全不选时, 指定为false
        this.isIndeterminate = false
      },
  
      /*
      角色列表选中项发生改变的监听
      */
      handleCheckedChange (value) {
        const {userRoleIds, allRoles} = this
        this.checkAll = userRoleIds.length === allRoles.length && allRoles.length>0
        this.isIndeterminate = userRoleIds.length>0 && userRoleIds.length<allRoles.length
      },
  
      assignRole () {
        let assginRoleVo = {
          userId: this.sysUser.id,
          roleIdList: this.userRoleIds
        }
        roleApi.assignRoles(assginRoleVo).then(response => {
          this.$message.success(response.message || '分配角色成功')
          this.dialogRoleVisible = false
          this.fetchData(this.page)
        })
      },
  
      switchStatus(row) {
        row.status = row.status === 1 ? 0 : 1
        api.updateStatus(row.id, row.status).then(response => {
          if (response.code) {
            this.$message.success(response.message || '操作成功')
            this.dialogVisible = false
            this.fetchData()
          }
        })
      }
    }
  }
  </script>

页面展示

在这里插入图片描述

8、菜单管理

8.1、菜单管理CRUD

需求分析

在这里插入图片描述

编写代码

SysMenuMapper

public interface SysMenuMapper extends BaseMapper<SysMenu> {
}

SysRoleMenuMapper

public interface SysRoleMenuMapper extends BaseMapper<SysRoleMenu> {

}

SysMenuService

public interface SysMenuService extends IService<SysMenu> {

    List<SysMenu> findNodes();

    // 删除菜单
    void removeMenuById(Long id);
}

SysRoleMenuService

public interface SysRoleMenuService extends IService<SysRoleMenu> {
}

SysMenuServiceImpl

@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {

    @Override
    public List<SysMenu> findNodes() {
        // 1、查询所有 的数据
        List<SysMenu> sysMenuList = baseMapper.selectList(null);

        // 2、构建树形结构
        List<SysMenu> list = MenuHelper.buildTree(sysMenuList);

        return list;
    }

    // 删除菜单
    @Override
    public void removeMenuById(Long id) {
        // 判断当前菜单是否有下一层菜单
        LambdaQueryWrapper<SysMenu> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(SysMenu::getParentId,id);

        Integer count = baseMapper.selectCount(lambdaQueryWrapper);

        if (count>0){
            throw new GuiguException(201,"菜单不能删除");
        }
        baseMapper.deleteById(id);
    }
}

MenuHelper

package com.jerry.auth.util;

import com.jerry.model.system.SysMenu;

import java.util.ArrayList;
import java.util.List;

/**
 * ClassName: MenuHelper
 * Package: com.jerry.auth.util
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-02 17:14
 * @Version 1.0
 */
public class MenuHelper {

    /**
     * 使用递归方法建菜单
     * @param sysMenuList
     * @return
     */
    public static List<SysMenu> buildTree(List<SysMenu> sysMenuList) {
        // 存放最终数据
        List<SysMenu> trees = new ArrayList<>();

        // 把所有的菜单数据进行遍历
        for (SysMenu sysMenu : sysMenuList) {
            // 递归入口 parentId = 0
            if (sysMenu.getParentId().longValue()==0){
                trees.add(getChildren(sysMenu,sysMenuList));
            }

        }
        return trees;

    }

    /**
     * 递归查找子节点
     * @param sysMenu
     * @param sysMenuList
     * @return
     */
    public static SysMenu getChildren(SysMenu sysMenu,List<SysMenu> sysMenuList){
        sysMenu.setChildren(new ArrayList<SysMenu>());

        // 遍历所有的菜单数据,判断id和parent_id的对应关系
        for (SysMenu menu : sysMenuList) {
            if (sysMenu.getId().longValue() == menu.getParentId().longValue()){
                if (sysMenu.getChildren() == null) {
                    sysMenu.setChildren(new ArrayList<>());
                }
                sysMenu.getChildren().add(getChildren(menu,sysMenuList));
            }
        }
        return sysMenu;
    }
}

SysRoleMenuServiceImpl

@Service
public class SysRoleMenuServiceImpl extends ServiceImpl<SysRoleMenuMapper, SysRoleMenu> implements SysRoleMenuService {
}

SysMenuController

package com.jerry.auth.controller;


import com.jerry.auth.service.SysMenuService;
import com.jerry.common.result.Result;
import com.jerry.model.system.SysMenu;
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 jerry
 * @since 2023-03-02
 */
@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.removeMenuById(id);
        return Result.ok();
    }
}

接口测试

在这里插入图片描述

整合前端

在这里插入图片描述

      {
        name: 'sysMenu',
        path: 'sysMenu',
        component: () => import('@/views/system/sysMenu/list'),
        meta: {
          title: '菜单管理',
          icon: 'el-icon-s-unfold'
        },
      }

sysMenu.js

在这里插入图片描述

import request from '@/utils/request'

/*
菜单管理相关的API请求函数
*/
const api_name = '/admin/system/sysMenu'

export default {

  /*
  获取权限(菜单/功能)列表
  */
  findNodes() {
    return request({
      url: `${api_name}/findNodes`,
      method: 'get'
    })
  },

  /*
  删除一个权限项
  */
  removeById(id) {
    return request({
      url: `${api_name}/remove/${id}`,
      method: "delete"
    })
  },

  /*
  保存一个权限项
  */
  save(sysMenu) {
    return request({
      url: `${api_name}/save`,
      method: "post",
      data: sysMenu
    })
  },

  /*
  更新一个权限项
  */
  updateById(sysMenu) {
    return request({
      url: `${api_name}/update`,
      method: "put",
      data: sysMenu
    })
  }
}

list.vue

在这里插入图片描述

<template>
    <div class="app-container">
  
      <!-- 工具条 -->
      <div class="tools-div">
        <el-button type="success" icon="el-icon-plus" size="mini" @click="add()">添 加</el-button>
      </div>
      <el-table
        :data="sysMenuList"
        style="width: 100%;margin-bottom: 20px;margin-top: 10px;"
        row-key="id"
        border
        :default-expand-all="false"
        :tree-props="{children: 'children'}">
  
        <el-table-column prop="name" label="菜单名称" width="160"/>
        <el-table-column label="图标">
          <template slot-scope="scope">
            <i :class="scope.row.icon"></i>
          </template>
        </el-table-column>
        <el-table-column prop="perms" label="权限标识" width="160"/>
        <el-table-column prop="path" label="路由地址" width="120"/>
        <el-table-column prop="component" label="组件路径" width="160"/>
        <el-table-column prop="sortValue" label="排序" width="60"/>
        <el-table-column label="状态" width="80">
          <template slot-scope="scope">
            <el-switch
              v-model="scope.row.status === 1" disabled="true">
            </el-switch>
          </template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" width="160"/>
        <el-table-column label="操作" width="180" align="center" fixed="right">
          <template slot-scope="scope">
            <el-button type="success" v-if="scope.row.type !== 2" icon="el-icon-plus" size="mini" @click="add(scope.row)" title="添加下级节点"/>
            <el-button type="primary" icon="el-icon-edit" size="mini" @click="edit(scope.row)" title="修改"/>
            <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeDataById(scope.row.id)" title="删除" :disabled="scope.row.children.length > 0"/>
          </template>
        </el-table-column>
      </el-table>
  
      <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="40%" >
        <el-form ref="dataForm" :model="sysMenu" label-width="150px" size="small" style="padding-right: 40px;">
          <el-form-item label="上级部门" v-if="sysMenu.id === ''">
            <el-input v-model="sysMenu.parentName" disabled="true"/>
          </el-form-item>
          <el-form-item label="菜单类型" prop="type">
            <el-radio-group v-model="sysMenu.type" :disabled="typeDisabled">
              <el-radio :label="0" :disabled="type0Disabled">目录</el-radio>
              <el-radio :label="1" :disabled="type1Disabled">菜单</el-radio>
              <el-radio :label="2" :disabled="type2Disabled">按钮</el-radio>
            </el-radio-group>
          </el-form-item>
          <el-form-item label="菜单名称" prop="name">
            <el-input v-model="sysMenu.name"/>
          </el-form-item>
          <el-form-item label="图标" prop="icon" v-if="sysMenu.type !== 2">
            <el-select v-model="sysMenu.icon" clearable>
              <el-option v-for="item in iconList" :key="item.class" :label="item.class" :value="item.class">
              <span style="float: left;">
               <i :class="item.class"></i>  <!-- 如果动态显示图标,这里添加判断 -->
              </span>
                <span style="padding-left: 6px;">{{ item.class }}</span>
              </el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="排序">
            <el-input-number v-model="sysMenu.sortValue" controls-position="right" :min="0" />
          </el-form-item>
          <el-form-item prop="path">
                <span slot="label">
                  <el-tooltip content="访问的路由地址,如:`sysUser`" placement="top">
                  <i class="el-icon-question"></i>
                  </el-tooltip>
                  路由地址
                </span>
            <el-input v-model="sysMenu.path" placeholder="请输入路由地址" />
          </el-form-item>
          <el-form-item prop="component" v-if="sysMenu.type !== 0">
                <span slot="label">
                  <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
                  <i class="el-icon-question"></i>
                  </el-tooltip>
                  组件路径
                </span>
            <el-input v-model="sysMenu.component" placeholder="请输入组件路径" />
          </el-form-item>
          <el-form-item v-if="sysMenu.type === 2">
            <el-input v-model="sysMenu.perms" placeholder="请输入权限标识" maxlength="100" />
            <span slot="label">
                  <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(hasAuthority('bnt.sysRole.list'))" placement="top">
                  <i class="el-icon-question"></i>
                  </el-tooltip>
                  权限字符
                </span>
          </el-form-item>
          <el-form-item label="状态" prop="type">
            <el-radio-group v-model="sysMenu.status">
              <el-radio :label="1">正常</el-radio>
              <el-radio :label="0">停用</el-radio>
            </el-radio-group>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="dialogVisible = false" size="small" icon="el-icon-refresh-right">取 消</el-button>
          <el-button type="primary" icon="el-icon-check" @click="saveOrUpdate()" size="small">确 定</el-button>
        </span>
      </el-dialog>
    </div>
  </template>
  
  
  <script>
  import api from '@/api/system/sysMenu'
  const defaultForm = {
    id: '',
    parentId: '',
    name: '',
    type: 0,
    path: '',
    component: '',
    perms: '',
    icon: '',
    sortValue: 1,
    status: 1
  }
  export default {
    // 定义数据
    data() {
      return {
        sysMenuList: [],
        expandKeys: [], // 需要自动展开的项
  
        typeDisabled: false,
        type0Disabled: false,
        type1Disabled: false,
        type2Disabled: false,
        dialogTitle: '',
  
        dialogVisible: false,
        sysMenu: defaultForm,
        saveBtnDisabled: false,
  
        iconList: [
          {
            class: "el-icon-s-tools",
          },
          {
            class: "el-icon-s-custom",
          },
          {
            class: "el-icon-setting",
          },
          {
            class: "el-icon-user-solid",
          },
          {
            class: "el-icon-s-help",
          },
          {
            class: "el-icon-phone",
          },
          {
            class: "el-icon-s-unfold",
          },
          {
            class: "el-icon-s-operation",
          },
          {
            class: "el-icon-more-outline",
          },
          {
            class: "el-icon-s-check",
          },
          {
            class: "el-icon-tickets",
          },
          {
            class: "el-icon-s-goods",
          },
          {
            class: "el-icon-document-remove",
          },
          {
            class: "el-icon-warning",
          },
          {
            class: "el-icon-warning-outline",
          },
          {
            class: "el-icon-question",
          },
          {
            class: "el-icon-info",
          }
        ]
      }
    },
  
    // 当页面加载时获取数据
    created() {
      this.fetchData()
    },
  
    methods: {
      // 调用api层获取数据库中的数据
      fetchData() {
        console.log('加载列表')
        api.findNodes().then(response => {
          this.sysMenuList = response.data
          console.log(this.sysMenuList)
        })
      },
  
      // 根据id删除数据
      removeDataById(id) {
        // debugger
        this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => { // promise
          // 点击确定,远程调用ajax
          return api.removeById(id)
        }).then((response) => {
          this.fetchData()
          this.$message({
            type: 'success',
            message: '删除成功!'
          })
        }).catch(() => {
           this.$message.info('取消删除')
        })
      },
  
      // -------------
      add(row){
        debugger
        this.typeDisabled = false
        this.dialogTitle = '添加下级节点'
        this.dialogVisible = true
  
        this.sysMenu = Object.assign({}, defaultForm)
        this.sysMenu.id = ''
        if(row) {
          this.sysMenu.parentId = row.id
          this.sysMenu.parentName = row.name
          //this.sysMenu.component = 'ParentView'
          if(row.type === 0) {
            this.sysMenu.type = 1
            this.typeDisabled = false
            this.type0Disabled = false
            this.type1Disabled = false
            this.type2Disabled = true
          } else if(row.type === 1) {
            this.sysMenu.type = 2
            this.typeDisabled = true
          }
        } else {
          this.dialogTitle = '添加目录节点'
          this.sysMenu.type = 0
          this.sysMenu.component = 'Layout'
          this.sysMenu.parentId = 0
          this.typeDisabled = true
        }
      },
  
      edit(row) {
        debugger
        this.dialogTitle = '修改节点'
        this.dialogVisible = true
  
        this.sysMenu = Object.assign({}, row)
        this.typeDisabled = true
      },
  
      saveOrUpdate() {
        if(this.sysMenu.type === 0 && this.sysMenu.parentId !== 0) {
          this.sysMenu.component = 'ParentView'
        }
        this.$refs.dataForm.validate(valid => {
          if (valid) {
            this.saveBtnDisabled = true // 防止表单重复提交
            if (!this.sysMenu.id) {
              this.saveData()
            } else {
              this.updateData()
            }
          }
        })
      },
  
      // 新增
      saveData() {
        api.save(this.sysMenu).then(response => {
          this.$message.success(response.message || '操作成功')
          this.dialogVisible = false
          this.fetchData(this.page)
        })
      },
  
      // 根据id更新记录
      updateData() {
        api.updateById(this.sysMenu).then(response => {
          this.$message.success(response.message || '操作成功')
          this.dialogVisible = false
          this.fetchData()
        })
      }
    }
  }
  </script>

页面展示

在这里插入图片描述

8.2、角色分配菜单功能

需求分析

在这里插入图片描述

编写代码

整合前端

router/index.js

在这里插入图片描述

      {
        path: 'assignAuth',
        component: () => import('@/views/system/sysRole/assignAuth'),
        meta: {
          activeMenu: '/system/sysRole',
          title: '角色授权'
        },
        hidden: true,
      }

sysRole/list.vue

在这里插入图片描述

添加一个分配权限的button按钮

          <el-button type="warning" icon="el-icon-baseball" size="mini" @click="showAssignAuth(scope.row)" title="分配权限"/>
    // 跳转到分配菜单的页面
    showAssignAuth(row) {
    this.$router.push('/system/assignAuth?id='+row.id+'&roleName='+row.roleName);
    },

sysMenu.js

在这里插入图片描述

  /*
查看某个角色的权限列表
*/
toAssign(roleId) {
    return request({
      url: `${api_name}/toAssign/${roleId}`,
      method: 'get'
    })
  },
  
  /*
  给某个角色授权
  */
  doAssign(assginMenuVo) {
    return request({
      url: `${api_name}/doAssign`,
      method: "post",
      data: assginMenuVo
    })
  }

assignAuth.vue

在这里插入图片描述

<template>
    <div class="app-container">
      <div style="padding: 20px 20px 0 20px;">
        授权角色:{{ $route.query.roleName }}
      </div>
      <el-tree
        style="margin: 20px 0"
        ref="tree"
        :data="sysMenuList"
        node-key="id"
        show-checkbox
        default-expand-all
        :props="defaultProps"
      />
      <div style="padding: 20px 20px;">
        <el-button :loading="loading" type="primary" icon="el-icon-check" size="mini" @click="save">保存</el-button>
        <el-button @click="$router.push('/system/sysRole')" size="mini" icon="el-icon-refresh-right">返回</el-button>
      </div>
    </div>
  </template>
  <script>
    import api from '@/api/system/sysMenu'
    export default {
      name: 'roleAuth',
  
      data() {
        return {
          loading: false, // 用来标识是否正在保存请求中的标识, 防止重复提交
          sysMenuList: [], // 所有
          defaultProps: {
            children: 'children',
            label: 'name'
          },
        };
      },
  
      created() {
        this.fetchData()
      },
  
      methods: {
        /*
        初始化
        */
        fetchData() {
          const roleId = this.$route.query.id
          api.toAssign(roleId).then(result => {
            const sysMenuList = result.data
            this.sysMenuList = sysMenuList
            const checkedIds = this.getCheckedIds(sysMenuList)
            console.log('getPermissions() checkedIds', checkedIds)
            this.$refs.tree.setCheckedKeys(checkedIds)
          })
        },
  
        /*
        得到所有选中的id列表
        */
        getCheckedIds (auths, initArr = []) {
          return auths.reduce((pre, item) => {
            if (item.select && item.children.length === 0) {
              pre.push(item.id)
            } else if (item.children) {
              this.getCheckedIds(item.children, initArr)
            }
            return pre
          }, initArr)
        },
  
        /*
        保存权限列表
        */
        save() {
        //   debugger
          //获取到当前子节点
          //const checkedNodes = this.$refs.tree.getCheckedNodes()
          //获取到当前子节点及父节点
          const allCheckedNodes = this.$refs.tree.getCheckedNodes(false, true);
          let idList = allCheckedNodes.map(node => node.id);
          console.log(idList)
          let assginMenuVo = {
            roleId: this.$route.query.id,
            menuIdList: idList
          }
          this.loading = true
          api.doAssign(assginMenuVo).then(result => {
            this.loading = false
            this.$message.success(result.$message || '分配权限成功')
            this.$router.push('/system/sysRole');
          })
        }
      }
    };
  </script>

关闭Vue语法校验,避免报错

在这里插入图片描述

页面展示

在这里插入图片描述

9、权限管理(重难点)

9.1、用户登录权限管理

需求分析

在这里插入图片描述

引入JWT

  • JWT是JSON Web Token的缩写

  • 一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上

  • 官网:https://jwt.io/

  • 最重要的作用就是对 token信息的防伪作用。

  • 由三个部分组成:JWT头、有效载荷、签名哈希

  • base64url算法编码得到JWT

common-util

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>

JwtHwlper

package com.jerry.common.jwt;

import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;

import java.util.Date;

/**
 * ClassName: JwtHwlper
 * Package: com.jerry.common
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-02 20:39
 * @Version 1.0
 */
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;
    }

    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;
        }
    }

    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");
        System.out.println(token);
        String username = JwtHelper.getUsername(token);
        Long userId = JwtHelper.getUserId(token);

        System.out.println("username = " + username);
        System.out.println("userId = " + userId);
    }

}

修改用户登录

先引入MD5工具类

package com.jerry.common.utils;

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"));
    }
}

修改SysUserControler保存用户的方法

在这里插入图片描述

修改IndexController的登录方法

package com.jerry.auth.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jerry.auth.service.SysMenuService;
import com.jerry.auth.service.SysUserService;
import com.jerry.common.config.exception.GuiguException;
import com.jerry.common.jwt.JwtHelper;
import com.jerry.common.result.Result;
import com.jerry.common.utils.MD5;
import com.jerry.model.system.SysUser;
import com.jerry.vo.system.LoginVo;
import com.jerry.vo.system.RouterVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * ClassName: IndexController
 * Package: com.jerry.auth.controller
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-01 18:15
 * @Version 1.0
 */
@Api(tags = "后台登录管理")
@RestController
@RequestMapping("/admin/system/index")
public class IndexController {

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private SysMenuService sysMenuService;

    /**
     * login
     *
     * @return
     */
    @ApiOperation("登录")
    @PostMapping("/login")
    public Result login(@RequestBody LoginVo loginVo) {
        // {"code":200,"data":{"token":"admin-token"}}
//        HashMap<String, Object> map = new HashMap<>();
//        map.put("token","admin-token");
//        return Result.ok(map);

        // 1、获取用户名和密码
        // 2、根据用户名查询数据库
        String username = loginVo.getUsername();
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getUsername, username);
        SysUser sysUser = sysUserService.getOne(queryWrapper);

        // 3、用户信息是否存在
        if (sysUser == null) {
            throw new GuiguException(201, "用户不存在...");
        }

        // 4、判断密码
        // 取出数据库中的密文密码(MD5)
        String password_dB = sysUser.getPassword();
        String password_input = MD5.encrypt(loginVo.getPassword());

        if (!password_dB.equals(password_input)) {
            throw new GuiguException(201, "密码错误...");
        }

        // 5、判断用户是否被禁用  1  可用    0   禁用
        if (sysUser.getStatus().intValue() == 0) {
            throw new GuiguException(201, "用户被禁用...");
        }

        // 6、使用jwt根据用户id和用户名称生成token的字符串
        String token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());

        // 7、返回
        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        return Result.ok(map);

    }

    /**
     * info
     *
     * @return
     */
    @GetMapping("/info")
    public Result info(HttpServletRequest request) {

        // 1、从请求头获取用户信息(获取请求头的 token 字符串)
        String token = request.getHeader("token");

        // 2、从 token 字符串中获取 用户id 或者 用户名称
        Long userId = JwtHelper.getUserId(token); //1L;

        // 3、根据 用户id 查询数据库, 获取用户信息
        SysUser sysUser = sysUserService.getById(userId);

        // 4、根据 用户id 获取用户可以操作的菜单列表
        // 查询数据库动态构建路由结构,进行显示
        List<RouterVo> routerList = sysMenuService.findUserMenuListByUserId(userId);


        // 5、根据 用户id 获取用户可以操作的按钮列表
        List<String> permsList = sysMenuService.findUserPermsByUserId(userId);

        // 6、返回相应的数据


        Map<String, Object> map = new HashMap<>();
        map.put("roles", "[admin]");
        map.put("name", sysUser.getName());
        map.put("avatar", "https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");

        // 返回用户可以操作的菜单
        map.put("routers", routerList);

        // 返回用户可以操作的按钮
        map.put("buttons", permsList);

        return Result.ok(map);
    }

    /**
     * logout
     *
     * @return
     */
    @ApiOperation("登出")
    @PostMapping("/logout")
    public Result logout() {
        return Result.ok();
    }
}

SysMenuService

    // 根据 用户id 获取用户可以操作的菜单列表
    List<RouterVo> findUserMenuListByUserId(Long userId);

    // 根据 用户id 获取用户可以操作的按钮列表
    List<String> findUserPermsByUserId(Long userId);

SysMenuServiceImpl

    // 根据 用户id 获取用户可以操作的菜单列表
    @Override
    public List<RouterVo> findUserMenuListByUserId(Long userId) {
        List<SysMenu> sysMenusList = null;
        // 1、判断当前用户是否是管理员       userId=1 是管理员
        // 1.1、 如果是管理员,查询所有菜单列表
        if (userId.longValue() == 1) {
            // 查询所有菜单列表
            LambdaQueryWrapper<SysMenu> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(SysMenu::getStatus, 1);
            queryWrapper.orderByAsc(SysMenu::getSortValue);
            sysMenusList = baseMapper.selectList(queryWrapper);
        } else {
            // 1.2、如果不是管理员,根据 userId 查询可以操作菜单列表
            // 多表关联查询:sys_role、sys_role_mexnu、sys_menu
            sysMenusList = baseMapper.findMenuListByUserId(userId);
        }


        // 2、把查询出来的数据列表, 构建成框架要求的路由结构
        // 先构建树形结构
        List<SysMenu> sysMenuTreeList = MenuHelper.buildTree(sysMenusList);
        // 构建框架要求的路由结构
        List<RouterVo> routerList = this.buildRouter(sysMenuTreeList);

        return routerList;
    }

    // 构建框架要求的路由结构
    private List<RouterVo> buildRouter(List<SysMenu> menus) {
        // 创建 list 集合,存值最终数据
        List<RouterVo> routers = new ArrayList<>();
        // menus 遍历
        for (SysMenu menu : menus) {
            RouterVo router = new RouterVo();
            router.setHidden(false);
            router.setAlwaysShow(false);
            router.setPath(getRouterPath(menu));
            router.setComponent(menu.getComponent());
            router.setMeta(new MetaVo(menu.getName(), menu.getIcon()));
            // 下一层数据
            List<SysMenu> children = menu.getChildren();
            if (menu.getType().intValue() == 1) {
                // 加载隐藏路由
                List<SysMenu> hiddenMenuList = children.stream().filter(item -> !StringUtils.isEmpty(item.getComponent())).collect(Collectors.toList());
                for (SysMenu hiddenMenu : hiddenMenuList) {
                    RouterVo hiddenRouter = new RouterVo();
                    hiddenRouter.setHidden(true);
                    hiddenRouter.setAlwaysShow(false);
                    hiddenRouter.setPath(getRouterPath(hiddenMenu));
                    hiddenRouter.setComponent(hiddenMenu.getComponent());
                    hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon()));
                    routers.add(hiddenRouter);
                }
            }else {
                if (!CollectionUtils.isEmpty(children)) {
                    if(children.size() > 0) {
                        router.setAlwaysShow(true);
                    }
                    // 递归
                    router.setChildren(buildRouter(children));
                }
            }
            routers.add(router);
        }
        return routers;

    }

    /**
     * 获取路由地址
     *
     * @param menu 菜单信息
     * @return 路由地址
     */
    public String getRouterPath(SysMenu menu) {
        String routerPath = "/" + menu.getPath();
        if (menu.getParentId().intValue() != 0) {
            routerPath = menu.getPath();
        }
        return routerPath;
    }

    // 根据 用户id 获取用户可以操作的按钮列表
    @Override
    public List<String> findUserPermsByUserId(Long userId) {
        // 1、判断是否是管理员,如果是管理员,查询所有按钮列表
        List<SysMenu> sysMenusList = null;
        if (userId.longValue() == 1) {
            // 查询所有菜单列表
            LambdaQueryWrapper<SysMenu> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(SysMenu::getStatus, 1);
            sysMenusList = baseMapper.selectList(queryWrapper);
        }else {
            // 2、如果不是管理员,根据userId查询可以操作按钮列表
            // 多表关联查询:sys_role、sys_role_menu、sys_menu
            sysMenusList = baseMapper.findMenuListByUserId(userId);
        }

        // 3、从查询出来的数据里面,获取可以操作按钮值的List集合,返回
        List<String> permsList = sysMenusList.stream()
                .filter(item -> item.getType() == 2)
                .map(item -> item.getPerms())
                .collect(Collectors.toList());
        return permsList;
    }

接口测试

登录接口测试

在这里插入图片描述

info接口测试

在这里插入图片描述

我这里没有报错,如果出现以下的报错信息

在这里插入图片描述

解决思路是:

在这里插入图片描述

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

整合前端

从这部分开始,整合前端不在写了,比较麻烦,直接复用现有的

页面展示

给李四分配没有添加的权限

在这里插入图片描述

9.2、用户认证

整合SpringSecurity

本项目采用 Spring-Security 来做用户认证和权限控制,也可以采用 Shiro

新建一个spring-security的module

在这里插入图片描述

引入依赖

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.jerry</groupId>
        <artifactId>common</artifactId>
        <version>1.0</version>
    </parent>
    <artifactId>spring-security</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.jerry</groupId>
            <artifactId>common-util</artifactId>
            <version>1.0</version>
        </dependency>
        <!-- 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>
    </dependencies>

</project>

添加配置类

package com.jerry.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

/**
 * ClassName: WebSecurityConfig
 * Package: com.jerry.security.config
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-03 13:44
 * @Version 1.0
 */

@Configuration
@EnableWebSecurity  //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig {
}

在 service-oa 中引入spring-security的module

在这里插入图片描述

测试

在浏览器访问:http://localhost:8800/admin/system/sysRole/getAll

在这里插入图片描述

这时候想绕过登录页是不能的,后台服务经过会spring-security做了用户认证,提示用户需要先登录

默认的登录名是:user

密码是IDEA中生成的一串随机字符,每次都不一样

在这里插入图片描述

用户认证

流程分析

在这里插入图片描述

自定义组件的编写

在这里插入图片描述

操作spring-securitymodule

自定义加密器PasswordEncoder
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }
}
自定义用户对象UserDetails
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;
    }
}
UserDetailsService
public interface UserDetailsService {
    /**
     * 根据用户名获取用户对象(获取不到直接抛异常)
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

操作service-oamodule

UserDetailsServiceImpl
@Service
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

    SysUser getUserByUserName(String username);

SysUserServiceImpl

    // 根据用户名查询
    @Override
    public SysUser getUserByUserName(String username) {
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SysUser::getUsername,username);

        SysUser sysUser = baseMapper.selectOne(queryWrapper);
        return sysUser;
    }
自定义用户认证接口

TokenLoginFilter

package com.jerry.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jerry.common.jwt.JwtHelper;
import com.jerry.common.result.ResponseUtil;
import com.jerry.common.result.Result;
import com.jerry.common.result.ResultCodeEnum;
import com.jerry.security.custom.CustomUser;
import com.jerry.vo.system.LoginVo;
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;

/**
 * ClassName: TokenLoginFilter <br>
 * Package: com.jerry.security.filter <br>
 * Description: 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
 *
 * @Author: jerry_jy
 * @Create: 2023-03-03 15:29
 * @Version: 1.0
 */

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

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

    // 登录认证过程
    // 获取输入的用户名和密码,调用方法认证
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            // 获取用户信息
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);

            //封装对象
            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());

            //调用方法
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 认证成功调用的方法
    @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));
    }

    // 认证失败调用的方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {

        if(e.getCause() instanceof RuntimeException) {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.DATA_ERROR));
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_AUTH));
        }
    }

}

common-util下的ResponseUtil

package com.jerry.common.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;

/**
 * ClassName: ResponseUtil <br>
 * Package: com.jerry.common.result <br>
 * Description:
 *
 * @Author: jerry_jy
 * @Create: 2023-03-03 15:55
 * @Version: 1.0
 */
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();
        }
    }
}
认证解析token

因为用户登录状态在token中存储在客户端,所以每次请求接口请求头携带token, 后台通过自定义token过滤器拦截解析token完成认证并填充用户信息实体

package com.jerry.security.filter;

import com.jerry.common.jwt.JwtHelper;
import com.jerry.common.result.ResponseUtil;
import com.jerry.common.result.Result;
import com.jerry.common.result.ResultCodeEnum;
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;

/**
 * ClassName: TokenAuthenticationFilter <br>
 * Package: com.jerry.security.filter <br>
 * Description: 认证解析token过滤器
 *
 * @Author: jerry_jy
 * @Create: 2023-03-03 16:01
 * @Version: 1.0
 */

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

        logger.info("uri:"+request.getRequestURI());
        //如果是登录接口,直接放行
        if("/admin/system/index/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(null != authentication) {
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置于header里
        String token = request.getHeader("token");
        logger.info("token:"+token);
        if (!StringUtils.isEmpty(token)) {
            String username = JwtHelper.getUsername(token);
            logger.info("username:"+username);
            if (!StringUtils.isEmpty(username)) {
                return new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
            }
        }
        return null;
    }
}
配置用户认证
package com.jerry.security.config;

import com.jerry.security.custom.CustomMd5PasswordEncoder;
import com.jerry.security.filter.TokenAuthenticationFilter;
import com.jerry.security.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.method.configuration.EnableGlobalMethodSecurity;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;

/**
 * ClassName: WebSecurityConfig
 * Package: com.jerry.security.config
 * Description:
 *
 * @Author jerry_jy
 * @Create 2023-03-03 13:44
 * @Version 1.0
 */

@Configuration
@EnableWebSecurity  //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService; // 装载的是 org.springframework.security.core.userdetails.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");
    }
}

测试

说明:

1、我们是前后端分离项目,使用jwt生成token ,即用户状态保存在客户端中,前后端交互通过api接口 无session生成,所以我们不需要配置formLogin,session禁用

2、在浏览器访问:http://localhost:8800/admin/system/sysRole/getAll

在这里插入图片描述

9.3、用户权限控制

流程分析

在这里插入图片描述

修改代码

修改UserDetailsServiceImpl中的loadUserByUsername

在这里插入图片描述


        // 根据 user_id 查询用户操作权限数据
        List<String> userPermsList = sysMenuService.findUserPermsByUserId(sysUser.getId());
        // 创建list集合,封装最终权限数据
        List<SimpleGrantedAuthority> authList =  new ArrayList<>();
        // 遍历 authList
        for (String perms : userPermsList) {
            authList.add(new SimpleGrantedAuthority(perms.trim()));
        }

        return new CustomUser(sysUser, authList);

spring-security模块配置redis

添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

修改TokenLoginFilter

在这里插入图片描述

在这里插入图片描述

修改TokenAuthenticationFilter

在这里插入图片描述

在这里插入图片描述

修改WebSecurityConfig类

配置类添加注解:

开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认

在这里插入图片描述

在这里插入图片描述

service-oa模块添加redis配置

application-dev.yml配文件

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 1800000
    password:
    jedis:
      pool:
        max-active: 20 #最大连接数
        max-wait: -1    #最大阻塞等待时间(负数表示没限制)
        max-idle: 5    #最大空闲
        min-idle: 0     #最小空闲

控制controller层接口权限

Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限

    @PreAuthorize("hasAuthority('bnt.sysRole.list')")

在这里插入图片描述

    @PreAuthorize("hasAuthority('bnt.sysRole.add')")

在这里插入图片描述

    @PreAuthorize("hasAuthority('bnt.sysRole.list')")

在这里插入图片描述

    @PreAuthorize("hasAuthority('bnt.sysRole.update')")

在这里插入图片描述

    @PreAuthorize("hasAuthority('bnt.sysRole.remove')")

在这里插入图片描述

异常处理

在service-util模块引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <scope>provided</scope>
        </dependency>

AccessDeniedException需要引入依赖,Spring Security对应的异常

在这里插入图片描述

    /**
     * spring security异常
     * @param e
     * @return
     */
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseBody
    public Result error(AccessDeniedException e) throws AccessDeniedException {
        return Result.build(null, ResultCodeEnum.PERMISSION);
    }

测试

在这里插入图片描述

10、Activiti

10.1、Activiti流程操作

配置Activiti

引入Activiti依赖

service-oa

<!--引入activiti的springboot启动器 -->
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter</artifactId>
    <version>7.1.0.M6</version>
    <exclusions>
        <exclusion>
            <artifactId>mybatis</artifactId>
            <groupId>org.mybatis</groupId>
        </exclusion>
    </exclusions>
</dependency>

添加配置

application-dev.yml中添加如下配置

spring:    
	activiti:
      #    false:默认,数据库表不变,但是如果版本不对或者缺失表会抛出异常(生产使用)
      #    true:表不存在,自动创建(开发使用)
      #    create_drop: 启动时创建,关闭时删除表(测试使用)
      #    drop_create: 启动时删除表,在创建表 (不需要手动关闭引擎)
      database-schema-update: true
      #监测历史表是否存在,activities7默认不开启历史表
      db-history-used: true
      #none:不保存任何历史数据,流程中这是最高效的
      #activity:只保存流程实例和流程行为
      #audit:除了activity,还保存全部的流程任务以及其属性,audit为history默认值
      #full:除了audit、还保存其他全部流程相关的细节数据,包括一些流程参数
      history-level: full
      #校验流程文件,默认校验resources下的process 文件夹的流程文件
      check-process-definitions: true

重启项目

会自己创建数据表

在这里插入图片描述

使用activiti插件

下载activiti-explorer

官网下载:https://www.activiti.org/get-started

在这里插入图片描述

解压部署

把解压出来的activiti-explorer.war放在Tomcat的webapps

在这里插入图片描述

在这里插入图片描述

启动Tomcat服务器

访问activiti-explorer

http://localhost:8080/activiti-explorer

默认登录账号: kermit kermit

在这里插入图片描述

10.2、流程控制

绘制流程

请假流程审批绘制

新建

在这里插入图片描述

绘制

在这里插入图片描述

导出

在这里插入图片描述

下载文件

qingjia.bpmn20.xml

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/processdef">
  <process id="qingjia" isExecutable="true">
    <startEvent id="sid-14A3E2A6-84E4-49E0-BF92-3DABD741430B"></startEvent>
    <userTask id="sid-38632C81-C407-4F0D-944D-FC30F90637A3" name="张三审批" activiti:assignee="zhangsan"></userTask>
    <sequenceFlow id="sid-081A176E-6756-4C4C-B36C-2649B12CFC5D" sourceRef="sid-14A3E2A6-84E4-49E0-BF92-3DABD741430B" targetRef="sid-38632C81-C407-4F0D-944D-FC30F90637A3"></sequenceFlow>
    <userTask id="sid-655780D5-8492-494F-9E30-2CFD6691E98D" name="李四审批" activiti:assignee="lisi"></userTask>
    <sequenceFlow id="sid-7DCE821D-4AE0-4F27-9811-80B575E7A758" sourceRef="sid-38632C81-C407-4F0D-944D-FC30F90637A3" targetRef="sid-655780D5-8492-494F-9E30-2CFD6691E98D"></sequenceFlow>
    <endEvent id="sid-7EE28419-BC61-49AC-8990-C63C4D2F7C0D"></endEvent>
    <sequenceFlow id="sid-2E583A5C-265A-4C05-B5E1-7F5DB98291F1" sourceRef="sid-655780D5-8492-494F-9E30-2CFD6691E98D" targetRef="sid-7EE28419-BC61-49AC-8990-C63C4D2F7C0D"></sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_qingjia">
    <bpmndi:BPMNPlane bpmnElement="qingjia" id="BPMNPlane_qingjia">
      <bpmndi:BPMNShape bpmnElement="sid-14A3E2A6-84E4-49E0-BF92-3DABD741430B" id="BPMNShape_sid-14A3E2A6-84E4-49E0-BF92-3DABD741430B">
        <omgdc:Bounds height="30.0" width="30.0" x="93.5" y="75.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-38632C81-C407-4F0D-944D-FC30F90637A3" id="BPMNShape_sid-38632C81-C407-4F0D-944D-FC30F90637A3">
        <omgdc:Bounds height="80.0" width="100.0" x="168.5" y="50.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-655780D5-8492-494F-9E30-2CFD6691E98D" id="BPMNShape_sid-655780D5-8492-494F-9E30-2CFD6691E98D">
        <omgdc:Bounds height="80.0" width="100.0" x="313.5" y="50.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-7EE28419-BC61-49AC-8990-C63C4D2F7C0D" id="BPMNShape_sid-7EE28419-BC61-49AC-8990-C63C4D2F7C0D">
        <omgdc:Bounds height="28.0" width="28.0" x="458.5" y="76.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="sid-7DCE821D-4AE0-4F27-9811-80B575E7A758" id="BPMNEdge_sid-7DCE821D-4AE0-4F27-9811-80B575E7A758">
        <omgdi:waypoint x="268.5" y="90.0"></omgdi:waypoint>
        <omgdi:waypoint x="313.5" y="90.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-081A176E-6756-4C4C-B36C-2649B12CFC5D" id="BPMNEdge_sid-081A176E-6756-4C4C-B36C-2649B12CFC5D">
        <omgdi:waypoint x="123.5" y="90.0"></omgdi:waypoint>
        <omgdi:waypoint x="168.5" y="90.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-2E583A5C-265A-4C05-B5E1-7F5DB98291F1" id="BPMNEdge_sid-2E583A5C-265A-4C05-B5E1-7F5DB98291F1">
        <omgdi:waypoint x="413.5" y="90.0"></omgdi:waypoint>
        <omgdi:waypoint x="458.5" y="90.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

下载流程定义图片

单击右键上图图片,图片另存为:qingjia.png

在这里插入图片描述

将资源文件放入项目

在service-oa模块resources下新建process资源文件夹

将qingjia.bpmn20.xml与qingjia.png放入process目录

部署流程

package com.jerry.auth.activiti;

import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * ClassName: ProcessTest <br>
 * Package: com.jerry.activiti <br>
 * Description:
 *
 * @Author: jerry_jy
 * @Create: 2023-03-05 10:51
 * @Version: 1.0
 */

@SpringBootTest
public class ProcessTest {

    @Autowired
    private RepositoryService repositoryService;

    // 单个文件的部署
    @Test
    public void deployProcess() {
        Deployment deploy = repositoryService.createDeployment()
                .addClasspathResource("process/qingjia.bpmn20.xml")
                .addClasspathResource("process/qingjia.png")
                .name("请假申请流程")
                .deploy();

        System.out.println("deploy.getId() = " + deploy.getId());
        System.out.println("deploy.getName() = " + deploy.getName());
    }
}

在这里插入图片描述

流程实例

package com.jerry.auth.activiti;

import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.history.HistoricTaskInstance;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

/**
 * ClassName: ProcessTest <br>
 * Package: com.jerry.activiti <br>
 * Description:
 *
 * @Author: jerry_jy
 * @Create: 2023-03-05 10:51
 * @Version: 1.0
 */

@SpringBootTest
public class ProcessTest1 {

    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private TaskService taskService;

    @Autowired
    private HistoryService historyService;

    // 单个流程实例挂起
    @Test
    public void SingleSuspendProcessInstance() {
        String processInstanceId = "71f6803b-bb19-11ed-a845-005056c00001";
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
        //获取到当前流程定义是否为暂停状态   suspended方法为true代表为暂停   false就是运行的
        boolean suspended = processInstance.isSuspended();
        if (suspended) {
            runtimeService.activateProcessInstanceById(processInstanceId);
            System.out.println("流程实例:" + processInstanceId + "激活");
        } else {
            runtimeService.suspendProcessInstanceById(processInstanceId);
            System.out.println("流程实例:" + processInstanceId + "挂起");
        }
    }


    // 全部流程实例挂起
    @Test
    public void suspendProcessInstance() {

        // 1、获取流程定义对象
        ProcessDefinition qingjia = repositoryService.createProcessDefinitionQuery().processDefinitionKey("qingjia").singleResult();
        // 2、调用流程定义对象的方法判断当前状态:挂起   激活
        boolean suspended = qingjia.isSuspended();
        if (suspended) {
            // 暂定,那就可以激活
            // 参数1:流程定义的id  参数2:是否激活    参数3:时间点
            repositoryService.activateProcessDefinitionById(qingjia.getId(), true, null);
            System.out.println("流程定义:" + qingjia.getId() + "激活");
        } else {
            repositoryService.suspendProcessDefinitionById(qingjia.getId(), true, null);
            System.out.println("流程定义:" + qingjia.getId() + "挂起");
        }
    }

    /**
     * 启动流程实例,添加businessKey
     */
    @Test
    public void startUpProcessAddBusinessKey(){
        // 启动流程实例,指定业务标识businessKey,也就是请假申请单id
        ProcessInstance processInstance = runtimeService.
                startProcessInstanceByKey("qingjia","1001");
        // 输出
        System.out.println("业务id:"+processInstance.getBusinessKey()); //1001
        System.out.println("processInstance.getId() = " + processInstance.getId()); // 71f6803b-bb19-11ed-a845-005056c00001
    }

    /**
     * 查询流程定义
     */
    @Test
    public void findProcessDefinitionList(){
        List<ProcessDefinition> definitionList = repositoryService.createProcessDefinitionQuery()
                .orderByProcessDefinitionVersion()
                .desc()
                .list();
        //输出流程定义信息
        for (ProcessDefinition processDefinition : definitionList) {
            System.out.println("流程定义 id="+processDefinition.getId());
            System.out.println("流程定义 name="+processDefinition.getName());
            System.out.println("流程定义 key="+processDefinition.getKey());
            System.out.println("流程定义 Version="+processDefinition.getVersion());
            System.out.println("流程部署ID ="+processDefinition.getDeploymentId());
        }
    }

    /**
     * 删除流程定义
     */
    @Test
    public void deleteDeployment() {
        //部署id
        String deploymentId = "qingjia:1:c493c327-bb02-11ed-8360-005056c00001";
//        //删除流程定义,如果该流程定义已有流程实例启动则删除时出错
//        repositoryService.deleteDeployment(deploymentId);
        //设置true 级联删除流程定义,即使该流程有流程实例启动也可以删除,设置为false非级别删除方式
        repositoryService.deleteDeployment(deploymentId, true);
    }

    // 查询已经处理的任务
    @Test
    public void findCompleteTaskList(){
        List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery()
                .taskAssignee("zhangsan")
                .finished().list();
        for (HistoricTaskInstance historicTaskInstance : list) {
            System.out.println("流程实例id:" + historicTaskInstance.getProcessInstanceId());
            System.out.println("任务id:" + historicTaskInstance.getId());
            System.out.println("任务负责人:" + historicTaskInstance.getAssignee());
            System.out.println("任务名称:" + historicTaskInstance.getName());
        }
    }

    // 处理当前任务
    @Test
    public void completeTask(){
        // 查询负责人需要处理的任务,返回一条
        Task task = taskService.createTaskQuery().taskAssignee("zhangsan").singleResult();
        // 完成任务
        taskService.complete(task.getId());
    }

    // 查询个人的代办任务--zhangsan
    @Test
    public void findTaskList(){
        String assign = "zhangsan";
        List<Task> list = taskService.createTaskQuery()
                .taskAssignee(assign).list();
        for (Task task : list) {
            System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId());
            System.out.println("任务id:" + task.getId());
            System.out.println("任务负责人:" + task.getAssignee());
            System.out.println("任务名称:" + task.getName());
        }

    }

    // 启动流程实例
    @Test
    public void startProcess(){
        ProcessInstance processInstance = runtimeService.startProcessInstanceById("qingjia");
        System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId());
        System.out.println("processInstance.getId() = " + processInstance.getId());
        System.out.println("processInstance.getActivityId() = " + processInstance.getActivityId());
    }

    // 单个文件的部署
    @Test
    public void deployProcess() {
        Deployment deploy = repositoryService.createDeployment()
                .addClasspathResource("process/qingjia.bpmn20.xml")
                .addClasspathResource("process/qingjia.png")
                .name("请假申请流程")
                .deploy();

        System.out.println("deploy.getId() = " + deploy.getId());
        System.out.println("deploy.getName() = " + deploy.getName());
    }
}

任务分配

package com.jerry.auth.activiti;

import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

/**
 * ClassName: ProcessTest2 <br>
 * Package: com.jerry.auth.activiti <br>
 * Description:
 *
 * @Author: jerry_jy
 * @Create: 2023-03-05 14:05
 * @Version: 1.0
 */

@SpringBootTest
public class ProcessTest2 {

    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private TaskService taskService;

    @Autowired
    private HistoryService historyService;

    ///
    // 监听器分配任务
    // 部署流程定义
    @Test
    public void deployProcess02() {
        Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban02.bpmn20.xml").name("加班申请流程02").deploy();
        System.out.println("deploy.getId() = " + deploy.getId()); // ed080f00-bb41-11ed-a6f2-005056c00001
        System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程02
    }

    @Test
    public void startProcessInstance02(){
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban02");
        System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban02:1:ed150752-bb41-11ed-a6f2-005056c00001
        System.out.println("processInstance.getId() = " + processInstance.getId()); // 06eca124-bb42-11ed-9bbc-005056c00001
    }


    // 查询个人的代办任务--Tim
    @Test
    public void findTaskList02(){
        String assign = "Tim";
        List<Task> list = taskService.createTaskQuery()
                .taskAssignee(assign).list();
        for (Task task : list) {
            System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); // 06eca124-bb42-11ed-9bbc-005056c00001
            System.out.println("任务id:" + task.getId()); // 06f071b8-bb42-11ed-9bbc-005056c00001
            System.out.println("任务负责人:" + task.getAssignee()); // Tim
            System.out.println("任务名称:" + task.getName()); // 经理审批
        }
    }

    ///
    // uel-method
    // 部署流程定义
    @Test
    public void deployProcess01() {
        Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban01.bpmn20.xml").name("加班申请流程01").deploy();
        System.out.println("deploy.getId() = " + deploy.getId()); // 8c4ac05e-bb20-11ed-8d65-005056c00001
        System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程01
    }

    @Test
    public void startProcessInstance01(){
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban01");
        System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban01:1:8c56a740-bb20-11ed-8d65-005056c00001
        System.out.println("processInstance.getId() = " + processInstance.getId()); // abb9c7c4-bb20-11ed-b608-005056c00001
    }

    // 查询个人的代办任务--LiLei
    @Test
    public void findTaskList01(){
        String assign = "LiLei";
        List<Task> list = taskService.createTaskQuery()
                .taskAssignee(assign).list();
        for (Task task : list) {
            System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); // abb9c7c4-bb20-11ed-b608-005056c00001
            System.out.println("任务id:" + task.getId()); // abbd4a38-bb20-11ed-b608-005056c00001
            System.out.println("任务负责人:" + task.getAssignee()); // LiLei
            System.out.println("任务名称:" + task.getName()); // 经理审批
        }
    }

    ///
    // uel-value
    // 部署流程定义
    @Test
    public void deployProcess() {
        Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban.bpmn20.xml").name("加班申请流程").deploy();
        System.out.println("deploy.getId() = " + deploy.getId()); // 5c5519ad-bb1d-11ed-b5c8-005056c00001
        System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程
    }

    // 启动流程实例
    @Test
    public void startProcessInstance() {
        Map<String, Object> map = new HashMap<>();
        // 设置任务人
        map.put("assignee1","tom");
        map.put("assignee2","jerry");
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban", map);

        System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban:1:5c60d97f-bb1d-11ed-b5c8-005056c00001

        System.out.println("processInstance.getId() = " + processInstance.getId()); // 7f720dd9-bb1d-11ed-b6e9-005056c00001
    }

    // 查询个人的代办任务--tom
    @Test
    public void findTaskList(){
        String assign = "tom";
        List<Task> list = taskService.createTaskQuery()
                .taskAssignee(assign).list();
        for (Task task : list) {
            System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); //7f720dd9-bb1d-11ed-b6e9-005056c00001
            System.out.println("任务id:" + task.getId()); // 7f759051-bb1d-11ed-b6e9-005056c00001
            System.out.println("任务负责人:" + task.getAssignee()); // tom
            System.out.println("任务名称:" + task.getName()); // 经理审批
        }
    }

}

配置监听器

package com.jerry.auth.activiti;

import org.springframework.stereotype.Component;

/**
 * ClassName: UserBean <br>
 * Package: com.jerry.auth.activiti <br>
 * Description:
 *
 * @Author: jerry_jy
 * @Create: 2023-03-05 14:25
 * @Version: 1.0
 */

@Component
public class UserBean {

    public String getUsername(int id) {
        if (id == 1) {
            return "LiLei";
        }
        if (id == 2) {
            return "HanMeiMei";
        }
        return "admin";
    }
}

任务组

package com.jerry.auth.activiti;

import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

/**
 * ClassName: ProcessTest3 <br>
 * Package: com.jerry.auth.activiti <br>
 * Description:
 *
 * @Author: jerry_jy
 * @Create: 2023-03-05 19:18
 * @Version: 1.0
 */

@SpringBootTest
public class ProcessTest3 {

    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private TaskService taskService;

    // 1、部署流程定义
    @Test
    public void deployProcess() {
        Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban04.bpmn20.xml").name("加班申请流程04").deploy();
        System.out.println("deploy.getId() = " + deploy.getId()); // f204be8a-bb48-11ed-950e-005056c00001
        System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程04
    }

    // 1.5、启动流程实例
    @Test
    public void startProcessInstance() {
//        Map<String, Object> map = new HashMap<>();
        // 设置任务人
//        map.put("assignee1","tom");
//        map.put("assignee2","jerry");
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban04");

        System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban04:1:f210f38c-bb48-11ed-950e-005056c00001

        System.out.println("processInstance.getId() = " + processInstance.getId()); // 428d0c0f-bb49-11ed-83a5-005056c00001
    }

    // 2、查询组任务
    @Test
    public void findGroupTaskList(){
        List<Task> list = taskService.createTaskQuery()
                .taskCandidateUser("tom")
                .list();
        for (Task task : list) {
            System.out.println("----------------------------");
            System.out.println("流程实例id:" + task.getProcessInstanceId());
            System.out.println("任务id:" + task.getId());
            System.out.println("任务负责人:" + task.getAssignee());
            System.out.println("任务名称:" + task.getName());
        }
    }

    // 3、分配组任务
    @Test
    public void claimTask(){
        Task task = taskService.createTaskQuery()
                .taskCandidateUser("tom")
                .singleResult();
        if (task!=null){
            taskService.claim(task.getId(),"tom");
            System.out.println("分配任务完成");
        }
    }

    // 4、查询个人的代办任务--tom
    @Test
    public void findTaskList(){
        String assign = "tom";
        List<Task> list = taskService.createTaskQuery()
                .taskAssignee(assign).list();
        for (Task task : list) {
            System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); //7f720dd9-bb1d-11ed-b6e9-005056c00001
            System.out.println("任务id:" + task.getId()); // 7f759051-bb1d-11ed-b6e9-005056c00001
            System.out.println("任务负责人:" + task.getAssignee()); // tom
            System.out.println("任务名称:" + task.getName()); // 经理审批
        }
    }


    // 5、办理个人任务
    @Test
    public void completeGroupTask() {
        Task task = taskService.createTaskQuery()
                .taskAssignee("tom")  //要查询的负责人
                .singleResult();//返回一条

        taskService.complete(task.getId());
    }
}

10.3、网关

网关用来控制流程的流向,通常会和流程变量一起使用。

排他网关

  • 排他网关:只有一条路径会被选择

当你的流程出现这样的场景:请假申请,两天以内,部门经理审批流程就结束了,两天以上需要总经理直接审批,这个时候就需要排他网关

在这里插入图片描述

并行网关

  • 并(平)行网关:所有路径会被同时选择

当出现这样的场景:请假申请开始,需要部门经理和总经理都审批,两者没有前后需要两个人全部审批才能进入下个节点人事审批。这个时候就需要并行网关

在这里插入图片描述

与排他网关的主要区别是,并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。

包含网关

包容网关:可以同时执行多条线路,也可以在网关上设置条件,可以看做是排他网关和并行网关的结合体。
当出现这样的场景:请假申请大于等于2天需要由部门总经理审批,小于2天由部门经理审批,请假申请必须经过人事经理审批。这个时候就需要包含网关

在这里插入图片描述

package com.jerry.auth.activiti;

import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

/**
 * ClassName: ProcessTestGateway <br>
 * Package: com.jerry.auth.activiti <br>
 * Description:
 *
 * @Author: jerry_jy
 * @Create: 2023-03-05 19:52
 * @Version: 1.0
 */
@SpringBootTest
public class ProcessTestGateway {

    @Autowired
    private RepositoryService repositoryService;

    //注入RuntimeService
    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private TaskService taskService;

    @Autowired
    private HistoryService historyService;

    //1 部署流程定义
    @Test
    public void deployProcess() {
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("process/qingjia003.bpmn20.xml")
                .name("请假申请流程003")
                .deploy();
        System.out.println(deployment.getId()); // af9242f0-bb4c-11ed-85bf-005056c00001
        System.out.println(deployment.getName()); // 请假申请流程002
    }

    //2 启动流程实例
    @Test
    public void startProcessInstance() {
        Map<String, Object> map = new HashMap<>();
        //设置请假天数
        map.put("day", "3");

        ProcessInstance processInstance =
//                runtimeService.startProcessInstanceByKey("qingjia002", map);
                runtimeService.startProcessInstanceByKey("qingjia003");
        System.out.println(processInstance.getProcessDefinitionId()); // qingjia002:1:afac0c82-bb4c-11ed-85bf-005056c00001
        System.out.println(processInstance.getId()); // 90d46e2c-bb4d-11ed-9b92-005056c00001
    }

    //3 查询个人的代办任务--zhao6
    @Test
    public void findTaskList() {

//        String assign = "zhao6";
//        String assign = "gousheng";
//        String assign = "xiaocui";
//        String assign = "wang5";
//        String assign = "gouwa";
        String assign = "xiaoli";
        List<Task> list = taskService.createTaskQuery()
                .taskAssignee(assign).list();
        for (Task task : list) {
            System.out.println("流程实例id:" + task.getProcessInstanceId());
            System.out.println("任务id:" + task.getId());
            System.out.println("任务负责人:" + task.getAssignee());
            System.out.println("任务名称:" + task.getName());
        }
    }

    //完成任务
    @Test
    public void completeTask() {
        Task task = taskService.createTaskQuery()
//                .taskAssignee("zhao6")  //要查询的负责人
//                .taskAssignee("xiaocui")  //要查询的负责人
//                .taskAssignee("gousheng")
//                .taskAssignee("wang5")
                .taskAssignee("gouwa")
                .singleResult();//返回一条

        //完成任务,参数:任务id
            taskService.complete(task.getId());
    }
}

在这里插入图片描述

11、审批管理

在这里插入图片描述

11.1、审批设置–CRUD

package com.jerry.process.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jerry.common.result.Result;
import com.jerry.model.process.ProcessType;
import com.jerry.process.service.OaProcessTypeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 * 审批类型 前端控制器
 * </p>
 *
 * @author jerry
 * @since 2023-03-05
 */
@Api(value = "审批类型", tags = "审批类型")
@RestController
@RequestMapping(value = "/admin/process/processType")
public class OaProcessTypeController {

    @Autowired
    private OaProcessTypeService processTypeService;

    @ApiOperation(value = "获取分页列表")
    @GetMapping("{page}/{pageSize}")
    public Result index(@PathVariable Long page, @PathVariable Long pageSize) {

        Page<ProcessType> pageInfo = new Page<>(page, pageSize);
        Page<ProcessType> pageModel = processTypeService.page(pageInfo);

        return Result.ok(pageModel);
    }

    @PreAuthorize("hasAuthority('bnt.processType.list')")
    @ApiOperation(value = "获取")
    @GetMapping("get/{id}")
    public Result get(@PathVariable Long id) {
        ProcessType processType = processTypeService.getById(id);
        return Result.ok(processType);
    }

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

    @PreAuthorize("hasAuthority('bnt.processType.update')")
    @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();
    }

}

在这里插入图片描述

11.2、模板审批–CRUD

package com.jerry.process.controller;


import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jerry.common.result.Result;
import com.jerry.model.process.ProcessTemplate;
import com.jerry.process.service.OaProcessTemplateService;
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 jerry
 * @since 2023-03-05
 */
@Api(value = "审批模板管理", tags = "审批模板管理")
@RestController
@RequestMapping(value = "/admin/process/processTemplate")
public class OaProcessTemplateController {

    @Autowired
    private OaProcessTemplateService processTemplateService;

    // 分页查询审批模板
    @ApiOperation("获取分页查询审批模板数据")
    @GetMapping("{page}/{pageSize}")
    public Result index(@PathVariable Long page, @PathVariable Long pageSize){
        Page<ProcessTemplate> pageInfo = new Page<>(page, pageSize);
        //分页查询审批模板,把审批类型对应名称查询
        IPage<ProcessTemplate> pageModel =
                processTemplateService.selectPageProcessTemplate(pageInfo);
        return Result.ok(pageModel);
    }

    //@PreAuthorize("hasAuthority('bnt.processTemplate.list')")
    @ApiOperation(value = "获取")
    @GetMapping("get/{id}")
    public Result get(@PathVariable Long id) {
        ProcessTemplate processTemplate = processTemplateService.getById(id);
        return Result.ok(processTemplate);
    }

    //@PreAuthorize("hasAuthority('bnt.processTemplate.templateSet')")
    @ApiOperation(value = "新增")
    @PostMapping("save")
    public Result save(@RequestBody ProcessTemplate processTemplate) {
        processTemplateService.save(processTemplate);
        return Result.ok();
    }

    //@PreAuthorize("hasAuthority('bnt.processTemplate.templateSet')")
    @ApiOperation(value = "修改")
    @PutMapping("update")
    public Result updateById(@RequestBody ProcessTemplate processTemplate) {
        processTemplateService.updateById(processTemplate);
        return Result.ok();
    }

    //@PreAuthorize("hasAuthority('bnt.processTemplate.remove')")
    @ApiOperation(value = "删除")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        processTemplateService.removeById(id);
        return Result.ok();
    }

}

11.3、添加审批模板

OaProcessTypeController


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

OaProcessTemplateController


    @ApiOperation(value = "上传流程定义")
    @PostMapping("/uploadProcessDefinition")
    public Result uploadProcessDefinition(MultipartFile file) throws FileNotFoundException {

        // 获取classes目录位置
        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 zipFile = new File(path + "/processes/" + filename);

        // 保存文件
        try {
            file.transferTo(zipFile);
        } catch (IOException e) {
           return Result.fail();
        }

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


    public static void main(String[] args) {
        try {
            String path = new File(ResourceUtils.getURL("classpath:").getPath()).getAbsolutePath();
            System.out.println("path = " + path); //E:\CodeLife\IdeaProject\guigu-oa\guigu-oa-parent\service-oa\target\classes
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

在这里插入图片描述

11.4、查看审批模板

整合前端,无后台接口

在这里插入图片描述

11.5、审批列表

分页查询

OaProcessController

package com.jerry.process.controller;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jerry.common.result.Result;
import com.jerry.model.process.Process;
import com.jerry.process.service.OaProcessService;
import com.jerry.vo.process.ProcessQueryVo;
import com.jerry.vo.process.ProcessVo;
import io.swagger.annotations.ApiOperation;
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;

/**
 * <p>
 * 审批类型 前端控制器
 * </p>
 *
 * @author jerry
 * @since 2023-03-06
 */
@RestController
@RequestMapping(value = "/admin/process")
public class OaProcessController {
    @Autowired
    private OaProcessService processService;

    //审批管理列表
    @ApiOperation(value = "获取分页列表")
    @GetMapping("{page}/{limit}")
    public Result index(@PathVariable Long page,
                        @PathVariable Long limit,
                        ProcessQueryVo processQueryVo) {
        Page<ProcessVo> pageInfo = new Page<>(page, limit);

      IPage<ProcessVo> pageModel =  processService.selectPage(pageInfo,processQueryVo);

      return Result.ok();
    }
}

OaProcessService

public interface OaProcessService extends IService<Process> {

    //审批管理列表
    IPage<ProcessVo> selectPage(Page<ProcessVo> pageInfo, ProcessQueryVo processQueryVo);
}

OaProcessServiceImpl

    //审批管理列表
    @Override
    public IPage<ProcessVo> selectPage(Page<ProcessVo> pageInfo, ProcessQueryVo processQueryVo) {
        IPage<ProcessVo> pageModel =  baseMapper.selectPage(pageInfo,processQueryVo);
        return pageModel;
    }

OaProcessMapper

    //审批管理列表
    IPage<ProcessVo> selectPage(Page<ProcessVo> pageInfo, @Param("vo") ProcessQueryVo processQueryVo);

涉及到4张表的多表查询,自己编写SQL语句

OaProcessMapper.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.jerry.process.mapper.OaProcessMapper">

    <select id="selectPage" resultType="com.jerry.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 oa_process a
                 LEFT JOIN sys_user d ON a.user_id =d.id
                 LEFT JOIN oa_process_template b ON a.process_template_id = b.id
                 LEFT JOIN oa_process_type c ON a.process_type_id = c.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},'%'))
            </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 >= #{vo.createTimeBegin}
            </if>
            <if test="vo.createTimeEnd != null and vo.createTimeEnd != ''">
                and a.create_time &lt;= #{vo.createTimeEnd}
            </if>
        </where>

    </select>
</mapper>

修改mapper的映射路径

在这里插入图片描述

页面展示

在这里插入图片描述

部署流程定义

OaProcessTemplateServiceImpl

    // 修改模板的发布状态 status==1 代表已发布
    // 流程定义部署
    @Override
    public void publish(Long id) {
        // 修改模板的发布状态 status==1 代表已发布
        ProcessTemplate processTemplate = baseMapper.selectById(id);
        processTemplate.setStatus(1);
        baseMapper.updateById(processTemplate);

        // 流程定义部署
        if (StringUtils.isEmpty(processTemplate.getProcessDefinitionPath())){
            processService.deployByZip(processTemplate.getProcessDefinitionPath());
        }
    }
}

OaProcessService

    // 流程定义部署
    void deployByZip(String deployPath);

OaProcessServiceImpl

    // 流程定义部署
    @Override
    public void deployByZip(String deployPath) {
        InputStream inputStream= this.getClass().getClassLoader().getResourceAsStream(deployPath);
        ZipInputStream zipInputStream = new ZipInputStream(inputStream);
        // 部署
        Deployment deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).deploy();

        System.out.println("deployment.getId() = " + deployment.getId());
        System.out.println("deployment.getName() = " + deployment.getName());
    }

12、前端审批

12.1、OA审批

在这里插入图片描述

node -v
v 16.16.0

报错

npm ERR! path F:\guigu-oa\guigu-oa-web\node_modules\node-sass
npm ERR! command failed
npm ERR! command C:\WINDOWS\system32\cmd.exe /d /s /c node scripts/build.js
npm ERR! Building: E:\nodejs\node.exe F:\guigu-oa\guigu-oa-web\node_modules\node-gyp\bin\node-gyp.js rebuild --verbose --libsass_ext= --libsass_cflags= --libsass_ldflags= --libsass_library=
npm ERR! gyp info it worked if it ends with ok
npm ERR! gyp verb cli [
npm ERR! gyp verb cli   'E:\\nodejs\\node.exe',
npm ERR! gyp verb cli   'F:\\guigu-oa\\guigu-oa-web\\node_modules\\node-gyp\\bin\\node-gyp.js',
npm ERR! gyp verb cli   'rebuild',
npm ERR! gyp verb cli   '--verbose',
npm ERR! gyp verb cli   '--libsass_ext=',
npm ERR! gyp verb cli   '--libsass_cflags=',
npm ERR! gyp verb cli   '--libsass_ldflags=',
npm ERR! gyp verb cli   '--libsass_library='
npm ERR! gyp verb cli ]
npm ERR! gyp info using node-gyp@3.8.0
npm ERR! gyp info using node@16.16.0 | win32 | x64
npm ERR! gyp verb command rebuild []
npm ERR! gyp verb command clean []
npm ERR! gyp verb clean removing "build" directory

在这里插入图片描述

nodejs版本过高,与node-sass不兼容,降级版本

v14.15.0

npm install没问题

在这里插入图片描述

13、代码托管

Git

在这里插入图片描述

Gitee

https://gitee.com/jinyang-jy/OnlineOfficeSystem.git

GitHub

网盘资料

链接:https://pan.baidu.com/s/1ZVNqzPlcfMH89NgUYNYZtQ?pwd=2022
提取码:2022

原文链接:https://blog.csdn.net/qq_44807756/article/details/129386363




所属网站分类: 技术文章 > 博客

作者:代码搬运工

链接:http://www.qianduanheidong.com/blog/article/528183/7293a63e7333c7cb29d5/

来源:前端黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

5 0
收藏该文
已收藏

评论内容:(最多支持255个字符)