【工作流Activiti7】2、Activiti7 与 Spring Boot 及 Spring Security 整合

news2024/11/18 21:36:25

1.  前言

其实,选择用Activiti7没别的原因,就是因为穷。但凡是有钱,谁还用开源版的啊,当然是用商业版啦。国外的工作流引擎没有考虑中国的实际情况,很多像回退、委派、撤销等等功能都没有,所以最省事的还是中国特色的BPM。

Activiti7的文档比较少,但是教程多。Flowable的文档比较齐全,但是网上教程少。

2.  Maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cjs.example</groupId>
    <artifactId>demo-activiti7</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo-activiti7</name>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-spring-boot-starter</artifactId>
            <version>7.1.0.M6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.4</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.10.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

配置 application.properties

server.port=8080
server.servlet.context-path=/activiti7

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=true
spring.datasource.username=root
spring.datasource.password=123456

spring.jpa.database=mysql
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
spring.jpa.show-sql=true

spring.redis.host=192.168.28.31
spring.redis.port=6379
spring.redis.password=123456
spring.redis.database=1

spring.activiti.database-schema-update=true
spring.activiti.db-history-used=true
spring.activiti.history-level=full
spring.activiti.check-process-definitions=false
spring.activiti.deployment-mode=never-fail

代码是最好的老师,查看代码所有配置项都一目了然

这里最好关闭自动部署,不然每次项目启动的时候就会自动部署一次

3.  集成 Spring Security

3.1.  实体类

权限

package com.cjs.example.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;

/**
 * 菜单表
 * @Author 
 * @Date 2021/6/12
 */
@Setter
@Getter
@Entity
@Table(name = "sys_menu")
public class SysMenuEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Integer id;

    /**
     * 资源编码
     */
    @Column(name = "code")
    private String code;

    /**
     * 资源名称
     */
    @Column(name = "name")
    private String name;

    /**
     * 菜单/按钮URL
     */
    @Column(name = "url")
    private String url;

    /**
     * 资源类型(1:菜单,2:按钮)
     */
    @Column(name = "type")
    private Integer type;

    /**
     * 父级菜单ID
     */
    @Column(name = "pid")
    private Integer pid;

    /**
     * 排序号
     */
    @Column(name = "sort")
    private Integer sort;

    @ManyToMany(mappedBy = "menus")
    private Set<SysRoleEntity> roles;

}

角色

package com.cjs.example.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;

/**
 * 角色表
 * @Author 
 */
@Setter
@Getter
@Entity
@Table(name = "sys_role")
public class SysRoleEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Integer id;

    /**
     * 角色名称
     */
    @Column(name = "name")
    private String name;

    @ManyToMany(mappedBy = "roles")
    private Set<SysUserEntity> users;

    @ManyToMany
    @JoinTable(name = "sys_role_menu",
            joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")})
    private Set<SysMenuEntity> menus;

    @ManyToMany
    @JoinTable(name = "sys_dept_role",
            joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "dept_id", referencedColumnName = "id")})
    private Set<SysDeptEntity> depts;

} 

部门 

package com.cjs.example.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;

/**
 * 部门表
 * @Author 
 * @Date 2021/6/12
 */
@Setter
@Getter
@Entity
@Table(name = "sys_dept")
public class SysDeptEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Integer id;

    /**
     * 部门名称
     */
    @Column(name = "name")
    private String name;

    /**
     * 父级部门ID
     */
    @Column(name = "pid")
    private Integer pid;

    /**
     * 组对应的角色
     */
    @ManyToMany(mappedBy = "depts")
    private Set<SysRoleEntity> roles;
} 

用户

package com.cjs.example.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Set;

/**
 * 用户表
 * @Author 
 * @Date 2021/6/12
 */
@Setter
@Getter
@Entity
@Table(name = "sys_user")
public class SysUserEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Integer id;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;

    @Column(name = "mobile")
    private String mobile;

    @Column(name = "enabled")
    private Integer enabled;

    @Column(name = "create_time")
    private LocalDate createTime;

    @Column(name = "update_time")
    private LocalDate updateTime;

    @OneToOne
    @JoinColumn(name = "dept_id")
    private SysDeptEntity dept;

    @ManyToMany
    @JoinTable(name = "sys_user_role",
            joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
    private Set<SysRoleEntity> roles;

}

3.2.  自定义 UserDetailsService

package com.cjs.example.domain;

import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

/**
 * @Author 
 * @Date 2021/6/12
 * @see User
 * @see User
 */
