配置实例

本节主要介绍常用流程的配置示例。

普通流程

以请假申请流程为例,介绍如何创建一个最简单的审批流程。本实例中将主要介绍任务表单和任务按钮的使用。

  • 在下面的流程图中,设置了请假申请、部门领导审批以及 HR 审批三个用户任务节点。
  • 在该流程中,只有「请假申请」节点可以修改表单数据,因此将「请假申请」任务的「允许编辑」标记设置为选中。
  • 「部门领导审批」和「HR 审批」节点都需要「同意」和「拒绝」两个按钮,所以要在这两个节点下添加相应的按钮。
  • 可以为不同的连线设置不同的按钮,当按钮被点击时,可以根据下图的配置,走不同的流转路径。

条件分支

本示例中的变量设置操作,仅适用于在线表单工作流,路由表单工作流的变量设置,会在后面的例子中给出。这里仍然以报销申请流程为例,介绍如何在流程中创建流程变量,并且使用流程变量作为条件流转的条件。

  • 在流程变量设置步骤,添加流程变量,并将变量绑定到数据表字段。
  • 为用户任务设置流程变量,只能选择上一步添加到流程中的变量。
  • 在分支中选择「条件流转」并设置流转条件,如下图所示。

会签加签减签

通过该实例介绍如何配置多人会签的多实例任务。

  • 会签任务一定要配置为多实例任务,同时一定要在多实例会签任务的上一个用户任务节点添加「会签、加签和减签」按钮。
  • 多实例任务内置变量。下图红框圈住的是流程引擎自带的内置变量,
  • 上图中没有被红框圈住的变量,属于橙单自定义的多实例会签任务变量,具体功能见如下表格。
变量名 功能描述
assigneeList 多实例集合列表。
multiNumOfInstances 实例数量 (用于多实例任务节分支中判断)
multiAgreeCount 同意实例数量,点击「同意 (会签)」类型的按钮触发。
multiRefuseCount 拒绝实例数量,点击「拒绝 (会签)」类型的按钮触发。
multiAbstainCount 弃权实例数量,点击「弃权 (会签)」类型的按钮触发。
  • 为多实例会签任务设置「完成条件」。在下图中,是要求所有会签候选人全部提交后才会结束当前会签任务。当然也可以设置百分比,如 ${nrOfCompletedInstances / nrOfInstances  > 0.8},表示有 80% 的会签用户提交审批后,即可结束当前的会签任务。
  • 根据会签任务的审批结果,设置下一个任务的审批流转分支。在下图中,我们可以看到「排他网关」后面的流程分支连线,每个连线都配置了「流转条件」表达式,表达式中使用了我们为会签内置的多个自定义变量,仅当表达式设置的条件满足时,才会走当前分支。
  • 多实例加签和减签。这两个按钮都是配置在「多实例会签任务」的上一个用户任务节点。加签的业务场景是,当会签的发起人发现需要更多的用户参与会签时,就可以在会签尚未结束时,添加新的用户。减签则反之,同样是会签的发起用户发现正在参与会签的某一用户,是不需要参与会签的,或是之前「手抖」误选进来的,此时就可以在会签尚未结束时,执行减签操作,移除其中的一到多个会签用户。

流程变量指定审批人

在流程编辑器中,将用户任务的审批人配置为流程变量,变量值来自于绑定的业务表字段。这里需要注意的是,如果指定的是用户,而非用户组 (部门、角色和岗位等),切记要使用「用户登录名」字段,目前并不支持使用「userId」字段值作为绑定的变量值。

  • 因为在后面的配置中,我们指定的是审批用户,而非用户组。因此这里配置的流程变量,必须绑定到「用户登录名」字段。如果是审批用户组,如部门、角色和岗位等,则可以指定 deptId、roleId 和 deptPostId 等字段。
  • 在流程编辑中,为指定审批人任务的上一个任务绑定「处理人」变量。
  • 在流程编辑中,为用户任务指定「候选类型」。本例中我们选择的是「处理用户」。
  • 在「选择用户」的弹框中,直接输入变量名,然后点击「添加用户」即可。
  • 保存流程后,发布流程,并将其设置为主版本。
  • 配置在线表单的页面组件,以便可以在流程录入页面选择「处理人」。在下图中,我们需要现为「username」字段设置字典,这样「处理人」字段所关联的下拉框组件,就可以显示用户列表了。下图中的「用户字典」字典的字典键必须为「loginName」,具体配置见下下张图。
  • 将「处理人」字段与「下拉框」组件绑定。下拉框中的数据列表,为上一步配置的「用户字典」的字典数据列表。
  • 启动流程,在录入业务数据时,为绑定字段「处理人」选择下一个任务的指定审批人。
  • 以上一步选择的「userA」身份登录,打开待办任务页面,可以看到「userA」已经是那个任务的待办用户了。

