> 文章列表 > 云上办公系统项目

云上办公系统项目

云上办公系统项目

云上办公系统项目

  • 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-oaprofiles: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.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/guigu-oa?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8username: rootpassword: root

导入实体类

云上办公系统项目

2.3、编写代码

云上办公系统项目

启动类

package com.jerry.auth;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** ClassName: ServiceAuthApplication* Package: com.jerry.auth* Description:** @Author jerry_jy* @Create 2023-02-28 22:03* @Version 1.0*/@SpringBootApplication
public class ServiceAuthApplication {public static void main(String[] args) {SpringApplication.run(ServiceAuthApplication.class, args);}
}

3、后端角色管理

3.1、查询所有角色

SysRoleMapper

package com.jerry.auth.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jerry.model.system.SysRole;
import org.apache.ibatis.annotations.Mapper;/*** ClassName: SysRoleMapper* Package: com.jerry.auth.mapper* Description:** @Author jerry_jy* @Create 2023-02-28 22:05* @Version 1.0*/@Mapper
public interface SysRoleMapper extends BaseMapper<SysRole> {
}

SysRoleService

package com.jerry.auth.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.jerry.model.system.SysRole;/*** ClassName: SysRoleService* Package: com.jerry.auth.service* Description:** @Author jerry_jy* @Create 2023-03-01 9:12* @Version 1.0*/public interface SysRoleService extends IService<SysRole> {
}

SysRoleServiceImpl

package com.jerry.auth.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jerry.auth.mapper.SysRoleMapper;
import com.jerry.auth.service.SysRoleService;
import com.jerry.model.system.SysRole;
import org.springframework.stereotype.Service;/*** ClassName: SysRoleServiceImpl* Package: com.jerry.auth.service.impl* Description:** @Author jerry_jy* @Create 2023-03-01 9:13* @Version 1.0*/@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {
}

编写测试类

目的是:

  • 测试数据源连接
  • 复习下MyBatisPlus对数据库的CRUD
package com.jerry.auth;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jerry.auth.mapper.SysRoleMapper;
import com.jerry.auth.service.SysRoleService;
import com.jerry.model.system.SysRole;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.Arrays;
import java.util.List;/*** ClassName: TestMpDemo1* Package: com.jerry.auth* Description:** @Author jerry_jy* @Create 2023-02-28 22:07* @Version 1.0*/@SpringBootTest
public class TestMpDemo1 {// MyBatisPlus 对 service 层和 dao 层都做了很好的封装,直接调对应的CRUD方法就行@Autowiredprivate SysRoleMapper sysRoleMapper;@Autowiredprivate SysRoleService sysRoleService;// 使用MP 封装的 service 来操作数据库,查询所有记录@Testpublic void getAllByService(){List<SysRole> list = sysRoleService.list();list.forEach(System.out::println);}// 使用MP 封装的 mapper查询所有记录@Testpublic void getAllByMapper(){List<SysRole> sysRoles = sysRoleMapper.selectList(null);sysRoles.forEach(System.out::println);}// 添加操作@Testpublic 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自动回填}// 修改操作@Testpublic void updateById(){// 根据id查询SysRole sysRole = sysRoleMapper.selectById(9);// 设置修改值sysRole.setRoleName("角色管理员1");// 调用方法实现最终修改int update = sysRoleMapper.updateById(sysRole);System.out.println("update = " + update); // 受影响的行数}// 根据id删除@Testpublic void deleteById(){int delete = sysRoleMapper.deleteById(9);System.out.println("delete = " + delete); // 受影响的行数}// 批量删除@Testpublic void deleteBatchByIds(){//        int delete = sysRoleMapper.delete(null);// 或者下面这种写法int delete = sysRoleMapper.deleteBatchIds(Arrays.asList(1, 2));System.out.println("ids = " + delete); // 受影响的行数}// 条件查询@Testpublic 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;}// 构造私有化 外部不能newprivate 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 {@Autowiredprivate SysRoleService sysRoleService;// http://localhost:8800/admin/system/sysRole/getAll// 测试查询所有的角色
//    @GetMapping("/getAll")
//    private List<SysRole> getAll(){
//        List<SysRole> list = sysRoleService.list();
//        return list;
//    }/*** 统一返回数据结果* @return*/@GetMapping("/getAll")private Result getAll(){List<SysRole> list = sysRoleService.list();return Result.ok(list);}}

测试

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

云上办公系统项目

3.2、集成knife4j

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

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

Swagger介绍

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

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

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

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

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

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

目的

