中州养老xm

权限系统

一、部门管理

1、部门条件查询

这个部门列表不需要分页,依据我们刚才的接口分析,我们直接来编码

1)定义接口

新增类DeptController中定义新的方法

import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;  
import com.zzyl.base.ResponseResult;  
import com.zzyl.dto.DeptDto;  
import com.zzyl.service.DeptService;  
import com.zzyl.vo.DeptVo;  
import com.zzyl.vo.TreeVo;  
import io.swagger.annotations.Api;  
import io.swagger.annotations.ApiOperation;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestBody;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RestController;  

import java.util.List;  

	/**  
	 * @author : like  
	 * @date : 2024/3/23 17:59  
	 * @Version: 1.0  
	 */
   @Api(tags = "部门管理")  
    @RestController  
    @RequestMapping("/dept")  
    public class DeptController {  

   @Autowired  
   private DeptService deptService;  

   @ApiOperation("部门列表")  
   @PostMapping("/list")  
   @ApiOperationSupport(includeParameters = {  
           "deptDto.dataState","deptDto.deptName","deptDto.parentDeptNo"  
   }) // 设置显示的字段  
   public ResponseResult<List<DeptVo>> deptList(@RequestBody DeptDto deptDto){  
       List<DeptVo> deptVoList = deptService.getList(deptDto);  
       return ResponseResult.success(deptVoList);  
   }  

   @PostMapping("/tree")  
   public ResponseResult<TreeVo> deptTree() {  
       TreeVo treeVo = deptService.deptTree();  
       return ResponseResult.success(treeVo);  
   }  
}

其中的ApiOperationSupport是可以在swagger接口文档中说明参数包含了哪些字段,比如你定义的dto是10个字段,但是当前接口只需要6个字段,就可以把这6个字段声明出来,这样做的好处就是dto可以在多个接口中复用

2)持久层mapper

在DeptMapper中新增方法

 ```java
package com.zzyl.mapper;  
 /**  
  * @author sjqn  
  */  
 @Mapper  
 public interface DeptMapper {  
     /**  
      * 查询列表  
      * @param deptDto  
      * @return  
      */  
     List<DeptVo> selectList(DeptDto deptDto);  
 }  

对应的xml映射文件

 <?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">  
<mapper namespace="com.zzyl.mapper.DeptMapper">  
  
    <resultMap id="BaseResultMap" type="com.zzyl.entity.Dept">  
        <id column="id" jdbcType="BIGINT" property="id"/>  
        <result column="parent_dept_no" jdbcType="VARCHAR" property="parentDeptNo"/>  
        <result column="dept_no" jdbcType="VARCHAR" property="deptNo"/>  
        <result column="dept_name" jdbcType="VARCHAR" property="deptName"/>  
        <result column="sort_no" jdbcType="INTEGER" property="sortNo"/>  
        <result column="data_state" jdbcType="CHAR" property="dataState"/>  
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>  
        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>  
        <result column="create_by" jdbcType="BIGINT" property="createBy"/>  
        <result column="update_by" jdbcType="BIGINT" property="updateBy"/>  
        <result column="leader_id" jdbcType="BIGINT" property="leaderId"/>  
        <result column="leader_name" jdbcType="BIGINT" property="leaderName"/>  
    </resultMap>  
    <!--java 不能识别 ’_‘ 所以需要映射 resultmap  -->  
    <resultMap id="BaseResultVoMap" type="com.zzyl.vo.DeptVo">  
        <id column="id" jdbcType="BIGINT" property="id"/>  
        <result column="parent_dept_no" jdbcType="VARCHAR" property="parentDeptNo"/>  
        <result column="dept_no" jdbcType="VARCHAR" property="deptNo"/>  
        <result column="dept_name" jdbcType="VARCHAR" property="deptName"/>  
        <result column="sort_no" jdbcType="INTEGER" property="sortNo"/>  
        <result column="data_state" jdbcType="CHAR" property="dataState"/>  
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>  
        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>  
        <result column="create_by" jdbcType="BIGINT" property="createBy"/>  
        <result column="update_by" jdbcType="BIGINT" property="updateBy"/>  
        <result column="leader_id" jdbcType="BIGINT" property="leaderId"/>  
        <result column="role_id" jdbcType="BIGINT" property="roleId"/>  
        <result column="create_day" jdbcType="BIGINT" property="createDay"/>  
    </resultMap>  
    <sql id="Base_Column_List">  
        id  
        , parent_dept_no, dept_no, dept_name, sort_no, data_state, create_time, update_time,    create_by, update_by, remark, leader_id    </sql>  
  
    <!--  
    例如,like concat('%',#{deptName},'%') 可以用来匹配包含特定部门名称的记录,因为 % 表示匹配任意字符。  
    而 like concat(#{parentDeptNo},'%') 则可以用来匹配以特定父部门编号开头的记录。  
    -->  
  
    <select id="selectList" parameterType="com.zzyl.dto.DeptDto" resultMap="BaseResultVoMap">  
        select  
        d.id, d.parent_dept_no, d.dept_no, d.dept_name, d.sort_no, d.data_state, d.create_time, d.update_time,        d.create_by, d.update_by, d.remark, d.leader_id        , u.real_name as leader_name,DATE_FORMAT(d.create_time,'%Y-%m-%d') as create_day        from sys_dept d        left join sys_user u on u.id = leader_id        <where>  
            <if test="deptName!=null and deptName!=''">  
                and d.dept_name like concat('%',#{deptName},'%')  
            </if>  
            <if test="parentDeptNo!=null and parentDeptNo!=''">  
                and d.parent_dept_no like concat(#{parentDeptNo},'%')  
            </if>  
            <if test="dataState!=null and dataState!=''">  
                and d.data_state=#{dataState}  
            </if>  
        </where>  
        order by d.sort_no asc, d.create_time desc  
    </select>  
</mapper>

由于在部门列表的展示中,需要展示部门的负责人,所以关联了sys_user用户表来查询负责人姓名,虽然现在我们还没有开发用户模块,但是用户表已经有了,在这里可以进行关联查询

  • 在sys_dept表和实体类中并没有一个叫做leader_name的字段,我们需要扩展一下,只需要在Dept实体类中新增一个leaderName字段即可,并且把对应的xml映射文件中的BaseResultVoMap中定义这个字段

  • 在返回的日期中,前端要求返回的是创建的天,比如是:2023-10-31,我们需要使用mysql的日期函数单独处理

    • 格式化

      • select DATE_FORMAT(new(),'%Y-%m-%d %H:%i:%s')--->返回字符串:2023-10-31 01:21:00

      • select str_to_DATE('2023-10-31 01:21:00','%Y-%m-%d %H:%i:%s')--->返回日期类型

3)业务层

在DeptService中定义新的方法

 ```java
package com.zzyl.service;  

public interface DeptService {// 多条件
        List<DeptVo> deptList(DeptDto deptDto);  
}

实现类

package com.zzyl.service.impl;  
 /**  
  * @author sjqn  
  * @date 2023/10/30  
  */  
 @Service  
 public class DeptServiceImpl implements DeptService {   
     @Autowired  
     private DeptMapper deptMapper;   
     /**  
      * 多条件查询部门  
      * @param deptDto  
      * @return  
      */  
     @Override  
     public List<DeptVo> deptList(DeptDto deptDto) {  
         return deptMapper.selectList(deptDto);  
     }  
 }
2、部门树形结构

controller

@ApiOperation("部门树形结构")  
@PostMapping("/tree")  
public ResponseResult<TreeVo> deptTree() {  
    TreeVo treeVo = deptService.deptTree();  
    return ResponseResult.success(treeVo);  
}

封装结构

{
// TreeVo
    "items": [
    // TreeItemVo
        {
            "id": "0",
            "label": "Parent",
            "children": [
                {
                    "id": "1",
                    "label": "Child 1",
                    "children": []
                },
                {
                    "id": "2",
                    "label": "Child 2",
                    "children": []
                }
            ]
        }
    ]
}
  • 需要的vo类
/**  
 * 资源树显示类  
 */  
@Data  
@NoArgsConstructor  
public class TreeVo implements Serializable {  
  
    @ApiModelProperty(value = "tree数据")  
    private List<TreeItemVo> items;  
  
    @Builder  
    public TreeVo(List<TreeItemVo> items) {  
       this.items = items;  
    }  
}

/**  
 * 资源树结构体  
 */  
@Data  
@NoArgsConstructor  
public class TreeItemVo implements Serializable {  
  
    @ApiModelProperty(value = "节点ID")  
    public String id;  
  
    @ApiModelProperty(value = "显示内容")  
    public String label;  
  
    @ApiModelProperty(value = "显示内容")  
    public List<TreeItemVo> children = new ArrayList<>();  
  
    @Builder  
    public TreeItemVo(String id, String label, List<TreeItemVo> children) {  
        this.id = id;  
        this.label = label;  
        this.children = children;  
    }  
}

实现方法并且递归

// 部门树  
@Override  
public TreeVo deptTree() {  
    // 将parentDeptNo 赋值,查询条件封装  
    String rootDeptParentId = SuperConstant.ROOT_DEPT_PARENT_ID;  
    DeptDto deptDto = DeptDto.builder()  
            .parentDeptNo(NoProcessing.processString(rootDeptParentId))  
            .build();  
    // 调用查询方法  
    List<DeptVo> deptVoList = getList(deptDto);  
  
    // 查找根节点  
    List<TreeItemVo> items = new ArrayList<>();  
    DeptVo rootDept = deptVoList.stream()  
            .filter(d -> d.getParentDeptNo().equals(rootDeptParentId))  
            .collect(Collectors.toList())  
            .get(0);  
    // 递归调用 (空集合,根节点,数据)  
    recursionTreeItem(items,rootDept,deptVoList);  
  
    return TreeVo.builder().items(items).build();  
}  
  
// 递归  
private void recursionTreeItem(List<TreeItemVo> items, DeptVo rootDept, List<DeptVo> deptVoList) {  
    // 根部门,封装TreeItemVo,后期向里面set子对象  
    TreeItemVo treeItemVo = TreeItemVo.builder()  
                .id(rootDept.getDeptNo())  
                .label(rootDept.getDeptName())  
                .build();  
    // 找子级  
    List<DeptVo> childrenList = deptVoList.stream()  
                .filter(d -> d.getParentDeptNo().equals(rootDept.getDeptNo()))  
                .collect(Collectors.toList());  
    // 不为空,执行递归  
    if (!EmptyUtil.isNullOrEmpty(childrenList)){  
        List<TreeItemVo> listChildrenList = new ArrayList<>();  
        childrenList.forEach(dept -> {  
            // 空集合,父级,父节点=子节点的父节点的数据  
            recursionTreeItem(listChildrenList,dept,deptVoList);  
        });  
        treeItemVo.setChildren(listChildrenList);  
    }  
  
    items.add(treeItemVo);  
}
3、Spring Cache

对于 getList(DeptDto deptDto) 方法:

  • @Cacheable(value = DeptCacheConstant.LIST, key = "#deptDto.hashCode()") 表示使用名为 DeptCacheConstant.LIST 的缓存区域,并且使用 #deptDto.hashCode() 作为缓存的键值。
  • 在方法调用时,系统会首先根据 deptDto 对象的哈希值来检查缓存中是否存在对应的结果,如果存在则直接返回缓存结果,否则执行实际的方法逻辑,并将结果放入缓存中。

对于 deptTree() 方法:

  • @Cacheable(value = DeptCacheConstant.TREE) 表示使用名为 DeptCacheConstant.TREE 的缓存区域,但未指定具体的键值生成策略。
  • 同样,系统会根据默认的键值生成策略来检查缓存中是否存在结果,并根据需要执行实际的方法逻辑。

对于 createDept()表示了在 createDept 方法执行之后,会触发两个缓存区域的清除操作。具体来说:

  • @CacheEvict(value = DeptCacheConstant.LIST, allEntries = true):清除名为 DeptCacheConstant.LIST 的缓存区域中的所有条目。
  • @CacheEvict(value = DeptCacheConstant.TREE, allEntries = true):清除名为 DeptCacheConstant.TREE 的缓存区域中的所有条目。

这样的设计可以确保在执行创建部门操作后,相关的缓存数据得到及时更新,避免脏数据或不一致的情况发生。

// 部门条件查询  
@Cacheable(value = DeptCacheConstant.LIST,key = "#deptDto.hashCode()")  
@Override  
public List<DeptVo> getList(DeptDto deptDto) {  
    return null;  
}  
  
// 部门树  
@Cacheable(value = DeptCacheConstant.TREE)  
@Override  
public TreeVo deptTree() {
	return null;
}

@Transactional  
@Override  
@Caching(evict = {  
        @CacheEvict(value = DeptCacheConstant.LIST,allEntries = true),  
        @CacheEvict(value = DeptCacheConstant.TREE,allEntries = true)  
})  
public Boolean createDept(DeptDto deptDto) {  
    return null;  
}

四、Spring Security JWT集成

  1. 引入所需依赖jwt

  2. 实现 UserDetail,对应需要的数据, 实现UserDetailsService 数据库认证

  3. SecurityConfig 注入自定义认证管理器 AuthenticationManager,登录接口放行

  4. 登录接口,所有认证管理器(会调用UserDetailsService),认证成功生成jwt

  5. jwt授权管理器

一、配置

1-1、引入依赖
<!--JWT-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.1</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!--工具包-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.0.M3</version>
</dependency>
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>
1-2、视图vo类
import com.zzyl.base.BaseVo;  
import io.swagger.annotations.ApiModelProperty;  
import lombok.Builder;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
  
import java.util.List;  
import java.util.Set;  
  
/**  
 * 用户表  
 */  
@Data  
@NoArgsConstructor  
public class UserVo extends BaseVo {  
  