代码实例

本小节主要介绍相关的代码示例。

发起人部门领导岗位

橙单目前已经支持「发起人本部门领导」和「发起人上级部门领导」,具体见下图。当前小节主要介绍后端相关的扩展代码。这样在缺省实现不满足需求的情况下,开发者可自行修改。

  • 在调用流程发布接口发布流程的时候,如果流程定义中包含分组类型是「发起人部门领导岗位」和「发起人上级领导岗位」的用户任务时,我们会自动为这个用户任务绑定用户监听器,监听器的具体作用后面会讲述。见下图及文字注释。
  • 在流程实例启动的时候,会判断当前流程任务中,是否包含「发起人本部门领导审批」和「发起人上级部门领导审批」的用户任务,如果包含,就会在流程实例启动的时候,根据实例的发起用户,找到该用户的「本部门领导岗位 ID」和「上级部门领导岗位 ID」,并赋值给上图中对应的变量。
  • 流程执行到设定为「发起人本部门领导审批」和「发起人上级部门领导审批」的用户任务时,就会触发流程发布时动态绑定的任务监听器,下面仅以「发起人本部门领导岗位」监听器为例。监听器最最主要的作用是当无法正常获得发起人部门领导岗位 ID 时 (deptPostId == null),监听器会自动把任务的 Assignee 缺省指派给发起人。这里只是缺省行为,也是为了给用户演示如何支持类似的功能。开发者可以根据需求,使用自定义的监听器完成自己所需的需求。
  • 前面介绍的是缺省流程,最后讲一下如何扩展缺省行为。

退回设置详解

对应于流程编辑器中的「退回设置」功能,具体见下图。

  • 重新审批。假设当前流程任务执行路径为 A -> B -> C,如从 C 退回到 A ,当 A 再次提交审批时,将再次提交给 B 节点。
  • 从当前节点审批。假设当前流程任务执行路径为 A -> B -> C,如从 C 退回到 A ,当 A 再次提交审批时,将直接提交给 C,同时将 C 节点的指派人赋值为之前的驳回用户。
  • 当退回设置为「从当前节点审批」选项时,可以指定驳回到并行网关内某一具体的节点,驳回后,只有被指定的驳回节点处于待审批状态,而并行网关内的其他节点,不会处于审批状态。当被驳回节点再次提交时,会直接提交到之前驳回的节点。并且将审批人指派人之前的驳回人。

待办任务通知

为待办任务处理人发送新待办任务通知。具体配置和开发步骤如下。

  • 为流程配置「通知类型」,配置后所有任务都会发送该类型的待办通知。
  • 为指定的任务配置「通知类型」,见下图。如果同时配置了上一步的「流程通知类型」,又配置了「任务通知类型」,那么该任务的通知类型是两者的并集。
  • 在橙单的后端代码中,我们使用「任务监听器」的方式,为待办任务的所有待办人发送指定类型的「通知」。开发者需要根据自己的实际业务需求,实现 BaseFlowNotifyExtHelper 扩展插件类的 doNotify 方法。
  • 这里给出 BaseFlowNotifyExtHelper 插件注册的简单示例。

级联删除业务数据

在删除流程数据的时候,可以级联删除与当前流程相关的业务数据。

基本配置和操作

  • 如下图所示打开「级联删除业务数据」开关,在删除流程实例时,会同步级联删除与流程实例关联的业务主表数据,以及与主表关联的一对一、一对多和多对多等关联从表数据。
  • 删除流程实例,即会同步删除该实例关联的业务主表数据。

在线表单级联删除

  • 在线表单中,不仅可以同步删除流程实例关联的业务主表数据,还能进一步级联删除一对一和一对多的关联从表数据。具体配置如下。
  • 在线表单业务表数据的级联删除,是通过在线表单流程模块 (common-flow-onine) 提供的扩展业务服务类 (FlowOnlineBusinessServiceImpl) 来实现的,具体可见该类的 deleteBusinessData 方法。

路由表单级联删除

  • 路由表单中,不仅可以同步删除流程实例关联的业务主表数据,还能进一步级联删除一对一、一对多和多对多的关联从表数据。在生成器中的具体配置如下。
  • 路由表单业务表数据的级联删除,是通过流程模块 (common-flow) 的流程服务基类 (BaseFlowService) 来实现的,具体可见该类的 removeByWorkOrder 方法。下面是该方法的代码解析说明。