@Setter
public class MyUserDetails implements UserDetails {

    private String username;
    private String password;
    private boolean enabled;
    private Set<SimpleGrantedAuthority> authorities;

    public MyUserDetails(String username, String password, boolean enabled, Set<SimpleGrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

MyUserDetailsService

package com.cjs.example.service;

import com.cjs.example.domain.MyUserDetails;
import com.cjs.example.entity.SysMenuEntity;
import com.cjs.example.entity.SysRoleEntity;
import com.cjs.example.entity.SysUserEntity;
import com.cjs.example.repository.SysUserRepository;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Service;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @Author 
 * @Date 2021/6/12
 */
@Service
public class MyUserDetailsService implements UserDetailsService {
    @Resource
    private SysUserRepository sysUserRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);
        Set<SysRoleEntity> userRoles = sysUserEntity.getRoles();
        Set<SysRoleEntity> deptRoles = sysUserEntity.getDept().getRoles();
        Set<SysRoleEntity> roleSet = new HashSet<>();
        roleSet.addAll(userRoles);
        roleSet.addAll(deptRoles);

        Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())
                .filter(menu-> StringUtils.isNotBlank(menu.getCode()))
                .map(SysMenuEntity::getCode)
//                .map(e -> "ROLE_" + e.getCode())
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());

        return new MyUserDetails(sysUserEntity.getUsername(), sysUserEntity.getPassword(), 1==sysUserEntity.getEnabled(), authorities);
    }
}

如果加了“ROLE_”前缀,那么比较的时候应该用 SimpleGrantedAuthority 进行比较

这里姑且不加这个前缀了,因为后面集成 Activiti 的时候用户组有一个前缀 GROUP_

package com.cjs.example.service;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Set;
import java.util.stream.Collectors;

@Component("myAccessDecisionService")
public class MyAccessDecisionService {

    public boolean hasPermission(String permission) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Object principal = authentication.getPrincipal();
        if (principal instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) principal;
            Set<String> set = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
            return set.contains(permission);

//            //  AuthorityUtils.createAuthorityList(permission);
//            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
//            return userDetails.getAuthorities().contains(simpleGrantedAuthority);
        }
        return false;
    }
}

3.3.  自定义Token过滤器

package com.cjs.example.filter;

import com.alibaba.fastjson.JSON;
import com.cjs.example.domain.MyUserDetails;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * @Author 
 * @Date 2021/6/17
 */
@Component
public class TokenFilter extends OncePerRequestFilter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String token = request.getHeader("token");
        String key = "TOKEN:" + token;
        if (StringUtils.isNotBlank(token)) {
            String value = stringRedisTemplate.opsForValue().get(key);
            if (StringUtils.isNotBlank(value)) {
                MyUserDetails user = JSON.parseObject(value, MyUserDetails.class);
                if (null != user && null == SecurityContextHolder.getContext().getAuthentication()) {
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                    //  刷新token
                    //  如果生存时间小于10分钟,则再续1小时
                    long time = stringRedisTemplate.getExpire(key);
                    if (time < 600) {
                        stringRedisTemplate.expire(key, (time + 3600), TimeUnit.SECONDS);
                    }
                }
            }
        }

        chain.doFilter(request, response);
    }
}

3.3.  WebSecurityConfig

package com.cjs.example.config;

import com.cjs.example.filter.TokenFilter;
import com.cjs.example.handler.*;
import com.cjs.example.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @Author 
 * @Date 2021/6/12
 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;
    @Autowired
    private TokenFilter tokenFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
                .and()
                .logout().logoutSuccessHandler(myLogoutSuccessHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/activiti7/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .accessDeniedHandler(new MyAccessDeniedHandler())
                .authenticationEntryPoint(new MyAuthenticationEntryPoint())
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false)
                .expiredSessionStrategy(new MyExpiredSessionStrategy());

        http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);

        http.csrf().disable();
    }

    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

至此一切都很顺利,毕竟之前也写过很多遍。

package com.cjs.example.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 
 * @Date 2021/6/12
 */
@RestController
@RequestMapping("/hello")
public class HelloController {

    @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHello')")
    @GetMapping("/sayHello")
    public String sayHello() {
        return "hello";
    }

    @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHi')")
    @GetMapping("/sayHi")
    public String sayHi() {
        return "hi";
    }
}

4. 集成 Activiti7

启动项目以后,activiti相关表已经创建好了

接下来,以简单的请假为例来演示