    @ApiModelProperty(value = "用户账号")  
    private String username;  
  
    @ApiModelProperty(value = "密码")  
    private String password;  
  
    @ApiModelProperty(value = "用户类型(0:系统用户,1:客户)")  
    private String userType;  
  
    @ApiModelProperty(value = "用户昵称")  
    private String nickName;  
  
    @ApiModelProperty(value = "用户职位")  
    private String post;  
  
    @ApiModelProperty(value = "用户部门")  
    private String dept;  
  
    @ApiModelProperty(value = "用户邮箱")  
    private String email;  
  
    @ApiModelProperty(value = "真实姓名")  
    private String realName;  
  
    @ApiModelProperty(value = "手机号码")  
    private String mobile;  
  
    @ApiModelProperty(value = "用户性别(0男 1女 2未知)")  
    private String sex;  
  
    @ApiModelProperty(value = "备注")  
    private String remark;  
  
    @ApiModelProperty(value = "选中节点")  
    private String[] checkedIds;  
  
    @ApiModelProperty(value = "三方openId")  
    private String openId;  
  
    @ApiModelProperty(value = "查询用户:用户角色Ids")  
    private Set<String>  roleVoIds;  
  
    @ApiModelProperty(value = "构建令牌:用户角色标识")  
    private Set<String> roleLabels;  
  
    @ApiModelProperty(value = "角色列表")  
    private List<RoleVo> roleList;  
  
    @ApiModelProperty(value = "构建令牌:用户权限路径")  
    private Set<String> resourceRequestPaths;  
  
    @ApiModelProperty(value = "部门编号【当前】")  
    private String deptNo;  
  
    @ApiModelProperty(value = "职位编号【当前】")  
    private String postNo;  
  
    @ApiModelProperty(value = "角色Id【当前】")  
    private Long roleId;  
  
    @ApiModelProperty(value = "用户令牌")  
    private String userToken;  
  
    private String dataState;  
  
    @Builder  
    public UserVo(Long id, String dataState, String username, String password, String userType, String nickName, String post, String dept, String email, String realName, String mobile, String sex, String remark, String[] checkedIds, String openId, Set<String> roleVoIds, Set<String> roleLabels, List<RoleVo> roleList, Set<String> resourceRequestPaths, String deptNo, String postNo, Long roleId, String userToken, String dataState1) {  
        super(id, dataState);  
        this.username = username;  
        this.password = password;  
        this.userType = userType;  
        this.nickName = nickName;  
        this.post = post;  
        this.dept = dept;  
        this.email = email;  
        this.realName = realName;  
        this.mobile = mobile;  
        this.sex = sex;  
        this.remark = remark;  
        this.checkedIds = checkedIds;  
        this.openId = openId;  
        this.roleVoIds = roleVoIds;  
        this.roleLabels = roleLabels;  
        this.roleList = roleList;  
        this.resourceRequestPaths = resourceRequestPaths;  
        this.deptNo = deptNo;  
        this.postNo = postNo;  
        this.roleId = roleId;  
        this.userToken = userToken;  
        this.dataState = dataState1;  
    }  
}
1-3、JWT工具类
import cn.hutool.core.date.DateField;  
import cn.hutool.core.date.DateUtil;  
import io.jsonwebtoken.Claims;  
import io.jsonwebtoken.JwtBuilder;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.SignatureAlgorithm;  
  
import java.nio.charset.StandardCharsets;  
import java.util.Date;  
import java.util.Map;  
  
public class JwtUtil {  
    /**  
     * 生成jwt  
     * 使用Hs256算法, 私匙使用固定秘钥  
     *  
     * @param secretKey jwt秘钥  
     * @param dateOffset jwt过期时间(小时)  
     * @param claims    设置的信息  
     * @return  
     */  
    public static String createJWT(String secretKey , int dateOffset, Map<String, Object> claims) {  
        // 指定签名的时候使用的签名算法,也就是header那部分  
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;  
  
        // 设置jwt的body  
        JwtBuilder builder = Jwts.builder()  
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的  
                .setClaims(claims)  
                // 设置签名使用的签名算法和签名使用的秘钥  
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))  
                // 设置过期时间  
                .setExpiration(DateUtil.offset(new Date(), DateField.HOUR_OF_DAY, dateOffset));  
  
        return builder.compact();  
    }  
  
    /**  
     * Token解密  
     *  
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个  
     * @param token     加密后的token  
     * @return  
     */  
    public static Claims parseJWT(String secretKey, String token) {  
        try {  
            // 得到DefaultJwtParser  
            Claims claims = Jwts.parser()  
                    // 设置签名的秘钥  
                    .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))  
                    // 设置需要解析的jwt  
                    .parseClaimsJws(token).getBody();  
            return claims;  
        } catch (Exception e) {  
//            throw new AccessDeniedException("没有权限,请登录");  
            throw new RuntimeException("没有权限,请登录");  
        }  
    }  
  
}

二、登录实现

2-1、配置类添加自定义认证管理器
import com.zzyl.properties.SecurityConfigProperties;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.context.properties.EnableConfigurationProperties;  
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.configuration.AuthenticationConfiguration;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;  
import org.springframework.security.web.SecurityFilterChain;  
  
/**  
 *  权限核心配置类  
 */  
@Configuration  
@EnableConfigurationProperties(SecurityConfigProperties.class)  
public class SecurityConfig  {  
  
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
  
        http.authorizeHttpRequests()  
                .antMatchers("/**")  
                .permitAll();  
  
  
        http.csrf().disable();  
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS );//关闭session  
        http.headers().cacheControl().disable();//关闭缓存  
  
        return http.build();  
    }  
  
    @Bean  
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {  
        return authenticationConfiguration.getAuthenticationManager();  
    }  
  
    /**  
     * BCrypt密码编码  
     * @return  
     */  
    @Bean  
    public BCryptPasswordEncoder bcryptPasswordEncoder() {  
        return new BCryptPasswordEncoder();  
    }  
  
}
2-2、自定义UserDetails
import com.zzyl.utils.EmptyUtil;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.core.authority.SimpleGrantedAuthority;  
import org.springframework.security.core.userdetails.UserDetails;  
  
import java.time.LocalDateTime;  
import java.util.ArrayList;  
import java.util.Collection;  
  
/**  
 *  自定认证用户  
 */  
@Data  
@NoArgsConstructor  
public class UserAuth implements UserDetails {  
  
    private String id;  
  
    /**  
     * 用户账号  
     */  
    private String username;  
  
    /**  
     * 密码  
     */  
    private String password;  
  
    /**  
     * 权限内置  
     */  
    private Collection<SimpleGrantedAuthority> authorities;  
  
    /**  
     * 用户类型(00系统用户)  
     */  
    private String userType;  
  
    /**  
     * 用户昵称  
     */  
    private String nickName;  
  
    /**  
     * 用户邮箱  
     */  
    private String email;  
  
    /**  
     * 真实姓名  
     */  
    private String realName;  
  
    /**  
     * 手机号码  
     */  
    private String mobile;  
  
    /**  
     * 用户性别(0男 1女 2未知)  
     */  
    private String sex;  
  
    /**  
     * 创建者  
     */  
    private Long createBy;  
  
    /**  
     * 创建时间  
     */  
    private LocalDateTime createTime;  
  
    /**  
     * 更新者  
     */  
    private Long updateBy;  
  
    /**  
     * 更新时间  
     */  
    private LocalDateTime updateTime;  
  
    /**  
     * 备注  
     */  
    private String remark;  
  
    /**  
     * 部门编号【当前】  
     */  
    private String deptNo;  
  
    /**  
     * 职位编号【当前】  
     */  
    private String postNo;  
  
    public UserAuth(UserVo userVo) {  
        this.setId(userVo.getId().toString());  
        this.setUsername(userVo.getUsername());  
        this.setPassword(userVo.getPassword());  
        if (!EmptyUtil.isNullOrEmpty(userVo.getResourceRequestPaths())) {  
            authorities = new ArrayList<>();  
            userVo.getResourceRequestPaths().forEach(resourceRequestPath -> authorities.add(new SimpleGrantedAuthority(resourceRequestPath)));  
        }  
        this.setUserType(userVo.getUserType());  
        this.setNickName(userVo.getNickName());  
        this.setEmail(userVo.getEmail());  
        this.setRealName(userVo.getRealName());  
        this.setMobile(userVo.getMobile());  
        this.setSex(userVo.getSex());  
        this.setCreateTime(userVo.getCreateTime());  
        this.setCreateBy(userVo.getCreateBy());  
        this.setUpdateTime(userVo.getUpdateTime());  
        this.setUpdateBy(userVo.getUpdateBy());  
        this.setRemark(userVo.getRemark());  
        this.setDeptNo(userVo.getDeptNo());  
        this.setPostNo(userVo.getPostNo());  
    }  
  
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() {  
        return this.authorities;  
    }  
  
    @Override  
    public String getPassword() {  
        return this.password;  
    }  
  
    @Override  
    public String getUsername() {  
        return this.username;  
    }  
  
    @Override  
    public boolean isAccountNonExpired() {  
        return true;  
    }  
  
    @Override  
    public boolean isAccountNonLocked() {  
        return true;  
    }  
  
    @Override  
    public boolean isCredentialsNonExpired() {  
        return true;  
    }  
  
    @Override  
    public boolean isEnabled() {  
        return true;  
    }  
}
2-3、配置数据库认证
import com.zzyl.service.UserService;  
import com.zzyl.vo.UserAuth;  
import com.zzyl.vo.UserVo;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.security.core.userdetails.UserDetailsService;  
import org.springframework.security.core.userdetails.UsernameNotFoundException;  
import org.springframework.stereotype.Component;  
  
/**  
 * @author : like  
 * @date : 2024/3/24 19:37  
 * @Version: 1.0  
 */@Component  
public class UserDetailsServiceImpl implements UserDetailsService {  
  
    @Autowired  
    UserService userService;  
  
    @Override  
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
        //这里我们默认使用账号密码登录,对于多种登录方式如何处理-->字符串分割  
        UserVo userVo = userService.findUserVoForLogin(username);  
        return new UserAuth(userVo);  
    }  
}
2-4、jwt配置文件
zzyl:  
  framework:  
    jwt:  
      base64-encoded-secret-key: $2a$10$PVtHnkj86mJgf6li/yron.LRx/cQAlaiZkBJ9BeogCNTryXJRT1YC  
      ttl: 3600000

读取配置文件

import lombok.Getter;  
import lombok.NoArgsConstructor;  
import lombok.Setter;  
import lombok.ToString;  
import org.springframework.boot.context.properties.ConfigurationProperties;  
import org.springframework.context.annotation.Configuration;  
  
import java.io.Serializable;  
  
/**  
 * jw配置文件  
 */  
@Setter  
@Getter  
@NoArgsConstructor  
@ToString  
@Configuration  
@ConfigurationProperties(prefix = "zzyl.framework.jwt")  
public class JwtTokenManagerProperties implements Serializable {  
  
    /**  
     *  签名密码  
     */  
    private String base64EncodedSecretKey;  
  
    /**  
     *  有效时间  
     */  
    private Integer ttl;  
}
2-5、登录实现
  1. 使用认证管理器,认证

  2. 获取用户数据

  3. 获取用户资源列表 获取用户角色列表

  4. 生成token,参数定义在 配置文件中 (敏感数据处理 )

  5. 存redis,使用uuid,不用将jwt传到前端,需要校验时,用uuid获取jwt

import cn.hutool.core.bean.BeanUtil;  
import cn.hutool.json.JSONUtil;  
import com.zzyl.constant.UserCacheConstant;  
import com.zzyl.dto.LoginDto;  
import com.zzyl.exception.BaseException;  
import com.zzyl.properties.JwtTokenManagerProperties;  
import com.zzyl.service.LoginService;  
import com.zzyl.service.ResourceService;  
import com.zzyl.service.RoleService;  
import com.zzyl.utils.JwtUtil;  
import com.zzyl.vo.ResourceVo;  
import com.zzyl.vo.RoleVo;  
import com.zzyl.vo.UserAuth;  
import com.zzyl.vo.UserVo;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.redis.core.StringRedisTemplate;  
import org.springframework.security.authentication.AuthenticationManager;  
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.core.Authentication;  
import org.springframework.stereotype.Service;  
  
import java.util.*;  
import java.util.concurrent.TimeUnit;  
import java.util.stream.Collectors;  
  
/**  
 * @author : like  
 * @date : 2024/3/24 19:33  
 * @Version: 1.0  
 */
 @Service  
public class LoginServiceImpl implements LoginService {  
  
    @Autowired  
    private AuthenticationManager authenticationManager;  
  
    @Autowired  
    private ResourceService resourceService;  
  
    @Autowired  
    private RoleService roleService;  
  
    @Autowired  
    private JwtTokenManagerProperties jwtTokenManagerProperties;  
  
    @Autowired  
    private StringRedisTemplate redisTemplate;  
  
    @Override  
    public UserVo login(LoginDto loginDto) {  
        // 1.使用认证管理器,认证  
        UsernamePasswordAuthenticationToken upat  
                = new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());  
        Authentication authenticate = authenticationManager.authenticate(upat);  
        if (!authenticate.isAuthenticated()){  
            throw new BaseException("用户登录失败");  
        }  
        // 2.获取用户数据  
        UserAuth userAuth = (UserAuth) authenticate.getPrincipal();  
        UserVo userVo = BeanUtil.toBean(userAuth, UserVo.class); // hutool 工具包  
  
