手摸手系列之SpringBoot+Vue整合snakeflow工作流实战

前言

技术栈:
SpringBoot: 2.3.5.RELEASE
Vue: 2.6.10
snakerflow: 2.5.1

最近做集团内部的悦通关平台项目,台账管理的付款申请模块需要用到工作流审批功能,本着轻量的目的,特选定了国内开发者开源的一款轻量级工作流引擎-snakeflow。官网了解到Snaker是一个基于Java的轻量级工作流引擎,适用于企业应用中常见的业务流程。本着轻量、简单、灵巧理念设计,定位于简单集成、多环境支持,基于Apache License Version 2.0开源协议发布。文档指南[点我直达]

snakeflow核心简介

  1. 7张核心表定义

    • WF_PROCESS:流程定义,例如请假申请,用车申请等流程定义。

    • WF_ORDER:流程实例,当前运行中的流程实例的信息,比如,我申请了一条请假流程,流程编号“请假-001”,这个请假流程“请假-001”就是一个流程实例。

    • WF_HIST_ORDER:历史流程实例,只要启动过的流程,历史流程实例表中就会存储流程的实例信息,比如这条流程是否结束。

    • WF_TASK:任务,某个流程中某个节点,叫做任务实例。比如“请假-001”中需要上级审批,“上级审批”这个节点就是一个任务实例。

    • WF_HIST_TASK:历史任务(就是执行完了的任务),所有任务结束以后都会在历史任务信息表里存一条记录。

    • WF_TASK_ACTOR:每个任务对应的参与者(记住谁可以签收或处理),可能会有多个人或者用户组。

    • WF_HIST_TASK_ACTOR:历史的参与者表,任务处理完以后,对应处理人的信息会存到这个表里面。

  2. 各核心表及流程详解:

    WF_PROCESS:存放流程定义,通过编辑一个后缀为.snaker的xml文档来定义流程的走向;在前端流程定义管理里有个部署流程的按钮,可以将编辑好的xml文档保存到WF_PROCESS表中,并产生一条记录。xml文档里有流程的名字,如果这个名字在WF_PROCESS已经存在了,则保存时产生的新的记录的version字段值会自动加1。processId是唯一的,不重复。

    WF_ORDER:存放流程实例的。开启一个流程实例时,WF_ORDER表有个字段PROCESS_ID和流程定义的process_id相关联,他们是一对多的关系。当开启一个流程实例WF_ORDER时,在WF_ORDER和WF_HIST_ORDER都新增一条记录,并且同时产生的那两条记录的主键id是一样的。,其中WF_HIST_ORDER比WF_ORDER多一个字段ORDER_STATUS 流程实例状态(0:结束;1:活动)。当流程没跑完时,ORDER_STATUS的值是1;当整个事件流程跑完了,WF_ORDER表的那条记录会被删除,WF_HIST_ORDER表对应的那条记录的ORDER_STATUS的值变成0,表示流程实例跑完了。

    WF_TASK:存当前任务的;当流程执行完当前任务节点时,WF_TASK的这条记录会被剪切到WF_HIST_TASK表中,然后在WF_TASK表中新增下一个任务节点的信息记录。新增的下一任务节点的task有个字段parent_task_id记住上一个任务节点在WF_HIST_TASK表里的WF_HIST_TASK_ID(上一个任务节点从WF_TASK表剪切到WF_HIST_TASK了);这样就可以实现回退等功能。

    WF_TASK_ACTOR:存哪个任务关联了那些参与者的,就是哪些人可以签收或者处理。如果当前任务节点被执行了,则相关的参与者会被剪切到WF_HIST_TASK_ACTOR表里,和WF_TASK、WF_HIST_TASK类似,这样回退时,就知道以前这一步是谁处理的。

    假如现在执行节点的任务是A2,上述中wf_task存的是执行中的记录,也就是说在执行A2前,wf_task中肯定会有一条执行中的任务记录,假设为A1,那么执行A2时的增删改为先将wf_task和wf_task_actor表中A1的记录插入wf_hist_task和wf_hist_task_actor中;其次是删除wf_task和wf_task_actor中A1记录,然后将A2的信息插入到wf_task和wf_task_actor中。此时wf_task表的parent_task_id是历史表A1记录的Id,通过此可将所有任务串联起来。task表的variable中的值为局部变量只能在当前task中使用。有人会有疑问,假设有如下流程图,执行task1时会怎么样?

    因为开始节点是一个比较特殊的nodemodel,称之为流转逻辑元素,它只负责流转到下一节点不负责执行,也就没有数据库的增删改。这样直接流转到task1,插入wf_hist_task和wf_hist_task_actor,并且又直接流转到end节点,end节点也是一个流转元素,它会直接删除wf_order表的记录并更改该流程实例wf_hist_order表的order_state的状态。

    至此一个流程的所有增删改查结束。

