申明: 未经许可,禁止以任何形式转载,若要引用,请标注链接地址。 全文共计13077字,阅读大概需要30分钟
更多学习内容, 欢迎关注我
个人公众号:不懂开发的程序猿
个人网站:https://jerry-jy.co/
【警告】本篇博客较长,若引起阅读不适,建议收藏,稍后再读
云上办公系统是一套自动办公系统,系统主要包含:管理端和员工端
管理端包含:权限管理、审批管理、公众号菜单管理
员工端采用微信公众号操作,包含:办公审批、微信授权登录、消息推送等功能
项目服务器端架构:SpringBoot + MyBatisPlus + SpringSecurity + Redis + Activiti+ MySQL
前端架构:vue-admin-template + Node.js + Npm + Vue + ElementUI + Axios
基础框架:SpringBoot |
---|
数据缓存:Redis |
数据库:MySQL |
权限控制:SpringSecurity |
工作流引擎:Activiti |
前端技术:vue-admin-template + Node.js + Npm + Vue + ElementUI + Axios |
微信公众号:公众号菜单 + 微信授权登录 + 消息推送 |
工具 | 版本 |
---|---|
后台 | 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 |
登录页
【系统管理】–【用户管理】
【系统管理】–【角色管理】
【系统管理】–【菜单管理】
【审批设置】–【审批类型】
【审批设置】–【审批模板】
【审批管理】–【审批列表】
【公众号菜单】–【菜单列表】
正常的前台页面是在微信公众号上,我这里没有整合
http://localhost:9090/#/
审批页面
测试号的页面展示
我认为该项目对我来说主要的帮助有:
1、项目是前后端分离的,符合目前主流业务开发逻辑,作为后端程序员,复习前端Vue + ElementUI框架, 巩固练习使用前端的脚手架工程,学习使用前后端联调开发过程
2、项目中引入JWT加密token,用作用户登录身份校验,用 SpringSecurity 来做权限控制,涉及多表查询,是项目的重难点学习对象,也是对前面学习SpringSecurity的一个巩固
3、前端使用微信公众号来作为前端接入口,以前没有开发过,也是亮点。
4、引入 工作流引擎:Activiti 作为组件,第一次用,学习下
5、集成Swagger,方便进行接口API的统一测试
db.sql
sql语句太多了,见文末的资料
本项目采用Maven聚合模块来管理工程
4.0.0 org.springframework.boot spring-boot-starter-parent 2.3.6.RELEASE com.jerry guigu-oa-parent 1.0 pom common model service-oa 1.8 3.4.1 8.0.27 3.0.3 0.9.1 2.0.21 com.baomidou mybatis-plus-boot-starter ${mybatis-plus.version} mysql mysql-connector-java ${mysql.version} com.github.xiaoymin knife4j-spring-boot-starter ${knife4j.version} io.jsonwebtoken jjwt ${jwt.version} com.alibaba fastjson ${fastjson.version} org.apache.maven.plugins maven-compiler-plugin 3.1 1.8 1.8
4.0.0 com.jerry guigu-oa-parent 1.0 common pom common-util service-util
4.0.0 com.jerry common 1.0 common-util jar org.springframework.boot spring-boot-starter-web provided io.jsonwebtoken jjwt org.projectlombok lombok com.alibaba fastjson
4.0.0 com.jerry common 1.0 service-util com.jerry common-util 1.0 org.springframework.boot spring-boot-starter-web com.baomidou mybatis-plus-boot-starter mysql mysql-connector-java
4.0.0 com.jerry guigu-oa-parent 1.0 model org.projectlombok lombok com.github.xiaoymin knife4j-spring-boot-starter provided com.baomidou mybatis-plus-boot-starter provided
4.0.0 com.jerry guigu-oa-parent 1.0 service-oa jar com.jerry model 1.0 com.jerry service-util 1.0 org.springframework.boot spring-boot-starter-test test ${project.artifactId} org.springframework.boot spring-boot-maven-plugin
spring:application:name: service-oaprofiles:active: dev
server:port: 8800
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志
spring:datasource:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/guigu-oa?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8username: rootpassword: root
package com.jerry.auth;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** ClassName: ServiceAuthApplication* Package: com.jerry.auth* Description:** @Author jerry_jy* @Create 2023-02-28 22:03* @Version 1.0*/@SpringBootApplication
public class ServiceAuthApplication {public static void main(String[] args) {SpringApplication.run(ServiceAuthApplication.class, args);}
}
package com.jerry.auth.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jerry.model.system.SysRole;
import org.apache.ibatis.annotations.Mapper;/*** ClassName: SysRoleMapper* Package: com.jerry.auth.mapper* Description:** @Author jerry_jy* @Create 2023-02-28 22:05* @Version 1.0*/@Mapper
public interface SysRoleMapper extends BaseMapper {
}
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 {
}
package com.jerry.auth.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jerry.auth.mapper.SysRoleMapper;
import com.jerry.auth.service.SysRoleService;
import com.jerry.model.system.SysRole;
import org.springframework.stereotype.Service;/*** ClassName: SysRoleServiceImpl* Package: com.jerry.auth.service.impl* Description:** @Author jerry_jy* @Create 2023-03-01 9:13* @Version 1.0*/@Service
public class SysRoleServiceImpl extends ServiceImpl implements SysRoleService {
}
目的是:
package com.jerry.auth;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jerry.auth.mapper.SysRoleMapper;
import com.jerry.auth.service.SysRoleService;
import com.jerry.model.system.SysRole;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.Arrays;
import java.util.List;/*** ClassName: TestMpDemo1* Package: com.jerry.auth* Description:** @Author jerry_jy* @Create 2023-02-28 22:07* @Version 1.0*/@SpringBootTest
public class TestMpDemo1 {// MyBatisPlus 对 service 层和 dao 层都做了很好的封装,直接调对应的CRUD方法就行@Autowiredprivate SysRoleMapper sysRoleMapper;@Autowiredprivate SysRoleService sysRoleService;// 使用MP 封装的 service 来操作数据库,查询所有记录@Testpublic void getAllByService(){List list = sysRoleService.list();list.forEach(System.out::println);}// 使用MP 封装的 mapper查询所有记录@Testpublic void getAllByMapper(){List sysRoles = sysRoleMapper.selectList(null);sysRoles.forEach(System.out::println);}// 添加操作@Testpublic void insert(){SysRole sysRole = new SysRole();sysRole.setRoleName("角色管理员");sysRole.setRoleCode("role");sysRole.setDescription("角色管理员");int result = sysRoleMapper.insert(sysRole);System.out.println(result); //影响的行数System.out.println(sysRole.getId()); //id自动回填}// 修改操作@Testpublic void updateById(){// 根据id查询SysRole sysRole = sysRoleMapper.selectById(9);// 设置修改值sysRole.setRoleName("角色管理员1");// 调用方法实现最终修改int update = sysRoleMapper.updateById(sysRole);System.out.println("update = " + update); // 受影响的行数}// 根据id删除@Testpublic void deleteById(){int delete = sysRoleMapper.deleteById(9);System.out.println("delete = " + delete); // 受影响的行数}// 批量删除@Testpublic void deleteBatchByIds(){// int delete = sysRoleMapper.delete(null);// 或者下面这种写法int delete = sysRoleMapper.deleteBatchIds(Arrays.asList(1, 2));System.out.println("ids = " + delete); // 受影响的行数}// 条件查询@Testpublic void testQueryWrapper(){// QueryWrapper queryWrapper = new QueryWrapper<>();
// queryWrapper.eq("role_name", "系统管理员");// 或者下面这种写法LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(SysRole::getRoleName,"系统管理员");List list = sysRoleMapper.selectList(queryWrapper);list.forEach(System.out::println);}
}
目的:定义好统一的返回状态码和返回结果信息
package com.jerry.common.result;import lombok.Getter;/*** ClassName: ResultCodeEnum* Package: com.jerry.common.result* Description:** @Author jerry_jy* @Create 2023-03-01 9:50* @Version 1.0*/@Getter
public enum ResultCodeEnum {SUCCESS(200, "成功"),FAIL(201, "失败"),SERVICE_ERROR(2012, "服务异常"),DATA_ERROR(204, "数据异常"),LOGIN_AUTH(208, "未登陆"),PERMISSION(209, "没有权限");private Integer code;private String message;private ResultCodeEnum(Integer code, String message) {this.code = code;this.message = message;}
}
package com.jerry.common.result;import lombok.Data;/*** ClassName: Result* Package: com.jerry.common.result* Description:** @Author jerry_jy* @Create 2023-03-01 9:52* @Version 1.0*/@Data
public class Result {private Integer code; // 状态码private String message; // 返回信息private T data; // 统一返回的结果数据/*** 封装返回数据* @param body* @param resultCodeEnum* @return* @param */public static Result build(T body, ResultCodeEnum resultCodeEnum) {Result result = new Result<>();// 封装数据if (body!=null){result.setData(body);}// 状态码result.setCode(resultCodeEnum.getCode());//返回信息result.setMessage(resultCodeEnum.getMessage());return result;}// 构造私有化 外部不能newprivate Result(){}// 成功 空结果public static Result ok(){return build(null,ResultCodeEnum.SUCCESS);}/*** 成功 返回有数据的结果* @param data* @return* @param */public static Result ok(T data){return build(data,ResultCodeEnum.SUCCESS);}// 失败public static Result fail(){return build(null,ResultCodeEnum.FAIL);}/*** 失败 返回有数据的结果* @param data* @return* @param */public static Result fail(T data){return build(data,ResultCodeEnum.FAIL);}public Result message(String msg){this.setMessage(msg);return this;}public Result code(Integer code){this.setCode(code);return this;}
}
package com.jerry.auth.controller;import com.jerry.auth.service.SysRoleService;
import com.jerry.common.result.Result;
import com.jerry.model.system.SysRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** ClassName: SysRoleController* Package: com.jerry.auth.controller* Description:** @Author jerry_jy* @Create 2023-03-01 9:38* @Version 1.0*/@RestController
@RequestMapping("/admin/system/sysRole")
public class SysRoleController {@Autowiredprivate SysRoleService sysRoleService;// http://localhost:8800/admin/system/sysRole/getAll// 测试查询所有的角色
// @GetMapping("/getAll")
// private List getAll(){
// List list = sysRoleService.list();
// return list;
// }/*** 统一返回数据结果* @return*/@GetMapping("/getAll")private Result getAll(){List list = sysRoleService.list();return Result.ok(list);}}
http://localhost:8800/admin/system/sysRole/getAll
文档地址:https://doc.xiaominfo.com/
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
前后端分离开发模式中,api文档是最好的沟通方式。
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。
1、及时性 (接口变更后,能够及时准确地通知相关前后端开发人员)
2、规范性 (并且保证接口的规范性,如接口的地址,请求方式,参数及响应格式和错误信息)
3、一致性 (接口信息一致,不会出现因开发人员拿到的文档版本不一致,而出现分歧)
4、可测性 (直接在接口文档上进行测试,以方便理解业务)
用来生成接口的API文档
方便后端Java程序员进行接口测试
service-uitl.pom
com.github.xiaoymin knife4j-spring-boot-starter
package com.jerry.common.config.knife4j;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;import java.util.ArrayList;
import java.util.List;/*** ClassName: knife4j* Package: com.jerry.common.config* Description:** @Author jerry_jy* @Create 2023-03-01 10:53* @Version 1.0*//*** knife4j配置信息*/
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {@Beanpublic Docket adminApiConfig(){List pars = new ArrayList<>();ParameterBuilder tokenPar = new ParameterBuilder();tokenPar.name("token").description("用户token").defaultValue("").modelRef(new ModelRef("string")).parameterType("header").required(false).build();pars.add(tokenPar.build());//添加head参数endDocket adminApi = new Docket(DocumentationType.SWAGGER_2).groupName("adminApi").apiInfo(adminApiInfo()).select()//只显示admin路径下的页面.apis(RequestHandlerSelectors.basePackage("com.jerry")).paths(PathSelectors.regex("/admin/.*")).build().globalOperationParameters(pars);return adminApi;}private ApiInfo adminApiInfo(){return new ApiInfoBuilder().title("后台管理系统-API文档").description("本文档描述了后台管理系统微服务接口定义").version("1.0").contact(new Contact("jerry", "https://jerry-jy.co", "jinyang9248@163.com")).build();}}
@Api(tags = "角色管理接口")
@ApiOperation("查询所有角色")
http://localhost:8800/doc.html
service-util模块下创建 MybatisPlusConfig
package com.jerry.common.config.mp;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** ClassName: MybatisPlusConfig* Package: com.jerry.common.config.mp* Description:** @Author jerry_jy* @Create 2023-03-01 11:17* @Version 1.0*/@Configuration
@MapperScan("com.jerry.auth.mapper")
public class MybatisPlusConfig {/*** 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}@Beanpublic ConfigurationCustomizer configurationCustomizer() {return configuration -> configuration.setUseDeprecatedExecutor(false);}
}
/*** 条件分页查询** @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 sysRolePage = new Page<>(page, pageSize);// 2、构造分页查询条件, 判断条件是否为空,不为空进行封装LambdaQueryWrapper 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);}
/*** 添加角色* @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 ids){boolean is_success = sysRoleService.removeByIds(ids);if (is_success) {return Result.ok();} else {return Result.fail();}}
配置日期时间格式
application-dev.yml添加以下内容
jackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: GMT+8
异常处理的思路流程
service-util 模块下
package com.jerry.common.config.exception;import com.jerry.common.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;/*** ClassName: GlobalExceptionHandler* Package: com.jerry.common.config.exception* Description:** @Author jerry_jy* @Create 2023-03-01 15:48* @Version 1.0*/
@ControllerAdvice
public class GlobalExceptionHandler {/*** 全局异常处理 执行的方法* @return*/@ExceptionHandler(Exception.class)@ResponseBodypublic Result error(Exception e){e.printStackTrace();return Result.fail().message("执行全局处理异常...");}/*** 特定异常处理* @param e* @return*/@ExceptionHandler(ArithmeticException.class)@ResponseBodypublic Result error(ArithmeticException e){e.printStackTrace();return Result.fail().message("执行特定处理异常...");}/*** 自定义异常处理* @param e* @return*/@ExceptionHandler(GuiguException.class)@ResponseBodypublic Result error(GuiguException e){e.printStackTrace();return Result.fail().code(e.getCode()).message(e.getMsg());}
}
package com.jerry.common.config.exception;import com.jerry.common.result.ResultCodeEnum;
import lombok.Data;/*** ClassName: GuiguException* Package: com.jerry.common.config.exception* Description:** @Author jerry_jy* @Create 2023-03-01 15:59* @Version 1.0*/
@Data
public class GuiguException extends RuntimeException {private Integer code;private String msg;/*** 通过状态码和错误消息创建异常对象* @param code* @param msg*/public GuiguException(Integer code, String msg) {super(msg);this.code = code;this.msg = msg;}/*** 接收枚举类型对象* @param resultCodeEnum*/public GuiguException(ResultCodeEnum resultCodeEnum) {super(resultCodeEnum.getMessage());this.code = resultCodeEnum.getCode();this.msg = resultCodeEnum.getMessage();}@Overridepublic String toString() {return "GuiguException{" +"code=" + code +", msg='" + msg + '\'' +'}';}
}
前端用的脚手架工程是:vue-element-admin
https://panjiachen.github.io/vue-element-admin-site/#/
# clone the project
git clone https://github.com/PanJiaChen/vue-element-admin.git# install dependency
npm install# develop
npm run dev
http://localhost:9528/#/dashboard
// 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 map = new HashMap<>();map.put("token","admin-token");return Result.ok(map);}/*** info* @return*/@GetMapping("/info")public Result info(){Map 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();}
}
重启前端、后端项目,可以发现请求头信息已经做了跳转、转发
重新定义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 }}
/*
角色管理相关的API请求函数
*/
import request from '@/utils/request'const api_name = '/admin/system/sysRole'export default {/*获取角色分页列表(带搜索)*/getPageList(page, limit, searchObj) {return request({url: `${api_name}/${page}/${limit}`,method: 'get',// 如果是普通对象参数写法,params:对象参数名// 如果是使用json格式传递,data:对象参数名params: searchObj})}
}
重新启动前端工程
http://localhost:9528/?#/system/sysRole
/*** 角色删除* @param {*} id * @returns */removeById(id) {return request({url: `${api_name}/delete/${id}`,method: 'delete'})}
// 根据id删除数据removeDataById(id) {// debuggerthis.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => { // promise// 点击确定,远程调用ajaxreturn api.removeById(id)}).then((response) => {// 刷新页面this.fetchData(this.page)// 提示信息this.$message.success(response.message || '删除成功')})}
前端CRUD完整代码
注意点:
前端中的url
请求路径要和后端的@DeleteMapping
,@PutMapping
,@PostMapping
,@GetMapping
路径一致
/*
角色管理相关的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})}}
搜索 重置 {{ (page - 1) * limit + scope.$index + 1 }}添 加 批量删除 取 消 确 定
mapper
,service
,impl
,controller
,service-oa
com.baomidou mybatis-plus-generator 3.4.1 org.apache.velocity velocity-engine-core 2.0
CodeGet.java
package com.jerry.code;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;public class CodeGet {public static void main(String[] args) {// 1、创建代码生成器AutoGenerator mpg = new AutoGenerator();// 2、全局配置// 全局配置GlobalConfig gc = new GlobalConfig();gc.setOutputDir("E:\\CodeLife\\IdeaProject\\guigu-oa\\guigu-oa-parent\\service-oa"+"/src/main/java");gc.setServiceName("%sService"); //去掉Service接口的首字母Igc.setAuthor("jerry");gc.setOpen(false);mpg.setGlobalConfig(gc);// 3、数据源配置DataSourceConfig dsc = new DataSourceConfig();dsc.setUrl("jdbc:mysql://localhost:3306/guigu-oa?serverTimezone=GMT%2B8&useSSL=false");dsc.setDriverName("com.mysql.cj.jdbc.Driver");dsc.setUsername("root");dsc.setPassword("root");dsc.setDbType(DbType.MYSQL);mpg.setDataSource(dsc);// 4、包配置PackageConfig pc = new PackageConfig();pc.setParent("com.jerry");pc.setModuleName("auth"); //模块名pc.setController("controller");pc.setService("service");pc.setMapper("mapper");mpg.setPackageInfo(pc);// 5、策略配置StrategyConfig strategy = new StrategyConfig();strategy.setInclude("sys_user");strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作strategy.setRestControllerStyle(true); //restful api风格控制器strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符mpg.setStrategy(strategy);// 6、执行mpg.execute();}
}
代码是写在service-oa类中的
SysUserMapper
public interface SysUserMapper extends BaseMapper {
}
SysUserService
public interface SysUserService extends IService {
}
SysUserServiceImpl
@Service
public class SysUserServiceImpl extends ServiceImpl implements SysUserService {
}
SysUserController
package com.jerry.auth.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jerry.auth.service.SysUserService;
import com.jerry.common.result.Result;
import com.jerry.model.system.SysUser;
import com.jerry.vo.system.SysUserQueryVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;/*** * 用户表 前端控制器*
** @author jerry* @since 2023-03-01*/
@Api(tags = "用户管理接口")
@RestController
@RequestMapping("/admin/system/sysUser")
public class SysUserController {@Autowiredprivate SysUserService sysUserService;/*** 用户条件分页查询** @param page* @param pageSize* @param sysUserQueryVo* @return*/@ApiOperation("用户条件分页查询")@GetMapping("/{page}/{pageSize}")public Result page(@PathVariable int page, @PathVariable int pageSize, SysUserQueryVo sysUserQueryVo) {Page sysUserPage = new Page<>(page, pageSize);LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();// 获取条件String userName = sysUserQueryVo.getKeyword();String createTimeBegin = sysUserQueryVo.getCreateTimeBegin();String createTimeEnd = sysUserQueryVo.getCreateTimeEnd();// 判断条件值不为空if (!StringUtils.isEmpty(userName)){lambdaQueryWrapper.like(SysUser::getUsername,userName);}if (!StringUtils.isEmpty(createTimeBegin)){lambdaQueryWrapper.ge(SysUser::getCreateTime,createTimeBegin);}if (!StringUtils.isEmpty(createTimeEnd)){lambdaQueryWrapper.le(SysUser::getCreateTime,createTimeEnd);}sysUserService.page(sysUserPage,lambdaQueryWrapper);return Result.ok(sysUserPage);}/*** 获取用户* @param id* @return*/@ApiOperation("获取用户")@GetMapping("/get/{id}")public Result get(@PathVariable long id){SysUser user = sysUserService.getById(id);return Result.ok(user);}/*** 更新用户* @param sysUser* @return*/@ApiOperation("更新用户")@PutMapping("/update")public Result update(@RequestBody SysUser sysUser){boolean is_success = sysUserService.updateById(sysUser);if (is_success) {return Result.ok();} else {return Result.fail();}}/*** 保存用户* @param sysUser* @return*/@ApiOperation("保存用户")@PostMapping("/save")public Result save(@RequestBody SysUser sysUser){boolean is_success = sysUserService.save(sysUser);if (is_success) {return Result.ok();} else {return Result.fail();}}/*** 删除用户* @param id* @return*/@ApiOperation("删除用户")@DeleteMapping("/remove/{id}")public Result remove(@PathVariable long id){boolean is_success = sysUserService.removeById(id);if (is_success) {return Result.ok();} else {return Result.fail();}}
}
全部测试通过
搜索 重置 添 加 {{ (page - 1) * limit + scope.$index + 1 }}{{ item.roleName }} , total, slot"@current-change="fetchData"@size-change="changeSize"/>取 消 确 定
{name: 'sysUser',path: 'sysUser',component: () => import('@/views/system/sysUser/list'),meta: {title: '用户管理',icon: 'el-icon-s-custom'},},
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)]
一个用户对应多个角色
一个角色可以有多个用户
多对多的关系
1、进入分配页面:获取已分配角色与全部角色,进行页面展示
2、保存分配角色:删除之前分配的角色和保存现在分配的角色
代码是写在service-oa类中的
SysUserRoleMapper
public interface SysUserRoleMapper extends BaseMapper {
}
SysUserRoleService
public interface SysUserRoleService extends IService {
}
SysUserRoleServiceImpl
@Service
public class SysUserRoleServiceImpl extends ServiceImpl implements SysUserRoleService {
}
SysRoleController
// 1、查询所有角色 和 当前用户所属角色@ApiOperation("根据用户获取角色数据")@GetMapping("/toAssign/{userId}")public Result toAssign(@PathVariable Long userId) {Map map = sysRoleService.findRoleDataByUserId(userId);return Result.ok(map);}// 2、为用户分配角色@ApiOperation("为用户分配角色")@PostMapping("/doAssign")public Result doAssign(@RequestBody AssginRoleVo assginRoleVo) {sysRoleService.doAssign(assginRoleVo);return Result.ok();}
SysRoleServiceImpl
/*** ClassName: SysRoleServiceImpl* Package: com.jerry.auth.service.impl* Description:** @Author jerry_jy* @Create 2023-03-01 9:13* @Version 1.0*/@Service
public class SysRoleServiceImpl extends ServiceImpl implements SysRoleService {@Autowiredprivate SysUserRoleService sysUserRoleService;//1 查询所有角色 和 当前用户所属角色@Overridepublic Map findRoleDataByUserId(Long userId) {//1 查询所有角色,返回list集合,返回List allRoleList =baseMapper.selectList(null);//2 根据userid查询 角色用户关系表,查询userid对应所有角色idLambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();wrapper.eq(SysUserRole::getUserId,userId);List existUserRoleList = sysUserRoleService.list(wrapper);//从查询出来的用户id对应角色list集合,获取所有角色id
// List list = new ArrayList<>();
// for (SysUserRole sysUserRole:existUserRoleList) {
// Long roleId = sysUserRole.getRoleId();
// list.add(roleId);
// }List existRoleIdList =existUserRoleList.stream().map(c -> c.getRoleId()).collect(Collectors.toList());//3 根据查询所有角色id,找到对应角色信息//根据角色id到所有的角色的list集合进行比较List assignRoleList = new ArrayList<>();for(SysRole sysRole : allRoleList) {//比较if(existRoleIdList.contains(sysRole.getId())) {assignRoleList.add(sysRole);}}//4 把得到两部分数据封装map集合,返回Map roleMap = new HashMap<>();roleMap.put("assginRoleList", assignRoleList);roleMap.put("allRolesList", allRoleList);return roleMap;}//2 为用户分配角色@Overridepublic void doAssign(AssginRoleVo assginRoleVo) {//把用户之前分配角色数据删除,用户角色关系表里面,根据userid删除LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();wrapper.eq(SysUserRole::getUserId,assginRoleVo.getUserId());sysUserRoleService.remove(wrapper);//重新进行分配List 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);}}
}
用户状态:状态(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 {// 更新状态void updateStatus(Long id, Integer status);
}
SysUserServiceImpl
@Service
@Slf4j
public class SysUserServiceImpl extends ServiceImpl implements SysUserService {// 更新状态@Override@Transactionalpublic void updateStatus(Long id, Integer status) {// 根据用户 userid 查询用户对象SysUser sysUser = baseMapper.selectById(id);// 设置修改状态if (status == 0 || status == 1) {sysUser.setStatus(status);} else {log.info("数值不合法");}// 调用方法进行修改baseMapper.updateById(sysUser);}
}
src/api/system/sysUser.js
updateStatus(id, status) {return request({url: `${api_name}/updateStatus/${id}/${status}`,method: 'get'})
}
src/api/system/sysRole.js
getRoles(adminId) {return request({url: `${api_name}/toAssign/${adminId}`,method: 'get'})
},assignRoles(assginRoleVo) {return request({url: `${api_name}/doAssign`,method: 'post',data: assginRoleVo})
}
list.vue
搜索 重置 添 加 {{ (page - 1) * limit + scope.$index + 1 }}{{ item.roleName }} , total, slot"@current-change="fetchData"@size-change="changeSize"/>取 消 确 定 全选 {{role.roleName}} 保存 取消
SysMenuMapper
public interface SysMenuMapper extends BaseMapper {
}
SysRoleMenuMapper
public interface SysRoleMenuMapper extends BaseMapper {}
SysMenuService
public interface SysMenuService extends IService {List findNodes();// 删除菜单void removeMenuById(Long id);
}
SysRoleMenuService
public interface SysRoleMenuService extends IService {
}
SysMenuServiceImpl
@Service
public class SysMenuServiceImpl extends ServiceImpl implements SysMenuService {@Overridepublic List findNodes() {// 1、查询所有 的数据List sysMenuList = baseMapper.selectList(null);// 2、构建树形结构List list = MenuHelper.buildTree(sysMenuList);return list;}// 删除菜单@Overridepublic void removeMenuById(Long id) {// 判断当前菜单是否有下一层菜单LambdaQueryWrapper 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 buildTree(List sysMenuList) {// 存放最终数据List trees = new ArrayList<>();// 把所有的菜单数据进行遍历for (SysMenu sysMenu : sysMenuList) {// 递归入口 parentId = 0if (sysMenu.getParentId().longValue()==0){trees.add(getChildren(sysMenu,sysMenuList));}}return trees;}/*** 递归查找子节点* @param sysMenu* @param sysMenuList* @return*/public static SysMenu getChildren(SysMenu sysMenu,List sysMenuList){sysMenu.setChildren(new ArrayList());// 遍历所有的菜单数据,判断id和parent_id的对应关系for (SysMenu menu : sysMenuList) {if (sysMenu.getId().longValue() == menu.getParentId().longValue()){if (sysMenu.getChildren() == null) {sysMenu.setChildren(new ArrayList<>());}sysMenu.getChildren().add(getChildren(menu,sysMenuList));}}return sysMenu;}
}
SysRoleMenuServiceImpl
@Service
public class SysRoleMenuServiceImpl extends ServiceImpl 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 {@Autowiredprivate SysMenuService sysMenuService;@ApiOperation(value = "菜单列表")@GetMapping("/findNodes")public Result findNodes() {List 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'},}
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})}
}
添 加 0"/> 目录 菜单 按钮 {{ item.class }} 路由地址 组件路径 权限字符正常 停用 取 消 确 定
{path: 'assignAuth',component: () => import('@/views/system/sysRole/assignAuth'),meta: {activeMenu: '/system/sysRole',title: '角色授权'},hidden: true,}
添加一个分配权限
的button按钮
// 跳转到分配菜单的页面showAssignAuth(row) {this.$router.push('/system/assignAuth?id='+row.id+'&roleName='+row.roleName);},
/*
查看某个角色的权限列表
*/
toAssign(roleId) {return request({url: `${api_name}/toAssign/${roleId}`,method: 'get'})},/*给某个角色授权*/doAssign(assginMenuVo) {return request({url: `${api_name}/doAssign`,method: "post",data: assginMenuVo})}
授权角色:{{ $route.query.roleName }}保存 返回
关闭Vue语法校验,避免报错
JWT是JSON Web Token的缩写
一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上
官网:https://jwt.io/
最重要的作用就是对 token信息的防伪作用。
由三个部分组成:JWT头、有效载荷、签名哈希
base64url算法编码得到JWT
common-util
io.jsonwebtoken jjwt
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 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 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);}}
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"));}
}
package com.jerry.auth.controller;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jerry.auth.service.SysMenuService;
import com.jerry.auth.service.SysUserService;
import com.jerry.common.config.exception.GuiguException;
import com.jerry.common.jwt.JwtHelper;
import com.jerry.common.result.Result;
import com.jerry.common.utils.MD5;
import com.jerry.model.system.SysUser;
import com.jerry.vo.system.LoginVo;
import com.jerry.vo.system.RouterVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;/*** ClassName: IndexController* Package: com.jerry.auth.controller* Description:** @Author jerry_jy* @Create 2023-03-01 18:15* @Version 1.0*/
@Api(tags = "后台登录管理")
@RestController
@RequestMapping("/admin/system/index")
public class IndexController {@Autowiredprivate SysUserService sysUserService;@Autowiredprivate SysMenuService sysMenuService;/*** login** @return*/@ApiOperation("登录")@PostMapping("/login")public Result login(@RequestBody LoginVo loginVo) {// {"code":200,"data":{"token":"admin-token"}}
// HashMap map = new HashMap<>();
// map.put("token","admin-token");
// return Result.ok(map);// 1、获取用户名和密码// 2、根据用户名查询数据库String username = loginVo.getUsername();LambdaQueryWrapper 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 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 routerList = sysMenuService.findUserMenuListByUserId(userId);// 5、根据 用户id 获取用户可以操作的按钮列表List permsList = sysMenuService.findUserPermsByUserId(userId);// 6、返回相应的数据Map 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();}
}
// 根据 用户id 获取用户可以操作的菜单列表List findUserMenuListByUserId(Long userId);// 根据 用户id 获取用户可以操作的按钮列表List findUserPermsByUserId(Long userId);
// 根据 用户id 获取用户可以操作的菜单列表@Overridepublic List findUserMenuListByUserId(Long userId) {List sysMenusList = null;// 1、判断当前用户是否是管理员 userId=1 是管理员// 1.1、 如果是管理员,查询所有菜单列表if (userId.longValue() == 1) {// 查询所有菜单列表LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(SysMenu::getStatus, 1);queryWrapper.orderByAsc(SysMenu::getSortValue);sysMenusList = baseMapper.selectList(queryWrapper);} else {// 1.2、如果不是管理员,根据 userId 查询可以操作菜单列表// 多表关联查询:sys_role、sys_role_mexnu、sys_menusysMenusList = baseMapper.findMenuListByUserId(userId);}// 2、把查询出来的数据列表, 构建成框架要求的路由结构// 先构建树形结构List sysMenuTreeList = MenuHelper.buildTree(sysMenusList);// 构建框架要求的路由结构List routerList = this.buildRouter(sysMenuTreeList);return routerList;}// 构建框架要求的路由结构private List buildRouter(List menus) {// 创建 list 集合,存值最终数据List 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 children = menu.getChildren();if (menu.getType().intValue() == 1) {// 加载隐藏路由List hiddenMenuList = children.stream().filter(item -> !StringUtils.isEmpty(item.getComponent())).collect(Collectors.toList());for (SysMenu hiddenMenu : hiddenMenuList) {RouterVo hiddenRouter = new RouterVo();hiddenRouter.setHidden(true);hiddenRouter.setAlwaysShow(false);hiddenRouter.setPath(getRouterPath(hiddenMenu));hiddenRouter.setComponent(hiddenMenu.getComponent());hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon()));routers.add(hiddenRouter);}}else {if (!CollectionUtils.isEmpty(children)) {if(children.size() > 0) {router.setAlwaysShow(true);}// 递归router.setChildren(buildRouter(children));}}routers.add(router);}return routers;}/*** 获取路由地址** @param menu 菜单信息* @return 路由地址*/public String getRouterPath(SysMenu menu) {String routerPath = "/" + menu.getPath();if (menu.getParentId().intValue() != 0) {routerPath = menu.getPath();}return routerPath;}// 根据 用户id 获取用户可以操作的按钮列表@Overridepublic List findUserPermsByUserId(Long userId) {// 1、判断是否是管理员,如果是管理员,查询所有按钮列表List sysMenusList = null;if (userId.longValue() == 1) {// 查询所有菜单列表LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(SysMenu::getStatus, 1);sysMenusList = baseMapper.selectList(queryWrapper);}else {// 2、如果不是管理员,根据userId查询可以操作按钮列表// 多表关联查询:sys_role、sys_role_menu、sys_menusysMenusList = baseMapper.findMenuListByUserId(userId);}// 3、从查询出来的数据里面,获取可以操作按钮值的List集合,返回List permsList = sysMenusList.stream().filter(item -> item.getType() == 2).map(item -> item.getPerms()).collect(Collectors.toList());return permsList;}
我这里没有报错,如果出现以下的报错信息
解决思路是:
1、在pom.xml添加
org.springframework.boot spring-boot-maven-plugin src/main/java **/*.yml **/*.properties **/*.xml false src/main/resources **/*.yml **/*.properties **/*.xml false
2、application-dev.yml添加
mybatis-plus:mapper-locations: classpath:com/atguigu/auth/mapper/xml/*.xml
从这部分开始,整合前端不在写了,比较麻烦,直接复用现有的
给李四分配没有添加的权限
本项目采用 Spring-Security
来做用户认证和权限控制,也可以采用 Shiro
新建一个spring-security
的module
4.0.0 com.jerry common 1.0 spring-security com.jerry common-util 1.0 org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web provided
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
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {public String encode(CharSequence rawPassword) {return MD5.encrypt(rawPassword.toString());}public boolean matches(CharSequence rawPassword, String encodedPassword) {return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));}
}
public class CustomUser extends User {/*** 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了)*/private SysUser sysUser;public CustomUser(SysUser sysUser, Collection extends GrantedAuthority> authorities) {super(sysUser.getUsername(), sysUser.getPassword(), authorities);this.sysUser = sysUser;}public SysUser getSysUser() {return sysUser;}public void setSysUser(SysUser sysUser) {this.sysUser = sysUser;}
}
public interface UserDetailsService {/*** 根据用户名获取用户对象(获取不到直接抛异常)*/UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
操作service-oa
module
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate SysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 根据用户名查询SysUser sysUser = sysUserService.getUserByUserName(username);if(null == sysUser) {throw new UsernameNotFoundException("用户名不存在!");}if(sysUser.getStatus().intValue() == 0) {throw new RuntimeException("账号已停用");}return new CustomUser(sysUser, Collections.emptyList());}
}
SysUserService
SysUser getUserByUserName(String username);
SysUserServiceImpl
// 根据用户名查询@Overridepublic SysUser getUserByUserName(String username) {LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(SysUser::getUsername,username);SysUser sysUser = baseMapper.selectOne(queryWrapper);return sysUser;}
TokenLoginFilter
package com.jerry.security.filter;import com.fasterxml.jackson.databind.ObjectMapper;
import com.jerry.common.jwt.JwtHelper;
import com.jerry.common.result.ResponseUtil;
import com.jerry.common.result.Result;
import com.jerry.common.result.ResultCodeEnum;
import com.jerry.security.custom.CustomUser;
import com.jerry.vo.system.LoginVo;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/*** ClassName: TokenLoginFilter
* Package: com.jerry.security.filter
* Description: 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验** @Author: jerry_jy* @Create: 2023-03-03 15:29* @Version: 1.0*/public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {// 构造方法public TokenLoginFilter(AuthenticationManager authenticationManager){this.setAuthenticationManager(authenticationManager);this.setPostOnly(false);//指定登录接口及提交方式,可以指定任意路径this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));}// 登录认证过程// 获取输入的用户名和密码,调用方法认证@Overridepublic Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)throws AuthenticationException {try {// 获取用户信息LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);//封装对象Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());//调用方法return this.getAuthenticationManager().authenticate(authenticationToken);} catch (IOException e) {throw new RuntimeException(e);}}// 认证成功调用的方法@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication auth) throws IOException, ServletException {// 获取当前用户CustomUser customUser = (CustomUser) auth.getPrincipal();// 生成tokenString token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());// 返回Map map = new HashMap<>();map.put("token", token);ResponseUtil.out(response, Result.ok(map));}// 认证失败调用的方法@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,AuthenticationException e) throws IOException, ServletException {if(e.getCause() instanceof RuntimeException) {ResponseUtil.out(response, Result.build(null, ResultCodeEnum.DATA_ERROR));} else {ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_AUTH));}}}
common-util
下的ResponseUtil
package com.jerry.common.result;import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** ClassName: ResponseUtil
* Package: com.jerry.common.result
* Description:** @Author: jerry_jy* @Create: 2023-03-03 15:55* @Version: 1.0*/
public class ResponseUtil {public static void out(HttpServletResponse response, Result r) {ObjectMapper mapper = new ObjectMapper();response.setStatus(HttpStatus.OK.value());response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);try {mapper.writeValue(response.getWriter(), r);} catch (IOException e) {e.printStackTrace();}}
}
因为用户登录状态在token中存储在客户端,所以每次请求接口请求头携带token, 后台通过自定义token过滤器拦截解析token完成认证并填充用户信息实体
package com.jerry.security.filter;import com.jerry.common.jwt.JwtHelper;
import com.jerry.common.result.ResponseUtil;
import com.jerry.common.result.Result;
import com.jerry.common.result.ResultCodeEnum;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;/*** ClassName: TokenAuthenticationFilter
* Package: com.jerry.security.filter
* Description: 认证解析token过滤器** @Author: jerry_jy* @Create: 2023-03-03 16:01* @Version: 1.0*/public class TokenAuthenticationFilter extends OncePerRequestFilter {public TokenAuthenticationFilter() {}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {logger.info("uri:"+request.getRequestURI());//如果是登录接口,直接放行if("/admin/system/index/login".equals(request.getRequestURI())) {chain.doFilter(request, response);return;}UsernamePasswordAuthenticationToken authentication = getAuthentication(request);if(null != authentication) {SecurityContextHolder.getContext().setAuthentication(authentication);chain.doFilter(request, response);} else {ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));}}private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {// token置于header里String token = request.getHeader("token");logger.info("token:"+token);if (!StringUtils.isEmpty(token)) {String username = JwtHelper.getUsername(token);logger.info("username:"+username);if (!StringUtils.isEmpty(username)) {return new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());}}return null;}
}
package com.jerry.security.config;import com.jerry.security.custom.CustomMd5PasswordEncoder;
import com.jerry.security.filter.TokenAuthenticationFilter;
import com.jerry.security.filter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;/*** ClassName: WebSecurityConfig* Package: com.jerry.security.config* Description:** @Author jerry_jy* @Create 2023-03-03 13:44* @Version 1.0*/@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService; // 装载的是 org.springframework.security.core.userdetails.UserDetailsService;@Autowiredprivate CustomMd5PasswordEncoder customMd5PasswordEncoder;@Bean@Overrideprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护http//关闭csrf跨站请求伪造.csrf().disable()// 开启跨域以便前端调用接口.cors().and().authorizeRequests()// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的.antMatchers("/admin/system/index/login").permitAll()// 这里意思是其它所有接口需要认证才能访问.anyRequest().authenticated().and()//TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。.addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).addFilter(new TokenLoginFilter(authenticationManager()));//禁用sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 指定UserDetailService和加密器auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);}/*** 配置哪些请求不拦截* 排除swagger相关请求** @param web* @throws Exception*/@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/favicon.ico", "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");}
}
说明:
1、我们是前后端分离项目,使用jwt
生成token ,即用户状态保存在客户端中,前后端交互通过api接口 无session
生成,所以我们不需要配置formLogin
,session禁用
2、在浏览器访问:http://localhost:8800/admin/system/sysRole/getAll
修改UserDetailsServiceImpl中的loadUserByUsername
// 根据 user_id 查询用户操作权限数据List userPermsList = sysMenuService.findUserPermsByUserId(sysUser.getId());// 创建list集合,封装最终权限数据List authList = new ArrayList<>();// 遍历 authListfor (String perms : userPermsList) {authList.add(new SimpleGrantedAuthority(perms.trim()));}return new CustomUser(sysUser, authList);
添加依赖
org.springframework.boot spring-boot-starter-data-redis
配置类添加注解:
开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
application-dev.yml配文件
spring:redis:host: localhostport: 6379database: 0timeout: 1800000password:jedis:pool:max-active: 20 #最大连接数max-wait: -1 #最大阻塞等待时间(负数表示没限制)max-idle: 5 #最大空闲min-idle: 0 #最小空闲
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模块引入依赖
org.springframework.boot spring-boot-starter-security provided
AccessDeniedException需要引入依赖,Spring Security对应的异常
/*** spring security异常* @param e* @return*/@ExceptionHandler(AccessDeniedException.class)@ResponseBodypublic Result error(AccessDeniedException e) throws AccessDeniedException {return Result.build(null, ResultCodeEnum.PERMISSION);}
在service-oa
中
org.activiti activiti-spring-boot-starter 7.1.0.M6 mybatis org.mybatis
在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
会自己创建数据表
官网下载:https://www.activiti.org/get-started
把解压出来的activiti-explorer.war
放在Tomcat的webapps
下
启动Tomcat服务器
http://localhost:8080/activiti-explorer
默认登录账号: kermit
kermit
请假流程审批绘制
qingjia.bpmn20.xml
下载流程定义图片
单击右键上图图片,图片另存为: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*/@SpringBootTest
public class ProcessTest {@Autowiredprivate RepositoryService repositoryService;// 单个文件的部署@Testpublic void deployProcess() {Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/qingjia.bpmn20.xml").addClasspathResource("process/qingjia.png").name("请假申请流程").deploy();System.out.println("deploy.getId() = " + deploy.getId());System.out.println("deploy.getName() = " + deploy.getName());}
}
package com.jerry.auth.activiti;import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.history.HistoricTaskInstance;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.List;/*** ClassName: ProcessTest
* Package: com.jerry.activiti
* Description:** @Author: jerry_jy* @Create: 2023-03-05 10:51* @Version: 1.0*/@SpringBootTest
public class ProcessTest1 {@Autowiredprivate RepositoryService repositoryService;@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;@Autowiredprivate HistoryService historyService;// 单个流程实例挂起@Testpublic void SingleSuspendProcessInstance() {String processInstanceId = "71f6803b-bb19-11ed-a845-005056c00001";ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();//获取到当前流程定义是否为暂停状态 suspended方法为true代表为暂停 false就是运行的boolean suspended = processInstance.isSuspended();if (suspended) {runtimeService.activateProcessInstanceById(processInstanceId);System.out.println("流程实例:" + processInstanceId + "激活");} else {runtimeService.suspendProcessInstanceById(processInstanceId);System.out.println("流程实例:" + processInstanceId + "挂起");}}// 全部流程实例挂起@Testpublic void suspendProcessInstance() {// 1、获取流程定义对象ProcessDefinition qingjia = repositoryService.createProcessDefinitionQuery().processDefinitionKey("qingjia").singleResult();// 2、调用流程定义对象的方法判断当前状态:挂起 激活boolean suspended = qingjia.isSuspended();if (suspended) {// 暂定,那就可以激活// 参数1:流程定义的id 参数2:是否激活 参数3:时间点repositoryService.activateProcessDefinitionById(qingjia.getId(), true, null);System.out.println("流程定义:" + qingjia.getId() + "激活");} else {repositoryService.suspendProcessDefinitionById(qingjia.getId(), true, null);System.out.println("流程定义:" + qingjia.getId() + "挂起");}}/*** 启动流程实例,添加businessKey*/@Testpublic void startUpProcessAddBusinessKey(){// 启动流程实例,指定业务标识businessKey,也就是请假申请单idProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia","1001");// 输出System.out.println("业务id:"+processInstance.getBusinessKey()); //1001System.out.println("processInstance.getId() = " + processInstance.getId()); // 71f6803b-bb19-11ed-a845-005056c00001}/*** 查询流程定义*/@Testpublic void findProcessDefinitionList(){List definitionList = repositoryService.createProcessDefinitionQuery().orderByProcessDefinitionVersion().desc().list();//输出流程定义信息for (ProcessDefinition processDefinition : definitionList) {System.out.println("流程定义 id="+processDefinition.getId());System.out.println("流程定义 name="+processDefinition.getName());System.out.println("流程定义 key="+processDefinition.getKey());System.out.println("流程定义 Version="+processDefinition.getVersion());System.out.println("流程部署ID ="+processDefinition.getDeploymentId());}}/*** 删除流程定义*/@Testpublic void deleteDeployment() {//部署idString deploymentId = "qingjia:1:c493c327-bb02-11ed-8360-005056c00001";
// //删除流程定义,如果该流程定义已有流程实例启动则删除时出错
// repositoryService.deleteDeployment(deploymentId);//设置true 级联删除流程定义,即使该流程有流程实例启动也可以删除,设置为false非级别删除方式repositoryService.deleteDeployment(deploymentId, true);}// 查询已经处理的任务@Testpublic void findCompleteTaskList(){List list = historyService.createHistoricTaskInstanceQuery().taskAssignee("zhangsan").finished().list();for (HistoricTaskInstance historicTaskInstance : list) {System.out.println("流程实例id:" + historicTaskInstance.getProcessInstanceId());System.out.println("任务id:" + historicTaskInstance.getId());System.out.println("任务负责人:" + historicTaskInstance.getAssignee());System.out.println("任务名称:" + historicTaskInstance.getName());}}// 处理当前任务@Testpublic void completeTask(){// 查询负责人需要处理的任务,返回一条Task task = taskService.createTaskQuery().taskAssignee("zhangsan").singleResult();// 完成任务taskService.complete(task.getId());}// 查询个人的代办任务--zhangsan@Testpublic void findTaskList(){String assign = "zhangsan";List list = taskService.createTaskQuery().taskAssignee(assign).list();for (Task task : list) {System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId());System.out.println("任务id:" + task.getId());System.out.println("任务负责人:" + task.getAssignee());System.out.println("任务名称:" + task.getName());}}// 启动流程实例@Testpublic void startProcess(){ProcessInstance processInstance = runtimeService.startProcessInstanceById("qingjia");System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId());System.out.println("processInstance.getId() = " + processInstance.getId());System.out.println("processInstance.getActivityId() = " + processInstance.getActivityId());}// 单个文件的部署@Testpublic void deployProcess() {Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/qingjia.bpmn20.xml").addClasspathResource("process/qingjia.png").name("请假申请流程").deploy();System.out.println("deploy.getId() = " + deploy.getId());System.out.println("deploy.getName() = " + deploy.getName());}
}
package com.jerry.auth.activiti;import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** ClassName: ProcessTest2
* Package: com.jerry.auth.activiti
* Description:** @Author: jerry_jy* @Create: 2023-03-05 14:05* @Version: 1.0*/@SpringBootTest
public class ProcessTest2 {@Autowiredprivate RepositoryService repositoryService;@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;@Autowiredprivate HistoryService historyService;///// 监听器分配任务// 部署流程定义@Testpublic void deployProcess02() {Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban02.bpmn20.xml").name("加班申请流程02").deploy();System.out.println("deploy.getId() = " + deploy.getId()); // ed080f00-bb41-11ed-a6f2-005056c00001System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程02}@Testpublic void startProcessInstance02(){ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban02");System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban02:1:ed150752-bb41-11ed-a6f2-005056c00001System.out.println("processInstance.getId() = " + processInstance.getId()); // 06eca124-bb42-11ed-9bbc-005056c00001}// 查询个人的代办任务--Tim@Testpublic void findTaskList02(){String assign = "Tim";List list = taskService.createTaskQuery().taskAssignee(assign).list();for (Task task : list) {System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); // 06eca124-bb42-11ed-9bbc-005056c00001System.out.println("任务id:" + task.getId()); // 06f071b8-bb42-11ed-9bbc-005056c00001System.out.println("任务负责人:" + task.getAssignee()); // TimSystem.out.println("任务名称:" + task.getName()); // 经理审批}}///// uel-method// 部署流程定义@Testpublic void deployProcess01() {Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban01.bpmn20.xml").name("加班申请流程01").deploy();System.out.println("deploy.getId() = " + deploy.getId()); // 8c4ac05e-bb20-11ed-8d65-005056c00001System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程01}@Testpublic void startProcessInstance01(){ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban01");System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban01:1:8c56a740-bb20-11ed-8d65-005056c00001System.out.println("processInstance.getId() = " + processInstance.getId()); // abb9c7c4-bb20-11ed-b608-005056c00001}// 查询个人的代办任务--LiLei@Testpublic void findTaskList01(){String assign = "LiLei";List list = taskService.createTaskQuery().taskAssignee(assign).list();for (Task task : list) {System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); // abb9c7c4-bb20-11ed-b608-005056c00001System.out.println("任务id:" + task.getId()); // abbd4a38-bb20-11ed-b608-005056c00001System.out.println("任务负责人:" + task.getAssignee()); // LiLeiSystem.out.println("任务名称:" + task.getName()); // 经理审批}}///// uel-value// 部署流程定义@Testpublic void deployProcess() {Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban.bpmn20.xml").name("加班申请流程").deploy();System.out.println("deploy.getId() = " + deploy.getId()); // 5c5519ad-bb1d-11ed-b5c8-005056c00001System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程}// 启动流程实例@Testpublic void startProcessInstance() {Map map = new HashMap<>();// 设置任务人map.put("assignee1","tom");map.put("assignee2","jerry");ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban", map);System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban:1:5c60d97f-bb1d-11ed-b5c8-005056c00001System.out.println("processInstance.getId() = " + processInstance.getId()); // 7f720dd9-bb1d-11ed-b6e9-005056c00001}// 查询个人的代办任务--tom@Testpublic void findTaskList(){String assign = "tom";List list = taskService.createTaskQuery().taskAssignee(assign).list();for (Task task : list) {System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); //7f720dd9-bb1d-11ed-b6e9-005056c00001System.out.println("任务id:" + task.getId()); // 7f759051-bb1d-11ed-b6e9-005056c00001System.out.println("任务负责人:" + task.getAssignee()); // tomSystem.out.println("任务名称:" + task.getName()); // 经理审批}}}
配置监听器
package com.jerry.auth.activiti;import org.springframework.stereotype.Component;/*** ClassName: UserBean
* Package: com.jerry.auth.activiti
* Description:** @Author: jerry_jy* @Create: 2023-03-05 14:25* @Version: 1.0*/@Component
public class UserBean {public String getUsername(int id) {if (id == 1) {return "LiLei";}if (id == 2) {return "HanMeiMei";}return "admin";}
}
package com.jerry.auth.activiti;import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** ClassName: ProcessTest3
* Package: com.jerry.auth.activiti
* Description:** @Author: jerry_jy* @Create: 2023-03-05 19:18* @Version: 1.0*/@SpringBootTest
public class ProcessTest3 {@Autowiredprivate RepositoryService repositoryService;@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;// 1、部署流程定义@Testpublic void deployProcess() {Deployment deploy = repositoryService.createDeployment().addClasspathResource("process/jiaban04.bpmn20.xml").name("加班申请流程04").deploy();System.out.println("deploy.getId() = " + deploy.getId()); // f204be8a-bb48-11ed-950e-005056c00001System.out.println("deploy.getName() = " + deploy.getName()); // 加班申请流程04}// 1.5、启动流程实例@Testpublic void startProcessInstance() {
// Map map = new HashMap<>();// 设置任务人
// map.put("assignee1","tom");
// map.put("assignee2","jerry");ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban04");System.out.println("processInstance.getProcessDefinitionId() = " + processInstance.getProcessDefinitionId()); // jiaban04:1:f210f38c-bb48-11ed-950e-005056c00001System.out.println("processInstance.getId() = " + processInstance.getId()); // 428d0c0f-bb49-11ed-83a5-005056c00001}// 2、查询组任务@Testpublic void findGroupTaskList(){List list = taskService.createTaskQuery().taskCandidateUser("tom").list();for (Task task : list) {System.out.println("----------------------------");System.out.println("流程实例id:" + task.getProcessInstanceId());System.out.println("任务id:" + task.getId());System.out.println("任务负责人:" + task.getAssignee());System.out.println("任务名称:" + task.getName());}}// 3、分配组任务@Testpublic void claimTask(){Task task = taskService.createTaskQuery().taskCandidateUser("tom").singleResult();if (task!=null){taskService.claim(task.getId(),"tom");System.out.println("分配任务完成");}}// 4、查询个人的代办任务--tom@Testpublic void findTaskList(){String assign = "tom";List list = taskService.createTaskQuery().taskAssignee(assign).list();for (Task task : list) {System.out.println("task.getProcessInstanceId() = " + task.getProcessInstanceId()); //7f720dd9-bb1d-11ed-b6e9-005056c00001System.out.println("任务id:" + task.getId()); // 7f759051-bb1d-11ed-b6e9-005056c00001System.out.println("任务负责人:" + task.getAssignee()); // tomSystem.out.println("任务名称:" + task.getName()); // 经理审批}}// 5、办理个人任务@Testpublic void completeGroupTask() {Task task = taskService.createTaskQuery().taskAssignee("tom") //要查询的负责人.singleResult();//返回一条taskService.complete(task.getId());}
}
网关用来控制流程的流向,通常会和流程变量一起使用。
当你的流程出现这样的场景:请假申请,两天以内,部门经理审批流程就结束了,两天以上需要总经理直接审批,这个时候就需要排他网关
当出现这样的场景:请假申请开始,需要部门经理和总经理都审批,两者没有前后需要两个人全部审批才能进入下个节点人事审批。这个时候就需要并行网关
与排他网关的主要区别是,并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。
包容网关:可以同时执行多条线路,也可以在网关上设置条件,可以看做是排他网关和并行网关的结合体。
当出现这样的场景:请假申请大于等于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*/
@SpringBootTest
public class ProcessTestGateway {@Autowiredprivate RepositoryService repositoryService;//注入RuntimeService@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;@Autowiredprivate HistoryService historyService;//1 部署流程定义@Testpublic void deployProcess() {Deployment deployment = repositoryService.createDeployment().addClasspathResource("process/qingjia003.bpmn20.xml").name("请假申请流程003").deploy();System.out.println(deployment.getId()); // af9242f0-bb4c-11ed-85bf-005056c00001System.out.println(deployment.getName()); // 请假申请流程002}//2 启动流程实例@Testpublic void startProcessInstance() {Map map = new HashMap<>();//设置请假天数map.put("day", "3");ProcessInstance processInstance =
// runtimeService.startProcessInstanceByKey("qingjia002", map);runtimeService.startProcessInstanceByKey("qingjia003");System.out.println(processInstance.getProcessDefinitionId()); // qingjia002:1:afac0c82-bb4c-11ed-85bf-005056c00001System.out.println(processInstance.getId()); // 90d46e2c-bb4d-11ed-9b92-005056c00001}//3 查询个人的代办任务--zhao6@Testpublic void findTaskList() {// String assign = "zhao6";
// String assign = "gousheng";
// String assign = "xiaocui";
// String assign = "wang5";
// String assign = "gouwa";String assign = "xiaoli";List list = taskService.createTaskQuery().taskAssignee(assign).list();for (Task task : list) {System.out.println("流程实例id:" + task.getProcessInstanceId());System.out.println("任务id:" + task.getId());System.out.println("任务负责人:" + task.getAssignee());System.out.println("任务名称:" + task.getName());}}//完成任务@Testpublic void completeTask() {Task task = taskService.createTaskQuery()
// .taskAssignee("zhao6") //要查询的负责人
// .taskAssignee("xiaocui") //要查询的负责人
// .taskAssignee("gousheng")
// .taskAssignee("wang5").taskAssignee("gouwa").singleResult();//返回一条//完成任务,参数:任务idtaskService.complete(task.getId());}
}
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 {@Autowiredprivate OaProcessTypeService processTypeService;@ApiOperation(value = "获取分页列表")@GetMapping("{page}/{pageSize}")public Result index(@PathVariable Long page, @PathVariable Long pageSize) {Page pageInfo = new Page<>(page, pageSize);Page 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();}}
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 {@Autowiredprivate OaProcessTemplateService processTemplateService;// 分页查询审批模板@ApiOperation("获取分页查询审批模板数据")@GetMapping("{page}/{pageSize}")public Result index(@PathVariable Long page, @PathVariable Long pageSize){Page pageInfo = new Page<>(page, pageSize);//分页查询审批模板,把审批类型对应名称查询IPage 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();}}
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 map = new HashMap<>();//根据上传地址后续部署流程定义,文件名称为流程定义的默认keymap.put("processDefinitionPath", "processes/" + filename);map.put("processDefinitionKey", filename.substring(0, filename.lastIndexOf(".")));return Result.ok(map);}public static void main(String[] args) {try {String path = new File(ResourceUtils.getURL("classpath:").getPath()).getAbsolutePath();System.out.println("path = " + path); //E:\CodeLife\IdeaProject\guigu-oa\guigu-oa-parent\service-oa\target\classes} catch (FileNotFoundException e) {throw new RuntimeException(e);}}
整合前端,无后台接口
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 {@Autowiredprivate OaProcessService processService;//审批管理列表@ApiOperation(value = "获取分页列表")@GetMapping("{page}/{limit}")public Result index(@PathVariable Long page,@PathVariable Long limit,ProcessQueryVo processQueryVo) {Page pageInfo = new Page<>(page, limit);IPage pageModel = processService.selectPage(pageInfo,processQueryVo);return Result.ok();}
}
OaProcessService
public interface OaProcessService extends IService {//审批管理列表IPage selectPage(Page pageInfo, ProcessQueryVo processQueryVo);
}
OaProcessServiceImpl
//审批管理列表@Overridepublic IPage selectPage(Page pageInfo, ProcessQueryVo processQueryVo) {IPage pageModel = baseMapper.selectPage(pageInfo,processQueryVo);return pageModel;}
OaProcessMapper
//审批管理列表IPage selectPage(Page pageInfo, @Param("vo") ProcessQueryVo processQueryVo);
涉及到4张表的多表查询,自己编写SQL语句
OaProcessMapper.xml
修改mapper的映射路径
OaProcessTemplateServiceImpl
// 修改模板的发布状态 status==1 代表已发布// 流程定义部署@Overridepublic void publish(Long id) {// 修改模板的发布状态 status==1 代表已发布ProcessTemplate processTemplate = baseMapper.selectById(id);processTemplate.setStatus(1);baseMapper.updateById(processTemplate);// 流程定义部署if (StringUtils.isEmpty(processTemplate.getProcessDefinitionPath())){processService.deployByZip(processTemplate.getProcessDefinitionPath());}}
}
OaProcessService
// 流程定义部署void deployByZip(String deployPath);
OaProcessServiceImpl
// 流程定义部署@Overridepublic void deployByZip(String deployPath) {InputStream inputStream= this.getClass().getClassLoader().getResourceAsStream(deployPath);ZipInputStream zipInputStream = new ZipInputStream(inputStream);// 部署Deployment deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).deploy();System.out.println("deployment.getId() = " + deployment.getId());System.out.println("deployment.getName() = " + deployment.getName());}
node -v
v 16.16.0
报错
npm ERR! path F:\guigu-oa\guigu-oa-web\node_modules\node-sass
npm ERR! command failed
npm ERR! command C:\WINDOWS\system32\cmd.exe /d /s /c node scripts/build.js
npm ERR! Building: E:\nodejs\node.exe F:\guigu-oa\guigu-oa-web\node_modules\node-gyp\bin\node-gyp.js rebuild --verbose --libsass_ext= --libsass_cflags= --libsass_ldflags= --libsass_library=
npm ERR! gyp info it worked if it ends with ok
npm ERR! gyp verb cli [
npm ERR! gyp verb cli 'E:\\nodejs\\node.exe',
npm ERR! gyp verb cli 'F:\\guigu-oa\\guigu-oa-web\\node_modules\\node-gyp\\bin\\node-gyp.js',
npm ERR! gyp verb cli 'rebuild',
npm ERR! gyp verb cli '--verbose',
npm ERR! gyp verb cli '--libsass_ext=',
npm ERR! gyp verb cli '--libsass_cflags=',
npm ERR! gyp verb cli '--libsass_ldflags=',
npm ERR! gyp verb cli '--libsass_library='
npm ERR! gyp verb cli ]
npm ERR! gyp info using node-gyp@3.8.0
npm ERR! gyp info using node@16.16.0 | win32 | x64
npm ERR! gyp verb command rebuild []
npm ERR! gyp verb command clean []
npm ERR! gyp verb clean removing "build" directory
nodejs版本过高,与node-sass不兼容,降级版本
v14.15.0
npm install
没问题
https://gitee.com/jinyang-jy/OnlineOfficeSystem.git
链接:https://pan.baidu.com/s/1ZVNqzPlcfMH89NgUYNYZtQ?pwd=2022
提取码:2022