public void removeByWorkOrder(FlowWorkOrder workOrder) {
   Serializable id = this.convertToKeyValue(workOrder.getBusinessKey());
   M data = this.getById(id);
   if (data == null) {
       String msg = StrFormatter.format(
               "WorkOrderId [{}] don't find business data by key [{}] while calling [removeByWorkOrder].",
               workOrder.getWorkOrderId(), workOrder.getBusinessKey());
       log.warn(msg);
       return;
   }
   // 级联删除业务数据,调用的是橙单默认生成的remove方法,参数类型为主键类型,如Long、String等。
   // 进一步补充说明,橙单默认生成的remove方法,会根据在生成器中的配置,生成关联从表数据的级联删除代码。
   this.remove((K) id);
}

在线表单状态数据同步

在流程执行结束后,需要将流程的执行状态数据同步到业务表中,以便于业务数据的查询和统计分析。

  • 在「在线表单」的「表单管理」菜单,编辑流程使用的在线表单。
  • 选择包含「流程状态」的业务表,然后点击「字段管理」进行字段编辑页面。
  • 在左侧选中指定字段,再将其字段类别设置为「流程状态」。
  • 在内置的流程结束监听器 FlowFlinshedListener 中,如果是在线表单工作流,就会调用指定的方法,更新上图设置的字段。
  • 在 common-flow-online 模块的 FlowOnlineBusinessServiceImpl 实现类中,会根据在线表单中的配置,找到业务表中字段类别是「流程状态」的字段,为其赋值最新的流程状态。

路由表单状态数据同步

在流程执行结束后,需要将流程的执行状态数据同步到业务表中,以便于业务数据的查询和统计分析。

  • 在橙单生成器中,为流程依赖的业务主表设置「流程状态」和「流程审批状态」字段。
  • 在生成后的代码中,会为上一步配置的实体类字段,添加 @FlowStatusColumn 和 @FlowApprovalStatusColumn 注解。
@Data
@TableName(value = "zz_test_flow_leave")
public class TestFlowLeave {

   // 主键Id。
   @TableId(value = "id")
   private Long id;
   // ... ... 中间忽略部分字段定义。
   
   // 流程状态。
   @FlowStatusColumn
   @TableField(value = "flow_status")
   private Integer flowStatus;
}
  • 在内置的流程结束监听器 FlowFlinshedListener 中,如果是路由表单工作流,就会调用指定的方法,更新上图设置的字段。
  • 路由表单工作流的状态更新逻辑,在 BaseFlowService 类的 updateFlowStatus 方法中提供了默认实现。

路由表单业务数据同步

为了避免审批中的数据污染最终业务表数据,我们可以分表存储审批中和审批通过完成后的数据,待审批流程以「审批通过」状态结束后,再将审批中业务表的数据,同步到最终的业务表中。

  • 在生成器中配置路由表单工作流的时候,可以打开下图所示的开关,即可生成相关的代码。
  • 下图是生成后工程中,内置的流程结束监听器 FlowFlinshedListener 的代码。红框圈住的逻辑会先判断流程是否是正常状态下结束的,而不是强制终止和撤销操作导致的流程结束。只有正常的流程结束,才会调用业务数据同步的方法。
  • 在业务数据扩展类中 BaseBusinessDataExtHelper,提供了业务服务注册 doRegister 方法,流程的业务服务实现类,会在系统启动时将 this 和关联的「流程定义标识」一起注册到扩展插件中。而当流程结束后进行数据同步时,就会根据工单对象中的「流程定义标识字段 (processDefinitionKey)」,查找已注册的流程服务实现类,之后就会调用该服务实现类的 syncBusinessData 方法执行数据同步,syncBusinessData 方法需要每个路由表单工作流的服务实现类自行实现。
  • 在业务服务中,我们会根据配置生成路由表单工作流所需的全部代码。自动生成的流程业务服务实现类,必须继承自 BaseFlowService。并在 Bean 初始化时,将 this 和在生成器中配置的「流程标识」,一起注册到业务数据扩展类 BaseBusinessDataExtHelper 中。

其他问题

本小节主要介绍与流程相关的一些常见问题。

测试环境流程迁移生产