        // 3.获取用户资源列表  
        List<ResourceVo> resourceVoList = resourceService.findResourceVoListByUserId(userVo.getId());  
        Set<String> resourceRequestPaths = resourceVoList.stream()  
                    .filter(r -> "r".equals(r.getResourceType()))  
                    .map(ResourceVo::getRequestPath)  
                    .collect(Collectors.toSet());  
        userVo.setResourceRequestPaths(resourceRequestPaths);  
        // 3.1。获取用户角色列表  
        List<RoleVo> roleVoList = roleService.findRoleVoListByUserId(userVo.getId());  
        Set<String> roleLabelSet = roleVoList.stream()  
                .map(RoleVo::getLabel)  
                .collect(Collectors.toSet());  
        userVo.setRoleLabels(roleLabelSet);
		userVo.setRoleList(roleVoList);
  
        // 4.生成token,参数定义在 配置文件中  
        String uuidToken = UUID.randomUUID().toString();  
        userVo.setUserToken(uuidToken);  
        // 敏感数据处理  
        userVo.setPassword("");  
  
        Map<String,Object> clamis = new HashMap<>();  
        clamis.put("currentUser",JSONUtil.toJsonStr(userVo));  
  
        String jwtToken = JwtUtil.createJWT(  
                jwtTokenManagerProperties.getBase64EncodedSecretKey(),  
                jwtTokenManagerProperties.getTtl(),  
                clamis);  
  
        // 5.存redis,使用uuid,不用将jwt传到前端,需要校验时,用uuid获取jwt  
        Long ttl = Long.valueOf(jwtTokenManagerProperties.getTtl() / 1000);  
  
        String userTokenKey = UserCacheConstant.USER_TOKEN + userVo.getUsername();  
        redisTemplate.opsForValue().set(userTokenKey,uuidToken,ttl, TimeUnit.SECONDS);  
  
        String jwtTokenKey = UserCacheConstant.JWT_TOKEN + uuidToken;  
        redisTemplate.opsForValue().set(jwtTokenKey,jwtToken,ttl,TimeUnit.SECONDS);  
  
        return userVo;  
    }  
}

三、用户数据存入线程中

3-1、ThreadLocal工具类
package com.zzyl.utils;  
  
import com.alibaba.fastjson.JSONObject;  
import com.zzyl.base.BaseVo;  
import lombok.extern.slf4j.Slf4j;  
  
/**  
 * subjectContent.java *  用户主体对象  
 */  
@Slf4j  
public class UserThreadLocal {  
  
  
    /***  
     *  创建线程局部userVO变量  
     */  
    public static ThreadLocal<String> subjectThreadLocal = new ThreadLocal<String>() {  
        @Override  
        protected String initialValue() {  
            return null;  
        }  
    };  
  
    // 提供线程局部变量set方法  
    public static void setSubject(String subject) {  
  
        subjectThreadLocal.set(subject);  
    }  
    // 提供线程局部变量get方法  
    public static String getSubject() {  
  
        return subjectThreadLocal.get();  
    }  
  
    //清空当前线程,防止内存溢出  
    public static void removeSubject() {  
  
        subjectThreadLocal.remove();  
    }  
    private static final ThreadLocal<Long> LOCAL = new ThreadLocal<>();  
  
    private UserThreadLocal() {  
  
    }  
  
    /**  
     * 将authUserInfo放到ThreadLocal中  
     *  
     * @param authUserInfo {@link Long}  
     */    public static void set(Long authUserInfo) {  
        LOCAL.set(authUserInfo);  
    }  
  
    /**  
     * 从ThreadLocal中获取authUserInfo  
     */    public static Long get() {  
        return LOCAL.get();  
    }  
  
    /**  
     * 从当前线程中删除authUserInfo  
     */    public static void remove() {  
        LOCAL.remove();  
    }  
  
    /**  
     * 从当前线程中获取前端用户id  
     * @return 用户id  
     */    public static Long getUserId() {  
        return LOCAL.get();  
    }  
  
    /**  
     * 从当前线程中获取前端后端id  
     * @return 用户id  
     */    public static Long getMgtUserId() {  
        String subject = subjectThreadLocal.get();  
        if (ObjectUtil.isEmpty(subject)) {  
            return null;  
        }  
        BaseVo baseVo = JSONObject.parseObject(subject, BaseVo.class);  
        return baseVo.getId() ;  
    } 
}
3-2、定义登录拦截器将数据存ThreadLocal
  1. 该拦截器实现了HandlerInterceptor接口,覆盖了preHandle和afterCompletion方法,分别用于处理请求前和请求后的逻辑。

  2. 在preHandle方法中,首先判断处理器(handler)是否为MethodHandle类型,如果不是,则直接返回true,表示继续处理下一个拦截器或执行Controller。这样可以避免拦截非Controller的请求。

  3. 然后从请求的header中获取名为SecurityConstant.USER_TOKEN的token(通常是用户的身份验证token),并从Redis中获取对应的token。如果获取到了token,则解析其中的数据并将用户信息存入ThreadLocal中,以便后续的Controller可以使用。

  4. 在afterCompletion方法中,移除当前线程中的用户信息,以确保线程安全和数据隔离。

import com.zzyl.constant.SecurityConstant;  
import com.zzyl.constant.UserCacheConstant;  
import com.zzyl.properties.JwtTokenManagerProperties;  
import com.zzyl.utils.JwtUtil;  
import com.zzyl.utils.ObjectUtil;  
import com.zzyl.utils.UserThreadLocal;  
import io.jsonwebtoken.Claims;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.redis.core.StringRedisTemplate;  
import org.springframework.stereotype.Component;  
import org.springframework.web.method.HandlerMethod;  
import org.springframework.web.servlet.HandlerInterceptor;  
  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
  
/**  
 * 登录拦截器:将数据存入线程  
 */  
@Component  
public class UserTokenIntercept implements HandlerInterceptor {  
  
    @Autowired  
    private StringRedisTemplate redisTemplate;  
  
    @Autowired  
    private JwtTokenManagerProperties jwtTokenManagerProperties;  
  
    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        // 只拦截 controller 请求  
        if (!(handler instanceof HandlerMethod)){  
            return true;  
        }  
        // 从 header获取 uuid的token  
        String uuidToken = request.getHeader(SecurityConstant.USER_TOKEN);// authorization  
        if (!ObjectUtil.isEmpty(uuidToken)){  
            String key = UserCacheConstant.JWT_TOKEN + uuidToken;  
            String token = redisTemplate.opsForValue().get(key);  
            if (!ObjectUtil.isEmpty(token)){  
                // 读取数据  
                Claims claims = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), token);  
                String currentUserStr = String.valueOf(claims.get("currentUser")); // 根据存的名修改  
  
                // 存入ThreadLocal中  
                UserThreadLocal.setSubject(currentUserStr);  
            }  
        }  
        return true;  
    }  
  
  
  
    @Override  
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {  
        // 移除当前线程中的参数  
        UserThreadLocal.remove();  
    }  
}
3-3、定义WebMvcConfig
  1. 导入必要的包和类: 首先导入了一些需要的类和包,包括拦截器、Jackson序列化相关的类以及一些Spring MVC的配置类。

  2. 定义WebMvcConfig类: 定义了一个名为WebMvcConfig的类,并使用了@Configuration注解,表明这是一个配置类。

  3. 实现WebMvcConfigurer接口: WebMvcConfig类实现了WebMvcConfigurer接口,这意味着它可以对Spring MVC进行高级配置。

  4. 注入拦截器: 在类中注入了UserInterceptor和UserTokenIntercept拦截器。

  5. 定义拦截器规则: 在addInterceptors方法中,配置了拦截器的规则。分别为后台和小程序端定义了不同的拦截规则,包括需要排除的路径和需要拦截的路径。

  6. 定义资源路径映射: 在addResourceHandlers方法中,配置了资源路径的映射,以支持webjars、swagger等资源。

  7. 配置Jackson序列化和反序列化规则: 在jackson2ObjectMapperBuilderCustomizer方法中,配置了Jackson的序列化和反序列化规则,包括对Long、LocalDateTime、LocalDate等类型的数据进行特殊处理。

import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;  
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;  
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;  
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;  
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;  
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;  
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;  
import com.zzyl.intercept.UserInterceptor;  
import com.zzyl.intercept.UserTokenIntercept;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.ComponentScan;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;  
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;  
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;  
  
import java.math.BigInteger;  
import java.time.LocalDate;  
import java.time.LocalDateTime;  
import java.time.LocalTime;  
import java.time.format.DateTimeFormatter;  
  
/**  
 * webMvc高级配置  
 */  
@Configuration  
@ComponentScan("springfox.documentation.swagger.web")  
public class WebMvcConfig implements WebMvcConfigurer {  
  
    @Autowired  
    UserInterceptor userInterceptor;  
  
    @Autowired  
    private UserTokenIntercept userTokenIntercept;  
  
  
    // 后台 滤掉swagger相关路径和登录相关接口  
    private static final String[] ADMIN_EXCLUDE_PATH_PATTERNS = new String[]{  
            "/swagger-ui.html",  
            "/webjars/**",  
            "/swagger-resources",  
            "/v2/api-docs",  
            "/customer/**",  
            "/security/login",  
            "/common/**",  
            "/user/refresh/**"};  
  
    // 小程序拦截 滤掉swagger相关路径和登录相关接口  
    private static final String[] EXCLUDE_PATH_PATTERNS = new String[]{  
            "/swagger-ui.html",  
            "/webjars/**",  
            "/swagger-resources",  
            "/v2/api-docs",  
            // 登录接口  
            "/customer/user/login",  
            // 房型列表接口  
            "/customer/roomTypes",  
            "/customer/orders/project/**",  
            "/user/refresh/**"};  
  
    /**  
     * 拦截器  
     */  
    @Override  
    public void addInterceptors(InterceptorRegistry registry) {  
  
        // 后台拦截器  
        registry.addInterceptor(userTokenIntercept).excludePathPatterns(ADMIN_EXCLUDE_PATH_PATTERNS).addPathPatterns("/**");  
  
        // 小程序端接口鉴权拦截器  
        registry.addInterceptor(userInterceptor).excludePathPatterns(EXCLUDE_PATH_PATTERNS).addPathPatterns("/customer/**");  
    }  
  
  
    /**  
     * 资源路径 映射  
     */  
    @Override  
    public void addResourceHandlers(ResourceHandlerRegistry registry) {  
        //支持webjars  
        registry.addResourceHandler("/webjars/**")  
                .addResourceLocations("classpath:/META-INF/resources/webjars/");  
        //支持swagger  
        registry.addResourceHandler("swagger-ui.html")  
                .addResourceLocations("classpath:/META-INF/resources/");  
        //支持小刀  
        registry.addResourceHandler("doc.html")  
                .addResourceLocations("classpath:/META-INF/resources/");  
    }  
  
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";  
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";  
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";  
  
    @Bean  
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {  
        return builder -> {  
            // 序列化  
            builder.serializerByType(Long.class, ToStringSerializer.instance);  
            builder.serializerByType(BigInteger.class, ToStringSerializer.instance);  
            builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));  
            builder.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));  
            builder.serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));  
  
            // 反序列化  
            builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));  
            builder.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));  
            builder.deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));  
  
        };  
    }  
}

四、自定义授权管理器

4-1、自定义授权管理器
  1. 获取uuidToken
  2. 根据uuidToken获取jwtTokenkey从而获取JwtToken
  3. 解析数据
  4. 判断前端的uuid和后端的uuid是否一致
  5. 判断token的过期时间,为jwt续期
  6. 封装当前请求路径
  7. 验证用户是否有权限
import cn.hutool.core.text.AntPathMatcher;  
import cn.hutool.json.JSONUtil;  
import com.zzyl.constant.SecurityConstant;  
import com.zzyl.constant.UserCacheConstant;  
import com.zzyl.properties.JwtTokenManagerProperties;  
import com.zzyl.utils.JwtUtil;  
import com.zzyl.utils.ObjectUtil;  
import com.zzyl.vo.UserVo;  
import io.jsonwebtoken.Claims;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.redis.core.StringRedisTemplate;  
import org.springframework.security.authorization.AuthorizationDecision;  
import org.springframework.security.authorization.AuthorizationManager;  
import org.springframework.security.core.Authentication;  
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;  
import org.springframework.stereotype.Component;  
  
import javax.servlet.http.HttpServletRequest;  
import java.util.HashMap;  
import java.util.Map;  
import java.util.concurrent.TimeUnit;  
import java.util.function.Supplier;  
  
/**  
 *  授权管理器  
 */  
@Slf4j  
@Component  
public class JwtAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {  
  
    @Autowired  
    private StringRedisTemplate redisTemplate;  
  
    @Autowired  
    private JwtTokenManagerProperties jwtTokenManagerProperties;  
  
    // 路径匹配  
    private AntPathMatcher antPathMatcher = new AntPathMatcher();  
  
    @Override  
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {  
  
        // 1、获取uuidToken  
        HttpServletRequest request = requestAuthorizationContext.getRequest();  
        String uuidToken = request.getHeader(SecurityConstant.USER_TOKEN); // 获取"authorization";  
        if (ObjectUtil.isEmpty(uuidToken)){  
            return new AuthorizationDecision(false);  
        }  
        // 2、从redis获取jwtToken  
        String jwtTokenKey = UserCacheConstant.JWT_TOKEN + uuidToken;  
        String jwtToken = redisTemplate.opsForValue().get(jwtTokenKey);  
        if (ObjectUtil.isEmpty(jwtToken)){  
            return new AuthorizationDecision(false);  
        }  
        // 3、解析数据  
        Claims claims = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtToken);  
        UserVo userVo = JSONUtil.toBean(String.valueOf(claims.get("currentUser")), UserVo.class);  
  
        // 4、判断前端的uuid和后端的uuid是否一致(安全性)  
        String redisUuidToken = redisTemplate.opsForValue().get(UserCacheConstant.USER_TOKEN + userVo.getUserToken());  
        if (!uuidToken.equals(redisUuidToken)){  
            return new AuthorizationDecision(false);  
        }  
  
