云上办公系统项目
- 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> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>${knife4j.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jwt.version}</version> </dependency> <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> <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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <scope>provided </scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <scope>provided </scope> </dependency> </dependencies></project>
service-oa
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.jerry</groupId> <artifactId>guigu-oa-parent</artifactId> <version>1.0</version> </parent> <artifactId>service-oa</artifactId> <packaging>jar</packaging> <dependencies> <dependency> <groupId>com.jerry</groupId> <artifactId>model</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>com.jerry</groupId> <artifactId>service-util</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>
配置数据源、服务器端口号
application.yml
spring: application: name: service-oa profiles: active: dev
application-dev.yml
server: port: 8800mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/guigu-oa" /> 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 */@SpringBootApplicationpublic 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 */@Mapperpublic 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 */@Servicepublic 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 */@SpringBootTestpublic class TestMpDemo1 { // MyBatisPlus 对 service 层和 dao 层都做了很好的封装,直接调对应的CRUD方法就行 @Autowired private SysRoleMapper sysRoleMapper; @Autowired private SysRoleService sysRoleService; // 使用MP 封装的 service 来操作数据库,查询所有记录 @Test public void getAllByService(){ List<SysRole> list = sysRoleService.list(); list.forEach(System.out::println); } // 使用MP 封装的 mapper查询所有记录 @Test public void getAllByMapper(){ List<SysRole> sysRoles = sysRoleMapper.selectList(null); sysRoles.forEach(System.out::println); } // 添加操作 @Test public void insert(){ SysRole sysRole = new SysRole(); sysRole.setRoleName("角色管理员"); sysRole.setRoleCode("role"); sysRole.setDescription("角色管理员"); int result = sysRoleMapper.insert(sysRole); System.out.println(result); //影响的行数 System.out.println(sysRole.getId()); //id自动回填 } // 修改操作 @Test public void updateById(){ // 根据id查询 SysRole sysRole = sysRoleMapper.selectById(9); // 设置修改值 sysRole.setRoleName("角色管理员1"); // 调用方法实现最终修改 int update = sysRoleMapper.updateById(sysRole); System.out.println("update = " + update); // 受影响的行数 } // 根据id删除 @Test public void deleteById(){ int delete = sysRoleMapper.deleteById(9); System.out.println("delete = " + delete); // 受影响的行数 } // 批量删除 @Test public void deleteBatchByIds(){// int delete = sysRoleMapper.delete(null); // 或者下面这种写法 int delete = sysRoleMapper.deleteBatchIds(Arrays.asList(1, 2)); System.out.println("ids = " + delete); // 受影响的行数 } // 条件查询 @Test public void testQueryWrapper(){// QueryWrapper 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 */@Getterpublic 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 */@Datapublic class Result<T> { private Integer code; // 状态码 private String message; // 返回信息 private T data; // 统一返回的结果数据 /** * 封装返回数据 * @param body * @param resultCodeEnum * @return * @param */ public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) { Result<T> result = new Result<>(); // 封装数据 if (body!=null){ result.setData(body); } // 状态码 result.setCode(resultCodeEnum.getCode()); //返回信息 result.setMessage(resultCodeEnum.getMessage()); return result; } // 构造私有化 外部不能new private Result(){} // 成功 空结果 public static <T>Result<T> ok(){ return build(null,ResultCodeEnum.SUCCESS); } /** * 成功 返回有数据的结果 * @param data * @return * @param */ 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 */ public static <T>Result<T> fail(T data){ return build(data,ResultCodeEnum.FAIL); } public Result<T> message(String msg){ this.setMessage(msg); return this; } public Result<T> code(Integer code){ this.setCode(code); return this; }}
SysRoleController
package com.jerry.auth.controller;import com.jerry.auth.service.SysRoleService;import com.jerry.common.result.Result;import com.jerry.model.system.SysRole;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.List;/** * ClassName: SysRoleController * Package: com.jerry.auth.controller * Description: * * @Author jerry_jy * @Create 2023-03-01 9:38 * @Version 1.0 */@RestController@RequestMapping("/admin/system/sysRole")public class SysRoleController { @Autowired private SysRoleService sysRoleService; // http://localhost:8800/admin/system/sysRole/getAll // 测试查询所有的角色// @GetMapping("/getAll")// private List getAll(){// List 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@EnableSwagger2WebMvcpublic class Knife4jConfig { @Bean public Docket adminApiConfig(){ List<Parameter> pars = new ArrayList<>(); ParameterBuilder tokenPar = new ParameterBuilder(); tokenPar.name("token") .description("用户token") .defaultValue("") .modelRef(new ModelRef("string")) .parameterType("header") .required(false) .build(); pars.add(tokenPar.build()); //添加head参数end Docket adminApi = new Docket(DocumentationType.SWAGGER_2) .groupName("adminApi") .apiInfo(adminApiInfo()) .select() //只显示admin路径下的页面 .apis(RequestHandlerSelectors.basePackage("com.jerry")) .paths(PathSelectors.regex("/admin/.*")) .build() .globalOperationParameters(pars); return adminApi; } private ApiInfo adminApiInfo(){ return new ApiInfoBuilder() .title("后台管理系统-API文档") .description("本文档描述了后台管理系统微服务接口定义") .version("1.0") .contact(new Contact("jerry", "https://jerry-jy.co", "jinyang9248@163.com")) .build(); }}
Controller层添加注解
- 类上加
@Api(tags = "角色管理接口")
- 方法上加
@ApiOperation("查询所有角色")
测试
http://localhost:8800/doc.html
3.3、分页查询所有角色
service-util模块下创建 MybatisPlusConfig
MybatisPlusConfig
package com.jerry.common.config.mp;import com.baomidou.mybatisplus.annotation.DbType;import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;import org.mybatis.spring.annotation.MapperScan;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/** * ClassName: MybatisPlusConfig * Package: com.jerry.common.config.mp * Description: * * @Author jerry_jy * @Create 2023-03-01 11:17 * @Version 1.0 */@Configuration@MapperScan("com.jerry.auth.mapper")public class MybatisPlusConfig { /** * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除) */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } @Bean public ConfigurationCustomizer configurationCustomizer() { return configuration -> configuration.setUseDeprecatedExecutor(false); }}
主启动类上添加包扫描
SysRoleController
/** * 条件分页查询 * * @param page 当前页 * @param pageSize 分页大小 * @param sysRoleQueryVo 条件查询对象 * @return */ @ApiOperation("条件分页查询") @GetMapping("{page}/{pageSize}") private Result page(@PathVariable int page, @PathVariable int pageSize, SysRoleQueryVo sysRoleQueryVo) { // 1、创建 page 对象, 传递分页查询的参数 Page<SysRole> sysRolePage = new Page<>(page, pageSize); // 2、构造分页查询条件, 判断条件是否为空,不为空进行封装 LambdaQueryWrapper<SysRole> lambdaQueryWrapper = new LambdaQueryWrapper<>(); String roleName = sysRoleQueryVo.getRoleName(); if (!StringUtils.isEmpty(roleName)) { // 封装 lambdaQueryWrapper.like(SysRole::getRoleName,roleName); } // 3、调用方法实现分页查询 sysRoleService.page(sysRolePage, lambdaQueryWrapper); return Result.ok(sysRolePage); }
测试
3.4、添加/修改/删除角色
/** * 添加角色 * @param sysRole * @return */ @ApiOperation("添加角色") @PostMapping("/save") public Result save(@RequestBody SysRole sysRole) { // 调用 service 方法 boolean is_success = sysRoleService.save(sysRole); if (is_success) { return Result.ok(); } else { return Result.fail(); } } /** * 根据 id 修改角色 * @param id * @return */ @ApiOperation("根据 id 查询角色") @GetMapping("/get/{id}") public Result get(@PathVariable long id){ SysRole sysRole = sysRoleService.getById(id); return Result.ok(sysRole); } /** * 修改角色 * @param sysRole * @return */ @ApiOperation("修改角色") @PutMapping("/update") public Result update(@RequestBody SysRole sysRole) { // 调用 service 方法 boolean is_success = sysRoleService.updateById(sysRole); if (is_success) { return Result.ok(); } else { return Result.fail(); } } /** * 根据 id 删除 * @param id * @return */ @ApiOperation("根据 id 删除") @DeleteMapping("delete/{id}") public Result deleteById(@PathVariable long id){ boolean is_success = sysRoleService.removeById(id); if (is_success) { return Result.ok(); } else { return Result.fail(); } } /** * 批量删除 * 说明: * Java 中的对象会转化为Json对象 * Java 中的List集合会转化为数组 * @param ids * @return */ @ApiOperation("批量删除") @DeleteMapping("/ids") public Result deleteByIds(@RequestBody List<Long> ids){ boolean is_success = sysRoleService.removeByIds(ids); if (is_success) { return Result.ok(); } else { return Result.fail(); } }
测试
配置日期时间格式
application-dev.yml添加以下内容
jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8
4、统一异常处理
异常处理的思路流程
4.1、全局异常处理
4.2、特定异常处理
4.3、自定义异常处理
service-util 模块下
GlobalExceptionHandler
package com.jerry.common.config.exception;import com.jerry.common.result.Result;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;/** * ClassName: GlobalExceptionHandler * Package: com.jerry.common.config.exception * Description: * * @Author jerry_jy * @Create 2023-03-01 15:48 * @Version 1.0 */@ControllerAdvicepublic class GlobalExceptionHandler { /** * 全局异常处理 执行的方法 * @return */ @ExceptionHandler(Exception.class) @ResponseBody public Result error(Exception e){ e.printStackTrace(); return Result.fail().message("执行全局处理异常..."); } /** * 特定异常处理 * @param e * @return */ @ExceptionHandler(ArithmeticException.class) @ResponseBody public Result error(ArithmeticException e){ e.printStackTrace(); return Result.fail().message("执行特定处理异常..."); } /** * 自定义异常处理 * @param e * @return */ @ExceptionHandler(GuiguException.class) @ResponseBody public Result error(GuiguException e){ e.printStackTrace(); return Result.fail().code(e.getCode()).message(e.getMsg()); }}
GuiguException
package com.jerry.common.config.exception;import com.jerry.common.result.ResultCodeEnum;import lombok.Data;/** * ClassName: GuiguException * Package: com.jerry.common.config.exception * Description: * * @Author jerry_jy * @Create 2023-03-01 15:59 * @Version 1.0 */@Datapublic class GuiguException extends RuntimeException { private Integer code; private String msg; /** * 通过状态码和错误消息创建异常对象 * @param code * @param msg */ public GuiguException(Integer code, String msg) { super(msg); this.code = code; this.msg = msg; } /** * 接收枚举类型对象 * @param resultCodeEnum */ public GuiguException(ResultCodeEnum resultCodeEnum) { super(resultCodeEnum.getMessage()); this.code = resultCodeEnum.getCode(); this.msg = resultCodeEnum.getMessage(); } @Override public String toString() { return "GuiguException{" + "code=" + code + ", msg='" + msg + '\'' + '}'; }}
5、前端环境搭建
安装脚手架工程
前端用的脚手架工程是:vue-element-admin
https://panjiachen.github.io/vue-element-admin-site/#/
# clone the projectgit clone https://github.com/PanJiaChen/vue-element-admin.git# install dependencynpm install# developnpm 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' }, } ] },
创建角色页面
搜索 重置 {{ (page - 1) * limit + scope.$index + 1 }} import api from '@/api/system/sysRole'export default { // 定义数据模型 // 定义数据模型 data() { return { list: [], // 列表 total: 0, // 总记录数 page: 1, // 页码 limit: 2, // 每页记录数 searchObj: {}, // 查询条件 multipleSelection: []// 批量删除选中的记录列表 } }, // 页面渲染成功后获取数据 created() { this.fetchData() }, // 定义方法 methods: { fetchData(current=1) { this.page = current // 调用api api.getPageList(this.page, this.limit, this.searchObj).then(response => { this.list = response.data.records this.total = response.data.total }) }, }}
定义角色管理相关的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 }) }}
测试
重新启动前端工程
6.2、角色删除
sysRole.js
/** * 角色删除 * @param {*} id * @returns */ removeById(id) { return request({ url: `${api_name}/delete/${id}`, method: 'delete' }) }
list.vue
// 根据id删除数据 removeDataById(id) { // debugger this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // promise // 点击确定,远程调用ajax return api.removeById(id) }).then((response) => { // 刷新页面 this.fetchData(this.page) // 提示信息 this.$message.success(response.message || '删除成功') }) }
6.3、角色添加
6.4、角色修改与数据回显
6.5、批量删除
前端CRUD完整代码
注意点:
前端中的url
请求路径要和后端的@DeleteMapping
,@PutMapping
,@PostMapping
,@GetMapping
路径一致
sysRole.js
/*角色管理相关的API请求函数*/import request from '@/utils/request'const api_name = '/admin/system/sysRole'export default { /** * 获取角色分页列表(带搜索) * @param {*} page * @param {*} limit * @param {*} searchObj * @returns */ getPageList(page, limit, searchObj) { return request({ url: `${api_name}/${page}/${limit}`, method: 'get', // 如果是普通对象参数写法,params:对象参数名 // 如果是使用json格式传递,data:对象参数名 params: searchObj }) }, /** * 角色删除 * @param {*} id * @returns */ removeById(id) { return request({ url: `${api_name}/delete/${id}`, method: 'delete' }) }, /** * 角色添加 * @param {*} role * @returns */ save(role) { return request({ url: `${api_name}/save`, method: 'post', data: role }) }, // 回显要修改的id信息 getById(id) { return request({ url: `${api_name}/get/${id}`, method: 'get' }) }, // 修改 updateById(role) { return request({ url: `${api_name}/update`, method: 'put', data: role }) }, // 批量删除 batchRemove(idList) { return request({ url: `${api_name}/ids`, method: `delete`, data: idList }) }}
list.vue
搜索 重置 {{ (page - 1) * limit + scope.$index + 1 }} 添 加 批量删除 7、用户管理
7.1、用户管理CRUD
需求分析
代码生成器
- 可以采用MyBatisPlus提供的代码生成器直接生成
mapper
,service
,impl
,controller
, - 手动创建的话,也行
service-oa
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.0</version> </dependency>
CodeGet.java
package com.jerry.code;import com.baomidou.mybatisplus.annotation.DbType;import com.baomidou.mybatisplus.generator.AutoGenerator;import com.baomidou.mybatisplus.generator.config.DataSourceConfig;import com.baomidou.mybatisplus.generator.config.GlobalConfig;import com.baomidou.mybatisplus.generator.config.PackageConfig;import com.baomidou.mybatisplus.generator.config.StrategyConfig;import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;public class CodeGet { public static void main(String[] args) { // 1、创建代码生成器 AutoGenerator mpg = new AutoGenerator(); // 2、全局配置 // 全局配置 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir("E:\\CodeLife\\IdeaProject\\guigu-oa\\guigu-oa-parent\\service-oa"+"/src/main/java"); gc.setServiceName("%sService");//去掉Service接口的首字母I gc.setAuthor("jerry"); gc.setOpen(false); mpg.setGlobalConfig(gc); // 3、数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/guigu-oa" /> 整合前端
前端页面 list.vue
搜索 重置 添 加 {{ (page - 1) * limit + scope.$index + 1 }} {{ item.roleName }} , total, slot" @current-change="fetchData" @size-change="changeSize" /> import api from '@/api/system/sysUser' const defaultForm = { id: '', username: '', password: '', name: '', phone: '', status: 1 } export default { data() { return { listLoading: true, // 数据是否正在加载 list: null, // banner列表 total: 0, // 数据库中的总记录数 page: 1, // 默认页码 limit: 5, // 每页记录数 searchObj: {}, // 查询表单对象 createTimes: [], dialogVisible: false, sysUser: defaultForm, saveBtnDisabled: false, } }, // 生命周期函数:内存准备完毕,页面尚未渲染 created() { console.log('list created......') this.fetchData() }, // 生命周期函数:内存准备完毕,页面渲染成功 mounted() { console.log('list mounted......') }, methods: { // 当页码发生改变的时候 changeSize(size) { console.log(size) this.limit = size this.fetchData(1) }, // 加载banner列表数据 fetchData(page = 1) { debugger this.page = page console.log('翻页。。。' + this.page) if(this.createTimes && this.createTimes.length ==2) { this.searchObj.createTimeBegin = this.createTimes[0] this.searchObj.createTimeEnd = this.createTimes[1] } api.getPageList(this.page, this.limit, this.searchObj).then( response => { //this.list = response.data.list this.list = response.data.records this.total = response.data.total // 数据加载并绑定成功 this.listLoading = false } ) }, // 重置查询表单 resetData() { console.log('重置查询表单') this.searchObj = {} this.createTimes = [] this.fetchData() }, // 根据id删除数据 removeDataById(id) { // debugger this.$confirm('此操作将永久删除该记录, 是否继续" /> { 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
@Servicepublic 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 */@Servicepublic class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService { @Autowired private SysUserRoleService sysUserRoleService; //1 查询所有角色 和 当前用户所属角色 @Override public Map<String, Object> findRoleDataByUserId(Long userId) { //1 查询所有角色,返回list集合,返回 List<SysRole> allRoleList = baseMapper.selectList(null); //2 根据userid查询 角色用户关系表,查询userid对应所有角色id LambdaQueryWrapper<SysUserRole> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUserRole::getUserId,userId); List<SysUserRole> existUserRoleList = sysUserRoleService.list(wrapper); //从查询出来的用户id对应角色list集合,获取所有角色id// List list = new ArrayList();// for (SysUserRole sysUserRole:existUserRoleList) {// Long roleId = sysUserRole.getRoleId();// list.add(roleId);// } List<Long> existRoleIdList = existUserRoleList.stream().map(c -> c.getRoleId()).collect(Collectors.toList()); //3 根据查询所有角色id,找到对应角色信息 //根据角色id到所有的角色的list集合进行比较 List<SysRole> assignRoleList = new ArrayList<>(); for(SysRole sysRole : allRoleList) { //比较 if(existRoleIdList.contains(sysRole.getId())) { assignRoleList.add(sysRole); } } //4 把得到两部分数据封装map集合,返回 Map<String, Object> roleMap = new HashMap<>(); roleMap.put("assginRoleList", assignRoleList); roleMap.put("allRolesList", allRoleList); return roleMap; } //2 为用户分配角色 @Override public void doAssign(AssginRoleVo assginRoleVo) { //把用户之前分配角色数据删除,用户角色关系表里面,根据userid删除 LambdaQueryWrapper<SysUserRole> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUserRole::getUserId,assginRoleVo.getUserId()); sysUserRoleService.remove(wrapper); //重新进行分配 List<Long> roleIdList = assginRoleVo.getRoleIdList(); for(Long roleId:roleIdList) { if(StringUtils.isEmpty(roleId)) { continue; } SysUserRole sysUserRole = new SysUserRole(); sysUserRole.setUserId(assginRoleVo.getUserId()); sysUserRole.setRoleId(roleId); sysUserRoleService.save(sysUserRole); } }}
前端展示
7.3、修改用户状态
需求分析
用户状态:状态(1:正常 0:停用),当用户状态为正常时,可以访问后台系统,当用户状态停用后,不可以登录后台系统
编写代码
SysRoleController
@ApiOperation(value = "更新状态") @GetMapping("/updateStatus/{id}/{status}") public Result updateStatus(@PathVariable Long id, @PathVariable Integer status){ sysUserService.updateStatus(id, status); return Result.ok(); }
SysUserService
public interface SysUserService extends IService<SysUser> { // 更新状态 void updateStatus(Long id, Integer status);}
SysUserServiceImpl
@Service@Slf4jpublic class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { // 更新状态 @Override @Transactional public void updateStatus(Long id, Integer status) { // 根据用户 userid 查询用户对象 SysUser sysUser = baseMapper.selectById(id); // 设置修改状态 if (status == 0 || status == 1) { sysUser.setStatus(status); } else { log.info("数值不合法"); } // 调用方法进行修改 baseMapper.updateById(sysUser); }}
整合前端
定义前端路由
src/api/system/sysUser.js
updateStatus(id, status) { return request({ url: `${api_name}/updateStatus/${id}/${status}`, method: 'get' })}
src/api/system/sysRole.js
getRoles(adminId) { return request({ url: `${api_name}/toAssign/${adminId}`, method: 'get' })},assignRoles(assginRoleVo) { return request({ url: `${api_name}/doAssign`, method: 'post', data: assginRoleVo })}
修改前端页面
list.vue
搜索 重置 添 加 {{ (page - 1) * limit + scope.$index + 1 }} {{ item.roleName }} , total, slot" @current-change="fetchData" @size-change="changeSize" /> 全选 {{role.roleName}} 保存 取消 import api from '@/api/system/sysUser' import roleApi from '@/api/system/sysRole' const defaultForm = { id: '', username: '', password: '', name: '', phone: '', status: 1 } export default { data() { return { listLoading: true, // 数据是否正在加载 list: null, // banner列表 total: 0, // 数据库中的总记录数 page: 1, // 默认页码 limit: 10, // 每页记录数 searchObj: {}, // 查询表单对象 createTimes: [], dialogVisible: false, sysUser: defaultForm, saveBtnDisabled: false, dialogRoleVisible: false, allRoles: [], // 所有角色列表 userRoleIds: [], // 用户的角色ID的列表 isIndeterminate: false, // 是否是不确定的 checkAll: false // 是否全选 } }, // 生命周期函数:内存准备完毕,页面尚未渲染 created() { console.log('list created......') this.fetchData() roleApi.findAll().then(response => { this.roleList = response.data; }) }, // 生命周期函数:内存准备完毕,页面渲染成功 mounted() { console.log('list mounted......') }, methods: { // 当页码发生改变的时候 changeSize(size) { console.log(size) this.limit = size this.fetchData(1) }, // 加载banner列表数据 fetchData(page = 1) { debugger this.page = page console.log('翻页。。。' + this.page) if(this.createTimes && this.createTimes.length ==2) { this.searchObj.createTimeBegin = this.createTimes[0] this.searchObj.createTimeEnd = this.createTimes[1] } api.getPageList(this.page, this.limit, this.searchObj).then( response => { //this.list = response.data.list this.list = response.data.records this.total = response.data.total // 数据加载并绑定成功 this.listLoading = false } ) }, // 重置查询表单 resetData() { console.log('重置查询表单') this.searchObj = {} this.createTimes = [] this.fetchData() }, // 根据id删除数据 removeDataById(id) { // debugger this.$confirm('此操作将永久删除该记录, 是否继续" /> 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
@Servicepublic class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService { @Override public List<SysMenu> findNodes() { // 1、查询所有 的数据 List<SysMenu> sysMenuList = baseMapper.selectList(null); // 2、构建树形结构 List<SysMenu> list = MenuHelper.buildTree(sysMenuList); return list; } // 删除菜单 @Override public void removeMenuById(Long id) { // 判断当前菜单是否有下一层菜单 LambdaQueryWrapper<SysMenu> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(SysMenu::getParentId,id); Integer count = baseMapper.selectCount(lambdaQueryWrapper); if (count>0){ throw new GuiguException(201,"菜单不能删除"); } baseMapper.deleteById(id); }}
MenuHelper
package com.jerry.auth.util;import com.jerry.model.system.SysMenu;import java.util.ArrayList;import java.util.List;/** * ClassName: MenuHelper * Package: com.jerry.auth.util * Description: * * @Author jerry_jy * @Create 2023-03-02 17:14 * @Version 1.0 */public class MenuHelper { /** * 使用递归方法建菜单 * @param sysMenuList * @return */ public static List<SysMenu> buildTree(List<SysMenu> sysMenuList) { // 存放最终数据 List<SysMenu> trees = new ArrayList<>(); // 把所有的菜单数据进行遍历 for (SysMenu sysMenu : sysMenuList) { // 递归入口 parentId = 0 if (sysMenu.getParentId().longValue()==0){ trees.add(getChildren(sysMenu,sysMenuList)); } } return trees; } /** * 递归查找子节点 * @param sysMenu * @param sysMenuList * @return */ public static SysMenu getChildren(SysMenu sysMenu,List<SysMenu> sysMenuList){ sysMenu.setChildren(new ArrayList<SysMenu>()); // 遍历所有的菜单数据,判断id和parent_id的对应关系 for (SysMenu menu : sysMenuList) { if (sysMenu.getId().longValue() == menu.getParentId().longValue()){ if (sysMenu.getChildren() == null) { sysMenu.setChildren(new ArrayList<>()); } sysMenu.getChildren().add(getChildren(menu,sysMenuList)); } } return sysMenu; }}
SysRoleMenuServiceImpl
@Servicepublic 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;/** * * 菜单表 前端控制器 *
* * @author jerry * @since 2023-03-02 */@Api(tags = "菜单管理接口")@RestController@RequestMapping("/admin/system/sysMenu")public class SysMenuController { @Autowired private SysMenuService sysMenuService; @ApiOperation(value = "菜单列表") @GetMapping("/findNodes") public Result findNodes() { List<SysMenu> list = sysMenuService.findNodes(); return Result.ok(list); } @ApiOperation(value = "新增菜单") @PostMapping("save") public Result save(@RequestBody SysMenu sysMenu) { sysMenuService.save(sysMenu); return Result.ok(); } @ApiOperation(value = "修改菜单") @PutMapping("update") public Result updateById(@RequestBody SysMenu sysMenu) { sysMenuService.updateById(sysMenu); return Result.ok(); } @ApiOperation(value = "删除菜单") @DeleteMapping("remove/{id}") public Result remove(@PathVariable Long id) { sysMenuService.removeMenuById(id); return Result.ok(); }}
接口测试
整合前端
{ name: 'sysMenu', path: 'sysMenu', component: () => import('@/views/system/sysMenu/list'), meta: { title: '菜单管理', icon: 'el-icon-s-unfold' }, }
sysMenu.js
import request from '@/utils/request'/*菜单管理相关的API请求函数*/const api_name = '/admin/system/sysMenu'export default { /* 获取权限(菜单/功能)列表 */ findNodes() { return request({ url: `${api_name}/findNodes`, method: 'get' }) }, /* 删除一个权限项 */ removeById(id) { return request({ url: `${api_name}/remove/${id}`, method: "delete" }) }, /* 保存一个权限项 */ save(sysMenu) { return request({ url: `${api_name}/save`, method: "post", data: sysMenu }) }, /* 更新一个权限项 */ updateById(sysMenu) { return request({ url: `${api_name}/update`, method: "put", data: sysMenu }) }}
list.vue
添 加 0"/> 目录 菜单 按钮 {{ item.class }} 路由地址 组件路径 权限字符 正常 停用 import api from '@/api/system/sysMenu' const defaultForm = { id: '', parentId: '', name: '', type: 0, path: '', component: '', perms: '', icon: '', sortValue: 1, status: 1 } export default { // 定义数据 data() { return { sysMenuList: [], expandKeys: [], // 需要自动展开的项 typeDisabled: false, type0Disabled: false, type1Disabled: false, type2Disabled: false, dialogTitle: '', dialogVisible: false, sysMenu: defaultForm, saveBtnDisabled: false, iconList: [ { class: "el-icon-s-tools", }, { class: "el-icon-s-custom", }, { class: "el-icon-setting", }, { class: "el-icon-user-solid", }, { class: "el-icon-s-help", }, { class: "el-icon-phone", }, { class: "el-icon-s-unfold", }, { class: "el-icon-s-operation", }, { class: "el-icon-more-outline", }, { class: "el-icon-s-check", }, { class: "el-icon-tickets", }, { class: "el-icon-s-goods", }, { class: "el-icon-document-remove", }, { class: "el-icon-warning", }, { class: "el-icon-warning-outline", }, { class: "el-icon-question", }, { class: "el-icon-info", } ] } }, // 当页面加载时获取数据 created() { this.fetchData() }, methods: { // 调用api层获取数据库中的数据 fetchData() { console.log('加载列表') api.findNodes().then(response => { this.sysMenuList = response.data console.log(this.sysMenuList) }) }, // 根据id删除数据 removeDataById(id) { // debugger this.$confirm('此操作将永久删除该记录, 是否继续" /> 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" /> /*查看某个角色的权限列表*/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
授权角色:{{ $route.query.roleName }} 保存 返回 import api from '@/api/system/sysMenu' export default { name: 'roleAuth', data() { return { loading: false, // 用来标识是否正在保存请求中的标识, 防止重复提交 sysMenuList: [], // 所有 defaultProps: { children: 'children', label: 'name' }, }; }, created() { this.fetchData() }, methods: { /* 初始化 */ fetchData() { const roleId = this.$route.query.id api.toAssign(roleId).then(result => { const sysMenuList = result.data this.sysMenuList = sysMenuList const checkedIds = this.getCheckedIds(sysMenuList) console.log('getPermissions() checkedIds', checkedIds) this.$refs.tree.setCheckedKeys(checkedIds) }) }, /* 得到所有选中的id列表 */ getCheckedIds (auths, initArr = []) { return auths.reduce((pre, item) => { if (item.select && item.children.length === 0) { pre.push(item.id) } else if (item.children) { this.getCheckedIds(item.children, initArr) } return pre }, initArr) }, /* 保存权限列表 */ save() { // debugger //获取到当前子节点 //const checkedNodes = this.$refs.tree.getCheckedNodes() //获取到当前子节点及父节点 const allCheckedNodes = this.$refs.tree.getCheckedNodes(false, true); let idList = allCheckedNodes.map(node => node.id); console.log(idList) let assginMenuVo = { roleId: this.$route.query.id, menuIdList: idList } this.loading = true api.doAssign(assginMenuVo).then(result => { this.loading = false this.$message.success(result.$message || '分配权限成功') this.$router.push('/system/sysRole'); }) } } };
关闭Vue语法校验,避免报错
页面展示
9、权限管理(重难点)
9.1、用户登录权限管理
需求分析
引入JWT
JWT是JSON Web Token的缩写
一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上
官网:https://jwt.io/
最重要的作用就是对 token信息的防伪作用。
由三个部分组成:JWT头、有效载荷、签名哈希
base64url算法编码得到JWT
common-util
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency>
JwtHwlper
package com.jerry.common.jwt;import io.jsonwebtoken.*;import org.springframework.util.StringUtils;import java.util.Date;/** * ClassName: JwtHwlper * Package: com.jerry.common * Description: * * @Author jerry_jy * @Create 2023-03-02 20:39 * @Version 1.0 */public class JwtHelper { private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000; private static String tokenSignKey = "123456"; // 根据用户 id 和用户名称, 生成token的字符串 public static String createToken(Long userId, String username) { String token = Jwts.builder() .setSubject("AUTH-USER") .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) .claim("userId", userId) .claim("username", username) .signWith(SignatureAlgorithm.HS512, tokenSignKey) .compressWith(CompressionCodecs.GZIP) .compact(); return token; } public static Long getUserId(String token) { try { if (StringUtils.isEmpty(token)) return null; Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); Claims claims = claimsJws.getBody(); Integer userId = (Integer) claims.get("userId"); return userId.longValue(); } catch (Exception e) { e.printStackTrace(); return null; } } public static String getUsername(String token) { try { if (StringUtils.isEmpty(token)) return ""; Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); Claims claims = claimsJws.getBody(); return (String) claims.get("username"); } catch (Exception e) { e.printStackTrace(); return null; } } public static void main(String[] args) { String token = JwtHelper.createToken(1L, "admin"); System.out.println(token); String username = JwtHelper.getUsername(token); Long userId = JwtHelper.getUserId(token); System.out.println("username = " + username); System.out.println("userId = " + userId); }}
修改用户登录
先引入MD5工具类
package com.jerry.common.utils;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public final class MD5 { public static String encrypt(String strSrc) { try { char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; byte[] bytes = strSrc.getBytes(); MessageDigest md = MessageDigest.getInstance("MD5"); md.update(bytes); bytes = md.digest(); int j = bytes.length; char[] chars = new char[j * 2]; int k = 0; for (int i = 0; i < bytes.length; i++) { byte b = bytes[i]; chars[k++] = hexChars[b >>> 4 & 0xf]; chars[k++] = hexChars[b & 0xf]; } return new String(chars); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new RuntimeException("MD5加密出错!!+" + e); } } public static void main(String[] args) { System.out.println(MD5.encrypt("111111")); }}
修改SysUserControler保存用户的方法
修改IndexController的登录方法
package com.jerry.auth.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.jerry.auth.service.SysMenuService;import com.jerry.auth.service.SysUserService;import com.jerry.common.config.exception.GuiguException;import com.jerry.common.jwt.JwtHelper;import com.jerry.common.result.Result;import com.jerry.common.utils.MD5;import com.jerry.model.system.SysUser;import com.jerry.vo.system.LoginVo;import com.jerry.vo.system.RouterVo;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Objects;/** * ClassName: IndexController * Package: com.jerry.auth.controller * Description: * * @Author jerry_jy * @Create 2023-03-01 18:15 * @Version 1.0 */@Api(tags = "后台登录管理")@RestController@RequestMapping("/admin/system/index")public class IndexController { @Autowired private SysUserService sysUserService; @Autowired private SysMenuService sysMenuService; /** * login * * @return */ @ApiOperation("登录") @PostMapping("/login") public Result login(@RequestBody LoginVo loginVo) { // {"code":200,"data":{"token":"admin-token"}}// HashMap map = new HashMap();// map.put("token","admin-token");// return Result.ok(map); // 1、获取用户名和密码 // 2、根据用户名查询数据库 String username = loginVo.getUsername(); LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysUser::getUsername, username); SysUser sysUser = sysUserService.getOne(queryWrapper); // 3、用户信息是否存在 if (sysUser == null) { throw new GuiguException(201, "用户不存在..."); } // 4、判断密码 // 取出数据库中的密文密码(MD5) String password_dB = sysUser.getPassword(); String password_input = MD5.encrypt(loginVo.getPassword()); if (!password_dB.equals(password_input)) { throw new GuiguException(201, "密码错误..."); } // 5、判断用户是否被禁用 1 可用 0 禁用 if (sysUser.getStatus().intValue() == 0) { throw new GuiguException(201, "用户被禁用..."); } // 6、使用jwt根据用户id和用户名称生成token的字符串 String token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername()); // 7、返回 Map<String, Object> map = new HashMap<>(); map.put("token", token); return Result.ok(map); } /** * info * * @return */ @GetMapping("/info") public Result info(HttpServletRequest request) { // 1、从请求头获取用户信息(获取请求头的 token 字符串) String token = request.getHeader("token"); // 2、从 token 字符串中获取 用户id 或者 用户名称 Long userId = JwtHelper.getUserId(token); //1L; // 3、根据 用户id 查询数据库, 获取用户信息 SysUser sysUser = sysUserService.getById(userId); // 4、根据 用户id 获取用户可以操作的菜单列表 // 查询数据库动态构建路由结构,进行显示 List<RouterVo> routerList = sysMenuService.findUserMenuListByUserId(userId); // 5、根据 用户id 获取用户可以操作的按钮列表 List<String> permsList = sysMenuService.findUserPermsByUserId(userId); // 6、返回相应的数据 Map<String, Object> map = new HashMap<>(); map.put("roles", "[admin]"); map.put("name", sysUser.getName()); map.put("avatar", "https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg"); // 返回用户可以操作的菜单 map.put("routers", routerList); // 返回用户可以操作的按钮 map.put("buttons", permsList); return Result.ok(map); } /** * logout * * @return */ @ApiOperation("登出") @PostMapping("/logout") public Result logout() { return Result.ok(); }}
SysMenuService
// 根据 用户id 获取用户可以操作的菜单列表 List<RouterVo> findUserMenuListByUserId(Long userId); // 根据 用户id 获取用户可以操作的按钮列表 List<String> findUserPermsByUserId(Long userId);
SysMenuServiceImpl
// 根据 用户id 获取用户可以操作的菜单列表 @Override public List<RouterVo> findUserMenuListByUserId(Long userId) { List<SysMenu> sysMenusList = null; // 1、判断当前用户是否是管理员 userId=1 是管理员 // 1.1、 如果是管理员,查询所有菜单列表 if (userId.longValue() == 1) { // 查询所有菜单列表 LambdaQueryWrapper<SysMenu> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysMenu::getStatus, 1); queryWrapper.orderByAsc(SysMenu::getSortValue); sysMenusList = baseMapper.selectList(queryWrapper); } else { // 1.2、如果不是管理员,根据 userId 查询可以操作菜单列表 // 多表关联查询:sys_role、sys_role_mexnu、sys_menu sysMenusList = baseMapper.findMenuListByUserId(userId); } // 2、把查询出来的数据列表, 构建成框架要求的路由结构 // 先构建树形结构 List<SysMenu> sysMenuTreeList = MenuHelper.buildTree(sysMenusList); // 构建框架要求的路由结构 List<RouterVo> routerList = this.buildRouter(sysMenuTreeList); return routerList; } // 构建框架要求的路由结构 private List<RouterVo> buildRouter(List<SysMenu> menus) { // 创建 list 集合,存值最终数据 List<RouterVo> routers = new ArrayList<>(); // menus 遍历 for (SysMenu menu : menus) { RouterVo router = new RouterVo(); router.setHidden(false); router.setAlwaysShow(false); router.setPath(getRouterPath(menu)); router.setComponent(menu.getComponent()); router.setMeta(new MetaVo(menu.getName(), menu.getIcon())); // 下一层数据 List<SysMenu> children = menu.getChildren(); if (menu.getType().intValue() == 1) { // 加载隐藏路由 List<SysMenu> hiddenMenuList = children.stream().filter(item -> !StringUtils.isEmpty(item.getComponent())).collect(Collectors.toList()); for (SysMenu hiddenMenu : hiddenMenuList) { RouterVo hiddenRouter = new RouterVo(); hiddenRouter.setHidden(true); hiddenRouter.setAlwaysShow(false); hiddenRouter.setPath(getRouterPath(hiddenMenu)); hiddenRouter.setComponent(hiddenMenu.getComponent()); hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon())); routers.add(hiddenRouter); } }else { if (!CollectionUtils.isEmpty(children)) { if(children.size() > 0) { router.setAlwaysShow(true); } // 递归 router.setChildren(buildRouter(children)); } } routers.add(router); } return routers; } /** * 获取路由地址 * * @param menu 菜单信息 * @return 路由地址 */ public String getRouterPath(SysMenu menu) { String routerPath = "/" + menu.getPath(); if (menu.getParentId().intValue() != 0) { routerPath = menu.getPath(); } return routerPath; } // 根据 用户id 获取用户可以操作的按钮列表 @Override public List<String> findUserPermsByUserId(Long userId) { // 1、判断是否是管理员,如果是管理员,查询所有按钮列表 List<SysMenu> sysMenusList = null; if (userId.longValue() == 1) { // 查询所有菜单列表 LambdaQueryWrapper<SysMenu> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysMenu::getStatus, 1); sysMenusList = baseMapper.selectList(queryWrapper); }else { // 2、如果不是管理员,根据userId查询可以操作按钮列表 // 多表关联查询:sys_role、sys_role_menu、sys_menu sysMenusList = baseMapper.findMenuListByUserId(userId); } // 3、从查询出来的数据里面,获取可以操作按钮值的List集合,返回 List<String> permsList = sysMenusList.stream() .filter(item -> item.getType() == 2) .map(item -> item.getPerms()) .collect(Collectors.toList()); return permsList; }
接口测试
登录接口测试
info接口测试
我这里没有报错,如果出现以下的报错信息
解决思路是:
1、在pom.xml添加
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.yml</include> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.yml</include> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> </build>
2、application-dev.yml添加
mybatis-plus: mapper-locations: classpath:com/atguigu/auth/mapper/xml/*.xml
整合前端
从这部分开始,整合前端不在写了,比较麻烦,直接复用现有的
页面展示
给李四分配没有添加的权限
9.2、用户认证
整合SpringSecurity
本项目采用 Spring-Security
来做用户认证和权限控制,也可以采用 Shiro
新建一个spring-security
的module
引入依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.jerry</groupId> <artifactId>common</artifactId> <version>1.0</version> </parent> <artifactId>spring-security</artifactId> <dependencies> <dependency> <groupId>com.jerry</groupId> <artifactId>common-util</artifactId> <version>1.0</version> </dependency> <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-security
module
自定义加密器PasswordEncoder
@Componentpublic 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<" /> 9.3、用户权限控制
流程分析
修改代码
修改UserDetailsServiceImpl中的loadUserByUsername
// 根据 user_id 查询用户操作权限数据 List<String> userPermsList = sysMenuService.findUserPermsByUserId(sysUser.getId()); // 创建list集合,封装最终权限数据 List<SimpleGrantedAuthority> authList = new ArrayList<>(); // 遍历 authList for (String perms : userPermsList) { authList.add(new SimpleGrantedAuthority(perms.trim())); } return new CustomUser(sysUser, authList);
spring-security模块配置redis
添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
修改TokenLoginFilter
修改TokenAuthenticationFilter
修改WebSecurityConfig类
配置类添加注解:
开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
service-oa模块添加redis配置
application-dev.yml配文件
spring: redis: host: localhost port: 6379 database: 0 timeout: 1800000 password: jedis: pool: max-active: 20 #最大连接数 max-wait: -1 #最大阻塞等待时间(负数表示没限制) max-idle: 5 #最大空闲 min-idle: 0 #最小空闲
控制controller层接口权限
Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限
@PreAuthorize("hasAuthority('bnt.sysRole.list')")
@PreAuthorize("hasAuthority('bnt.sysRole.add')")
@PreAuthorize("hasAuthority('bnt.sysRole.list')")
@PreAuthorize("hasAuthority('bnt.sysRole.update')")
@PreAuthorize("hasAuthority('bnt.sysRole.remove')")
异常处理
在service-util模块引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <scope>provided</scope> </dependency>
AccessDeniedException需要引入依赖,Spring Security对应的异常
/** * spring security异常 * @param e * @return */ @ExceptionHandler(AccessDeniedException.class) @ResponseBody public Result error(AccessDeniedException e) throws AccessDeniedException { return Result.build(null, ResultCodeEnum.PERMISSION); }
测试
10、Activiti
10.1、Activiti流程操作
配置Activiti
引入Activiti依赖
在service-oa
中
<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
<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
* Package: com.jerry.activiti
* Description: * * @Author: jerry_jy * @Create: 2023-03-05 10:51 * @Version: 1.0 */@SpringBootTestpublic class ProcessTest { @Autowired private RepositoryService repositoryService; // 单个文件的部署 @Test public void deployProcess() { Deployment deploy = repositoryService.createDeployment() .addClasspathResource("process/qingjia.bpmn20.xml") .addClasspathResource("process/qingjia.png") .name("请假申请流程") .deploy(); System.out.println("deploy.getId() = " + deploy.getId()); System.out.println("deploy.getName() = " + deploy.getName()); }}
流程实例
package com.jerry.auth.activiti;import org.activiti.engine.HistoryService;import org.activiti.engine.RepositoryService;import org.activiti.engine.RuntimeService;import org.activiti.engine.TaskService;import org.activiti.engine.history.HistoricTaskInstance;import org.activiti.engine.repository.Deployment;import org.activiti.engine.repository.ProcessDefinition;import org.activiti.engine.runtime.ProcessInstance;import org.activiti.engine.task.Task;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.List;/** * ClassName: ProcessTest
* Package: com.jerry.activiti
* Description: * * @Author: jerry_jy * @Create: 2023-03-05 10:51 * @Version: 1.0 */@SpringBootTestpublic class ProcessTest1 { @Autowired private RepositoryService repositoryService; @Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; @Autowired private HistoryService historyService; // 单个流程实例挂起 @Test public void SingleSuspendProcessInstance() { String processInstanceId = "71f6803b-bb19-11ed-a845-005056c00001"; ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); //获取到当前流程定义是否为暂停状态 suspended方法为true代表为暂停 false就是运行的 boolean suspended = processInstance.isSuspended(); if (suspended) { runtimeService.activateProcessInstanceById(processInstanceId); System.out.println("流程实例:" + processInstanceId + "激活"); } else { runtimeService.suspendProcessInstanceById(processInstanceId); System.out.println("流程实例:" + processInstanceId + "挂起"); } } // 全部流程实例挂起 @Test public void suspendProcessInstance() { // 1、获取流程定义对象 ProcessDefinition qingjia = repositoryService.createProcessDefinitionQuery().processDefinitionKey("qingjia").singleResult(); // 2、调用流程定义对象的方法判断当前状态:挂起 激活 boolean suspended = qingjia.isSuspended(); if (suspended) { // 暂定,那就可以激活 // 参数1:流程定义的id 参数2:是否激活 参数3:时间点 repositoryService.activateProcessDefinitionById(qingjia.getId(), true, null); System.out.println("流程定义:" + qingjia.getId() + "激活"); } else { repositoryService.suspendProcessDefinitionById(qingjia.getId(), true, null); System.out.println("流程定义:" + qingjia.getId() + "挂起"); } } /** * 启动流程实例,添加businessKey */ @Test public void startUpProcessAddBusinessKey(){ // 启动流程实例,指定业务标识businessKey,也就是请假申请单id ProcessInstance processInstance = runtimeService. startProcessInstanceByKey("qingjia","1001"); // 输出 System.out.println("业务id:"+processInstance.getBusinessKey()); //1001 System.out.println("processInstance.getId() = " + processInstance.getId()); // 71f6803b-bb19-11ed-a845-005056c00001 } /** * 查询流程定义 */ @Test public void findProcessDefinitionList(){ List<ProcessDefinition> definitionList = repositoryService.createProcessDefinitionQuery() .orderByProcessDefinitionVersion() .desc() .list(); //输出流程定义信息 for (ProcessDefinition processDefinition : definitionList) { System.out.println("流程定义token operator">+processDefinition.getId()); System.out.println("流程定义 name="+processDefinition.getName()); System.out.println("流程定义 key="+processDefinition.getKey()); System.out.println("流程定义 Version="+processDefinition.getVersion()); System.out.println("流程部署ID ="+processDefinition.getDeploymentId()); } } /** * 删除流程定义 */ @Test public void deleteDeployment() { //部署id String deploymentId = "qingjia:1:c493c327-bb02-11ed-8360-005056c00001";// //删除流程定义,如果该流程定义已有流程实例启动则删除时出错// repositoryService.deleteDeployment(deploymentId); //设置true 级联删除流程定义,即使该流程有流程实例启动也可以删除,设置为false非级别删除方式 repositoryService.deleteDeployment(deploymentId, true); } // 查询已经处理的任务 @Test public void findCompleteTaskList(){ List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery() .taskAssignee("zhangsan") .finished().list(); for (HistoricTaskInstance historicTaskInstance : list) { System.out.println("流程实例id:" + historicTaskInstance.getProcessInstanceId()); System.out.println("任务id:" + historicTaskInstance.getId()); System.out.println("任务负责人:" + historicTaskInstance.getAssignee()); System.out.println("任务名称:" + historicTaskInstance.getName()); } } // 处理当前任务 @Test public void completeTask(){ // 查询负责人需要处理的任务,返回一条 Task task = taskService.createTaskQuery().taskAssignee("zhangsan").singleResult(); // 完成任务 taskService.complete(task.getId()); } // 查询个人的代办任务--zhangsan @Test public void findTaskList(){ String assign = "zhangsan"; List<Task> list = taskService.createTaskQuery() .taskAssignee(assign).list(); for (Task task : list) { System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); System.out.println("任务id:" + task.getId()); System.out.println("任务负责人:" + task.getAssignee()); System.out.println("任务名称:" + task.getName()); } } // 启动流程实例 @Test public void startProcess(){ ProcessInstance processInstance = runtimeService.startProcessInstanceById("qingjia"); System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); System.out.println("processInstance.getId() = " + processInstance.getId()); System.out.println("processInstance.getActivityId() = " + processInstance.getActivityId()); } // 单个文件的部署 @Test public void deployProcess() { Deployment deploy = repositoryService.createDeployment() .addClasspathResource("process/qingjia.bpmn20.xml") .addClasspathResource("process/qingjia.png") .name("请假申请流程") .deploy(); System.out.println("deploy.getId() = " + deploy.getId()); System.out.println("deploy.getName() = " + deploy.getName()); }}
任务分配
package com.jerry.auth.activiti;import org.activiti.engine.HistoryService;import org.activiti.engine.RepositoryService;import org.activiti.engine.RuntimeService;import org.activiti.engine.TaskService;import org.activiti.engine.repository.Deployment;import org.activiti.engine.runtime.ProcessInstance;import org.activiti.engine.task.Task;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.HashMap;import java.util.List;import java.util.Map;/** * ClassName: ProcessTest2
* Package: com.jerry.auth.activiti
* Description: * * @Author: jerry_jy * @Create: 2023-03-05 14:05 * @Version: 1.0 */@SpringBootTestpublic class ProcessTest2 { @Autowired private RepositoryService repositoryService; @Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; @Autowired private HistoryService historyService; /// // 监听器分配任务 // 部署流程定义 @Test public void deployProcess02() { Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban02.bpmn20.xml").name("加班申请流程02").deploy(); System.out.println("deploy.getId() = " + deploy.getId()); // ed080f00-bb41-11ed-a6f2-005056c00001 System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程02 } @Test public void startProcessInstance02(){ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban02"); System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban02:1:ed150752-bb41-11ed-a6f2-005056c00001 System.out.println("processInstance.getId() = " + processInstance.getId()); // 06eca124-bb42-11ed-9bbc-005056c00001 } // 查询个人的代办任务--Tim @Test public void findTaskList02(){ String assign = "Tim"; List<Task> list = taskService.createTaskQuery() .taskAssignee(assign).list(); for (Task task : list) { System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); // 06eca124-bb42-11ed-9bbc-005056c00001 System.out.println("任务id:" + task.getId()); // 06f071b8-bb42-11ed-9bbc-005056c00001 System.out.println("任务负责人:" + task.getAssignee()); // Tim System.out.println("任务名称:" + task.getName()); // 经理审批 } } /// // uel-method // 部署流程定义 @Test public void deployProcess01() { Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban01.bpmn20.xml").name("加班申请流程01").deploy(); System.out.println("deploy.getId() = " + deploy.getId()); // 8c4ac05e-bb20-11ed-8d65-005056c00001 System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程01 } @Test public void startProcessInstance01(){ ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban01"); System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban01:1:8c56a740-bb20-11ed-8d65-005056c00001 System.out.println("processInstance.getId() = " + processInstance.getId()); // abb9c7c4-bb20-11ed-b608-005056c00001 } // 查询个人的代办任务--LiLei @Test public void findTaskList01(){ String assign = "LiLei"; List<Task> list = taskService.createTaskQuery() .taskAssignee(assign).list(); for (Task task : list) { System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); // abb9c7c4-bb20-11ed-b608-005056c00001 System.out.println("任务id:" + task.getId()); // abbd4a38-bb20-11ed-b608-005056c00001 System.out.println("任务负责人:" + task.getAssignee()); // LiLei System.out.println("任务名称:" + task.getName()); // 经理审批 } } /// // uel-value // 部署流程定义 @Test public void deployProcess() { Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban.bpmn20.xml").name("加班申请流程").deploy(); System.out.println("deploy.getId() = " + deploy.getId()); // 5c5519ad-bb1d-11ed-b5c8-005056c00001 System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程 } // 启动流程实例 @Test public void startProcessInstance() { Map<String, Object> map = new HashMap<>(); // 设置任务人 map.put("assignee1","tom"); map.put("assignee2","jerry"); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban", map); System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban:1:5c60d97f-bb1d-11ed-b5c8-005056c00001 System.out.println("processInstance.getId() = " + processInstance.getId()); // 7f720dd9-bb1d-11ed-b6e9-005056c00001 } // 查询个人的代办任务--tom @Test public void findTaskList(){ String assign = "tom"; List<Task> list = taskService.createTaskQuery() .taskAssignee(assign).list(); for (Task task : list) { System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); //7f720dd9-bb1d-11ed-b6e9-005056c00001 System.out.println("任务id:" + task.getId()); // 7f759051-bb1d-11ed-b6e9-005056c00001 System.out.println("任务负责人:" + task.getAssignee()); // tom System.out.println("任务名称:" + task.getName()); // 经理审批 } }}
配置监听器
package com.jerry.auth.activiti;import org.springframework.stereotype.Component;/** * ClassName: UserBean
* Package: com.jerry.auth.activiti
* Description: * * @Author: jerry_jy * @Create: 2023-03-05 14:25 * @Version: 1.0 */@Componentpublic 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
* Package: com.jerry.auth.activiti
* Description: * * @Author: jerry_jy * @Create: 2023-03-05 19:18 * @Version: 1.0 */@SpringBootTestpublic class ProcessTest3 { @Autowired private RepositoryService repositoryService; @Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; // 1、部署流程定义 @Test public void deployProcess() { Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban04.bpmn20.xml").name("加班申请流程04").deploy(); System.out.println("deploy.getId() = " + deploy.getId()); // f204be8a-bb48-11ed-950e-005056c00001 System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程04 } // 1.5、启动流程实例 @Test public void startProcessInstance() {// Map map = new HashMap(); // 设置任务人// map.put("assignee1","tom");// map.put("assignee2","jerry"); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban04"); System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban04:1:f210f38c-bb48-11ed-950e-005056c00001 System.out.println("processInstance.getId() = " + processInstance.getId()); // 428d0c0f-bb49-11ed-83a5-005056c00001 } // 2、查询组任务 @Test public void findGroupTaskList(){ List<Task> list = taskService.createTaskQuery() .taskCandidateUser("tom") .list(); for (Task task : list) { System.out.println("----------------------------"); System.out.println("流程实例id:" + task.getProcessInstanceId()); System.out.println("任务id:" + task.getId()); System.out.println("任务负责人:" + task.getAssignee()); System.out.println("任务名称:" + task.getName()); } } // 3、分配组任务 @Test public void claimTask(){ Task task = taskService.createTaskQuery() .taskCandidateUser("tom") .singleResult(); if (task!=null){ taskService.claim(task.getId(),"tom"); System.out.println("分配任务完成"); } } // 4、查询个人的代办任务--tom @Test public void findTaskList(){ String assign = "tom"; List<Task> list = taskService.createTaskQuery() .taskAssignee(assign).list(); for (Task task : list) { System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); //7f720dd9-bb1d-11ed-b6e9-005056c00001 System.out.println("任务id:" + task.getId()); // 7f759051-bb1d-11ed-b6e9-005056c00001 System.out.println("任务负责人:" + task.getAssignee()); // tom System.out.println("任务名称:" + task.getName()); // 经理审批 } } // 5、办理个人任务 @Test public void completeGroupTask() { Task task = taskService.createTaskQuery() .taskAssignee("tom") //要查询的负责人 .singleResult();//返回一条 taskService.complete(task.getId()); }}
10.3、网关
网关用来控制流程的流向,通常会和流程变量一起使用。
排他网关
- 排他网关:只有一条路径会被选择
当你的流程出现这样的场景:请假申请,两天以内,部门经理审批流程就结束了,两天以上需要总经理直接审批,这个时候就需要排他网关
并行网关
- 并(平)行网关:所有路径会被同时选择
当出现这样的场景:请假申请开始,需要部门经理和总经理都审批,两者没有前后需要两个人全部审批才能进入下个节点人事审批。这个时候就需要并行网关
与排他网关的主要区别是,并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。
包含网关
包容网关:可以同时执行多条线路,也可以在网关上设置条件,可以看做是排他网关和并行网关的结合体。
当出现这样的场景:请假申请大于等于2天需要由部门总经理审批,小于2天由部门经理审批,请假申请必须经过人事经理审批。这个时候就需要包含网关
package com.jerry.auth.activiti;import org.activiti.engine.HistoryService;import org.activiti.engine.RepositoryService;import org.activiti.engine.RuntimeService;import org.activiti.engine.TaskService;import org.activiti.engine.repository.Deployment;import org.activiti.engine.runtime.ProcessInstance;import org.activiti.engine.task.Task;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.HashMap;import java.util.List;import java.util.Map;/** * ClassName: ProcessTestGateway
* Package: com.jerry.auth.activiti
* Description: * * @Author: jerry_jy * @Create: 2023-03-05 19:52 * @Version: 1.0 */@SpringBootTestpublic class ProcessTestGateway { @Autowired private RepositoryService repositoryService; //注入RuntimeService @Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; @Autowired private HistoryService historyService; //1 部署流程定义 @Test public void deployProcess() { Deployment deployment = repositoryService.createDeployment() .addClasspathResource("process/qingjia003.bpmn20.xml") .name("请假申请流程003") .deploy(); System.out.println(deployment.getId()); // af9242f0-bb4c-11ed-85bf-005056c00001 System.out.println(deployment.getName()); // 请假申请流程002 } //2 启动流程实例 @Test public void startProcessInstance() { Map<String, Object> map = new HashMap<>(); //设置请假天数 map.put("day", "3"); ProcessInstance processInstance =// runtimeService.startProcessInstanceByKey("qingjia002", map); runtimeService.startProcessInstanceByKey("qingjia003"); System.out.println(processInstance.getProcessDefinitionId()); // qingjia002:1:afac0c82-bb4c-11ed-85bf-005056c00001 System.out.println(processInstance.getId()); // 90d46e2c-bb4d-11ed-9b92-005056c00001 } //3 查询个人的代办任务--zhao6 @Test public void findTaskList() {// String assign = "zhao6";// String assign = "gousheng";// String assign = "xiaocui";// String assign = "wang5";// String assign = "gouwa"; String assign = "xiaoli"; List<Task> list = taskService.createTaskQuery() .taskAssignee(assign).list(); for (Task task : list) { System.out.println("流程实例id:" + task.getProcessInstanceId()); System.out.println("任务id:" + task.getId()); System.out.println("任务负责人:" + task.getAssignee()); System.out.println("任务名称:" + task.getName()); } } //完成任务 @Test public void completeTask() { Task task = taskService.createTaskQuery()// .taskAssignee("zhao6") //要查询的负责人// .taskAssignee("xiaocui") //要查询的负责人// .taskAssignee("gousheng")// .taskAssignee("wang5") .taskAssignee("gouwa") .singleResult();//返回一条 //完成任务,参数:任务id taskService.complete(task.getId()); }}
11、审批管理
11.1、审批设置–CRUD
package com.jerry.process.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.jerry.common.result.Result;import com.jerry.model.process.ProcessType;import com.jerry.process.service.OaProcessTypeService;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.web.bind.annotation.*;/** * * 审批类型 前端控制器 *
* * @author jerry * @since 2023-03-05 */@Api(value = "审批类型", tags = "审批类型")@RestController@RequestMapping(value = "/admin/process/processType")public class OaProcessTypeController { @Autowired private OaProcessTypeService processTypeService; @ApiOperation(value = "获取分页列表") @GetMapping("{page}/{pageSize}") public Result index(@PathVariable Long page, @PathVariable Long pageSize) { Page<ProcessType> pageInfo = new Page<>(page, pageSize); Page<ProcessType> pageModel = processTypeService.page(pageInfo); return Result.ok(pageModel); } @PreAuthorize("hasAuthority('bnt.processType.list')") @ApiOperation(value = "获取") @GetMapping("get/{id}") public Result get(@PathVariable Long id) { ProcessType processType = processTypeService.getById(id); return Result.ok(processType); } @PreAuthorize("hasAuthority('bnt.processType.add')") @ApiOperation(value = "新增") @PostMapping("save") public Result save(@RequestBody ProcessType processType) { processTypeService.save(processType); return Result.ok(); } @PreAuthorize("hasAuthority('bnt.processType.update')") @ApiOperation(value = "修改") @PutMapping("update") public Result updateById(@RequestBody ProcessType processType) { processTypeService.updateById(processType); return Result.ok(); } @ApiOperation(value = "删除") @DeleteMapping("remove/{id}") public Result remove(@PathVariable Long id) { processTypeService.removeById(id); return Result.ok(); }}
11.2、模板审批–CRUD
package com.jerry.process.controller;import com.baomidou.mybatisplus.core.metadata.IPage;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.jerry.common.result.Result;import com.jerry.model.process.ProcessTemplate;import com.jerry.process.service.OaProcessTemplateService;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;/** * * 审批模板 前端控制器 *
* * @author jerry * @since 2023-03-05 */@Api(value = "审批模板管理", tags = "审批模板管理")@RestController@RequestMapping(value = "/admin/process/processTemplate")public class OaProcessTemplateController { @Autowired private OaProcessTemplateService processTemplateService; // 分页查询审批模板 @ApiOperation("获取分页查询审批模板数据") @GetMapping("{page}/{pageSize}") public Result index(@PathVariable Long page, @PathVariable Long pageSize){ Page<ProcessTemplate> pageInfo = new Page<>(page, pageSize); //分页查询审批模板,把审批类型对应名称查询 IPage<ProcessTemplate> pageModel = processTemplateService.selectPageProcessTemplate(pageInfo); return Result.ok(pageModel); } //@PreAuthorize("hasAuthority('bnt.processTemplate.list')") @ApiOperation(value = "获取") @GetMapping("get/{id}") public Result get(@PathVariable Long id) { ProcessTemplate processTemplate = processTemplateService.getById(id); return Result.ok(processTemplate); } //@PreAuthorize("hasAuthority('bnt.processTemplate.templateSet')") @ApiOperation(value = "新增") @PostMapping("save") public Result save(@RequestBody ProcessTemplate processTemplate) { processTemplateService.save(processTemplate); return Result.ok(); } //@PreAuthorize("hasAuthority('bnt.processTemplate.templateSet')") @ApiOperation(value = "修改") @PutMapping("update") public Result updateById(@RequestBody ProcessTemplate processTemplate) { processTemplateService.updateById(processTemplate); return Result.ok(); } //@PreAuthorize("hasAuthority('bnt.processTemplate.remove')") @ApiOperation(value = "删除") @DeleteMapping("remove/{id}") public Result remove(@PathVariable Long id) { processTemplateService.removeById(id); return Result.ok(); }}
11.3、添加审批模板
OaProcessTypeController
@ApiOperation(value = "获取全部审批分类") @GetMapping("findAll") public Result findAll() { return Result.ok(processTypeService.list()); }
OaProcessTemplateController
@ApiOperation(value = "上传流程定义") @PostMapping("/uploadProcessDefinition") public Result uploadProcessDefinition(MultipartFile file) throws FileNotFoundException { // 获取classes目录位置 String path = new File(ResourceUtils.getURL("classpath:").getPath()).getAbsolutePath(); // 设置上传文件夹 File tempFile = new File(path + "/processes/"); if (!tempFile.exists()) { tempFile.mkdirs(); } // 创建空文件,实现文件写入 String filename = file.getOriginalFilename(); File zipFile = new File(path + "/processes/" + filename); // 保存文件 try { file.transferTo(zipFile); } catch (IOException e) { return Result.fail(); } Map<String, Object> map = new HashMap<>(); //根据上传地址后续部署流程定义,文件名称为流程定义的默认key map.put("processDefinitionPath", "processes/" + filename); map.put("processDefinitionKey", filename.substring(0, filename.lastIndexOf("."))); return Result.ok(map); } public static void main(String[] args) { try { String path = new File(ResourceUtils.getURL("classpath:").getPath()).getAbsolutePath(); System.out.println("path = " + path); //E:\CodeLife\IdeaProject\guigu-oa\guigu-oa-parent\service-oa\target\classes } catch (FileNotFoundException e) { throw new RuntimeException(e); } }
11.4、查看审批模板
整合前端,无后台接口
11.5、审批列表
分页查询
OaProcessController
package com.jerry.process.controller;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.baomidou.mybatisplus.core.metadata.IPage;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.jerry.common.result.Result;import com.jerry.model.process.Process;import com.jerry.process.service.OaProcessService;import com.jerry.vo.process.ProcessQueryVo;import com.jerry.vo.process.ProcessVo;import io.swagger.annotations.ApiOperation;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/** * * 审批类型 前端控制器 *
* * @author jerry * @since 2023-03-06 */@RestController@RequestMapping(value = "/admin/process")public class OaProcessController { @Autowired private OaProcessService processService; //审批管理列表 @ApiOperation(value = "获取分页列表") @GetMapping("{page}/{limit}") public Result index(@PathVariable Long page, @PathVariable Long limit, ProcessQueryVo processQueryVo) { Page<ProcessVo> pageInfo = new Page<>(page, limit); IPage<ProcessVo> pageModel = processService.selectPage(pageInfo,processQueryVo); return Result.ok(); }}
OaProcessService
public interface OaProcessService extends IService<Process> { //审批管理列表 IPage<ProcessVo> selectPage(Page<ProcessVo> pageInfo, ProcessQueryVo processQueryVo);}
OaProcessServiceImpl
//审批管理列表 @Override public IPage<ProcessVo> selectPage(Page<ProcessVo> pageInfo, ProcessQueryVo processQueryVo) { IPage<ProcessVo> pageModel = baseMapper.selectPage(pageInfo,processQueryVo); return pageModel; }
OaProcessMapper
//审批管理列表 IPage selectPage(Page pageInfo, @Param("vo") ProcessQueryVo processQueryVo);
涉及到4张表的多表查询,自己编写SQL语句
OaProcessMapper.xml
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.jerry.process.mapper.OaProcessMapper"> <select id="selectPage" resultType="com.jerry.vo.process.ProcessVo"> SELECT a.id,a.process_code,a.user_id,a.process_template_id,a.process_type_id,a.title,a.description,a.form_values,a.process_instance_id,a.current_auditor,a.status,a.create_time,a.update_time, b.name AS processTemplateName, c.name AS processTypeName, d.name FROM oa_process a LEFT JOIN sys_user d ON a.user_id =d.id LEFT JOIN oa_process_template b ON a.process_template_id = b.id LEFT JOIN oa_process_type c ON a.process_type_id = c.id <where> <if test="vo.keyword != null and vo.keyword != ''"> and (a.process_code like CONCAT('%',#{vo.keyword},'%') or a.title like CONCAT('%',#{vo.keyword},'%')) </if> <if test="vo.userId != null and vo.userId != ''"> and a.user_id = #{vo.userId} </if> <if test="vo.status != null and vo.status != ''"> and a.status = #{vo.status} </if> <if test="vo.createTimeBegin != null and vo.createTimeBegin != ''"> and a.create_time >= #{vo.createTimeBegin} </if> <if test="vo.createTimeEnd != null and vo.createTimeEnd != ''"> and a.create_time <span class="token entity named-entity" title="<
= #{vo.createTimeEnd} </if> </where> </select></mapper>
修改mapper的映射路径
页面展示
部署流程定义
OaProcessTemplateServiceImpl
// 修改模板的发布状态 status==1 代表已发布 // 流程定义部署 @Override public void publish(Long id) { // 修改模板的发布状态 status==1 代表已发布 ProcessTemplate processTemplate = baseMapper.selectById(id); processTemplate.setStatus(1); baseMapper.updateById(processTemplate); // 流程定义部署 if (StringUtils.isEmpty(processTemplate.getProcessDefinitionPath())){ processService.deployByZip(processTemplate.getProcessDefinitionPath()); } }}
OaProcessService
// 流程定义部署 void deployByZip(String deployPath);
OaProcessServiceImpl
// 流程定义部署 @Override public void deployByZip(String deployPath) { InputStream inputStream= this.getClass().getClassLoader().getResourceAsStream(deployPath); ZipInputStream zipInputStream = new ZipInputStream(inputStream); // 部署 Deployment deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).deploy(); System.out.println("deployment.getId() = " + deployment.getId()); System.out.println("deployment.getName() = " + deployment.getName()); }
12、前端审批
12.1、OA审批
node -vv 16.16.0
报错
npm ERR! path F:\guigu-oa\guigu-oa-web\node_modules\node-sassnpm ERR! command failednpm ERR! command C:\WINDOWS\system32\cmd.exe /d /s /c node scripts/build.jsnpm 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 oknpm 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.0npm ERR! gyp info using node@16.16.0 | win32 | x64npm 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
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('此操作将永久删除该记录, 是否继续" />