在测试环境中,通常已经配置好了最终的业务流程图,并用于测试。在部署生产环境时,希望只是迁移配置好的流程数据和在线表单配置数据,而忽略测试过程中产生的流程审批数据。具体操作如下。

  • 拷贝所有流程引擎 (flowable) 内置的表 (以 act_ 和 flw_ 开头) 数据到生产数据库。
  • 清空除「act_ge_property」和「act_id_property」以外,其他流程引擎内置表的数据。
  • 拷贝所有橙单流程模块内置表 (以 zz_flow_ 开头) 数据到生产数据库。
  • 清空除「zz_flow_entry」、「zz_flow_entry_variable」和「zz_flow_category」以外,其他橙单流程模块内置表的数据。
  • 拷贝所有橙单在线表单内置表 (以 zz_online_ 开头) 数据到生产数据库。
  • 登录生产环境的业务系统,重新发布所有流程,并同时设置为主版本。

工单编码的计算和补偿

流程工单表 zz_flow_work_order 的 work_order_code 字段值是根据规则配置自动计算而得的。具体可参考 流程管理章节的流程基础信息配置小节。下面我们重点介绍一下在负责规则计算的 Redis 出现异常或人为执行 flushall 命令而导致计数器丢失时,流程工单是如何自动修复并重新执行工单插入操作的。具体实现逻辑可见如下代码及关键性注释。

// 如下代码为FlowWorkOrderServiceImpl类的saveNew方法。
@Transactional(rollbackFor = Exception.class)
@Override
public FlowWorkOrder saveNew(ProcessInstance instance, Object dataId, Long onlineTableId, String tableName) {
   // 正常插入流程工单数据。
   FlowWorkOrder flowWorkOrder = this.createWith(instance);
   flowWorkOrder.setWorkOrderCode(this.generateWorkOrderCode(instance.getProcessDefinitionKey()));
   flowWorkOrder.setBusinessKey(dataId.toString());
   flowWorkOrder.setOnlineTableId(onlineTableId);
   flowWorkOrder.setTableName(tableName);
   flowWorkOrder.setFlowStatus(FlowTaskStatus.SUBMITTED);
   try {
       flowWorkOrderMapper.insert(flowWorkOrder);
   } catch (DuplicateKeyException e) {
       // 数据插入过程中,如果抛出 “数据重复值 (DuplicationKeyException)” 时,会捕捉该异常。
       // 执行 SQL 查询操作,判断本次计算的工单编码是否已经存在。如不存在,该异常则为其他字段值重复所引起,可直接再次抛出。
       if (flowWorkOrderMapper.getCountByWorkOrderCode(flowWorkOrder.getWorkOrderCode()) == 0) {
           throw e;
       }
       log.info("WorkOrderCode [{}] exists and recalculate.", flowWorkOrder.getWorkOrderCode());
       // 如存在该工单编码的数据,则可以理解为负责计算工单编码的 Redis 出现了问题,
       // 需要为该工单字段所关联的 Redis 原子计数器重新设置初始值。
       this.recalculateWorkOrderCode(instance.getProcessDefinitionKey());
       // 重新初始化后,再次执行generateWorkOrderCode方法计算出新的工单编码。
       flowWorkOrder.setWorkOrderCode(this.generateWorkOrderCode(instance.getProcessDefinitionKey()));
       // 并再次提交当前的工单数据。
       flowWorkOrderMapper.insert(flowWorkOrder);
   }
   return flowWorkOrder;
}
private void recalculateWorkOrderCode(String processDefinitionKey) {
   FlowEntry flowEntry = flowEntryService.getFlowEntryFromCache(processDefinitionKey);
   if (StrUtil.isBlank(flowEntry.getEncodedRule())) {
       return;
   }
   // 获取当前流程定义中,为工单编码字段设置的规则配置信息。
   ColumnEncodedRule rule = JSON.parseObject(flowEntry.getEncodedRule(), ColumnEncodedRule.class);
   if (rule.getIdWidth() == null) {
       rule.setIdWidth(10);
   }
   // 根据当前规则中的数据,计算出该规则在Redis中AtomicLong对象的键。
   String prefix = commonRedisUtil.calculateTransIdPrefix(rule.getPrefix(), rule.getPrecisionTo(), rule.getMiddle());
   // 根据该键(规则前缀)计算出符合该前缀的工单编码的最大值。
   String maxWorkOrderCode = flowWorkOrderMapper.getMaxWorkOrderCodeByPrefix(prefix + "%");
   // 移除前缀部分,剩余部分即为计数器的最大值。
   String maxValue = StrUtil.removePrefix(maxWorkOrderCode, prefix);
   // 用当前的最大值,为该key的AtomicLong对象设置初始值,后面的请求都会在该值上原子性加一了。
   commonRedisUtil.initTransId(prefix, Long.valueOf(maxValue));
}

结语

赠人玫瑰,手有余香,感谢您的支持和关注,选择橙单,效率乘三,收入翻番。