原项目中用到了工作流引擎,使用若依框架开发,
原二开使用项目:https://gitee.com/y_project/RuoYi-Vue
基于activiti7地址:https://gitee.com/smell2/ruoyi-vue-activiti
导入模块到原二开项目中

使用步骤

  1. admin导入bpmn文件,或者绘制工作流,并激活。
  2. post为普通员工进入我的审批可以查看所有审批(经销商角色)
  3. 数据会存入历史表和任务表
    在这里插入图片描述
  4. 历史表展现全部任务数据,task会展示对应岗位的相应条数据
  5. 登录商管账号,进入代办任务
  6. 进行审批,通过则状态变为“待财务审核”,进入下一节点;不通过则状态变为“审核失败”,流转结束
  7. 登录财务账号,进入代办任务
  8. 进行审批,通过则状态变为“审核成功”,流转结束;不通过则状态变为“审核失败”,流转结束
  9. 经销商可以查看审批,流转完成

注意事项

在这里插入图片描述
工作流程菜单在开发工具查看(没有的话需要事先插入db和相关代码),在线绘制进行bpmn格式的流程图,部署流程导入已经存在的bpmn文件,根据流程key,在java中进行声明和调用该key即可正式使用该工作流

后端相关

activiti7相关依赖

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter</artifactId>
    <version>7.1.0.M4</version>
</dependency>
<dependency>
    <groupId>org.activiti.dependencies</groupId>
    <artifactId>activiti-dependencies</artifactId>
    <version>7.1.0.M4</version>
    <type>pom</type>
</dependency>

解决依赖冲突
activiti7新版本会和mybatis和spring security起冲突,如果项目使用shiro等安全框架会有更大的适配问题
原项目有spring security的情况下需加上配置文件属性

main:
    allow-bean-definition-overriding: true

activity配置文件属性如下,包括检测流程定义,自动更新和生成db系统表,历史数据级别和使用等

activiti:
  check-process-definitions: false
  database-schema-update: true
  history-level: full
  db-history-used: true

db依赖问题或mybatis版本冲突解决方法如下

<exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </exclusion>
 </exclusions>

数据源配置,给url加上如下后缀属性

%2B8&nullCatalogMeansCurrent=true

完整url格式如下:

url: jdbc:mysql://localhost:99999/test11111111?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true

在使用mysql-connect 8.+以上版本的时候需要添加***nullCatalogMeansCurrent=true***参数,否则在使用mybatis-generator生成表对应的xml等时会扫描整个服务器里面的全部数据库中的表,而不是扫描对应数据库的表。因此mysql会扫描所有的库来找表,如果其他库中有相同名称的表,activiti就以为找到了,本质上这个表在当前数据库中并不存在。

接口

流程部署
在这里插入图片描述

可以将bpmn文件放在resource下的processes目录下,activiti启动的时候会自动加载该目录下的bpmn文件,或者通过调用接口方式部署:

  • 上传文件部署
@PostMapping("/uploadFileAndDeployment")
public BaseResponse uploadFileAndDeployment(
    @RequestParam("processFile")MultipartFile processFile,
    @RequestParam(value = "processName",required = false) String processName){
    
    String originalFilename = processFile.getOriginalFilename();
    String extension = FilenameUtils.getExtension(originalFilename);
    if (processName != null){
        processName = originalFilename;
    }
    try {
        InputStream inputStream = processFile.getInputStream();
        Deployment deployment = null;
        if ("zip".equals(extension)){
            // 压缩包部署方式
            ZipInputStream zipInputStream = new ZipInputStream(inputStream);
            deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).name(processName).deploy();
        }else if ("bpmn".equals(extension)){
            // bpmn文件部署方式
            deployment = repositoryService.createDeployment().addInputStream(originalFilename,inputStream).name(processName).deploy();
        }
        return BaseResponse.success(deployment);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return BaseResponse.success();
}
  • 上传BPMN内容字符串部署
 @PostMapping("/postBPMNAndDeployment")