  • 用来生成接口的API文档

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

使用步骤

添加依赖

service-uitl.pom

<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
添加knife4j配置类
package com.jerry.common.config.knife4j;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;import java.util.ArrayList;
import java.util.List;/*** ClassName: knife4j* Package: com.jerry.common.config* Description:** @Author jerry_jy* @Create 2023-03-01 10:53* @Version 1.0*//*** knife4j配置信息*/
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {@Beanpublic 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参数endDocket adminApi = new Docket(DocumentationType.SWAGGER_2).groupName("adminApi").apiInfo(adminApiInfo()).select()//只显示admin路径下的页面.apis(RequestHandlerSelectors.basePackage("com.jerry")).paths(PathSelectors.regex("/admin/.*")).build().globalOperationParameters(pars);return adminApi;}private ApiInfo adminApiInfo(){return new ApiInfoBuilder().title("后台管理系统-API文档").description("本文档描述了后台管理系统微服务接口定义").version("1.0").contact(new Contact("jerry", "https://jerry-jy.co", "jinyang9248@163.com")).build();}}
Controller层添加注解
  • 类上加@Api(tags = "角色管理接口")
  • 方法上加@ApiOperation("查询所有角色")

云上办公系统项目

测试

http://localhost:8800/doc.html

云上办公系统项目

3.3、分页查询所有角色

service-util模块下创建 MybatisPlusConfig

MybatisPlusConfig

package com.jerry.common.config.mp;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** ClassName: MybatisPlusConfig* Package: com.jerry.common.config.mp* Description:** @Author jerry_jy* @Create 2023-03-01 11:17* @Version 1.0*/@Configuration
@MapperScan("com.jerry.auth.mapper")
public class MybatisPlusConfig {/*** 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}@Beanpublic 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:sstime-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)@ResponseBodypublic Result error(Exception e){e.printStackTrace();return Result.fail().message("执行全局处理异常...");}/*** 特定异常处理* @param e* @return*/@ExceptionHandler(ArithmeticException.class)@ResponseBodypublic Result error(ArithmeticException e){e.printStackTrace();return Result.fail().message("执行特定处理异常...");}/*** 自定义异常处理* @param e* @return*/@ExceptionHandler(GuiguException.class)@ResponseBodypublic 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();}@Overridepublic String toString() {return "GuiguException{" +"code=" + code +", msg='" + msg + '\\'' +'}';}
}

5、前端环境搭建

安装脚手架工程

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

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

# clone the project
git clone https://github.com/PanJiaChen/vue-element-admin.git# install dependency
npm install# develop
npm run dev

http://localhost:9528/#/dashboard

云上办公系统项目

前后联调的流程

云上办公系统项目

云上办公系统项目

修改前端的IP地址

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

云上办公系统项目

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

IndexController

package com.jerry.auth.controller;import com.jerry.common.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.Map;
import java.util.Objects;/*** ClassName: IndexController* Package: com.jerry.auth.controller* Description:** @Author jerry_jy* @Create 2023-03-01 18:15* @Version 1.0*/
@Api(tags = "后台登录管理")
@RestController
@RequestMapping("/admin/system/index")
public class IndexController {/*** login* @return*/@ApiOperation("登录")@PostMapping("/login")public Result login(){// {"code":200,"data":{"token":"admin-token"}}HashMap<String, Object> map = new HashMap<>();map.put("token","admin-token");return Result.ok(map);}/*** info* @return*/@GetMapping("/info")public Result info(){Map<String, Object> map = new HashMap<>();map.put("roles","[admin]");map.put("name","admin");map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");return Result.ok(map);}/*** logout* @return*/@ApiOperation("登出")@PostMapping("/logout")public Result logout(){return Result.ok();}
}

修改前端的跳转地址

云上办公系统项目

修改响应状态码

云上办公系统项目

测试

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

云上办公系统项目

6、前端角色管理

6.1、角色列表

修改路由

重新定义constantRoutes

云上办公系统项目

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

创建角色页面

云上办公系统项目

<template>
<div class="app-container"><!--查询表单--><div class="search-div"><el-form label-width="70px" size="small"><el-row><el-col :span="24"><el-form-item label="角色名称"><el-input style="width: 100%" v-model="searchObj.roleName" placeholder="角色名称"></el-input></el-form-item></el-col></el-row><el-row style="display:flex"><el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="fetchData()">搜索</el-button><el-button icon="el-icon-refresh" size="mini" @click="resetData">重置</el-button></el-row></el-form></div><!-- 表格 --><el-tablev-loading="listLoading":data="list"stripeborderstyle="width: 100%;margin-top: 10px;"@selection-change="handleSelectionChange"><el-table-column type="selection"/><el-table-columnlabel="序号"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// 调用apiapi.getPageList(this.page, this.limit, this.searchObj).then(response => {this.list = response.data.recordsthis.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) {// debuggerthis.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => { // promise// 点击确定,远程调用ajaxreturn 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-tablev-loading="listLoading":data="list"stripeborderstyle="width: 100%;margin-top: 10px;"@selection-change="handleSelectionChange"><el-table-column type="selection"/><el-table-columnlabel="序号"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)})// 调用apireturn 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// 调用apiapi.getPageList(this.page, this.limit, this.searchObj).then(response => {this.list = response.data.recordsthis.total = response.data.total})},// 根据id删除数据removeDataById(id) {// debuggerthis.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => { // promise// 点击确定,远程调用ajaxreturn api.removeById(id)}).then((response) => {// 刷新页面this.fetchData(this.page)// 提示信息this.$message.success(response.message || '删除成功')})}}
}
</script>

页面展示

云上办公系统项目

7、用户管理

7.1、用户管理CRUD

需求分析

云上办公系统项目

代码生成器

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

service-oa