        // 5、判断token的过期时间,为jwt续期,避免二次登录浪费时间(实用性)  
        // 重新生成 JwtToken,uuidToken重新设置过期时间  
        Long expire = redisTemplate.opsForValue().getOperations().getExpire(jwtTokenKey);  
        if (expire <= 600){  
            Map<String, Object> clamis = new HashMap<>();  
            clamis.put("currentUser",String.valueOf(clamis.get("currentUser")));  
            // 生成新的jwt newJwtToken  
            String newJwtToken = JwtUtil.createJWT(  
                    jwtTokenManagerProperties.getBase64EncodedSecretKey(),  
                    jwtTokenManagerProperties.getTtl(),  
                    clamis);  
            Long ttl = Long.valueOf(jwtTokenManagerProperties.getTtl() / 1000);  
            // 将新的jwtToken存redis  
            redisTemplate.opsForValue().set(jwtTokenKey,newJwtToken,ttl,TimeUnit.SECONDS);  
            // 为uuid更新过期时间  
            redisTemplate.expire(UserCacheConstant.USER_TOKEN+userVo.getUsername(),ttl,TimeUnit.SECONDS);  
  
        }  
  
        // 当前请求的路径: GET/nursing_project/**        String method = request.getMethod(); // GET  
        String requestURI = request.getRequestURI(); // /nursing_project/**  
        String targetUrl = method + requestURI;  
  
        // 验证用户是否有权限  
        for (String resourceRequestPath : userVo.getResourceRequestPaths()){  
            boolean isMatch = antPathMatcher.match(resourceRequestPath, targetUrl);  
            if (isMatch){  
                return new AuthorizationDecision(true);  
            }  
        }  
        return new AuthorizationDecision(false);  
    }  
}
4-2、配置放行的url

所以,我们需要在上述配置中添加放行的url列表,为了更方便的维护,我们可以放到application.yml文件中,如下:

 zzyl:  
   framework:  
     security:  
       ignore-url:  
 #        - /**  
         - /resource/menus/**  
         - /resource/myButten/**  
         - /customer/**  
         - /security/login  
         - /security/logout  
         - /doc.html  
         - /*-swagger/**  
         - /swagger-resources  
         - /v2/api-docs  
         - /webjars/**  
         - /common/**  
         - /ws/**
4-3、新增对应的properties配置类
import lombok.Data;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.boot.context.properties.ConfigurationProperties;  
import org.springframework.context.annotation.Configuration;  
  
import java.util.ArrayList;  
import java.util.List;  
  
/**  
 *  忽略配置及跨域  
 */  
@Slf4j  
@Data  
@ConfigurationProperties(prefix = "zzyl.framework.security")  
@Configuration  
public class SecurityConfigProperties {  
  
    String defaulePassword ;  
  
    List<String> ignoreUrl = new ArrayList<>();  
  
    List<String> origins = new ArrayList<>();  
  
    String loginPage;  
  
    //令牌有效时间  
    Integer accessTokenValiditySeconds = 3*24*3600;  
  
    //刷新令牌有效时间  
    Integer refreshTokenValiditySeconds= 7*24*3600;  
}
4-4、SecurityConfig 添加 授权管理器
  • .antMatchers( ignoreUrl.toArray(new String[ignoreUrl.size()]) ) 获取放行的url
  • .anyRequest().access(jwtAuthorizationManager); 注入自定义授权管理器
import com.zzyl.properties.SecurityConfigProperties;  
import com.zzyl.security.JwtAuthorizationManager;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.context.properties.EnableConfigurationProperties;  
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.configuration.AuthenticationConfiguration;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;  
import org.springframework.security.web.SecurityFilterChain;  
  
import java.util.List;  
  
/**  
 *  权限核心配置类  
 */  
@Configuration  
@EnableConfigurationProperties(SecurityConfigProperties.class)  
public class SecurityConfig  {  
  
    @Autowired  
    JwtAuthorizationManager jwtAuthorizationManager;  
  
    @Autowired  
    private SecurityConfigProperties securityConfigProperties;  
  
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
  
        List<String> ignoreUrl = securityConfigProperties.getIgnoreUrl();  
  
        http.authorizeHttpRequests()  
                .antMatchers( ignoreUrl.toArray(new String[ignoreUrl.size()]) )  
                .permitAll()  
                // 指定对于任何请求,使用jwtAuthorizationManager来进行访问控制的判断。  
                .anyRequest().access(jwtAuthorizationManager);  
  
        http.csrf().disable();  
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS );//关闭session  
        http.headers().cacheControl().disable();//关闭缓存  
  
        return http.build();  
    }  
  
    @Bean  
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {  
        return authenticationConfiguration.getAuthenticationManager();  
    }  
  
    /**  
     * BCrypt密码编码  
     * @return  
     */  
    @Bean  
    public BCryptPasswordEncoder bcryptPasswordEncoder() {  
        return new BCryptPasswordEncoder();  
    }  
}

五、数据权限

5-1、权限分类
  • 全部(状态为4):我们查询数据不做任何限制

  • 本人数据(状态为1):目前的表中,都有一个字段叫做create_by,获取当前登录人的id去匹配即可

and create_by = ${userId}
  • 本级(状态3):在当前表中需要设置一个部门编号,根据当前登录人查询对应部门编号,再到表中去匹配即可
OR dept_no = ${deptNo}
  • 本级及以下(状态2),找到当前表中的部门编号,再找到部门下的子部门编号进行匹配