public BaseResponse postBPMNAndDeployment(@RequestBody AddXMLRequest addXMLRequest){
    Deployment deploy = repositoryService.createDeployment()
        // .addString 第一次参数的名字如果没有添加.bpmn的话,不会插入到 ACT_RE_DEPLOYMENT 表中
        .addString(addXMLRequest.getProcessName()+".bpmn", addXMLRequest.getBpmnContent())
        .name(addXMLRequest.getProcessName())
        .deploy();
    return BaseResponse.success(deploy);
}
  • 获取流程资源文件
@GetMapping("/getProcessDefineXML")
public void getProcessDefineXML(String deploymentId, String resourceName, HttpServletResponse response){
    try {
        InputStream inputStream = repositoryService.getResourceAsStream(deploymentId,resourceName);
        int count = inputStream.available();
        byte[] bytes = new byte[count];
        response.setContentType("text/xml");
        OutputStream outputStream = response.getOutputStream();
        while (inputStream.read(bytes) != -1) {
            outputStream.write(bytes);
        }
        inputStream.close();
    } catch (Exception e) {
        e.toString();
    }
}

流程实例

  • 启动
@PostMapping("/startProcess")
public BaseResponse startProcess(
    String processDefinitionKey, 
    String instanceName,
    @AuthenticationPrincipal LocalUserDetail userDetail){
    
    ProcessInstance processInstance = null;
    try{
        StartProcessPayload startProcessPayload = ProcessPayloadBuilder.start().withProcessDefinitionKey(processDefinitionKey)
            .withBusinessKey("businessKey")
            .withVariable("sponsor",userDetail.getUsername())
            .withName(instanceName).build();
        processInstance = processRuntime.start(startProcessPayload);
    }catch (Exception e){
        System.out.println(e);
        return BaseResponse.error("开启失败:"+e.getLocalizedMessage());
    }
    return BaseResponse.success(processInstance);
}
  • 挂起
@PostMapping("/suspendInstance/{instanceId}")
public BaseResponse suspendInstance(@PathVariable String instanceId){
    ProcessInstance processInstance = processRuntime.suspend(ProcessPayloadBuilder.suspend().withProcessInstanceId(instanceId).build());
    return BaseResponse.success(processInstance);
}
  • 激活
@PostMapping("/resumeInstance/{instanceId}")
public BaseResponse resumeInstance(@PathVariable String instanceId){
    ProcessInstance processInstance = processRuntime
        .resume(ProcessPayloadBuilder.resume().withProcessInstanceId(instanceId).build());
    return BaseResponse.success(processInstance);
}

任务数据

  • 通过taskid完成任务
@PostMapping("/completeTask/{taskId}")
public BaseResponse completeTask(@PathVariable String taskId){
    Task task = taskRuntime.task(taskId);
    if (task.getAssignee()==null){
        // 说明任务需要拾取
        taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(taskId).build());
    }
    taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskId).build());
    return BaseResponse.success();
}
  • 获取自己的任务(与鉴权机制挂钩)
@GetMapping("/getTasks")
public BaseResponse getTasks(){
    Page<Task> taskPage = taskRuntime.tasks(Pageable.of(0, 100));
    List<Task> tasks = taskPage.getContent();
    List<TaskVO> taskVOS = new ArrayList<>();
    for (Task task : tasks) {
        TaskVO taskVO = TaskVO.of(task);
        ProcessInstance instance = processRuntime.processInstance(task.getProcessInstanceId());
        taskVO.setInstanceName(instance.getName());
        taskVOS.add(taskVO);
    }
    return BaseResponse.success(taskVOS);
}

历史数据

  • 查询
public List<HistoricActivityInstanceVO> getProcessHistoryByBusinessKey(String businessKey) {
    ProcessInstance instance = runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(businessKey).singleResult();
    List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery().processInstanceId(instance.getId())
        .orderByHistoricActivityInstanceStartTime().asc().list();
    List<HistoricActivityInstanceVO> historicActivityInstanceVOList = new ArrayList<>();
    historicActivityInstanceList.forEach(historicActivityInstance -> historicActivityInstanceVOList.add(VOConverter.getHistoricActivityInstanceVO(historicActivityInstance)));
    return historicActivityInstanceVOList;
}
  • 详情查询