<process id="leave" name="leave" isExecutable="true">
    <startEvent id="startevent1" name="Start"></startEvent>
    <userTask id="usertask1" name="填写请假单" activiti:assignee="${sponsor}"></userTask>
    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
    <endEvent id="endevent1" name="End"></endEvent>
    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow>
    <userTask id="usertask2" name="经理审批" activiti:candidateGroups="${manager}"></userTask>
    <sequenceFlow id="flow3" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow>
    <endEvent id="endevent2" name="End"></endEvent>
    <sequenceFlow id="flow4" sourceRef="usertask2" targetRef="endevent2"></sequenceFlow>
</process>

4.1.  部署流程定义

package com.cjs.example.controller;

import com.cjs.example.domain.RespResult;
import com.cjs.example.util.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.ProcessDefinition;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipInputStream;

/**
 * @Author 
 * @Date 2021/7/12
 */
@Slf4j
@RestController
@RequestMapping("/deploy")
public class DeploymentController {

    @Autowired
    private RepositoryService repositoryService;

    /**
     * 部署
     * @param file  ZIP压缩包文件
     * @param processName   流程名称
     * @return
     */
    @PostMapping("/upload")
    public RespResult<String> upload(@RequestParam("zipFile") MultipartFile file, @RequestParam("processName") String processName) {
        String originalFilename = file.getOriginalFilename();
        if (!originalFilename.endsWith("zip")) {
            return ResultUtils.error("文件格式错误");
        }
        ProcessDefinition processDefinition = null;
        try {
            ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream());
            Deployment deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).name(processName).deploy();

            processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult();
        } catch (IOException e) {
            log.error("流程部署失败!原因: {}", e.getMessage(), e);
        }
        return ResultUtils.success(processDefinition.getId());
    }

    /**
     * 查看流程图
     * @param deploymentId  部署ID
     * @param resourceName  图片名称
     * @param response
     * @return
     */
    @GetMapping("/getDiagram")
    public void getDiagram(@RequestParam("deploymentId") String deploymentId, @RequestParam("resourceName") String resourceName, HttpServletResponse response) {
        InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, resourceName);
//        response.setContentType(MediaType.IMAGE_PNG_VALUE);
        try {
            IOUtils.copy(inputStream, response.getOutputStream());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
    }
}

首先登录一下

然后,将流程图文件打成zip压缩包

查看流程图

4.2.  启动流程实例

最开始,我是这样写的

ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder
                    .start()
                    .withProcessDefinitionId(processDefinitionId)
                    .withVariable("sponsor", authentication.getName())
                    .build());

当我这样写了以后,第一个问题出现了,没有权限访问

查看代码之后,我发现调用ProcessRuntime的方法需要当前登录用户有“ACTIVITI_USER” 权限

于是,我在数据库sys_menu表里加了一条数据

重新登录后,zhangsan可以调用ProcessRuntime里面的方法了

很快,第二个问题出现了, 当我用 ProcessRuntime#start() 启动流程实例的时候报错了

org.activiti.engine.ActivitiException: Query return 2 results instead of max 1
	at org.activiti.engine.impl.DeploymentQueryImpl.executeSingleResult(DeploymentQueryImpl.java:213) ~[activiti-engine-7.1.0.M6.jar:na]
	at org.activiti.engine.impl.DeploymentQueryImpl.executeSingleResult(DeploymentQueryImpl.java:30) ~[activiti-engine-7.1.0.M6.jar:na]

查看代码,终于找到问题所在了

这明显就是 Activiti 的Bug,查询所有部署的流程没有加任何查询条件,吐了

于是,百度了一下,网上有人建议换一个版本,于是我将activiti-spring-boot-starter的版本从“7.1.0.M6”换成了“7.1.0.M5”,呵呵,又一个错,缺少字段

原来M6和M5的表结构不一样。我又将版本将至“7.1.0.M4”,这次直接起不来了

没办法,版本改回7.1.0.M6,不用ProcessRuntime,改用原来的RuntimeService

package com.cjs.example.controller;

import com.cjs.example.domain.RespResult;
import com.cjs.example.util.ResultUtils;
import org.activiti.api.process.model.ProcessInstance;
import org.activiti.api.process.model.builders.ProcessPayloadBuilder;
import org.activiti.api.process.runtime.ProcessRuntime;
import org.activiti.engine.RuntimeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author 
 * @Date 2021/7/12
 */
@RestController
@RequestMapping("/processInstance")
public class ProcessInstanceController {
    @Autowired
    private ProcessRuntime processRuntime;