项目整合

pom引入snakeflow依赖

<dependency>
  <groupId>com.github.snakerflowgroupId>
  <artifactId>snaker-springartifactId>
  <version>2.5.1version>
dependency>
<dependency>
  <groupId>com.github.snakerflowgroupId>
  <artifactId>snaker-mybatisartifactId>
  <version>2.4.0version>
dependency>
<dependency>
  <groupId>com.github.snakerflowgroupId>
  <artifactId>snaker-coreartifactId>
  <version>2.5.1version>
dependency>

根据业务设计流程定义

官方的流程设计器比较老旧比较丑,推荐一个第三方图形化的流程设计器:模型设计 - mldong快速开发平台

手摸手系列之SpringBoot+Vue整合snakeflow工作流实战_第1张图片

界面如上图,根据自己的业务需求,设计出相应的流程图,然后点击查看按钮导出xml代码:

手摸手系列之SpringBoot+Vue整合snakeflow工作流实战_第2张图片

在项目的resources目录下新建.snaker后缀的流程定义文件,将xml代码复制进去,此文件即为流程定义模型文件,工作流引擎会自动生成对应的流程定义模型。

手摸手系列之SpringBoot+Vue整合snakeflow工作流实战_第3张图片

具体xml代码:


<process  name="payreq" displayName="付款申请流程" instanceUrl="/snaker/flow/all">
    <start name="start" displayName="开始" layout="780,-180,120,80">
        <transition name="b288a53d-e54e-473d-8ed1-fc4fa21f0210" to="apply" g="780,-162;780,-132;780,-132;780,-130;780,-130;780,-100"/>
    start>
    <task name="apply" displayName="付款申请" layout="780,-60,120,80" assignee="apply.operator" performType="ANY">
        <transition name="0796e532-f9fd-414d-81a8-04415bae48f2" to="gbmjl" g="780,-20;780,10;780,10;780,-10;780,-10;780,20"/>
    task>
    <task name="gbmjl" displayName="各部门经理" layout="780,60,120,80" assignmentHandler="com.yorma.flow.handle.GBMJLHandler" performType="ANY">
        <transition name="de03eafd-5a16-41d1-94b1-eb72864be456" to="30e3c5fe-aa7d-4cdf-98e5-85613d61e2c1" g="780,100;780,130;780,130;780,125;780,125;780,155"/>
    task>
    <decision name="30e3c5fe-aa7d-4cdf-98e5-85613d61e2c1" layout="780,180,120,80">
        <transition name="59bdf184-7a83-4bcd-9ad8-19ce53d47f2c" displayName="代垫已收款" to="cwjl" expr="#isddysk == 1" g="805,180;1090,180;1090,700;840,700"/>
        <transition name="70239222-b35d-46ef-a8f8-c4e62c27a46e" displayName="非代垫已收款" to="fgbmld" expr="#isddysk == 0" g="780,205;780,235;780,235;780,250;780,250;780,280"/>
    decision>
    <task name="cwjl" displayName="财务经理" layout="780,700,120,80" assignee="cwjl.operator" performType="ANY">
        <transition name="cd789408-b162-4918-9a5b-62f91c6fa266" to="cnfk" g="780,740;780,800"/>
    task>
    <task name="cnfk" displayName="出纳付款" layout="780,840,120,80" assignee="cnfk.operator" performType="ANY">
        <transition name="e2d934c5-a656-4870-a984-3874cfc24d0e" to="1cb24741-9c96-4624-a9b9-311b4327b007" g="780,880;780,962"/>
    task>
    <end name="1cb24741-9c96-4624-a9b9-311b4327b007" displayName="结束" layout="780,980,120,80">
    end>
    <task name="fgbmld" displayName="分管部门领导" layout="780,320,120,80" assignee="fgbmld.operator" performType="ANY">
        <transition name="1eec0e66-d27c-4968-a1bc-0af8369faad0" to="8d46ec89-91d2-4ab3-b4e0-03d603beb2a1" g="780,360;780,390;780,390;780,365;780,365;780,395"/>
    task>
    <decision name="8d46ec89-91d2-4ab3-b4e0-03d603beb2a1" layout="780,420,120,80">
        <transition name="b4210332-ea95-4b5c-9a4f-9b7efa71fab1" displayName="代垫未收款" to="3a5427a1-fd3b-4638-9f3e-cf0960b30a76" expr="#moneyType == 3" g="805,420;980,420;980,535"/>
        <transition name="cd19027f-df5e-4c88-bc22-f3645fd4774e" displayName="业务款项" to="0715456f-f696-4d28-b4fc-ae1380349278" expr="#moneyType == 1" g="755,420;300,420;300,535"/>
    decision>
    <decision name="3a5427a1-fd3b-4638-9f3e-cf0960b30a76" layout="980,560,120,80">
        <transition name="e24c7368-b555-4ff5-af2b-eabc8e4e3f0d" displayName="5W以上" to="zjldsz" expr="#wskMoney >= 50000" g="955,560;840,560"/>
        <transition name="5cf86118-b12e-4378-8b55-987ad6310ea7" displayName="5W以下" to="cwjl" expr="#wskMoney < 50000" g="980,585;980,700;840,700"/>
    decision>
    <task name="zjldsz" displayName="总经理董事长" layout="780,560,120,80" assignee="zjldsz.operator" performType="ANY">
        <transition name="f30af5a8-f9f0-4fe8-acb3-6f7f0b46e219" to="cwjl" g="780,600;780,660"/>
    task>
    <decision name="0715456f-f696-4d28-b4fc-ae1380349278" layout="300,560,120,80">
        <transition name="cb99aefa-c5da-4492-baef-166b60607851" displayName="人民币" to="c72c188c-6ad3-49f6-abf9-e501fb68e315" expr="#currtype == 1" g="325,560;475,560"/>
        <transition name="776eaba1-1ef1-4ef7-982e-67c141e6141a" displayName="美元" to="a32c6d94-d4a2-402e-aa29-f4938c01bdb9" expr="#currtype == 2" g="300,585;300,780;475,780"/>
    decision>
    <decision name="c72c188c-6ad3-49f6-abf9-e501fb68e315" layout="500,560,120,80">
        <transition name="99d8103a-f619-4631-a26e-4a9574cc5023" displayName="人民币20W以下" to="cwjl" expr="#ywMoney < 200000" g="500,585;500,700;720,700"/>
        <transition name="0bb52a5e-357c-4bd3-a653-4275692f9173" displayName="人民币20W以上" to="zjldsz" expr="#ywMoney >= 200000" g="500,535;500,499;780,499;780,520"/>
    decision>
    <decision name="a32c6d94-d4a2-402e-aa29-f4938c01bdb9" layout="500,780,120,80">
        <transition name="b9850dba-8b59-4c28-97f7-c9f07ebb5515" displayName="美元3W以上" to="zjldsz" expr="#ywMoney >= 30000" g="525,780;555,780;555,559;720,559"/>
        <transition name="7eb1d054-ae82-4972-921b-aede3a7e27e6" displayName="美元3W以下" to="cwjl" expr="#ywMoney < 30000" g="500,805;500,835;690,835;690,700;720,700"/>
    decision>