HistoricDetailQuery historicDetailQuery = historyService.createHistoricDetailQuery();
List<HistoricDetail> historicDetails = historicDetailQuery.processInstanceId(instanceId).orderByTime().list();
for (HistoricDetail hd: historicDetails) {
    System.out.println("流程实例ID:"+hd.getProcessInstanceId());
    System.out.println("活动实例ID:"+hd.getActivityInstanceId());
    System.out.println("执行ID:"+hd.getTaskId());
    System.out.println("记录时间:"+hd.getTime());
}
  • 历史流程实例查询
HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
List<HistoricProcessInstance> processInstances = historicProcessInstanceQuery.processDefinitionId(processDefinitionId).list();
for (HistoricProcessInstance hpi : processInstances) {
    System.out.println("业务ID:"+hpi.getBusinessKey());
    System.out.println("流程定义ID:"+hpi.getProcessDefinitionId());
    System.out.println("流程定义Key:"+hpi.getProcessDefinitionKey());
    System.out.println("流程定义名称:"+hpi.getProcessDefinitionName());
    System.out.println("流程定义版本:"+hpi.getProcessDefinitionVersion());
    System.out.println("流程部署ID:"+hpi.getDeploymentId());
    System.out.println("开始时间:"+hpi.getStartTime());
    System.out.println("结束时间:"+hpi.getEndTime());
}
  • 任务历史查询(某一次流程的执行经历的多少任务)
HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
List<HistoricTaskInstance> taskInstances = historicTaskInstanceQuery.taskId(taskId).list();
for (HistoricTaskInstance hti : taskInstances) {
    System.out.println("开始时间:"+hti.getStartTime());
    System.out.println("结束时间:"+hti.getEndTime());
    System.out.println("任务拾取时间:"+hti.getClaimTime());
    System.out.println("删除原因:"+hti.getDeleteReason());
}

github例子:https://github.com/Activiti/activiti-examples

鉴权机制

官方security轮子

@Component
public class SecurityUtil {
    // 模拟调用了SpringSecurity 登录鉴权
    private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
    @Autowired
    private UserDetailsService userDetailsService;
    public void logInAs(String username) {
        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (user == null) {
            throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
        }
        logger.info("> Logged in as: " + username);
        SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return user.getAuthorities();
            }
            @Override
            public Object getCredentials() {
                return user.getPassword();
            }
            @Override
            public Object getDetails() {
                return user;
            }
            @Override
            public Object getPrincipal() {
                return user;
            }
            @Override
            public boolean isAuthenticated() {
                return true;
            }
            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            } 
            @Override
            public String getName() {
                return user.getUsername();
            }
        }));
        org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
    }
}

与原项目spring security整合
新api包括taskRuntime和processRuntime都会强制使用security,源码使用了如下注解:
@PreAuthorize(“hasRole(‘ACTIVITI_USER’)”)
直接使用接口会导致无权限不允许访问

activiti7中对原有的一些接口做了二次封装,从而进一步简化了用户的使用流程。
通过查看这个两个API的实现类源码来看,调用的话需要调用的用户含有ACTIVITI_USER角色权限。所以,如果没有使用SpringSecurity的话,这两个API便不能直接调用。

  • 先在用户验证处理中插入GROUP_的岗位post和加入ACTIVITI_USER的role
public UserDetails createLoginUser(SysUser user)
{
    Set<String> postCode = sysPostService.selectPostCodeByUserId(user.getUserId());
    postCode = postCode.parallelStream().map( s ->  "GROUP_" + s).collect(Collectors.toSet());
    postCode.add("ROLE_ACTIVITI_USER");
    List<SimpleGrantedAuthority> collect = postCode.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList());
    return new LoginUser(user, permissionService.getMenuPermission(user), collect);
    //return new LoginUser(user, permissionService.getMenuPermission(user));
}
  • 在login controller中,每次登录的时候鉴权
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
        .authenticate(new UsernamePasswordAuthenticationToken(username, password));

最后将信息放入token中处理

if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
    tokenService.verifyToken(loginUser);
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
  • SecurityConfig也别忘记配