    @Autowired
    private RuntimeService runtimeService;

    @GetMapping("/start")
    public RespResult start(@RequestParam("processDefinitionId") String processDefinitionId) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        org.activiti.engine.runtime.ProcessInstance processInstance = null;
        try {
//            ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder
//                    .start()
//                    .withProcessDefinitionId(processDefinitionId)
//                    .withVariable("sponsor", authentication.getName())
//                    .build());

            Map<String, Object> variables = new HashMap<>();
            variables.put("sponsor", authentication.getName());
            processInstance = runtimeService.startProcessInstanceById(processDefinitionId, variables);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return ResultUtils.success(processInstance);
    }
}

这里注意 org.activiti.engine.runtime.ProcessInstance 和 org.activiti.api.process.model.ProcessInstance 别搞混了 

查看流程定义

package com.cjs.example.controller;

import com.cjs.example.domain.RespResult;
import com.cjs.example.util.ResultUtils;
import org.activiti.api.process.model.ProcessDefinition;
import org.activiti.api.process.runtime.ProcessAdminRuntime;
import org.activiti.api.process.runtime.ProcessRuntime;
import org.activiti.api.runtime.shared.query.Page;
import org.activiti.api.runtime.shared.query.Pageable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 
 * @Date 2021/7/12
 */
@RestController
@RequestMapping("/processDefinition")
public class ProcessDefinitionController {

    @Autowired
    private ProcessAdminRuntime processAdminRuntime;
//    private ProcessRuntime processRuntime;

    @GetMapping("/list")
    public RespResult<Page<ProcessDefinition>> getProcessDefinition(){
        Page<ProcessDefinition> processDefinitionPage = processAdminRuntime.processDefinitions(Pageable.of(0, 10));
        return ResultUtils.success(processDefinitionPage);
    }
}

4.3.  查询待办任务并完成

按照我们的流程定义,zhangsan提交了请假申请,所以第一个任务是zhangsan的,先让zhangsan登录

Page<Task> page = taskRuntime.tasks(Pageable.of(0, 10));

if (null != page && page.getTotalItems() > 0) {
    for (Task task : page.getContent()) {
        taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(task.getId()).build());
    }
}

由于第一个任务是一个个人任务,所以不需要先认领任务,直接去完成即可

第二个任务是一个组任务,而且我还用了流程变量,因此要么在启动流程实例的时候就给这个流程变量赋值,要么在上一个任务完成时给变量赋值。

这里,我用的是候选组(Candidate Groups),而不是候选者(Candidate Users)。二者差不多,都是组任务,区别在于如果用候选者的话需要列出所有候选用户并用逗号分隔,如果用候选组的话就只需要写组名即可,多个组之间用逗号分隔。

本例中,我也不用流程变量,例如直接写 activiti:candidateGroups="caiwu" 

taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(task.getId()).withVariable("manager", "caiwu").build());

有没有发现,这里查询任务的时候没有指定要查谁的任务,完成任务的时候也没有指定是谁完成的,这都是Spring Security的功劳

到这里可以看出,取的是当前登录用户,即 SecurityContextHolder.getContext().getAuthentication().getName()

SecurityContextHolder.getContext().getAuthentication().getName()

同理,完成任务

接下来的是一个组任务,任务必须由“canwu”这个组的人去完成,为了让 lisi 能看到这个任务,需要在sys_menu表中加一条记录

当lisi登录进来以后,调用 taskRuntime.tasks(Pageable.of(0, 10)) 查询自己的任务时

通过跟代码,我们知道,查询任务其实是这样的,等价于下面这段代码

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String authenticatedUserId = authentication.getName();
List<String> userGroups = authentication.getAuthorities()
        .stream()
        .map(GrantedAuthority::getAuthority)
        .filter(a -> a.startsWith("GROUP_"))
        .map(a -> a.substring("GROUP_".length()))
        .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));

List<Task> taskList = taskService.createTaskQuery()
        .taskCandidateOrAssigned(authenticatedUserId, userGroups)
        .processInstanceId("xxx")
        .listPage(0,10);

查询当前登录用户的个人任务和组任务 

接下来,让 zhaoliu 登录进来 

package com.cjs.example.controller;

import org.activiti.api.runtime.shared.query.Page;
import org.activiti.api.runtime.shared.query.Pageable;
import org.activiti.api.task.model.Task;
import org.activiti.api.task.model.builders.TaskPayloadBuilder;
import org.activiti.api.task.runtime.TaskRuntime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 
 * @Date 2021/7/12
 */