        <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.4.1</version></dependency><dependency><groupId>org.apache.velocity</groupId><artifactId>velocity-engine-core</artifactId><version>2.0</version></dependency>

CodeGet.java

云上办公系统项目

package com.jerry.code;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;public class CodeGet {public static void main(String[] args) {// 1、创建代码生成器AutoGenerator mpg = new AutoGenerator();// 2、全局配置// 全局配置GlobalConfig gc = new GlobalConfig();gc.setOutputDir("E:\\\\CodeLife\\\\IdeaProject\\\\guigu-oa\\\\guigu-oa-parent\\\\service-oa"+"/src/main/java");gc.setServiceName("%sService");	//去掉Service接口的首字母Igc.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 {@Autowiredprivate 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-pickerv-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-tablev-loading="listLoading":data="list"stripeborderstyle="width: 100%;margin-top: 10px;"><el-table-columnlabel="序号"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-switchv-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 = sizethis.fetchData(1)},// 加载banner列表数据fetchData(page = 1) {debuggerthis.page = pageconsole.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.listthis.list = response.data.recordsthis.total = response.data.total// 数据加载并绑定成功this.listLoading = false})},// 重置查询表单resetData() {console.log('重置查询表单')this.searchObj = {}this.createTimes = []this.fetchData()},// 根据id删除数据removeDataById(id) {// debuggerthis.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => { // promise// 点击确定,远程调用ajaxreturn api.removeById(id)}).then((response) => {this.fetchData()this.$message.success(response.message || '删除成功')}).catch(() => {this.$message.info('取消删除')})},// -------------add(){this.dialogVisible = truethis.sysUser = Object.assign({}, defaultForm)},edit(id) {this.dialogVisible = truethis.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 = falsethis.fetchData(this.page)})},// 根据id更新记录updateData() {api.updateById(this.sysUser).then(response => {this.$message.success(response.message || '操作成功')this.dialogVisible = falsethis.fetchData(this.page)})}}}</script>

添加路由

云上办公系统项目

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

定义API接口

云上办公系统项目

import request from '@/utils/request'const api_name = '/admin/system/sysUser'export default {getPageList(page, limit, searchObj) {return request({url: `${api_name}/${page}/${limit}`,method: 'get',params: searchObj // url查询字符串或表单键值对})},getById(id) {return request({url: `${api_name}/get/${id}`,method: 'get'})},save(role) {return request({url: `${api_name}/save`,method: 'post',data: role})},updateById(role) {return request({url: `${api_name}/update`,method: 'put',data: role})},removeById(id) {return request({url: `${api_name}/remove/${id}`,method: 'delete'})},updateStatus(id, status) {return request({url: `${api_name}/updateStatus/${id}/${status}`,method: 'get'})}
}

页面展示

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

7.2、用户管理分配角色

需求分析

  • 一个用户对应多个角色

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

多对多的关系

云上办公系统项目

接口分析

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

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

编写代码

代码是写在service-oa类中的

SysUserRoleMapper

public interface SysUserRoleMapper extends BaseMapper<SysUserRole> {
}

SysUserRoleService

public interface SysUserRoleService extends IService<SysUserRole> {
}

SysUserRoleServiceImpl

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

SysRoleController

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

SysRoleServiceImpl

/*** ClassName: SysRoleServiceImpl* Package: com.jerry.auth.service.impl* Description:** @Author jerry_jy* @Create 2023-03-01 9:13* @Version 1.0*/@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {@Autowiredprivate SysUserRoleService sysUserRoleService;//1 查询所有角色 和 当前用户所属角色@Overridepublic Map<String, Object> findRoleDataByUserId(Long userId) {//1 查询所有角色,返回list集合,返回List<SysRole> allRoleList =baseMapper.selectList(null);//2 根据userid查询 角色用户关系表,查询userid对应所有角色idLambdaQueryWrapper<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 为用户分配角色@Overridepublic 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@Transactionalpublic 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-pickerv-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-tablev-loading="listLoading":data="list"stripeborderstyle="width: 100%;margin-top: 10px;"><el-table-columnlabel="序号"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-switchv-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 = sizethis.fetchData(1)},// 加载banner列表数据fetchData(page = 1) {debuggerthis.page = pageconsole.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.listthis.list = response.data.recordsthis.total = response.data.total// 数据加载并绑定成功this.listLoading = false})},// 重置查询表单resetData() {console.log('重置查询表单')this.searchObj = {}this.createTimes = []this.fetchData()},// 根据id删除数据removeDataById(id) {// debuggerthis.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => { // promise// 点击确定,远程调用ajaxreturn api.removeById(id)}).then((response) => {this.fetchData(this.page)this.$message.success(response.message || '删除成功')}).catch(() => {this.$message.info('取消删除')})},// -------------add(){this.dialogVisible = truethis.sysUser = Object.assign({}, defaultForm)},edit(id) {this.dialogVisible = truethis.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 = falsethis.fetchData(this.page)})},// 根据id更新记录updateData() {api.updateById(this.sysUser).then(response => {this.$message.success(response.message || '操作成功')this.dialogVisible = falsethis.fetchData(this.page)})},showAssignRole (row) {this.sysUser = rowthis.dialogRoleVisible = truethis.getRoles()},getRoles () {roleApi.getRoles(this.sysUser.id).then(response => {const {allRolesList, assginRoleList} = response.datathis.allRoles = allRolesListthis.userRoleIds = assginRoleList.map(item => item.id)this.checkAll = allRolesList.length===assginRoleList.lengththis.isIndeterminate = assginRoleList.length>0 && assginRoleList.length<allRolesList.length})},/*全选勾选状态发生改变的监听*/handleCheckAllChange (value) {// value 当前勾选状态true/false// 如果当前全选, userRoleIds就是所有角色id的数组, 否则是空数组this.userRoleIds = value ? this.allRoles.map(item => item.id) : []// 如果当前不是全选也不全不选时, 指定为falsethis.isIndeterminate = false},/*角色列表选中项发生改变的监听*/handleCheckedChange (value) {const {userRoleIds, allRoles} = thisthis.checkAll = userRoleIds.length === allRoles.length && allRoles.length>0this.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 = falsethis.fetchData(this.page)})},switchStatus(row) {row.status = row.status === 1 ? 0 : 1api.updateStatus(row.id, row.status).then(response => {if (response.code) {this.$message.success(response.message || '操作成功')this.dialogVisible = falsethis.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 {@Overridepublic List<SysMenu> findNodes() {// 1、查询所有 的数据List<SysMenu> sysMenuList = baseMapper.selectList(null);// 2、构建树形结构List<SysMenu> list = MenuHelper.buildTree(sysMenuList);return list;}// 删除菜单@Overridepublic 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 = 0if (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 {@Autowiredprivate 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-switchv-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.dataconsole.log(this.sysMenuList)})},// 根据id删除数据removeDataById(id) {// debuggerthis.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => { // promise// 点击确定,远程调用ajaxreturn api.removeById(id)}).then((response) => {this.fetchData()this.$message({type: 'success',message: '删除成功!'})}).catch(() => {this.$message.info('取消删除')})},// -------------add(row){debuggerthis.typeDisabled = falsethis.dialogTitle = '添加下级节点'this.dialogVisible = truethis.sysMenu = Object.assign({}, defaultForm)this.sysMenu.id = ''if(row) {this.sysMenu.parentId = row.idthis.sysMenu.parentName = row.name//this.sysMenu.component = 'ParentView'if(row.type === 0) {this.sysMenu.type = 1this.typeDisabled = falsethis.type0Disabled = falsethis.type1Disabled = falsethis.type2Disabled = true} else if(row.type === 1) {this.sysMenu.type = 2this.typeDisabled = true}} else {this.dialogTitle = '添加目录节点'this.sysMenu.type = 0this.sysMenu.component = 'Layout'this.sysMenu.parentId = 0this.typeDisabled = true}},edit(row) {debuggerthis.dialogTitle = '修改节点'this.dialogVisible = truethis.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 = falsethis.fetchData(this.page)})},// 根据id更新记录updateData() {api.updateById(this.sysMenu).then(response => {this.$message.success(response.message || '操作成功')this.dialogVisible = falsethis.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-treestyle="margin: 20px 0"ref="tree":data="sysMenuList"node-key="id"show-checkboxdefault-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.idapi.toAssign(roleId).then(result => {const sysMenuList = result.datathis.sysMenuList = sysMenuListconst 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 = trueapi.doAssign(assginMenuVo).then(result => {this.loading = falsethis.$message.success(result.$message || '分配权限成功')this.$router.push('/system/sysRole');})}}};</script>

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

云上办公系统项目

页面展示

云上办公系统项目

9、权限管理(重难点)

9.1、用户登录权限管理

需求分析

云上办公系统项目

引入JWT

  • JWT是JSON Web Token的缩写

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

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

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

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

  • base64url算法编码得到JWT

common-util

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

JwtHwlper

package com.jerry.common.jwt;import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;import java.util.Date;/*** ClassName: JwtHwlper* Package: com.jerry.common* Description:** @Author jerry_jy* @Create 2023-03-02 20:39* @Version 1.0*/
public class JwtHelper {private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;private static String tokenSignKey = "123456";// 根据用户 id 和用户名称, 生成token的字符串public static String createToken(Long userId, String username) {String token = Jwts.builder().setSubject("AUTH-USER").setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)).claim("userId", userId).claim("username", username).signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();return token;}public static Long getUserId(String token) {try {if (StringUtils.isEmpty(token)) return null;Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);Claims claims = claimsJws.getBody();Integer userId = (Integer) claims.get("userId");return userId.longValue();} catch (Exception e) {e.printStackTrace();return null;}}public static String getUsername(String token) {try {if (StringUtils.isEmpty(token)) return "";Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);Claims claims = claimsJws.getBody();return (String) claims.get("username");} catch (Exception e) {e.printStackTrace();return null;}}public static void main(String[] args) {String token = JwtHelper.createToken(1L, "admin");System.out.println(token);String username = JwtHelper.getUsername(token);Long userId = JwtHelper.getUserId(token);System.out.println("username = " + username);System.out.println("userId = " + userId);}}

修改用户登录

先引入MD5工具类

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

修改SysUserControler保存用户的方法

云上办公系统项目

修改IndexController的登录方法

package com.jerry.auth.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jerry.auth.service.SysMenuService;
import com.jerry.auth.service.SysUserService;
import com.jerry.common.config.exception.GuiguException;
import com.jerry.common.jwt.JwtHelper;
import com.jerry.common.result.Result;
import com.jerry.common.utils.MD5;
import com.jerry.model.system.SysUser;
import com.jerry.vo.system.LoginVo;
import com.jerry.vo.system.RouterVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;/*** ClassName: IndexController* Package: com.jerry.auth.controller* Description:** @Author jerry_jy* @Create 2023-03-01 18:15* @Version 1.0*/
@Api(tags = "后台登录管理")
@RestController
@RequestMapping("/admin/system/index")
public class IndexController {@Autowiredprivate SysUserService sysUserService;@Autowiredprivate SysMenuService sysMenuService;/*** login** @return*/@ApiOperation("登录")@PostMapping("/login")public Result login(@RequestBody LoginVo loginVo) {// {"code":200,"data":{"token":"admin-token"}}
//        HashMap<String, Object> map = new HashMap<>();
//        map.put("token","admin-token");
//        return Result.ok(map);// 1、获取用户名和密码// 2、根据用户名查询数据库String username = loginVo.getUsername();LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(SysUser::getUsername, username);SysUser sysUser = sysUserService.getOne(queryWrapper);// 3、用户信息是否存在if (sysUser == null) {throw new GuiguException(201, "用户不存在...");}// 4、判断密码// 取出数据库中的密文密码(MD5)String password_dB = sysUser.getPassword();String password_input = MD5.encrypt(loginVo.getPassword());if (!password_dB.equals(password_input)) {throw new GuiguException(201, "密码错误...");}// 5、判断用户是否被禁用  1  可用    0   禁用if (sysUser.getStatus().intValue() == 0) {throw new GuiguException(201, "用户被禁用...");}// 6、使用jwt根据用户id和用户名称生成token的字符串String token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());// 7、返回Map<String, Object> map = new HashMap<>();map.put("token", token);return Result.ok(map);}/*** info** @return*/@GetMapping("/info")public Result info(HttpServletRequest request) {// 1、从请求头获取用户信息(获取请求头的 token 字符串)String token = request.getHeader("token");// 2、从 token 字符串中获取 用户id 或者 用户名称Long userId = JwtHelper.getUserId(token); //1L;// 3、根据 用户id 查询数据库, 获取用户信息SysUser sysUser = sysUserService.getById(userId);// 4、根据 用户id 获取用户可以操作的菜单列表// 查询数据库动态构建路由结构,进行显示List<RouterVo> routerList = sysMenuService.findUserMenuListByUserId(userId);// 5、根据 用户id 获取用户可以操作的按钮列表List<String> permsList = sysMenuService.findUserPermsByUserId(userId);// 6、返回相应的数据Map<String, Object> map = new HashMap<>();map.put("roles", "[admin]");map.put("name", sysUser.getName());map.put("avatar", "https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");// 返回用户可以操作的菜单map.put("routers", routerList);// 返回用户可以操作的按钮map.put("buttons", permsList);return Result.ok(map);}/*** logout** @return*/@ApiOperation("登出")@PostMapping("/logout")public Result logout() {return Result.ok();}
}

SysMenuService

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

SysMenuServiceImpl

    // 根据 用户id 获取用户可以操作的菜单列表@Overridepublic 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_menusysMenusList = 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 获取用户可以操作的按钮列表@Overridepublic 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_menusysMenusList = 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 {@Autowiredprivate SysUserService sysUserService;@Overridepublic 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

    // 根据用户名查询@Overridepublic 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"));}// 登录认证过程// 获取输入的用户名和密码,调用方法认证@Overridepublic 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);}}// 认证成功调用的方法@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication auth) throws IOException, ServletException {// 获取当前用户CustomUser customUser = (CustomUser) auth.getPrincipal();// 生成tokenString 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));}// 认证失败调用的方法@Overrideprotected 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() {}@Overrideprotected 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 {@Autowiredprivate UserDetailsService userDetailsService; // 装载的是 org.springframework.security.core.userdetails.UserDetailsService;@Autowiredprivate CustomMd5PasswordEncoder customMd5PasswordEncoder;@Bean@Overrideprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}@Overrideprotected 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()));//禁用sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 指定UserDetailService和加密器auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);}/*** 配置哪些请求不拦截* 排除swagger相关请求** @param web* @throws Exception*/@Overridepublic 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<>();// 遍历 authListfor (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: localhostport: 6379database: 0timeout: 1800000password: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)@ResponseBodypublic 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 {@Autowiredprivate RepositoryService repositoryService;// 单个文件的部署@Testpublic 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 {@Autowiredprivate RepositoryService repositoryService;@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;@Autowiredprivate HistoryService historyService;// 单个流程实例挂起@Testpublic 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 + "挂起");}}// 全部流程实例挂起@Testpublic 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*/@Testpublic void startUpProcessAddBusinessKey(){// 启动流程实例,指定业务标识businessKey,也就是请假申请单idProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia","1001");// 输出System.out.println("业务id:"+processInstance.getBusinessKey()); //1001System.out.println("processInstance.getId() = " + processInstance.getId()); // 71f6803b-bb19-11ed-a845-005056c00001}/*** 查询流程定义*/@Testpublic 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());}}/*** 删除流程定义*/@Testpublic void deleteDeployment() {//部署idString deploymentId = "qingjia:1:c493c327-bb02-11ed-8360-005056c00001";
//        //删除流程定义,如果该流程定义已有流程实例启动则删除时出错
//        repositoryService.deleteDeployment(deploymentId);//设置true 级联删除流程定义,即使该流程有流程实例启动也可以删除,设置为false非级别删除方式repositoryService.deleteDeployment(deploymentId, true);}// 查询已经处理的任务@Testpublic 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());}}// 处理当前任务@Testpublic void completeTask(){// 查询负责人需要处理的任务,返回一条Task task = taskService.createTaskQuery().taskAssignee("zhangsan").singleResult();// 完成任务taskService.complete(task.getId());}// 查询个人的代办任务--zhangsan@Testpublic 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());}}// 启动流程实例@Testpublic 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());}// 单个文件的部署@Testpublic 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 {@Autowiredprivate RepositoryService repositoryService;@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;@Autowiredprivate HistoryService historyService;///// 监听器分配任务// 部署流程定义@Testpublic 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-005056c00001System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程02}@Testpublic void startProcessInstance02(){ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban02");System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban02:1:ed150752-bb41-11ed-a6f2-005056c00001System.out.println("processInstance.getId() = " + processInstance.getId()); // 06eca124-bb42-11ed-9bbc-005056c00001}// 查询个人的代办任务--Tim@Testpublic 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-005056c00001System.out.println("任务id:" + task.getId()); // 06f071b8-bb42-11ed-9bbc-005056c00001System.out.println("任务负责人:" + task.getAssignee()); // TimSystem.out.println("任务名称:" + task.getName()); // 经理审批}}///// uel-method// 部署流程定义@Testpublic 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-005056c00001System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程01}@Testpublic void startProcessInstance01(){ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban01");System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban01:1:8c56a740-bb20-11ed-8d65-005056c00001System.out.println("processInstance.getId() = " + processInstance.getId()); // abb9c7c4-bb20-11ed-b608-005056c00001}// 查询个人的代办任务--LiLei@Testpublic 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-005056c00001System.out.println("任务id:" + task.getId()); // abbd4a38-bb20-11ed-b608-005056c00001System.out.println("任务负责人:" + task.getAssignee()); // LiLeiSystem.out.println("任务名称:" + task.getName()); // 经理审批}}///// uel-value// 部署流程定义@Testpublic void deployProcess() {Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban.bpmn20.xml").name("加班申请流程").deploy();System.out.println("deploy.getId() = " + deploy.getId()); // 5c5519ad-bb1d-11ed-b5c8-005056c00001System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程}// 启动流程实例@Testpublic 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-005056c00001System.out.println("processInstance.getId() = " + processInstance.getId()); // 7f720dd9-bb1d-11ed-b6e9-005056c00001}// 查询个人的代办任务--tom@Testpublic 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-005056c00001System.out.println("任务id:" + task.getId()); // 7f759051-bb1d-11ed-b6e9-005056c00001System.out.println("任务负责人:" + task.getAssignee()); // tomSystem.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 {@Autowiredprivate RepositoryService repositoryService;@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;// 1、部署流程定义@Testpublic 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-005056c00001System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程04}// 1.5、启动流程实例@Testpublic 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-005056c00001System.out.println("processInstance.getId() = " + processInstance.getId()); // 428d0c0f-bb49-11ed-83a5-005056c00001}// 2、查询组任务@Testpublic 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、分配组任务@Testpublic void claimTask(){Task task = taskService.createTaskQuery().taskCandidateUser("tom").singleResult();if (task!=null){taskService.claim(task.getId(),"tom");System.out.println("分配任务完成");}}// 4、查询个人的代办任务--tom@Testpublic 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-005056c00001System.out.println("任务id:" + task.getId()); // 7f759051-bb1d-11ed-b6e9-005056c00001System.out.println("任务负责人:" + task.getAssignee()); // tomSystem.out.println("任务名称:" + task.getName()); // 经理审批}}// 5、办理个人任务@Testpublic 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 {@Autowiredprivate RepositoryService repositoryService;//注入RuntimeService@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;@Autowiredprivate HistoryService historyService;//1 部署流程定义@Testpublic void deployProcess() {Deployment deployment = repositoryService.createDeployment().addClasspathResource("process/qingjia003.bpmn20.xml").name("请假申请流程003").deploy();System.out.println(deployment.getId()); // af9242f0-bb4c-11ed-85bf-005056c00001System.out.println(deployment.getName()); // 请假申请流程002}//2 启动流程实例@Testpublic 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-005056c00001System.out.println(processInstance.getId()); // 90d46e2c-bb4d-11ed-9b92-005056c00001}//3 查询个人的代办任务--zhao6@Testpublic 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());}}//完成任务@Testpublic void completeTask() {Task task = taskService.createTaskQuery()
//                .taskAssignee("zhao6")  //要查询的负责人
//                .taskAssignee("xiaocui")  //要查询的负责人
//                .taskAssignee("gousheng")
//                .taskAssignee("wang5").taskAssignee("gouwa").singleResult();//返回一条//完成任务,参数:任务idtaskService.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 {@Autowiredprivate 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 {@Autowiredprivate 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<>();//根据上传地址后续部署流程定义,文件名称为流程定义的默认keymap.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 {@Autowiredprivate 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

    //审批管理列表@Overridepublic 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">SELECTa.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.nameFROM oa_process aLEFT JOIN sys_user d ON a.user_id =d.idLEFT JOIN oa_process_template b ON a.process_template_id = b.idLEFT 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},'%') ora.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 代表已发布// 流程定义部署@Overridepublic 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

    // 流程定义部署@Overridepublic 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