云上办公系统项目

  • 1、云上办公系统
    • 1.1、介绍
    • 1.2、核心技术
    • 1.3、开发环境说明
    • 1.4、产品展示
      • 后台
      • 前台
    • 1.5、 个人总结
  • 2、后端环境搭建
    • 2.1、建库建表
    • 2.2、创建Maven项目
      • pom文件
        • guigu-oa-parent
        • common
        • common-util
        • service-util
        • model
        • service-oa
      • 配置数据源、服务器端口号
        • application.yml
        • application-dev.yml
      • 导入实体类
    • 2.3、编写代码
      • 启动类
  • 3、后端角色管理
    • 3.1、查询所有角色
      • SysRoleMapper
      • SysRoleService
      • SysRoleServiceImpl
      • 编写测试类
      • 编写统一结果返回类
        • ResultCodeEnum
        • Result
      • SysRoleController
      • 测试
    • 3.2、集成knife4j
        • Swagger介绍
        • 目的
        • 使用步骤
          • 添加依赖
          • 添加knife4j配置类
          • Controller层添加注解
          • 测试
    • 3.3、分页查询所有角色
        • MybatisPlusConfig
        • 主启动类上添加包扫描
        • SysRoleController
        • 测试
    • 3.4、添加/修改/删除角色
      • 测试
  • 4、统一异常处理
    • 4.1、全局异常处理
    • 4.2、特定异常处理
    • 4.3、自定义异常处理
      • GlobalExceptionHandler
      • GuiguException
  • 5、前端环境搭建
    • 安装脚手架工程
    • 前后联调的流程
      • 修改前端的IP地址
      • 编写后台登录/登出的请求
      • 修改前端的跳转地址
      • 修改响应状态码
      • 测试
  • 6、前端角色管理
    • 6.1、角色列表
      • 修改路由
      • 创建角色页面
      • 定义角色管理相关的API请求函数
      • 测试
    • 6.2、角色删除
      • sysRole.js
      • list.vue
    • 6.3、角色添加
    • 6.4、角色修改与数据回显
    • 6.5、批量删除
      • sysRole.js
      • list.vue
      • 页面展示
  • 7、用户管理
    • 7.1、用户管理CRUD
      • 需求分析
      • 代码生成器
      • 编写代码
      • 测试
      • 整合前端
        • 前端页面 list.vue
        • 添加路由
        • 定义API接口
      • 页面展示
    • 7.2、用户管理分配角色
      • 需求分析
      • 接口分析
      • 编写代码
      • 前端展示
    • 7.3、修改用户状态
      • 需求分析
      • 编写代码
      • 整合前端
        • 定义前端路由
        • 修改前端页面
      • 页面展示
  • 8、菜单管理
    • 8.1、菜单管理CRUD
      • 需求分析
      • 编写代码
      • 接口测试
      • 整合前端
        • sysMenu.js
        • list.vue
      • 页面展示
    • 8.2、角色分配菜单功能
      • 需求分析
      • 编写代码
      • 整合前端
        • router/index.js
        • sysRole/list.vue
        • sysMenu.js
        • assignAuth.vue
      • 页面展示
  • 9、权限管理(重难点)
    • 9.1、用户登录权限管理
      • 需求分析
      • 引入JWT
      • 修改用户登录
        • 先引入MD5工具类
        • 修改SysUserControler保存用户的方法
        • 修改IndexController的登录方法
        • SysMenuService
        • SysMenuServiceImpl
      • 接口测试
        • 登录接口测试
        • info接口测试
      • 整合前端
      • 页面展示
    • 9.2、用户认证
      • 整合SpringSecurity
        • 引入依赖
        • 添加配置类
        • 测试
      • 用户认证
        • 流程分析
        • 自定义组件的编写
          • 自定义加密器PasswordEncoder
          • 自定义用户对象UserDetails
          • UserDetailsService
          • UserDetailsServiceImpl
          • 自定义用户认证接口
          • 认证解析token
          • 配置用户认证
      • 测试
    • 9.3、用户权限控制
      • 流程分析
      • 修改代码
        • spring-security模块配置redis
        • 修改TokenLoginFilter
        • 修改TokenAuthenticationFilter
        • 修改WebSecurityConfig类
        • service-oa模块添加redis配置
        • 控制controller层接口权限
        • 异常处理
      • 测试
  • 10、Activiti
    • 10.1、Activiti流程操作
      • 配置Activiti
        • 引入Activiti依赖
        • 添加配置
        • 重启项目
      • 使用activiti插件
        • 下载activiti-explorer
        • 解压部署
        • 访问activiti-explorer
    • 10.2、流程控制
      • 绘制流程
        • 新建
        • 绘制
        • 导出
        • 下载文件
      • 部署流程
      • 流程实例
      • 任务分配
      • 任务组
    • 10.3、网关
        • 排他网关
        • 并行网关
        • 包含网关
  • 11、审批管理
    • 11.1、审批设置--CRUD
    • 11.2、模板审批--CRUD
    • 11.3、添加审批模板
    • 11.4、查看审批模板
    • 11.5、审批列表
      • 分页查询
      • 页面展示
      • 部署流程定义
  • 12、前端审批
    • 12.1、OA审批
  • 13、代码托管
    • Git
    • Gitee
    • GitHub
    • 网盘资料

申明: 未经许可,禁止以任何形式转载,若要引用,请标注链接地址。 全文共计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 Tools Maven 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 {
}

编写测试类

目的是:

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、可测性 (直接在接口文档上进行测试,以方便理解业务)

目的

使用步骤

添加依赖

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层添加注解

云上办公系统项目

测试

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://www.yuucn.com/wp-content/uploads/2023/04/1682063997-10d538687c383fb.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

需求分析

云上办公系统项目

代码生成器

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、用户管理分配角色

需求分析

多对多的关系

云上办公系统项目

接口分析

编写代码

代码是写在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

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://www.yuucn.com/wp-content/uploads/2023/04/1682063997-10d538687c383fb.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

发表回复