@RestController
@RequestMapping("/task")
public class TaskController {

    @Autowired
    private TaskRuntime taskRuntime;

    @GetMapping("/pageList")
    public void pageList() {
        //  查询待办任务(个人任务 + 组任务)
        Page<Task> page = taskRuntime.tasks(Pageable.of(0, 10));
        
        if (null != page && page.getTotalItems() > 0) {
            for (Task task : page.getContent()) {
                //  认领任务
                taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(task.getId()).build());
                //  完成任务
                taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(task.getId()).build());
            }
        }
    }
}

zhaoliu完成任务后,整个流程就结束了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/111075.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

若依前后端分离框架去掉首页 登录后跳转至动态路由的第一个路由

若依前后端分离框架去掉首页 登录后跳转至动态路由的第一个路由若依框架VUE前端界面&#xff0c;登录后默认跳转至动态路由第一路由&#xff08;第一个子菜单&#xff09;一、登录后跳转第一路由界面二、设置路由的首页路径&#xff0c;方便后续的获取三、点击若依的logo跳转的…

字符串函数(4)--strtok函数和strerror函数

1.strtok函数 — 分割字符串函数 2.strerror函数 — 错误报告函数 1.strstok函数的用法 char* strtok(char* str, const char* delimiters);直接看函数的声明&#xff1a; 参数1&#xff1a;要切割的地址&#xff0c;参数2&#xff1a;切割时的标记物 注意&#xff1a;strt…

单链表的python实现

首先说下线性表&#xff0c;线性表是一种最基本&#xff0c;最简单的数据结构&#xff0c;通俗点讲就是一维的存储数据的结构。 线性表分为顺序表和链接表&#xff1a; 顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素&#xff0c;称为线性表的顺序存储结构或…

Nginx安装配置及vue项目部署【Centos7】

Background 网上好多都是源码安装&#xff0c;各种编译环境安装配置&#xff0c;感觉太麻烦了&#xff0c;这里直接下载一个rpm包就行了&#xff0c;离线安装也方便。 1、nginx rpm包下载 选择你要使用的版本下载。 nginx官方下载地址&#xff1a;http://nginx.org/packages/ce…

垃圾回收机制

1.什么场景下该使用什么垃圾回收策略&#xff1f; (1).对内存要求苛刻的场景 想办法提高对象的回收效率&#xff0c;尽可能的多回收一些对象&#xff0c;腾出更多内存 (2).在CPU使用率较高的情况下 降低高并发时垃圾回收频率&#xff0c;让CPU更多地去执行业务而不是垃圾回收 …

一文搞懂 Redis 架构演化之路

现如今 Redis 变得越来越流行&#xff0c;几乎在很多项目中都要被用到&#xff0c;不知道你在使用 Redis 时&#xff0c;有没有思考过&#xff0c;Redis 到底是如何稳定、高性能地提供服务的&#xff1f; 我使用 Redis 的场景很简单&#xff0c;只使用单机版 Redis 会有什么问…

vite + react +typescript 环境搭建,小白入门级教程

目录前言1. 使用 vite 创建 react 项目1. npm / yarn 命令初始化2. 输入项目名称3. 选择框架4. 选择 Js / Ts5. 项目创建完成6. 启动项目2. 规范项目目录3. 使用 react-router-dom 路由1. 使用 npm / yarn 命令下载2. 更改 react-router-dom 版本1. 通过 npm 命令降低版本2. 手…

rabbitmq基础5——集群节点类型、集群节点基础运维,集群管理命令,API接口工具

文章目录一、集群节点类型1.1 内存节点1.2 磁盘节点二、集群基础运维2.1 剔除单个节点2.1.1 集群正常踢出正常节点2.1.2 服务器异常宕机踢出节点2.1.3 集群正常重置并踢出节点2.2 集群节点版本升级2.3 集群某单节点故障恢复2.3.1 机器硬件故障2.3.2 机器掉电2.3.3 网络故障2.3.…

Android ShapeableImageView使用

ShapeableImageView使用使用 导包 implementation com.google.android.material:material:1.4.0属性 属性描述shapeAppearance样式shapeAppearanceOverlay样式&#xff0c;叠加层strokeWidth描边宽度strokeColor描边颜色 样式 名称属性值描述cornerFamilyrounded圆角cut裁剪…

学习记录-mybatis+vue+elementUi实现品牌查询