process>

当然,流程设计器也支持根据xml生成流程图,方便修改流程图,点击导入按钮粘贴xml即可:

手摸手系列之SpringBoot+Vue整合snakeflow工作流实战_第4张图片

创建流程定义,同时启动一个流程实例执行任务

package com.yorma.flow.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yorma.entity.YmMsg;
import com.yorma.flow.entity.WfProcess;
import com.yorma.flow.entity.dto.WfProcessParam;
import com.yorma.flow.mapper.WfProcessMapper;
import com.yorma.flow.service.ISysUserService;
import com.yorma.flow.service.IWfProcessService;
import com.yorma.util.RedisUtil;
import com.yorma.util.RequestKit;
import lombok.extern.slf4j.Slf4j;
import org.snaker.engine.SnakerEngine;
import org.snaker.engine.access.QueryFilter;
import org.snaker.engine.entity.Order;
import org.snaker.engine.entity.Process;
import org.snaker.engine.entity.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static cn.hutool.core.util.ObjectUtil.isEmpty;
import static cn.hutool.core.util.StrUtil.isBlank;
import static com.yorma.flow.utils.Const.*;

/**
 * 

* 流程定义表 服务实现类 *

* * @author ZHANGCHAO * @since 2022-06-22 */
@Slf4j @Service public class WfProcessServiceImpl extends ServiceImpl<WfProcessMapper, WfProcess> implements IWfProcessService { @Autowired private SnakerEngine engine; @Autowired private ISysUserService userService; @Autowired private RedisUtil redisUtil; /** * 获取或创建流程定义并启动流程实例执行任务 * * @param processParam * @return com.yorma.entity.YmMsg * @apiNote
     *   获取或创建流程定义并启动流程实例执行任务
     * 
* @author ZHANGCHAO 2022/6/22 16:51 * @version 1.0 */
@Override public YmMsg<Order> initAndStartProcess(WfProcessParam processParam) { if (isBlank(processParam.getFlowFile())) { return YmMsg.error("未知的流程定义!"); } String tenantId = RequestKit.getRequestIn().getHeader(TENANT_ID); String token = RequestKit.getRequestIn().getHeader(HEADER_TOKEN); // 从redis获取当前登录用户名 String username = (String) ((HashMap<String, Object>) redisUtil.get(PREFIX_USER_TOKEN_INFO + token)).get(USERNAME); String userId = userService.getUserId(username, tenantId); Map<String, Object> args = new HashMap<>(16); args.put("apply.operator", userId); Process process; if (processParam.isNewProcess()) { String deploy = initFlows(processParam.getFlowFile(), userId); process = engine.process().getProcessById(deploy); } else { QueryFilter filter = new QueryFilter().setName(processParam.getFlowFile()).orderBy("version").order("DESC"); List<Process> processList = engine.process().getProcesss(filter); if (isEmpty(processList)) { String deploy = initFlows(processParam.getFlowFile(), userId); process = engine.process().getProcessById(deploy); } else { process = processList.get(0); } } if (isEmpty(process)) { return YmMsg.error("未获取到流程定义!"); } log.info("[initAndStartProcess]获取到的流程定义process:" + process); // 启动实例 Order order = engine.startInstanceById(process.getId(), userId, args); log.info("[initAndStartProcess]获取到的流程实例order:" + order); List<Task> tasks = engine.query().getActiveTasks(new QueryFilter().setOrderId(order.getId())); if (isEmpty(tasks)) { return YmMsg.error("流程实例Order[" + order.getId() + "]下无活动的任务列表!"); } List<Task> newTasks = new ArrayList<>(); if (tasks != null && tasks.size() > 0) { Task task = tasks.get(0); newTasks.addAll(engine.executeTask(task.getId(), userId)); } return YmMsg.ok(order); } /** * 初始化状态机流程 * * @return 流程主键 */ private String initFlows(String flowFile, String creator) { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); InputStream stream = classLoader.getResourceAsStream("flows/flowFile.snaker".replace("flowFile", flowFile)); return engine.process().deploy(stream, creator); } }

调用此方法后,会根据传进来的流程定义名称去生成相应的流程定义并启动一个实例,而且此处让其自动执行了下一节点,因为下一节点的操作人也是流程发起人。执行完后,到达了2节点:

手摸手系列之SpringBoot+Vue整合snakeflow工作流实战_第5张图片

任务执行方法,即节点的移动逻辑

package com.yorma.flow.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yorma.entity.YmMsg;
import com.yorma.flow.entity.WfTask;
import com.yorma.flow.entity.dto.WfConstants;
import com.yorma.flow.entity.dto.WfTaskParam;
import com.yorma.flow.entity.dto.WfWorkItemParam;
import com.yorma.flow.mapper.WfTaskMapper;
import com.yorma.flow.service.ISysUserService;
import com.yorma.flow.service.IWfTaskService;
import com.yorma.flow.utils.exception.ExceptionUtil;
import com.yorma.util.RedisUtil;
import com.yorma.util.RequestKit;
import lombok.extern.slf4j.Slf4j;
import org.snaker.engine.SnakerEngine;
import org.snaker.engine.access.Page;
import org.snaker.engine.access.QueryFilter;
import org.snaker.engine.entity.Order;
import org.snaker.engine.entity.Task;
import org.snaker.engine.entity.WorkItem;
import org.snaker.engine.model.EndModel;
import org.snaker.engine.model.ProcessModel;
import org.snaker.engine.model.TaskModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import static cn.hutool.core.util.ObjectUtil.isEmpty;
import static cn.hutool.core.util.ObjectUtil.isNotEmpty;
import static cn.hutool.core.util.StrUtil.isNotBlank;
import static com.yorma.flow.utils.Const.*;

/**
 * 

* 任务表 服务实现类 *

* * @author ZHANGCHAO * @since 2022-06-24 */
@Slf4j @Service public class WfTaskServiceImpl extends ServiceImpl<WfTaskMapper, WfTask> implements IWfTaskService { @Autowired private SnakerEngine engine; @Autowired private ISysUserService userService; @Autowired private RedisUtil redisUtil; /** * 执行流程任务task * * @param taskParam * @return com.yorma.entity.YmMsg * @apiNote
     *   执行流程任务task
     * 
* @author ZHANGCHAO 2022/6/24 14:34 * @version 1.0 */
@Override public YmMsg<List<Task>> executeTask(WfTaskParam taskParam) { if (isEmpty(taskParam)) { return YmMsg.error("参数[taskParam]不能为空!"); } List<Task> newTasks; try { String tenantId = RequestKit.getRequestIn().getHeader(TENANT_ID); String token = RequestKit.getRequestIn().getHeader(HEADER_TOKEN); // 从redis获取当前登录用户名 String username = (String) ((HashMap<String, Object>) redisUtil.get(PREFIX_USER_TOKEN_INFO + token)).get(USERNAME); String userId = userService.getUserId(username, tenantId); // 同意 if (Integer.valueOf(1).equals(taskParam.getArgs().get(WfConstants.APPROVAL_TYPE)) || "1".equals(taskParam.getArgs().get(WfConstants.APPROVAL_TYPE))) { if (isNotBlank(taskParam.getTaskId())) { List<Task> tasks = engine.executeTask(taskParam.getTaskId(), userId, taskParam.getArgs()); if (isNotEmpty(tasks)) { for (Task task : tasks) { task.setModel(null); } } return YmMsg.ok(tasks); } List<Task> tasks = engine.query().getActiveTasks(new QueryFilter().setOrderId(taskParam.getOrderId()).setOperator(userId)); if (isEmpty(tasks)) { tasks = engine.query().getActiveTasks(new QueryFilter().setOrderId(taskParam.getOrderId())); } if (isEmpty(tasks)) { return YmMsg.error("流程实例Order[" + taskParam.getOrderId() + "]下无活动的任务列表!"); } newTasks = new ArrayList<>(); if (tasks != null && tasks.size() > 0) { Task task = tasks.get(0); newTasks.addAll(engine.executeTask(task.getId(), userId, taskParam.getArgs())); } // 撤回操作,直接结束流程 } else if (Integer.valueOf(3).equals(taskParam.getArgs().get(WfConstants.APPROVAL_TYPE)) || "3".equals(taskParam.getArgs().get(WfConstants.APPROVAL_TYPE))) { Task task; if (isNotBlank(taskParam.getTaskId())) { task = engine.query().getTask(taskParam.getTaskId()); } else { List<Task> tasks = engine.query().getActiveTasks(new QueryFilter().setOrderId(taskParam.getOrderId())); task = isNotEmpty(tasks) ? tasks.get(0) : new Task(); } Order order = engine.query().getOrder(task.getOrderId()); // 1.2 直接跳到结束节点 ProcessModel processModel = engine.process().getProcessById(order.getProcessId()).getModel(); engine.executeAndJumpTask(task.getId(), userId, taskParam.getArgs(), processModel.getModels(EndModel.class).get(0).getName()); newTasks = new ArrayList<>(); // 不同意 } else { // 1.1 给流程实例追加额外参数 Task task; if (isNotBlank(taskParam.getTaskId())) { task = engine.query().getTask(taskParam.getTaskId()); } else { List<Task> tasks = engine.query().getActiveTasks(new QueryFilter().setOrderId(taskParam.getOrderId())); task = isNotEmpty(tasks) ? tasks.get(0) : new Task(); } Order order = engine.query().getOrder(task.getOrderId()); // Map addArgs = new HashMap<>(taskParam.getArgs()); // addArgs.put(WfConstants.ORDER_STATE_KEY, WfOrderStateEnum.DISAGREE.getValue()); // engine.order().addVariable(taskParam.getOrderId(), addArgs); // 1.2 直接跳到结束节点 ProcessModel processModel = engine.process().getProcessById(order.getProcessId()).getModel(); // engine.executeAndJumpTask(task.getId(), userId, taskParam.getArgs(), processModel.getModels(EndModel.class).get(0).getName()); // 跳到流程发起点 engine.executeAndJumpTask(task.getId(), userId, taskParam.getArgs(), processModel.getModels(TaskModel.class).get(0).getName()); newTasks = new ArrayList<>(); } if (isNotEmpty(newTasks)) { for (Task task : newTasks) { task.setModel(null); } } } catch (Exception e) { e.printStackTrace(); ExceptionUtil.getFullStackTrace(e); return YmMsg.error(e.getMessage()); } return YmMsg.ok(newTasks); } }

每次执行此方法,都会执行当前节点的任务,然后移动到下一节点。

注意:每次执行任务时,都需要把下一节点的操作人即任务决策者标识传入进去,不然引擎无法知晓下一节点决策者是谁,也就无法验证权限!

snakeflow的任务参与者支持三种方式:

  • 直接设置静态参与者,即assignee值为用户、部门或角色的标识符

  • 通过运行时动态传递,即assignee值为变量名称,在调用流程引擎的api时,通过map参数传递这个变量值

  • 通过自定义类[继承Assignment类],设置assignmentHandler属性,assign方法返回值就是参与者

任务执行预处理

项目采用的RPC调用方式:前端调用中间服务,中间服务再通过RPC调用流程的任务执行接口。中间服务的预处理方法可以设置参数,设置流程参与者:

 /**
     * 执行任务前的预处理
     *
     * @param approvalDTO
     * @return com.yorma.entity.YmMsg
     * @apiNote 
     *   执行任务前的预处理
     * 
* @author ZHANGCHAO 2022/6/28 16:18 * @version 1.0 */
@Override public YmMsg<String> dealAndExecuteTask(ApprovalDTO approvalDTO) { if (isEmpty(approvalDTO)) { return YmMsg.error("参数[审批意见对象DTO]不能为空!"); } if (isBlank(approvalDTO.getApprovalType())) { return YmMsg.error("未知的处理意见类型!"); } if (isBlank(approvalDTO.getTaskName())) { return YmMsg.error("未知的任务名称!"); } String tenantId = RequestKit.getRequestIn().getHeader(TENANT_ID); WfTaskParam wfTaskParam = new WfTaskParam(); Map<String, Object> args = new HashMap<>(); TaskNodePaymentEnum nodePaymentEnum = TaskNodePaymentEnum.getEnum(approvalDTO.getTaskName()); log.info("[dealAndExecuteTask]任务名称:" + approvalDTO.getTaskName()); if (isEmpty(nodePaymentEnum)) { return YmMsg.error("未知的任务节点!"); } /* * 执行任务时,需传递下一节点处理人!! * 2022/6/28 17:36@ZHANGCHAO */ AccountPutPayment payment = baseMapper.selectById(approvalDTO.getPaymentId()); if (isEmpty(payment)) { return YmMsg.error("未获取到流水号[" + approvalDTO.getPaymentId() + "]的付款申请数据!"); } switch (Objects.requireNonNull(nodePaymentEnum)) { case GBMJL_NODE: // 执行[各部门经理]节点,代垫已收款类型,前端自动传下一节点[分管部门领]的流程处理人 // 代垫已收款,直接走财务经理 if (isNotEmpty(payment.getIsDd()) && isNotEmpty(payment.getIsPut()) && payment.getIsDd() && payment.getIsPut()) { args.put("isddysk", 1); args.put("cwjl.operator", getUserIdsByCond("财务部", tenantId, 2)); // 下一节点财务经理处理人 // 非代垫已收款 } else { args.put("isddysk", 0); if (!"2".equals(approvalDTO.getApprovalType())) { if (isBlank(approvalDTO.getFgbmldUserId())) { return YmMsg.error("无法获取下一节点[分管部门领导]的流程处理人!"); } } args.put("fgbmld.operator", isNotBlank(approvalDTO.getFgbmldUserId()) ? approvalDTO.getFgbmldUserId().split(",") : null); // 下一节点处理人 } break; case FGBMLD_NODE: // 执行[分管部门领导]节点 // 付款申请币制 String curr = isNotBlank(payment.getCurr()) ? payment.getCurr() : "CNY"; // 费用 BigDecimal payAmount = isEmpty(payment.getAmount()) ? BigDecimal.ZERO : payment.getAmount(); // 代垫已收款,直接走财务经理 if (isNotEmpty(payment.getIsDd()) && isNotEmpty(payment.getIsPut()) && payment.getIsDd() && payment.getIsPut()) { return YmMsg.error("执行[分管部门领导]节点,费用类型为[代垫已收款]无法继续执行工作流!"); } // 代垫未付款 if (isNotEmpty(payment.getIsDd()) && payment.getIsDd() && (isEmpty(payment.getIsPut()) || !payment.getIsPut())) { // 代垫已收款,to财务经理 // if (isNotEmpty(putPayment.getIsPut()) && putPayment.getIsPut()) { // args.put("moneyType", 2); // args.put("cwjl.operator", getUserIdsByCond("财务部", tenantId, 2)); // 下一节点财务经理处理人 // // 代垫未收款 // } else { args.put("moneyType", 3); args.put("wskMoney", payAmount); // 小于5W,to财务经理 if (payAmount.compareTo(new BigDecimal("50000")) < 0) { args.put("cwjl.operator", getUserIdsByCond("财务部", tenantId, 2)); // 下一节点财务经理处理人 // 大于5W,to总经理董事长 } else { args.put("zjldsz.operator", getUserIdsByCond("总经办", tenantId, 2)); // 下一节点总经理董事长处理人 } // } // 业务款项 } else { args.put("moneyType", 1); args.put("ywMoney", payAmount); if ("CNY".equals(curr)) { args.put("currtype", 1); // 小于20W,to财务经理 // 2022/7/27 16:14@ZHANGCHAO 追加/变更/完善:人民币20W或者美元3W以下的为财务经理 if (payAmount.compareTo(new BigDecimal("200000")) < 0) { args.put("cwjl.operator", getUserIdsByCond("财务部", tenantId, 2)); // 下一节点财务经理处理人 // 大于20W,to总经理董事长 // 2022/7/27 16:14@ZHANGCHAO 追加/变更/完善:人民币20W或者美元3W以上的(包含) } else { args.put("zjldsz.operator", getUserIdsByCond("总经办", tenantId, 2)); // 下一节点总经理董事长处理人 } } else if ("USD".equals(curr)) { args.put("currtype", 2); // 小于20W,to财务经理 // 2022/7/27 16:14@ZHANGCHAO 追加/变更/完善:人民币20W或者美元3W以下的为财务经理 if (payAmount.compareTo(new BigDecimal("30000")) < 0) { args.put("cwjl.operator", getUserIdsByCond("财务部", tenantId, 2)); // 下一节点财务经理处理人 // 大于20W,to总经理董事长 // 2022/7/27 16:14@ZHANGCHAO 追加/变更/完善:人民币20W或者美元3W以上的(包含) } else { args.put("zjldsz.operator", getUserIdsByCond("总经办", tenantId, 2)); // 下一节点总经理董事长处理人 } } else { args.put("currtype", 1); // 小于20W,to财务经理 // 2022/7/27 16:14@ZHANGCHAO 追加/变更/完善:人民币20W或者美元3W以下的为财务经理 if (payAmount.compareTo(new BigDecimal("200000")) < 0) { args.put("cwjl.operator", getUserIdsByCond("财务部", tenantId, 2)); // 下一节点财务经理处理人 // 大于20W,to总经理董事长 // 2022/7/27 16:14@ZHANGCHAO 追加/变更/完善:人民币20W或者美元3W以上的(包含) } else { args.put("zjldsz.operator", getUserIdsByCond("总经办", tenantId, 2)); // 下一节点总经理董事长处理人 } } } break; case ZJLDSZ_NODE: // 执行[总经理董事长]节点 args.put("cwjl.operator", getUserIdsByCond("财务部", tenantId, 2)); // 下一节点财务经理处理人 break; case CWJL_NODE: // 执行[财务经理]节点 args.put("cnfk.operator", getUserIdsByCond("财务部", tenantId, 1)); // 下一节点出纳付款处理人 break; case CNFK_NODE: // 出纳付款,直接结束任务 // TODO: ... break; case APPLY_NODE: // 重新跳转到流程发起点 break; default: } args.put(APPROVAL_TYPE, approvalDTO.getApprovalType()); // 意见类型 args.put(OPINION, approvalDTO.getOpinion()); // 处理意见 wfTaskParam.setTaskId(approvalDTO.getTaskId()); // 任务ID wfTaskParam.setArgs(args); YmMsg<List<Task>> taskYmMsg = RpcKit.get(flowApi, FlowApi::executeTask, wfTaskParam); if (taskYmMsg.isSuccess()) { List<String> users = new ArrayList<>(16); // 2022/6/29 14:37@ZHANGCHAO 追加/变更/完善:执行成功发送消息! wfTaskParam.getArgs().forEach((k, v) -> { if (k.contains("operator")) { if (isNotEmpty(v)) { sendMessageForExcute(payment.getPutNo(), (String[]) v); try { if (isNotEmpty(v)) { for (String userId : (String[]) v) { SysUser user = baseMapper.getUserByUserId(userId); if (isNotEmpty(user)) { users.add(user.getRealname()); } } } } catch (Exception e) { e.printStackTrace(); ExceptionUtil.getFullStackTrace(e); log.error("获取用户真实姓名出现异常:" + e.getMessage()); } } } }); log.info(users + "任务执行成功!" + taskYmMsg.getData()); String returnMsg = isEmpty(users) ? "操作成功!" : ("操作成功!下一步的流程处理人[" + String.join(",", users) + "]"); if ("2".equals(approvalDTO.getApprovalType())) { returnMsg = "操作成功!已退回到流程发起点!"; /* * 付款审批如果审批人不同意需要填写理由,并给到申请人提醒 * 2022/7/25 14:19@ZHANGCHAO */ try { WfHistTask task = wfHistTaskService.getById(wfTaskParam.getTaskId()); if (isNotEmpty(task)) { WfHistTask wfHistTask = wfHistTaskService.getOne(new QueryWrapper<WfHistTask>().lambda() .eq(WfHistTask::getOrderId, task.getOrderId()) .eq(WfHistTask::getTaskName, "apply") .orderByDesc(WfHistTask::getCreateTime) .last("LIMIT 1")); if (isNotEmpty(wfHistTask)) { sendMessageRollback(payment.getPutNo(), wfHistTask.getOperator()); } } } catch (Exception e) { e.printStackTrace(); ExceptionUtil.getFullStackTrace(e); log.error("[dealAndExecuteTask]==>:退回操作发送消息给流程发起人出现异常!!!" + e.getMessage()); } } if ("3".equals(approvalDTO.getApprovalType())) { // 2022/7/1 9:08@ZHANGCHAO 追加/变更/完善:删除操作,直接删除此付款申请,费用明细取消标记已申请! List<AccountPutRel> accountPutRelList = accountPutRelService.list(new QueryWrapper<AccountPutRel>().lambda() .eq(AccountPutRel::getClaId, payment.getId())); if (isNotEmpty(accountPutRelList)) { List<Long> expIds = accountPutRelList.stream().map(AccountPutRel::getExpId).collect(Collectors.toList()); List<AccountExpense> accountExpenseList = accountExpenseMapper.selectList(new QueryWrapper<AccountExpense>().lambda() .in(AccountExpense::getId, expIds)); if (isNotEmpty(accountExpenseList)) { List<String> accountNos = new ArrayList<>(16); for (AccountExpense accountExpense : accountExpenseList) { // 费用明细标记 是否核销改为0,核销金额清0,核销日期清空、是否已申请改为0 accountExpenseMapper.update(null, new UpdateWrapper<AccountExpense>().lambda() .set(AccountExpense::getIsSq, false) .set(AccountExpense::getIsHx, false) .set(AccountExpense::getVerificationAmount, null) .set(AccountExpense::getVerificationData, null) .eq(AccountExpense::getId, accountExpense.getId())); accountNos.add(accountExpense.getAccountNo()); } changeAccountPutStatus(accountNos); } accountPutRelService.remove(new QueryWrapper<AccountPutRel>().lambda() .eq(AccountPutRel::getClaId, payment.getId())); } baseMapper.deleteById(approvalDTO.getPaymentId()); returnMsg = "操作成功!流程已关闭!"; } /* * 业务申请状态:PUT_STATUS:1已申请,2审批中,3审批通过 * 工作流开始,关联付款申请数据状态改成2审批中。 * 工作流结束,关联付款申请数据状态改成3审批通过。 * 2022/7/1 15:14@ZHANGCHAO */ List<String> orderStates = baseMapper.getOrderStatesByPaymentId(approvalDTO.getPaymentId()); if (isNotEmpty(orderStates)) { boolean isFinish = orderStates.stream().allMatch("0"::equals); if (isFinish) { baseMapper.update(null, new UpdateWrapper<AccountPutPayment>().lambda() .set(AccountPutPayment::getPutStatus, "3") // 3审批通过 .eq(AccountPutPayment::getId, approvalDTO.getPaymentId())); // 审批最终结束后,提醒一下申请人,哪一票审批已通过。 WfRelBusiness wfRelBusiness = wfRelBusinessService.getOne(new QueryWrapper<WfRelBusiness>().lambda() .eq(WfRelBusiness::getBusinessId, approvalDTO.getPaymentId())); WfHistTask wfHistTask = wfHistTaskService.getOne(new QueryWrapper<WfHistTask>().lambda() .eq(WfHistTask::getOrderId, wfRelBusiness.getOrderId()) .eq(WfHistTask::getTaskName, "apply") .orderByDesc(WfHistTask::getCreateTime) .last("LIMIT 1")); if (isNotEmpty(wfHistTask)) { sendMessageFinished(payment.getPutNo(), wfHistTask.getOperator()); } } } return YmMsg.ok(returnMsg); } else { log.info("任务执行失败,原因:" + taskYmMsg.getMessage()); return YmMsg.error("任务执行失败,原因:" + taskYmMsg.getMessage()); } }

此方法根据每一步执行的任务节点标识,自动获取下一节点的流程参与者标识传递过去。根据审批类型可以选择回退到流程发起人或者直接跳转到流程终点。

前端页面展示

任务参与者可以查看到当前分配给他的任务,可以进行办理

手摸手系列之SpringBoot+Vue整合snakeflow工作流实战_第6张图片
手摸手系列之SpringBoot+Vue整合snakeflow工作流实战_第7张图片
手摸手系列之SpringBoot+Vue整合snakeflow工作流实战_第8张图片

总结

本文只是简单展示了前后端分离项目整合snakeflow的简单用法,暂未涉及到子流程、时限控制、实例抄送、Fork/Join、自定义节点等功能,可以自行根据官方文档体验此工作流引擎的强大之处。

你可能感兴趣的:(vue,springboot,spring,boot,vue.js,前端,工作流,snakeflow)