OR dept_no IN ( SELECT dept_no FROM sys_dept WHERE dept_no = ${deptNo} or parent_dept_no like ${deptNo}
  • 自定义(状态0):需要进行关联查询;根据当前登录人找到对应的角色,然后通过角色再查询关联的部门列表,中间表为:sys_role_dept
 OR dept_no IN ( SELECT dept_no FROM sys_role_dept WHERE role_id = ${roleId} ) 
示例:
select * from retreat where 1=1 OR dept_no IN ( SELECT dept_no FROM sys_role_dept WHERE role_id = ${roleId} )
5-2、定义注解 @DataScope
import java.lang.annotation.*;  
  
/**  
 * DataScope * @describe: 数据权限过滤注解  
 **/  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface DataScope {  
  
    public String deptAlias() default "";  
  
    public String userAlias() default "";  
}
5-3、定义注解切入点
  1. dataScopeFilter方法根据用户拥有的角色和数据权限设置,构建相应的SQL字符串来过滤数据:

    • 根据不同的数据权限级别,拼接不同的SQL条件,例如:只能查看自己的数据、只能查看本部门数据等。
    • 将生成的SQL字符串放入BaseDto的params中,以便在查询操作中使用。
  2. getAnnotationLog方法用于获取被@DataScope注解标记的方法上的注解信息。

import cn.hutool.json.JSONUtil;  
import com.zzyl.base.BaseDto;  
import com.zzyl.base.DataScope;  
import com.zzyl.utils.NoProcessing;  
import com.zzyl.utils.ObjectUtil;  
import com.zzyl.utils.StringUtils;  
import com.zzyl.utils.UserThreadLocal;  
import com.zzyl.vo.RoleVo;  
import com.zzyl.vo.UserVo;  
import org.aspectj.lang.JoinPoint;  
import org.aspectj.lang.Signature;  
import org.aspectj.lang.annotation.Aspect;  
import org.aspectj.lang.annotation.Before;  
import org.aspectj.lang.annotation.Pointcut;  
import org.aspectj.lang.reflect.MethodSignature;  
import org.springframework.stereotype.Component;  
  
import java.lang.reflect.Method;  
  
/**  
 * DataScopeAspect * @author itheima  
 **/@Aspect  
@Component  
public class DataScopeAspect {  
  
    /**  
     * 自定数据权限  
     */  
    public static final String DATA_SCOPE_CUSTOM = "0";  
  
    /**  
     * 仅本人数据权限  
     */  
    public static final String DATA_SCOPE_SELF = "1";  
  
    /**  
     * 部门及以下数据权限  
     */  
    public static final String DATA_SCOPE_DEPT_AND_CHILD = "2";  
  
    /**  
     * 部门数据权限  
     */  
    public static final String DATA_SCOPE_DEPT = "3";  
  
    /**  
     * 全部数据权限  
     */  
    public static final String DATA_SCOPE_ALL = "4";  
  
  
    /**  
     * 数据权限过滤关键字  
     */  
    public static final String DATA_SCOPE = "dataScope";  
  
    // 配置织入点  
    @Pointcut("@annotation(com.zzyl.base.DataScope)")  
    public void dataScopePointCut() {  
    }  
  
    //@Before 注解标记的方法会在目标方法执行之前执行 handleDataScope(point);方法  
    @Before("dataScopePointCut()")  
    public void doBefore(JoinPoint point) throws Throwable {  
        handleDataScope(point);  
    }  
  
    /**  
     * 1、对加入 @DataScope 注解的方法 先进入这个方法  
     * @param joinPoint  
     */  
    protected void handleDataScope(final JoinPoint joinPoint) {  
        // 获得注解  
        DataScope controllerDataScope = getAnnotationLog(joinPoint);  
        if (controllerDataScope == null) {  
            return;  
        }  
        // 获取当前的用户  
        String subject = UserThreadLocal.getSubject();  
        UserVo userVo = JSONUtil.toBean(subject, UserVo.class);  
        // 如果是超级管理员,则不过滤数据  
        if (StringUtils.isNotNull(userVo) && !userVo.getUsername().equals("admin")) {  
            dataScopeFilter(  
                    joinPoint,  
                    userVo,  
                    controllerDataScope.deptAlias(),  
                    controllerDataScope.userAlias());  
        }  
    }  
  
    /**  
     * 3、数据范围过滤  
     *  
     * @param joinPoint 切点  
     * @param user      用户  
     * @param userAlias 别名  
     */  
    public static void dataScopeFilter(JoinPoint joinPoint, UserVo user, String deptAlias, String userAlias) {  
        System.out.println("过滤数据---------------");  
        StringBuilder sqlString = new StringBuilder();  
  
        for (RoleVo role : user.getRoleList()) {  
            String dataScope = role.getDataScope();//拥有的数据权限  
            // 如果是全部数据权限,则不过滤数据  
            if (DATA_SCOPE_ALL.equals(dataScope)) {  
                sqlString = new StringBuilder();  
                break;  
                // 如果是自定数据权限,则只查看自己的数据  
            } else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {  
                sqlString.append(" OR dept_no IN ( SELECT dept_no FROM sys_role_dept WHERE role_id = " + role.getId() + " ) ");  
                // 如果是部门数据权限,则只查看本部门数据  
            } else if (DATA_SCOPE_DEPT.equals(dataScope)) {  
                sqlString.append(" and (dept_no = " + user.getDeptNo() + ") ");  
                // 如果是部门及以下数据权限,则查看本部门以及下级数据  
            } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {  
                String str = NoProcessing.processString(user.getDeptNo()) + "%";  
                sqlString.append(  
                        " OR dept_no IN ( SELECT dept_no FROM sys_dept WHERE dept_no = " + user.getDeptNo() + " or parent_dept_no like  '" + str + "')");  
                // 如果是仅本人数据权限,则只查看本人的数据  
            } else if (DATA_SCOPE_SELF.equals(dataScope)) {//  or u.user_id = 登录用户id  
                sqlString.append(" and ( create_by = " + user.getId()+")");  
            }  
        }  
  
        if (StringUtils.isNotBlank(sqlString.toString())) {  
            Object params = joinPoint.getArgs()[0];  //获取第一个参数  要求一定得是一个BaseEntity  在Service执行前 则就已经加上了 Sql   or u.user_id = 登录用户id  
            if (ObjectUtil.isNotNull(params) && params instanceof BaseDto) {  
                BaseDto baseDto = (BaseDto) params;  
                baseDto.getParams().put(DATA_SCOPE,  sqlString);  
                // todo 给 BaseDto put 进入一个 key为:DATA_SCOPE,值为sqlString 的字段  
            }  
        }  
    }  
  
    /**  
     * 2、是否存在注解,如果存在就获取  
     */  
    private DataScope getAnnotationLog(JoinPoint joinPoint) {  
        Signature signature = joinPoint.getSignature();  
        MethodSignature methodSignature = (MethodSignature) signature;  
        Method method = methodSignature.getMethod();  
  
        if (method != null) {  
            return method.getAnnotation(DataScope.class);  
        }  
        return null;  
    }  
}
5-4、退住管理示例

controller

@ApiOperation(value = "退住管理", notes = "退住管理列表页")  
@PostMapping("/selectByPage")  
@DataScope  
public ResponseResult selectByPage(@RequestBody RetreatReqDto retreatReqDto) {  
    // 1、RetreatReqDto extends BaseDto  
    // 2、在DataScopeAspect 里面 baseDto.getParams().put(DATA_SCOPE,  sqlString);      
    // 3、 <if test="params.dataScope != null">    
    //            ${params.dataScope}    
    //        </if>    
    // todo RetreatReqDto 里面有一个 dataScope 字段,存储sql拼接  
    return retreatService.selectByPage(retreatReqDto);  
}

实现类

/**  
 * 退住管理列表查询  
 * @return  
 */  
@Override  
public ResponseResult selectByPage(RetreatReqDto retreatReqDto) {  
    PageHelper.startPage(retreatReqDto.getPageNum(), retreatReqDto.getPageSize());  
    Page<List<Retreat>> pages = retreatMapper.selectByPage(retreatReqDto);  
    PageResponse<Retreat> of = PageResponse.of(pages, Retreat.class);  
    return ResponseResult.success(of);  
}

mapper

<if test="params.dataScope != null">  
    ${params.dataScope}  
</if>  
<select id="selectByPage" resultMap="BaseResultMap">  
    select  
    <include refid="Base_Column_List"/>  
    from retreat  
    where status = 2    <if test="retreatCode != null and retreatCode != ''">  
        AND retreat_code = #{retreatCode}  
    </if>  
    <if test="name != null">  
        and name like concat('%', #{name}, '%')  
    </if>  
    <if test="idCardNo != null and idCardNo != ''">  
        AND id_card_no = #{idCardNo}  
    </if>  
    <if test="startTime != null and endTime != null">  
        and check_out_time between #{startTime} and #{endTime}  
    </if>  
    <if test="params.dataScope != null">  
        ${params.dataScope}  
    </if>  
    order by create_time desc  
</select>

五、Activiti7集成

一、依赖配置

	<dependency>
		<groupId>org.activiti</groupId>
		<artifactId>activiti-spring-boot-starter</artifactId>
		<version>7.10.0</version>
	</dependency>

<!--如果activiti依赖下载不了,可以配置如下地址进行下载-->
    <repositories>
        <repository>
            <id>activiti-releases</id>
            <url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases</url>
        </repository>
    </repositories>

二、生成流程图(入住)

使用bpmnjs工具生成bpmn文件,和截图保存在resource/bpmn里面

入住申请流程 bpmn 文件

<?xml version="1.0" encoding="UTF-8"?>  
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:activiti="http://activiti.org/bpmn" id="sample-diagram" targetNamespace="http://activiti.org/bpmn" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">  
  <bpmn2:process id="checkIn-new" isExecutable="true">  
    <bpmn2:startEvent id="StartEvent_1">  
      <bpmn2:outgoing>Flow_0hsq6o3</bpmn2:outgoing>  
    </bpmn2:startEvent>  
    <bpmn2:userTask id="Activity_1kgujpb" name="发起人申请" activiti:formKey="0" activiti:assignee="${assignee0}">  
      <bpmn2:incoming>Flow_0hsq6o3</bpmn2:incoming>  
      <bpmn2:incoming>Flow_0a87sox</bpmn2:incoming>  
      <bpmn2:outgoing>Flow_0agpc1o</bpmn2:outgoing>  
    </bpmn2:userTask>  
    <bpmn2:sequenceFlow id="Flow_0hsq6o3" sourceRef="StartEvent_1" targetRef="Activity_1kgujpb" />  
    <bpmn2:userTask id="Activity_1a3ot1n" name="入住评估-处理" activiti:formKey="1" activiti:assignee="${assignee1}">  
      <bpmn2:incoming>Flow_0agpc1o</bpmn2:incoming>  
      <bpmn2:outgoing>Flow_0kn6i1r</bpmn2:outgoing>  
    </bpmn2:userTask>  
    <bpmn2:sequenceFlow id="Flow_0agpc1o" sourceRef="Activity_1kgujpb" targetRef="Activity_1a3ot1n" />  
    <bpmn2:userTask id="Activity_18g3qee" name="副院长审批-处理" activiti:formKey="2" activiti:assignee="${assignee2}">  
      <bpmn2:incoming>Flow_0kn6i1r</bpmn2:incoming>  
      <bpmn2:outgoing>Flow_0kqma66</bpmn2:outgoing>  
    </bpmn2:userTask>  
    <bpmn2:sequenceFlow id="Flow_0kn6i1r" sourceRef="Activity_1a3ot1n" targetRef="Activity_18g3qee" />  
    <bpmn2:userTask id="Activity_12u7891" name="入住选配-处理" activiti:formKey="3" activiti:assignee="${assignee3}">  
      <bpmn2:incoming>Flow_0s8ila1</bpmn2:incoming>  
      <bpmn2:outgoing>Flow_0vvo1b1</bpmn2:outgoing>  
    </bpmn2:userTask>  
    <bpmn2:userTask id="Activity_0k9is7s" name="签约办理-处理" activiti:formKey="4" activiti:assignee="${assignee4}">  
      <bpmn2:incoming>Flow_0vvo1b1</bpmn2:incoming>  
      <bpmn2:outgoing>Flow_1dmh6fk</bpmn2:outgoing>  
    </bpmn2:userTask>  
    <bpmn2:sequenceFlow id="Flow_0vvo1b1" sourceRef="Activity_12u7891" targetRef="Activity_0k9is7s" />  
    <bpmn2:endEvent id="Event_0qrw42r">  
      <bpmn2:incoming>Flow_1dmh6fk</bpmn2:incoming>  
      <bpmn2:incoming>Flow_0u7ajk5</bpmn2:incoming>  
    </bpmn2:endEvent>  
    <bpmn2:sequenceFlow id="Flow_1dmh6fk" sourceRef="Activity_0k9is7s" targetRef="Event_0qrw42r" />  
    <bpmn2:exclusiveGateway id="Gateway_1bq869n" name="是否通过">  
      <bpmn2:incoming>Flow_0kqma66</bpmn2:incoming>  
      <bpmn2:outgoing>Flow_0s8ila1</bpmn2:outgoing>  
      <bpmn2:outgoing>Flow_1t9mtuw</bpmn2:outgoing>  
    </bpmn2:exclusiveGateway>  
    <bpmn2:sequenceFlow id="Flow_0kqma66" sourceRef="Activity_18g3qee" targetRef="Gateway_1bq869n" />  
    <bpmn2:sequenceFlow id="Flow_0s8ila1" sourceRef="Gateway_1bq869n" targetRef="Activity_12u7891">  
      <bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression">${ops == 1}</bpmn2:conditionExpression>  
    </bpmn2:sequenceFlow>  
    <bpmn2:exclusiveGateway id="Gateway_1vz7ax5" name="是否结束流程">  
      <bpmn2:incoming>Flow_1t9mtuw</bpmn2:incoming>  
      <bpmn2:outgoing>Flow_0u7ajk5</bpmn2:outgoing>  
      <bpmn2:outgoing>Flow_0a87sox</bpmn2:outgoing>  
    </bpmn2:exclusiveGateway>  
    <bpmn2:sequenceFlow id="Flow_1t9mtuw" sourceRef="Gateway_1bq869n" targetRef="Gateway_1vz7ax5">  
      <bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression">${ops &gt; 1}</bpmn2:conditionExpression>  
    </bpmn2:sequenceFlow>  
    <bpmn2:sequenceFlow id="Flow_0u7ajk5" sourceRef="Gateway_1vz7ax5" targetRef="Event_0qrw42r">  
      <bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression">${ops == 2}</bpmn2:conditionExpression>  
    </bpmn2:sequenceFlow>  
    <bpmn2:sequenceFlow id="Flow_0a87sox" sourceRef="Gateway_1vz7ax5" targetRef="Activity_1kgujpb">  
      <bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression">${ops == 3}</bpmn2:conditionExpression>  
    </bpmn2:sequenceFlow>  
  </bpmn2:process>  
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">  
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="checkIn-new">  
      <bpmndi:BPMNEdge id="Flow_0hsq6o3_di" bpmnElement="Flow_0hsq6o3">  
        <di:waypoint x="268" y="250" />  
        <di:waypoint x="320" y="250" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNEdge id="Flow_0agpc1o_di" bpmnElement="Flow_0agpc1o">  
        <di:waypoint x="420" y="250" />  
        <di:waypoint x="480" y="250" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNEdge id="Flow_0kn6i1r_di" bpmnElement="Flow_0kn6i1r">  
        <di:waypoint x="580" y="250" />  
        <di:waypoint x="640" y="250" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNEdge id="Flow_0vvo1b1_di" bpmnElement="Flow_0vvo1b1">  
        <di:waypoint x="880" y="410" />  
        <di:waypoint x="960" y="410" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNEdge id="Flow_1dmh6fk_di" bpmnElement="Flow_1dmh6fk">  
        <di:waypoint x="1060" y="410" />  
        <di:waypoint x="1116" y="410" />  
        <di:waypoint x="1116" y="502" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNEdge id="Flow_0kqma66_di" bpmnElement="Flow_0kqma66">  
        <di:waypoint x="690" y="290" />  
        <di:waypoint x="690" y="385" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNEdge id="Flow_0s8ila1_di" bpmnElement="Flow_0s8ila1">  
        <di:waypoint x="715" y="410" />  
        <di:waypoint x="780" y="410" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNEdge id="Flow_1t9mtuw_di" bpmnElement="Flow_1t9mtuw">  
        <di:waypoint x="690" y="435" />  
        <di:waypoint x="690" y="495" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNEdge id="Flow_0u7ajk5_di" bpmnElement="Flow_0u7ajk5">  
        <di:waypoint x="715" y="520" />  
        <di:waypoint x="1098" y="520" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNEdge id="Flow_0a87sox_di" bpmnElement="Flow_0a87sox">  
        <di:waypoint x="665" y="520" />  
        <di:waypoint x="370" y="520" />  
        <di:waypoint x="370" y="290" />  
      </bpmndi:BPMNEdge>  
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">  
        <dc:Bounds x="232" y="232" width="36" height="36" />  
      </bpmndi:BPMNShape>  
      <bpmndi:BPMNShape id="Activity_1a3ot1n_di" bpmnElement="Activity_1a3ot1n">  
        <dc:Bounds x="480" y="210" width="100" height="80" />  
      </bpmndi:BPMNShape>  
      <bpmndi:BPMNShape id="Activity_18g3qee_di" bpmnElement="Activity_18g3qee">  
        <dc:Bounds x="640" y="210" width="100" height="80" />  
      </bpmndi:BPMNShape>  
      <bpmndi:BPMNShape id="Activity_1kgujpb_di" bpmnElement="Activity_1kgujpb">  
        <dc:Bounds x="320" y="210" width="100" height="80" />  
      </bpmndi:BPMNShape>  
      <bpmndi:BPMNShape id="Gateway_1bq869n_di" bpmnElement="Gateway_1bq869n" isMarkerVisible="true">  
        <dc:Bounds x="665" y="385" width="50" height="50" />  
        <bpmndi:BPMNLabel>  
          <dc:Bounds x="589" y="400" width="44" height="14" />  
        </bpmndi:BPMNLabel>  
      </bpmndi:BPMNShape>  
      <bpmndi:BPMNShape id="Activity_12u7891_di" bpmnElement="Activity_12u7891">  
        <dc:Bounds x="780" y="370" width="100" height="80" />  
      </bpmndi:BPMNShape>  
      <bpmndi:BPMNShape id="Activity_0k9is7s_di" bpmnElement="Activity_0k9is7s">  
        <dc:Bounds x="960" y="370" width="100" height="80" />  
      </bpmndi:BPMNShape>  
      <bpmndi:BPMNShape id="Event_0qrw42r_di" bpmnElement="Event_0qrw42r">  
        <dc:Bounds x="1098" y="502" width="36" height="36" />  
      </bpmndi:BPMNShape>  
      <bpmndi:BPMNShape id="Gateway_1vz7ax5_di" bpmnElement="Gateway_1vz7ax5" isMarkerVisible="true">  
        <dc:Bounds x="665" y="495" width="50" height="50" />  
        <bpmndi:BPMNLabel>  
          <dc:Bounds x="659" y="552" width="67" height="14" />  
        </bpmndi:BPMNLabel>  
      </bpmndi:BPMNShape>  
    </bpmndi:BPMNPlane>  
  </bpmndi:BPMNDiagram>  
</bpmn2:definitions>

三、部署流程实列

因为流程属于纯后台开发使用,无需在页面展示,并且项目启动之后,只需要部署一次即可,我们暂时可以手动进行部署,可以编写单元测试部署流程,代码如下:

@Test  
public void testDeploymentByCheckIn(){  
  
    String fileName = "bpmn/checkIn_new.bpmn"; // 文件名  
    //定义流程  
    Deployment deployment = repositoryService.createDeployment()  
            .addClasspathResource(fileName)  
            .name("checkIn-new") // 文件里面的id值  
            .deploy();  
    //部署流程  
    System.out.println(deployment.getId());  
    System.out.println(deployment.getName());  
}

四、申请入住

1:输入表单之后,根据老人姓名和身份证号查询老人信息

2:如果能查询到老人,并且在申请入住中,则不处理该老人

3:保存老人信息到老人表中

4:保存入住信息到入住表中

5:保存操作记录(需查询下一个审核人)

6:启动流程实例

6.1 为流程图中的5个assignee设置流程变量,需要按照部门找到对应的办理人

6.2 在流程实例启动之前,则需要设计bussinessKey–>checkIn:入住id

6.3 判断第一个任务是否是当前登录人,如果是,则直接完成任务

4-1、申请入住接口
@RestController  
@RequestMapping("/checkIn")  
@Api(tags = "入住")  
public class CheckInController {  
  
    @Autowired  
    private CheckInService checkInService;  
  
    @PostMapping("/create")  
    @ApiOperation("申请入住")  
    public ResponseResult createCheckIn(@RequestBody CheckInDto checkInDto){  
        return checkInService.createCheckIn(checkInDto);  
    }  

}

接收类 CheckInDto

package com.zzyl.dto;

import com.zzyl.base.BaseDto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.time.LocalDateTime;
import java.util.List;

/**
 * @author sjqn
 */
@Data
@ApiModel(description = "入住实体类")
public class CheckInDto extends BaseDto {

    @ApiModelProperty(value = "保存1 提交2")
    private Integer save;

    @ApiModelProperty(value = "老人")
    private ElderDto elderDto;

    /**
     * 其他申请信息
     */
    @ApiModelProperty(value = "其他信息")
    private String otherApplyInfo;

    /**
     * 健康评估信息
     */
    @ApiModelProperty(value = "健康评估信息")
    private String reviewInfo;

    /**
     * 能力评估
     */
    @ApiModelProperty(value = "能力评估")
    private String reviewInfo1;
    /**
     * 评估报告
     */
    @ApiModelProperty(value = "评估信息")
    private String reviewInfo2;

    /**
     * 入住时间
     */
    @ApiModelProperty(value = "入住时间")
    private LocalDateTime checkInTime;

    /**
     * 家属信息
     */
    @ApiModelProperty(value = "家属信息")
    private List<MemberElderDto> memberElderDtos;

    /**
     * 一寸照片
     */
    @ApiModelProperty(value = "一寸照片")
    private String url1;

    /**
     * 身份证人像面
     */
    @ApiModelProperty(value = "身份证人像面")
    private String url2;

    /**
     * 身份证国徽面
     */
    @ApiModelProperty(value = "身份证国徽面")
    private String url3;


    /**
     * 任务ID
     */
    @ApiModelProperty(value = "任务ID")
    private String taskId;


}
4-2、接口-实现
import cn.hutool.core.bean.BeanUtil;  
import cn.hutool.core.collection.CollectionUtil;  
import cn.hutool.json.JSONObject;  
import cn.hutool.json.JSONUtil;  
import com.zzyl.base.ResponseResult;  
import com.zzyl.constant.AccraditationRecordConstant;  
import com.zzyl.constant.RetreatConstant;  
import com.zzyl.dto.CheckInDto;  
import com.zzyl.dto.ElderDto;  
import com.zzyl.entity.CheckIn;  
import com.zzyl.entity.Dept;  
import com.zzyl.entity.Elder;  
import com.zzyl.entity.User;  
import com.zzyl.mapper.CheckInMapper;  
import com.zzyl.mapper.DeptMapper;  
import com.zzyl.mapper.UserMapper;  
import com.zzyl.service.CheckInService;  
import com.zzyl.service.ElderService;  
import com.zzyl.utils.CodeUtil;  
import com.zzyl.utils.UserThreadLocal;  
import com.zzyl.vo.RecordVo;  
import com.zzyl.vo.retreat.ElderVo;  
import org.activiti.engine.RuntimeService;  
import org.activiti.engine.TaskService;  
import org.activiti.engine.task.Task;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.redis.core.RedisTemplate;  
import org.springframework.stereotype.Service;  
  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
import java.util.stream.Collectors;  
  
/**  
 * @author : like  
 * @date : 2024/3/28 20:26  
 * @Version: 1.0  
 */@Service  
public class CheckInServiceImpl implements CheckInService {  
  
    // 老人信息表操作  
    @Autowired  
    private ElderService elderService;  
  
    // 申请入住表  
    @Autowired  
    private CheckInMapper checkInMapper;  
  
    @Autowired  
    private RedisTemplate<String, String> redisTemplate;  
  
    // 部门管理  
    @Autowired  
    private DeptMapper deptMapper;  
  
    // 用户管理  
    @Autowired  
    private UserMapper userMapper;  
  
    @Autowired  
    private AccraditationRecordService accraditationRecordService;  
  
    private static final String CHECK_IN_CODE_PREFIX="RZ";  
  
    // 申请入住  
    @Override  
    public ResponseResult createCheckIn(CheckInDto checkInDto) {  
  
        // 1、校验老人是否入住中(操作老人表)  
        ElderVo elderVo = elderService.selectByIdCardAndName(checkInDto.getElderDto().getIdCardNo(), checkInDto.getElderDto().getName());  
        if (null != elderVo && elderVo.getStatus().equals(4)){  
            return ResponseResult.error(checkInDto.getElderDto().getName()+"已经发起了入住申请");  
        }  
        // 2、保存老人数据(操作老人表)  
        ElderDto elderDto = BeanUtil.toBean(checkInDto.getElderDto(), ElderDto.class);  
        elderDto.setImage(checkInDto.getUrl1()); // 头像  
        JSONObject jsonObject = BeanUtil.toBean(checkInDto.getOtherApplyInfo(), JSONObject.class);  
        elderDto.setSex(jsonObject.getStr("sex"));  
        elderDto.setAge(jsonObject.getStr("age"));  
        Elder elder = elderService.insert(elderDto);  
        // 3、保存入住数据(操作申请表)
        CheckIn checkIn = new CheckIn();  
            // 编号(redis生成)  
        String code = CodeUtil.generateCode(CHECK_IN_CODE_PREFIX, redisTemplate, 5);  
        checkIn.setCheckInCode(code);  
            // 标题(老人表获取)  
        checkIn.setTitle(elder.getName()+"的入住申请");  
        checkIn.setElderId(elder.getId());  
            // 获取当前登录人的信息  
        String subject = UserThreadLocal.getSubject();  
        User user = JSONUtil.toBean(subject, User.class);  
        checkIn.setCounselor(user.getRealName()); // 真实姓名getRealName  
        checkIn.setApplicat(user.getRealName());  // 申请人applicat  
        checkIn.setApplicatId(user.getId());  
        checkIn.setDeptNo(user.getDeptNo()); // 部门编号  
            // 状态  
        checkIn.setFlowStatus(CheckIn.FlowStatus.APPLY.getCode()); // 流程状态  
        checkIn.setStatus(CheckIn.Status.APPLICATION.getCode()); // 状态  
            // 其他信息  
        checkIn.setOtherApplyInfo(JSONUtil.toJsonStr(checkInDto));  
        checkInMapper.insert(checkIn);  
  
  
        // 4、准备流程变量的参数 map        
        Map<String ,Object> variables = setVariables(checkIn);
        
        // 5、启动流程实列  
        start(checkIn.getId(),user,"checkIn-new",variables,true);  
  
        // 6、保存操作记录  
        Long nextAssignee = getNextAssignee("checkIn", "checkIn:" + checkIn.getId());  
        RecordVo recordVo = getRecordVo(  
                checkIn,
                user, 
                AccraditationRecordConstant.AUDIT_STATUS_PASS,  
                "同意",
                "发起申请 - 申请入住",
                "护理组组长 - 入住评估",  
                nextAssignee,  
                AccraditationRecordConstant.RECORD_HANDLE_TYPE_PROCESSED  
        );
        accraditationRecordService.insert(recordVo);  
  
        return ResponseResult.success();  
    }
4、封装流程数据

// 4、准备流程变量的参数 map
Map<String ,Object> variables = setVariables(checkIn);

/**  
 * 4、封装变量数据(流程)  
 * @param checkIn  
 * @return  
 */  
public Map<String,Object> setVariables(CheckIn checkIn){  
    Map<String, Object> variables = new HashMap<>();  
  
    // 申请入住 - 养老顾问  
    variables.put("assignee0",checkIn.getApplicatId());  
    // 入住评估 - 护理主管  
    String nursingDeptCode = RetreatConstant.NURSING_DEPT_CODE; // 护理部门  
    Dept dept = deptMapper.selectByDeptNo(nursingDeptCode);  
    variables.put("assignee1",dept.getLeaderId());  
    // 审核 - 副院长  
    List<Long> idList = userMapper.selectByDeptNo(RetreatConstant.DEAN_OFFICE_DEPT_CODE);  
    variables.put("assignee2",idList.get(0));  
    // 入住选配 - 养老顾问  
    variables.put("assignee3",checkIn.getApplicatId());  
    // 签约 - 法务  
    List<Long> userIdList = userMapper.selectByDeptNo(RetreatConstant.LEGAL_DEPT_CODE); // 法务部门  
    variables.put("assignee4",userIdList.get(userIdList.size()-1));  
  
    // 业务类型  
    variables.put("processType",3);  
    // 业务状态  
    variables.put("processStatus",1);  
    // 流程Code  
    variables.put("processCode",checkIn.getCheckInCode());  

	variables.put("processTitle", checkIn.getTitle()); // 标题  
	variables.put("assignee0Name", checkIn.getApplicat()); // 申请人姓名  
	variables.put("applicationTime", checkIn.getCreateTime()); // 申请时间
  
    return variables;  
}
5、启动流程实列

// 5、启动流程实列
start(checkIn.getId(),user,“checkIn-new”,variables,true);

@Autowired  
private RuntimeService runtimeService;  
  
@Autowired  
private TaskService taskService;  
  
/**  
 * 5、启动流程实列  
 * @param id 业务id  
 * @param user 当前登录用户  
 * @param processDefinitionKey  流程定义的key  
 * @param variables 流程变量参数  
 * @param isAuto 是否自动完成  
 */  
private void start(Long id, User user, String processDefinitionKey, Map<String,Object> variables,boolean isAuto){  
  
    // 1、启动流程实列 (流程key,流程唯一键值,流程变量)  
    String businessKey = processDefinitionKey + ":" + id; // 标识业务实例的唯一键,checkIn.getId() 老人id  
    runtimeService.startProcessInstanceByKey(processDefinitionKey,businessKey,variables);  
    // 2、查询任务,执行任务  
    List<Task> taskList = taskService.createTaskQuery()  
                .processDefinitionKey(processDefinitionKey) // 流程key  
                .processInstanceBusinessKey(businessKey)    // 流程唯一键值  
                .taskAssignee(user.getId().toString())      // 执行任务人  
                .list();  
    // 3、判断是否首节点  
    taskList = taskList.stream().filter(t -> "0".equals(t.getFormKey())).collect(Collectors.toList());  
    if (!CollectionUtil.isEmpty(taskList) && isAuto){  
        // 4、执行任务  
        for (Task task:taskList){  
            Map<String, Object> variables2 = new HashMap<>();  
            variables2.put("processStatus",1);  
            variables2.put("ops",1);  
            taskService.complete(task.getId(),variables2); // 完成任务id为task.getId()的,并且put流程值  
        }  
    }  
}
6-1、获取下一个审核人

// 6、保存操作记录
Long nextAssignee = getNextAssignee(“checkIn”, “checkIn:” + checkIn.getId());

/**  
 * 6-1、获取下一个审核人  
 * @param processDifinitionKey  
 * @param bussinessKey  
 * @return  
 */  
private Long getNextAssignee(String processDifinitionKey, String bussinessKey) {  
  
    Task task = taskService.createTaskQuery()  
            .processDefinitionKey(processDifinitionKey)  
            .processInstanceBusinessKey(bussinessKey)  
            .singleResult();  
    if(task != null){  
        return Long.valueOf(task.getAssignee());  
    }  
    return null;  
}
6-2、封装审核记录参数

// 6、保存操作记录
Long nextAssignee = getNextAssignee(“checkIn”, “checkIn:” + checkIn.getId());
RecordVo recordVo = getRecordVo(
checkIn,
user,
AccraditationRecordConstant.AUDIT_STATUS_PASS,
“同意”,
“发起申请 - 申请入住”,
“护理组组长 - 入住评估”,
nextAssignee,
AccraditationRecordConstant.RECORD_HANDLE_TYPE_PROCESSED
);

/**  
 * 6-2、封装审核记录参数  
 * @param checkIn  入住对象  
 * @param user  当前登录用户  
 * @param status  审核状态  
 * @param option  操作意见  
 * @param step  当前步骤  
 * @param nextSetp  下一步操作人  
 * @param nextAssignee  下一个审核人  
 * @param handleType  处理类型(0:已审批,1:已处理)  
 * @return  
 */  
private RecordVo getRecordVo(CheckIn checkIn, User user, Integer status, String option, String step, String nextSetp, Long nextAssignee, Integer handleType) {  
  
    RecordVo recordVo = new RecordVo();  
    recordVo.setId(checkIn.getId());  
    recordVo.setType(AccraditationRecordConstant.RECORD_TYPE_CHECK_IN);  
    recordVo.setFlowStatus(checkIn.getFlowStatus());  
    recordVo.setHandleType(handleType);  
    recordVo.setStatus(status);  
    recordVo.setOption(option);  
    recordVo.setNextStep(nextSetp);  
    recordVo.setNextAssignee(nextAssignee);  
    recordVo.setUserId(user.getId());  
    recordVo.setStep(step);  
    recordVo.setRealName(user.getRealName());  
  
    return recordVo;  
}
6-3、操作申请表保存记录
import com.zzyl.entity.AccraditationRecord;  
import com.zzyl.mapper.AccraditationRecordMapper;  
import com.zzyl.mapper.UserRoleMapper;  
import com.zzyl.utils.ObjectUtil;  
import com.zzyl.vo.RecordVo;  
import com.zzyl.vo.UserRoleVo;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.redis.core.RedisTemplate;  
import org.springframework.stereotype.Service;  
  
import java.time.LocalDateTime;  
import java.util.List;  
  
@Service 
public class AccraditationRecordService implements com.zzyl.service.AccraditationRecordService {  
  
    @Autowired  
    private AccraditationRecordMapper accraditationRecordMapper;  
  
    @Autowired  
    private RedisTemplate<String,Object> redisTemplate;  
  
    @Autowired  
    private UserRoleMapper userRoleMapper;  
  
    @Override  
    public void insert(RecordVo recordVo) {  
        // 封装数据  
        AccraditationRecord acc = new AccraditationRecord();  
        acc.setHandleType(recordVo.getHandleType());  
        acc.setAuditStatus(recordVo.getStatus());  
        acc.setBussniessId(recordVo.getId());  
        acc.setCurrentStep(recordVo.getStep());  
        acc.setCreateTime(LocalDateTime.now());  
        acc.setOpinion(recordVo.getOption());  
        acc.setApproverName(recordVo.getRealName());  
        acc.setApproverId(recordVo.getUserId());  
        acc.setType(recordVo.getType());  
  
        // 当前的审核步骤  
        Long increment = redisTemplate.boundValueOps("RD:" + recordVo.getId().toString()).increment();  
        acc.setStepNo(increment);  
  
        // 下一个审核人  
        if (ObjectUtil.isEmpty(recordVo.getNextAssignee())){  
            acc.setNextStep(recordVo.getNextStep());  
            // 根据id查询,  
            List<UserRoleVo> userRoleVos = userRoleMapper.selectByUserId(recordVo.getNextAssignee());  
  
            UserRoleVo userRoleVo = userRoleVos.get(0);  
            acc.setNextApproverId(userRoleVo.getId());  
            acc.setNextApproverRole(userRoleVo.getRoleName());  
            acc.setNextApprover(userRoleVo.getUserName());  
        }  
  
        accraditationRecordMapper.insert(acc);  
    }  
}
4-3、完善现有代码

将启动流程实例,保存操作记录,封装接口

import com.zzyl.base.PageResponse;  
import com.zzyl.dto.PendingTasksDto;  
import com.zzyl.entity.CheckIn;  
import com.zzyl.entity.PendingTasks;  
import com.zzyl.entity.User;  
  
import java.util.Map;  
  
/**  
 * @author : like  
 * @date : 2024/3/30 20:41  
 * @Version: 1.0  
 */public interface ActFlowCommService {  
  
    /**  
     * 查询下一个审核人  
     * @param processDefinitionKey   流程定义key  
     * @param businessKey  
     * @return  
     */  
    public Long getNextAssignee(String processDefinitionKey, String businessKey);  
  
    /**  
     * 启动流程实例,并且自动执行首页点  
     * @param id  业务id  
     * @param user  当前登录用户  
     * @param processDefinitionKey    流程定义的key  
     * @param variables  流程变量参数  
     * @param isAuto  是否自动完成  
     */  
    public void start(Long id, User user, String processDefinitionKey, Map<String, Object> variables, boolean isAuto);  
  
}

实现类

import cn.hutool.core.collection.CollectionUtil;  
import com.zzyl.entity.User;  
import com.zzyl.service.ActFlowCommService;  
import org.activiti.engine.HistoryService;  
import org.activiti.engine.RuntimeService;  
import org.activiti.engine.TaskService;  
import org.activiti.engine.task.Task;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
import java.util.stream.Collectors;  
  
@Service  
public class ActFlowCommServiceImpl implements ActFlowCommService {  
  
    @Autowired  
    private RuntimeService runtimeService;  
  
    @Autowired  
    private TaskService taskService;  
  
    @Autowired  
    private HistoryService historyService;  
  
    /**  
     * 查询下一个审核人  
     * @param processDefinitionKey   流程定义key  
     * @param businessKey  
     * @return  
     */  
    @Override  
    public Long getNextAssignee(String processDefinitionKey, String businessKey) {  
  
        Task task = taskService.createTaskQuery()  
                .processDefinitionKey(processDefinitionKey)  
                .processInstanceBusinessKey(businessKey).singleResult();  
        if(task != null){  
            return Long.valueOf(task.getAssignee());  
        }  
        return null;  
    }  
  
  
    /**  
     * 启动流程实例,并且自动执行首页点  
     * @param id                   业务id  
     * @param user                 当前登录用户  
     * @param processDefinitionKey 流程定义的key  
     * @param variables            流程变量参数  
     * @param isAuto               是否自动完成  
     */  
    @Override  
    public void start(Long id, User user, String processDefinitionKey, Map<String, Object> variables, boolean isAuto) {  
  
        //启动流程实例  
        String businessKey = processDefinitionKey +":" + id;  
        variables.put("bussinessKey",businessKey);  
        runtimeService.startProcessInstanceByKey(processDefinitionKey,businessKey,variables);  
  
        //查询任务,执行任务  
        List<Task> list = taskService.createTaskQuery()  
                .processDefinitionKey(processDefinitionKey)  
                .processInstanceBusinessKey(businessKey)  
                .taskAssignee(user.getId().toString())  
                .list();  
        //判断是否是首节点  
        list = list.stream().filter(t->"0".equals(t.getFormKey())).collect(Collectors.toList());  
        if(!CollectionUtil.isEmpty(list) && isAuto){  
            //执行任务  
            for (Task task : list) {  
                Map<String, Object> variables2 = new HashMap<>();  
                variables2.put("processStatus",1);  
                variables2.put("ops",1);  
                taskService.complete(task.getId(),variables2);  
            }  
        }  
    }  
}

完整申请入住

import cn.hutool.core.bean.BeanUtil;  
import cn.hutool.core.collection.CollectionUtil;  
import cn.hutool.json.JSONObject;  
import cn.hutool.json.JSONUtil;  
import com.zzyl.base.ResponseResult;  
import com.zzyl.constant.AccraditationRecordConstant;  
import com.zzyl.constant.RetreatConstant;  
import com.zzyl.dto.CheckInDto;  
import com.zzyl.dto.ElderDto;  
import com.zzyl.entity.CheckIn;  
import com.zzyl.entity.Dept;  
import com.zzyl.entity.Elder;  
import com.zzyl.entity.User;  
import com.zzyl.mapper.CheckInMapper;  
import com.zzyl.mapper.DeptMapper;  
import com.zzyl.mapper.UserMapper;  
import com.zzyl.service.ActFlowCommService;  
import com.zzyl.service.CheckInService;  
import com.zzyl.service.ElderService;  
import com.zzyl.utils.CodeUtil;  
import com.zzyl.utils.UserThreadLocal;  
import com.zzyl.vo.RecordVo;  
import com.zzyl.vo.retreat.ElderVo;  
import org.activiti.engine.RuntimeService;  
import org.activiti.engine.TaskService;  
import org.activiti.engine.task.Task;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.redis.core.RedisTemplate;  
import org.springframework.stereotype.Service;  
  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
import java.util.stream.Collectors;  
  
/**  
 * @author : like  
 * @date : 2024/3/28 20:26  
 * @Version: 1.0  
 */@Service  
public class CheckInServiceImpl implements CheckInService {  
  
    // 老人信息表操作  
    @Autowired  
    private ElderService elderService;  
  
    // 申请入住表  
    @Autowired  
    private CheckInMapper checkInMapper;  
  
    @Autowired  
    private RedisTemplate<String, String> redisTemplate;  
  
    // 部门管理  
    @Autowired  
    private DeptMapper deptMapper;  
  
    // 用户管理  
    @Autowired  
    private UserMapper userMapper;  
  
    @Autowired  
    private AccraditationRecordService accraditationRecordService;  
  
    private static final String CHECK_IN_CODE_PREFIX="RZ";  
  
    @Autowired  
    private ActFlowCommService actFlowCommService;  
  
    // 申请入住  
    @Override  
    public ResponseResult createCheckIn(CheckInDto checkInDto) {  
  
        // 1、校验老人是否入住中(操作老人表)  
        ElderVo elderVo = elderService.selectByIdCardAndName(checkInDto.getElderDto().getIdCardNo(), checkInDto.getElderDto().getName());  
        if (null != elderVo && elderVo.getStatus().equals(4)){  
            return ResponseResult.error(checkInDto.getElderDto().getName()+"已经发起了入住申请");  
        }  
        // 2、保存老人数据(操作老人表)  
        ElderDto elderDto = BeanUtil.toBean(checkInDto.getElderDto(), ElderDto.class);  
        elderDto.setImage(checkInDto.getUrl1()); // 头像  
        JSONObject jsonObject = BeanUtil.toBean(checkInDto.getOtherApplyInfo(), JSONObject.class);  
        elderDto.setSex(jsonObject.getStr("sex"));  
        elderDto.setAge(jsonObject.getStr("age"));  
        Elder elder = elderService.insert(elderDto);  
        // 3、保存入住数据(操作申请表)  
        CheckIn checkIn = new CheckIn();  
            // 编号(redis生成)  
        String code = CodeUtil.generateCode(CHECK_IN_CODE_PREFIX, redisTemplate, 5);  
        checkIn.setCheckInCode(code);  
            // 标题(老人表获取)  
        checkIn.setTitle(elder.getName()+"的入住申请");  
        checkIn.setElderId(elder.getId());  
            // 获取当前登录人的信息  
        String subject = UserThreadLocal.getSubject();  
        User user = JSONUtil.toBean(subject, User.class);  
        checkIn.setCounselor(user.getRealName()); // 真实姓名getRealName  
        checkIn.setApplicat(user.getRealName());  // 申请人applicat  
        checkIn.setApplicatId(user.getId());  
        checkIn.setDeptNo(user.getDeptNo()); // 部门编号  
            // 状态  
        checkIn.setFlowStatus(CheckIn.FlowStatus.APPLY.getCode()); // 流程状态  
        checkIn.setStatus(CheckIn.Status.APPLICATION.getCode()); // 状态  
            // 其他信息  
        checkIn.setOtherApplyInfo(JSONUtil.toJsonStr(checkInDto));  
        checkInMapper.insert(checkIn);  
  
  
        // 4、准备流程变量的参数 map        Map<String ,Object> variables = setVariables(checkIn);  
        // 5、启动流程实列  
        actFlowCommService.start(checkIn.getId(),user,"checkIn-new",variables,true);  
  
        // 6、保存操作记录  
        Long nextAssignee = actFlowCommService.getNextAssignee("checkIn", "checkIn:" + checkIn.getId());  
        // 封装 操作记录  
        RecordVo recordVo = getRecordVo(  
                checkIn,  
                user,  
                AccraditationRecordConstant.AUDIT_STATUS_PASS,  
                "同意",  
                "发起申请 - 申请入住",  
                "护理组组长 - 入住评估",  
                nextAssignee,  
                AccraditationRecordConstant.RECORD_HANDLE_TYPE_PROCESSED  
        );  
        accraditationRecordService.insert(recordVo);  
  
        return ResponseResult.success();  
    }  
  
    /**  
     * 4、封装变量数据(流程)  
     * @param checkIn  
     * @return  
     */  
    public Map<String,Object> setVariables(CheckIn checkIn){  
        Map<String, Object> variables = new HashMap<>();  
  
        // 申请入住 - 养老顾问  
        variables.put("assignee0",checkIn.getApplicatId());  
        // 入住评估 - 护理主管  
        String nursingDeptCode = RetreatConstant.NURSING_DEPT_CODE; // 护理部门  
        Dept dept = deptMapper.selectByDeptNo(nursingDeptCode);  
        variables.put("assignee1",dept.getLeaderId());  
        // 审核 - 副院长  
        List<Long> idList = userMapper.selectByDeptNo(RetreatConstant.DEAN_OFFICE_DEPT_CODE);  
        variables.put("assignee2",idList.get(0));  
        // 入住选配 - 养老顾问  
        variables.put("assignee3",checkIn.getApplicatId());  
        // 签约 - 法务  
        List<Long> userIdList = userMapper.selectByDeptNo(RetreatConstant.LEGAL_DEPT_CODE); // 法务部门  
        variables.put("assignee4",userIdList.get(userIdList.size()-1));  
  
        // 业务类型  
        variables.put("processType",3);  
        // 业务状态  
        variables.put("processStatus",1);  
        // 流程Code  
        variables.put("processCode",checkIn.getCheckInCode());  
  
        return variables;  
    }  
  
    /**  
     * 6-2、封装审核记录参数  
     * @param checkIn  入住对象  
     * @param user  当前登录用户  
     * @param status  审核状态  
     * @param option  操作意见  
     * @param step  当前步骤  
     * @param nextSetp  下一步操作人  
     * @param nextAssignee  下一个审核人  
     * @param handleType  处理类型(0:已审批,1:已处理)  
     * @return  
     */  
    private RecordVo getRecordVo(CheckIn checkIn, User user, Integer status, String option, String step, String nextSetp, Long nextAssignee, Integer handleType) {  
  
        RecordVo recordVo = new RecordVo();  
        recordVo.setId(checkIn.getId());  
        recordVo.setType(AccraditationRecordConstant.RECORD_TYPE_CHECK_IN);  
        recordVo.setFlowStatus(checkIn.getFlowStatus());  
        recordVo.setHandleType(handleType);  
        recordVo.setStatus(status);  
        recordVo.setOption(option);  
        recordVo.setNextStep(nextSetp);  
        recordVo.setNextAssignee(nextAssignee);  
        recordVo.setUserId(user.getId());  
        recordVo.setStep(step);  
        recordVo.setRealName(user.getRealName());  
  
        return recordVo;  
    }  
  
}

五、查询代办

5-1、查询代办接口
@RestController
@RequestMapping("/pending_tasks")
@Api(tags = "待办")
public class PendingTasksController extends BaseController {

    @Autowired
    private ActFlowCommService actFlowCommService;

    @PostMapping("/selectByPage")
    @ApiOperation(value = "查询待办", notes = "传入退住对象")
    public ResponseResult<PendingTasks> selectByPage(@RequestBody PendingTasksDto pendingTasksDto){
        //只查询有当前登录人的任务
        Long userId = UserThreadLocal.getMgtUserId();
        pendingTasksDto.setAssigneeId(userId);

        PageResponse<PendingTasks> pendingTasksPageResponse = actFlowCommService.myTaskInfoList(pendingTasksDto);
        return success(pendingTasksPageResponse);
    }
}
5-2、实现方法
5-2-1、查询条件
  1. 查询是否已处理

    • 如果 pendingTasksDto.getIsHandle() 不为空,根据其值判断是否已处理:
      • 若为 1,则已处理,使用 taskQuery.finished() 方法;
      • 若不为 1,则未处理,使用 taskQuery.unfinished() 方法。
  2. 任务名称模糊查询

    • 使用 taskQuery.taskNameLike("%处理") 方法设置任务名称模糊查询条件,其中 %处理 是一个通配符,表示任务名称以“处理”结尾的任务。
  3. 任务受让人查询

    • 使用 taskQuery.taskAssignee(pendingTasksDto.getAssigneeId().toString()) 方法设置任务受让人的查询条件。
  4. 时间范围查询

    • 如果 pendingTasksDto.getStartTime() 和 pendingTasksDto.getEndTime() 都不为空,则设置任务创建时间的范围条件:
      • 使用 taskQuery.taskCreatedAfter(pendingTasksDto.getStartTime()) 方法设置任务创建时间在起始时间之后;
      • 使用 taskQuery.taskCompletedBefore(pendingTasksDto.getEndTime()) 方法设置任务完成时间在结束时间之前。
  5. 单据类别查询

    • 如果 pendingTasksDto.getType() 不为空,则根据单据类别设置查询条件:
      • 使用 taskQuery.processVariableValueEquals("processType", pendingTasksDto.getType()) 方法。
  6. 单据编码查询

    • 如果 pendingTasksDto.getCode() 不为空,则根据单据编码设置查询条件:
      • 使用 taskQuery.processVariableValueEquals("processCode", pendingTasksDto.getCode()) 方法。
  7. 任务状态查询

    • 如果 pendingTasksDto.getStatus() 不为空,则根据任务状态设置查询条件:
      • 使用 taskQuery.processVariableValueEquals("processStatus", pendingTasksDto.getStatus()) 方法。
5-2-2、分页查询条件
  1. includeProcessVariables(): 这个方法是用来包括查询结果中的流程变量。这意味着每个返回的历史任务实例将包含关联的流程变量,这可以让你获取更多关于每个任务实例的上下文信息。

  2. orderByHistoricTaskInstanceStartTime().desc(): 这是排序条件,表示根据历史任务实例的开始时间(startTime)进行降序排序。这意味着最新开始的任务将首先出现在结果列表中。

  3. listPage(startIndex, pageSize): 这个方法用于分页查询结果。它接受两个参数:

    • startIndex(起始索引): 这是结果列表中的起始位置,用于指定从哪里开始获取结果。这个值是通过计算得出的,pendingTasksDto.getPageNum() - 1 计算得到当前页的前一页,然后乘以 pendingTasksDto.getPageSize() 得到跳过的结果数量。如果 getPageNum() 返回的是当前页码,那么这个计算确保了从正确的位置开始分页。
    • pageSize(页面大小): 这指定了每页要返回的结果数量。这个值是直接从 pendingTasksDto.getPageSize() 获取的,表示每页显示的任务实例数。
// 二、分页封装  
long count = taskQuery.count(); // 条数  
  
List<HistoricTaskInstance> list = taskQuery  
            .includeProcessVariables() // 获取流程实例数据  
            .orderByHistoricTaskInstanceStartTime().desc() // 根据时间降序  
            .listPage(  
                    (pendingTasksDto.getPageNum() - 1) * pendingTasksDto.getPageSize(),  
                    pendingTasksDto.getPageSize()  
            ); // (起始索引,每页条数)
5-2-3、完整实现
@Autowired  
private HistoryService historyService;

/**  
 * 查询我的代办和我的申请列表  
 * @param pendingTasksDto  
 * @return  
 */  
@Override  
public PageResponse getMyTaskList(PendingTasksDto pendingTasksDto) {  
  
    // 一、创建查询条件  
    HistoricTaskInstanceQuery taskQuery = historyService.createHistoricTaskInstanceQuery();  
  
        // 查询是否已处理  
    if (ObjectUtil.isNotEmpty(pendingTasksDto.getIsHandle())){  
        if (pendingTasksDto.getIsHandle() == 1){  
            taskQuery.finished();  
        } else {  
            taskQuery.unfinished();  
        }  
    }  
        // 查询条件  
    taskQuery.taskNameLike("%处理");  
    taskQuery.taskAssignee(pendingTasksDto.getAssigneeId().toString());  
        // 时间范围查询  
    if (ObjectUtil.isNotEmpty(pendingTasksDto.getStartTime()) && ObjectUtil.isNotEmpty(pendingTasksDto.getEndTime())){  
        // 加入开始,结束时间不为空则  
        taskQuery  
                .taskCreatedAfter(pendingTasksDto.getStartTime())  
                .taskCompletedBefore(pendingTasksDto.getEndTime());  
    }  
        // 单据类型  
    if (ObjectUtil.isNotEmpty(pendingTasksDto.getType())){  
        taskQuery.processVariableValueEquals("processType",pendingTasksDto.getType());  
    }  
        // 单据code  
    if (ObjectUtil.isNotEmpty(pendingTasksDto.getCode())) {  
        taskQuery.processVariableValueEquals("processCode", pendingTasksDto.getCode());  
    }  
        // 任务状态  
    if (ObjectUtil.isNotEmpty(pendingTasksDto.getStatus())) {  
        taskQuery.processVariableValueEquals("processStatus", pendingTasksDto.getStatus());  
    }  
  
    // 二、分页封装  
    long count = taskQuery.count(); // 条数  
  
    List<HistoricTaskInstance> list = taskQuery  
                .includeProcessVariables() // 获取流程实例数据  
                .orderByHistoricTaskInstanceStartTime().desc() // 根据时间降序  
                .listPage(  
                        (pendingTasksDto.getPageNum() - 1) * pendingTasksDto.getPageSize(),  
                        pendingTasksDto.getPageSize()  
                ); // (起始索引,每页条数)  
  
    // 三、封装数据  
    List<PendingTasks> pendingTasksList = new ArrayList<>();  
  
    for (HistoricTaskInstance task : list){  
        // 获取流程变量  
        Map<String, Object> processVariables = task.getProcessVariables();  
        //  
        PendingTasks pendingTasks = new PendingTasks();  
        pendingTasks.setId(task.getId());  
        pendingTasks.setCode(processVariables.get("processCode").toString());  
        pendingTasks.setType(Integer.parseInt(processVariables.get("processType").toString()));  
        pendingTasks.setTitle(processVariables.get("processTitle").toString());  
        pendingTasks.setApplicat(processVariables.get("assignee0Name").toString());  
        pendingTasks.setStatus(Integer.parseInt(processVariables.get("processStatus").toString()));  
        pendingTasks.setAssigneeId(Long.valueOf(task.getAssignee()));  
        LocalDateTime applicationTime = LocalDateTimeUtil.parse(processVariables.get("applicationTime").toString());  
        pendingTasks.setApplicationTime(applicationTime);  
  
        pendingTasksList.add(pendingTasks);  
    }  
    return PageResponse.of(  
            pendingTasksList,  
            pendingTasksDto.getPageNum(),  
            pendingTasksDto.getPageSize(),  
            (count + pendingTasksDto.getPageSize() - 1) / pendingTasksDto.getPageSize(),  
            count  
    );  
}
5-2-4、获取当前登录人信息,完善控制层
@RestController
@RequestMapping("/pending_tasks")
@Api(tags = "待办")
public class PendingTasksController extends BaseController {

    @Autowired
    private ActFlowCommService actFlowCommService;

    @PostMapping("/selectByPage")
    @ApiOperation(value = "查询待办", notes = "传入退住对象")
    public ResponseResult<PendingTasks> selectByPage(@RequestBody PendingTasksDto pendingTasksDto){
        //只查询有当前登录人的任务
        Long userId = UserThreadLocal.getMgtUserId();
        pendingTasksDto.setAssigneeId(userId);

	    PageResponse<PendingTasks> pendingTasksPageResponse = actFlowCommService.getMyTaskList(pendingTasksDto);
        return success(pendingTasksPageResponse);
    }
}

六、查询申请

新增ApplicationsController类,直接调用actFlowCommServicemyTaskInfoList方法即可

@RestController  
@RequestMapping("/applications")  
@Api(tags = "我的申请")  
public class ApplicationsController {  
  
    @Autowired  
    private ActFlowCommService actFlowCommService;  
  
    @PostMapping("/selectByPage")  
    @ApiOperation(value = "查询我的申请")  
    public ResponseResult<ApplicationsVo> selectByPage(@RequestBody ApplicationsDto applicationsDto){  
  
        Long mgtUserId = UserThreadLocal.getMgtUserId();  
        applicationsDto.setApplicatId(mgtUserId);  
  
        PendingTasksDto pendingTasksDto = BeanUtil.toBean(applicationsDto, PendingTasksDto.class);  
        PageResponse<PendingTasks> pendingTasksPageResponse = actFlowCommService.getMyTaskList(pendingTasksDto);  
        return ResponseResult.success(pendingTasksPageResponse);  
    }  
  
}

修改查询条件 actFlowCommService.getMyTaskList

    // 判断是申请、还是处理条件  
if (ObjectUtil.isNotEmpty(pendingTasksDto.getApplicatId())){  
    taskQuery.taskAssignee(pendingTasksDto.getApplicatId().toString());  
    taskQuery.taskNameLike("%申请");  
} else {  
    taskQuery.taskNameLike("%处理");  
    taskQuery.taskAssignee(pendingTasksDto.getAssigneeId().toString());  
}

七、入住表单查询,审批时显示数据

7-1、接口定义

点击处理时,获取数据

@GetMapping  
@ApiOperation(value = "入住表单查询")  
public ResponseResult<TasVo> getCheckIn(  
        @RequestParam @ApiParam(value = "入住编码") String code,  
        @RequestParam @ApiParam(value = "处理人ID") String assigneeId,  
        @RequestParam @ApiParam(value = "流程状态") Integer flowStatus,  
        @RequestParam(required = false) @ApiParam(value = "任务Id") String taskId) {  
    return checkInService.getCheckIn(code, assigneeId, flowStatus, taskId);  
}
7-2、实现方法
@Autowired  
private AccraditationRecordMapper accraditationRecordMapper;
/**  
 * 查询入住信息  
 * @param code  
 * @param assigneeId  
 * @param flowStatus  
 * @param taskId  
 * @return  
 */  
@Override  
public ResponseResult<TasVo> getCheckIn(String code, String assigneeId, Integer flowStatus, String taskId) {  
  
    // 一、封装显示数据  
    CheckIn checkIn = checkInMapper.selectByCheckInCode(code); // 根据code查询入住单  
    CheckInVo checkInVo = BeanUtil.toBean(checkIn, CheckInVo.class);  
        // 封装详细信息  
    JSONObject jsonObject = JSON.parseObject(checkInVo.getOtherApplyInfo(), JSONObject.class);  
    if (ObjectUtil.isNotEmpty(jsonObject)) {  
        JSONObject otherApplyInfo = JSON.parseObject(jsonObject.getStr("otherApplyInfo"), JSONObject.class);  
        checkInVo.setOtherApplyInfo(otherApplyInfo.toString());  
  
        List<MemberElderDto> memberElderDtos = JSON.parseArray(jsonObject.getStr("memberElderDtos"), MemberElderDto.class);  
        checkInVo.setMemberElderDtos(memberElderDtos);  
  
        ElderDto elderDto1 = JSON.parseObject(jsonObject.getStr("elderDto"), ElderDto.class);  
        checkInVo.setElderDto(elderDto1);  
  
        //取出url信息  
        checkInVo.setUrl1(jsonObject.getStr("url1"));  
        checkInVo.setUrl2(jsonObject.getStr("url2"));  
        checkInVo.setUrl3(jsonObject.getStr("url3"));  
    }  
        //如果flowStatus小于0,重新赋值  
    if (flowStatus < 0) {  
        flowStatus = checkIn.getFlowStatus();  
    }  
  
  
    // 二、控制数据显示还是隐藏  
    Integer isShow = 1; // isShow字段:0 只展示操作记录,1:展示详细数据集操作记录  
    int step = 1;  
    if (ObjectUtil.isNotEmpty(taskId)) {  
        //查询当前流程在第几步  
        isShow = actFlowCommService.isCurrentUserAndStep(taskId, flowStatus, checkIn.getFlowStatus());  
    }  
    checkInVo.setIsShow(isShow);  
  
	// TODO 查询入住评估内容  
	JSONObject reviewInfoJsonObj = JSON.parseObject(checkInVo.getReviewInfo());  
	if(ObjectUtil.isNotEmpty(reviewInfoJsonObj)){  
	    String reviewInfo = reviewInfoJsonObj.getString("reviewInfo");  
	    String reviewInfo1 = reviewInfoJsonObj.getString("reviewInfo1");  
	    String reviewInfo2 = reviewInfoJsonObj.getString("reviewInfo2");  
	    checkInVo.setReviewInfo(reviewInfo);  
	    checkInVo.setReviewInfo1(reviewInfo1);  
	    checkInVo.setReviewInfo2(reviewInfo2);  
	}

    // 三、数据封装显示阶段  
    TasVo tasVo = new TasVo();  
    tasVo.setCheckIn(checkInVo);  
    tasVo.setIsShow(isShow);  
    tasVo.setType(3); //流程类型:入住  
  
    // 四、获取审批记录数据  
    List<AccraditationRecord> accraditationRecordList  
            = accraditationRecordMapper.getAccraditationRecordByBuisId(checkIn.getId(), PendingTasksConstant.TASK_TYPE_CHECK_IN);  
    tasVo.setAccraditationRecords(accraditationRecordList);  
  
    return ResponseResult.success(tasVo);  
}
7-2-1、判断当前操作是第几步
/**  
 * 判断当前操作的是第几步  
 * @param taskId  
 * @param flowStatus  
 * @param dbFlowStatus  
 * @return  
 */  
@Override  
public Integer isCurrentUserAndStep(String taskId, Integer flowStatus, Integer dbFlowStatus) {  
  
    //当前任务人的节点  
    HistoricTaskInstance historicTaskInstance = historyService.createHistoricTaskInstanceQuery()  
            .taskId(taskId)  
            .singleResult();  
  
    String formKey = historicTaskInstance.getFormKey();  
  
    //如果前端传递的流程状态跟数据库一致,当前操作人的任务节点与状态一致  
    if (dbFlowStatus.equals(flowStatus) && dbFlowStatus.equals(CheckIn.Status.APPLICATION.getCode())  
            && formKey.equals(dbFlowStatus.toString())) {  
        return 1;  
    }  
    return 0;  
}
7-2-2、获取审批记录数据
@Select("select * from accraditation_record where bussniess_id = #{bussniessId} and type = #{type} order by step_no")  
public List<AccraditationRecord> getAccraditationRecordByBuisId(@Param("bussniessId")long bussniessId, @Param("type")Integer type);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值