和以往不同的是&#xff0c;这一次使用了vue和axios来接收后端传过来的参数&#xff0c;并且新建了impl文件来继承service层的接口。该过程实现一共分为4步骤 步骤一 ①mapper mapper 写mapper接口 其实就是写SQL语句了&#xff0c;第一步就是写SQL。这里使用的是注解开发&…

程序员危机如何化解?

很多人认为程序员一定要干到管理层&#xff0c;才会有继续走下去的希望&#xff0c;而踏实做技术的程序员&#xff0c;只会面临淘汰。事实真是如此吗&#xff1f; △ 截图来源脉脉&#xff0c;如侵删 先不说结论&#xff0c;我们一起先看看网友们的看法&#xff1a; △ 截…

报表控件Stimulsoft v2023.1全新发布 | 附免费下载试用

Stimulsoft Ultimate 宣布发布新版本2023.1&#xff01;在最新版本中添加了对Razor Pages的支持、新的数据监视器可视化工具、为PHP和Blazor平台更新了组件,欢迎下载试用&#xff01; Stimulsoft Ultimate官方正版下载 Razor Pages 在 2023.1.1 版中&#xff0c;我们添加了对…

跨域问题以及解决跨域问题的vue-cli解决方案

跨域问题 写项目前要问后端,接口支持跨域吗? 支持就不会出现问题,不支持就需要解决跨域问题 1.如何判断一个浏览器的请求是否跨域&#xff1f; 在A地址&#xff08;发起请求的页面地址&#xff09;向B地址&#xff08;要请求的目标页面地址&#xff09;发起请求时&#xff…

【JavaEE】多线程(三)线程的状态

✨哈喽&#xff0c;进来的小伙伴们&#xff0c;你们好耶&#xff01;✨ &#x1f6f0;️&#x1f6f0;️系列专栏:【JavaEE】 ✈️✈️本篇内容:线程的状态&#xff0c;线程安全问题&#xff01; &#x1f680;&#x1f680;代码存放仓库gitee&#xff1a;JavaEE初阶代码存放&a…

抗癌药物之多肽药物偶联物技术(PDC)介绍

长期以来&#xff0c;心血管疾病是全球中年人死亡的主要原因。这种情况最近在高收入国家发生了变化&#xff0c;现在癌症导致的死亡人数是心血管疾病的两倍。2018 年&#xff0c;癌症导致全球 960 万人死亡&#xff0c;1810 万新病例被诊断出来。女性最常发生的癌症是乳腺癌&am…

12Python继承与多态

继承 面向对象三大特性 封装 根据 职责 将 属性 和 方法 封装 到一个抽象的 类 中继承 实现代码的重用&#xff0c;相同的代码不需要重复的编写多态 不同的对象调用相同的方法&#xff0c;产生不同的执行结果&#xff0c;增加代码的灵活度 1继承的概念、语法和特点 继承的概…

奖项快报 | ALVA Systems 上榜 《2022 高成长企业 TOP100》!

近日&#xff0c;《2022 高成长企业 TOP 100》榜单发布&#xff0c;凭借卓越的创新能力与在工业 AR 领域的赋能价值&#xff0c;ALVA Systems 在2022年度高成长企业TOP100大赛活动中脱颖而出&#xff0c;成功入选榜单。 *ALVA Systems 入选榜单 创新驱动&#xff0c;赋能数字经…

【ARM -- stm32 汇编代码点亮LED灯】

ARM -- stm32 汇编代码点亮LED灯实现过程查询开发手册分析RCC章节分析GPIO章节编写代码实现过程 查询开发手册 分析RCC章节 一、确定RCC基地址 二、分析RCC_MP_AHB4ENSETR寄存器 1、RCC_MP_AHB4ENSETR寄存器的功能是使能GPIO组相关控制器&#xff1b; 2、RCC_MP_AHB4ENSET…

07第四章:01_常用注解

常用注解 1、Table 作用&#xff1a;建立实体类和数据库表之间的对应关系。 默认规则&#xff1a;实体类类名首字母小写作为表名&#xff0c;如 Employee -> employee 表 用法&#xff1a;在 Table 注解的 name 属性中指定目标数据库的表名&#xff1b; 案例&#xff1…

WPF依赖属性、附加属性、属性继承、类型转换详解

依赖属性 依赖属性回调方法与参数 具有依赖属性的类必须继承自DependencyObject&#xff0c;定义依赖属性要有2个步骤 //1属性包装器&#xff0c;目的是为了向正常属性一样使用依赖属性 public int Name {get { return (int)GetValue(NameProperty); }set { SetValue(NamePr…