httpSecurity
        // CSRF禁用,因为不使用session
        .csrf().disable()
        // 认证失败处理类
        .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
        // 基于token,所以不需要session
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        // 过滤请求
        .authorizeRequests()
        // 对于登录login 验证码captchaImage 允许匿名访问
        .antMatchers("/ssologin/token", "/ssologin","/login", "/captchaImage","/getssourl","/ssologin").anonymous()
        .antMatchers(
                HttpMethod.GET,
                "/*.html",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js"
        ).permitAll()
        .antMatchers("/processDefinition/**").permitAll()
        .antMatchers("/activitiHistory/**").permitAll()
        .antMatchers("/profile/**").anonymous()
        .antMatchers("/common/download**").anonymous()
        .antMatchers("/common/download/resource**").anonymous()
        .antMatchers("/swagger-ui.html").anonymous()
        .antMatchers("/swagger-resources/**").anonymous()
        .antMatchers("/webjars/**").anonymous()
        .antMatchers("/*/api-docs").anonymous()
        .antMatchers("/druid/**").anonymous()
        // 除上面外的所有请求全部需要鉴权认证
        .anyRequest().authenticated()
        .and()
        .headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

查询当前用户任务

  • taskservice方法
//1.得到ProcessEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.得到TaskService对象
TaskService taskService = processEngine.getTaskService();
//3.根据流程定义的key,负责人assignee来实现当前用户的任务列表查询
        Task task = taskService.createTaskQuery()
                .processDefinitionKey("holiday")
                .taskAssignee(SecurityUtils.getUsername() //通过鉴权拿到当前角色)
                .singleResult();
//4.任务列表的展示
        System.out.println("流程实例ID:"+task.getProcessInstanceId());
        System.out.println("任务ID:"+task.getId());  //5002
        System.out.println("任务负责人:"+task.getAssignee());
        System.out.println("任务名称:"+task.getName());
  • taskruntime方法(新版本api)
    通过taskRuntime.tasks获取任务列表并分页,在实例化一条工作流后,activiti会将数据存到ACT_RU_TASK和ACT_RU_IDENTITYLINK表中,IDENTITYLINK通过task_id作为外键关联TASK表,IDENTITYLINK根据用户身份鉴别相应的角色,通过GROUP_ID筛选出对应数据,TYPE_字段则显示该角色是参与者还是贡献者,db如下:
    在这里插入图片描述
    若衣源码:
@Override
public Page<ActTaskDTO> selectProcessDefinitionList(PageDomain pageDomain) {	
    Page<ActTaskDTO> list = new Page<ActTaskDTO>();
    org.activiti.api.runtime.shared.query.Page<Task> pageTasks = taskRuntime.tasks(Pageable.of((pageDomain.getPageNum() - 1) * pageDomain.getPageSize(), pageDomain.getPageSize()));
    List<Task> tasks = pageTasks.getContent();
    int totalItems = pageTasks.getTotalItems();
    list.setTotal(totalItems);
    if (totalItems != 0) {
        Set<String> processInstanceIdIds = tasks.parallelStream().map(t -> t.getProcessInstanceId()).collect(Collectors.toSet());
        List<ProcessInstance> processInstanceList = runtimeService.createProcessInstanceQuery().processInstanceIds(processInstanceIdIds).list();
        List<ActTaskDTO> actTaskDTOS = tasks.stream()
                .map(t -> new ActTaskDTO(t, processInstanceList.parallelStream().filter(pi -> t.getProcessInstanceId().equals(pi.getId())).findAny().get()))
                .collect(Collectors.toList());
        list.addAll(actTaskDTOS);
    }
    return list;
}

实际项目遇到的问题及解决方法

Error updating database. Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘VERSION_’ in ‘field list’
Error updating database. Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘PROJECT_RELEASE_VERSION_’ in ‘field list’
原因:
创建表缺少VERSION_字段
添加两个字段。

新版bug问题解决

alter table ACT_RE_DEPLOYMENT add column PROJECT_RELEASE_VERSION_ varchar(255) DEFAULT NULL;
alter table ACT_RE_DEPLOYMENT add column VERSION_ varchar(255) DEFAULT NULL;

在这里插入图片描述

前端相关

基于BPMN2.0的工作流
demo实例https://demo.bpmn.io/s/start
节点如下,教程https://www.jianshu.com/p/a8a21870986a
在这里插入图片描述

审批流程图:
在这里插入图片描述
bpmnjs引入
导入相关代码,包括我的审批,代办任务,历史流程等的表单设计和模块划分,其他的调整等

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