Camunda 工作流发起表单节点,实践总结

在企业级审批系统的设计中,如何高效地设计和实现工作流是一个关键问题。本文将基于 Camunda 流程引擎,探讨在不同业务需求下,如何选择和设计工作流的发起表单节点,并结合实际场景给出最佳实践建议。

背景与挑战

通常情况下,一个完整的审批流程包括以下几个步骤:

  • 用户填写表单并提交
  • 审批人进行审批
  • 根据审批结果,决定是否驳回给发起人或继续下一步

然而,在实际应用中,不同的业务场景对流程设计有不同的要求。例如:

  • 某些场景需要支持“驳回给发起人”重新修改表单。
  • 而另一些场景则不需要这种回退机制,仅需直接进入审批阶段。
  • 还有些流程是由系统自动发起的,不存在“人工发起人”这一角色。

主要观点与设计原则

1. 常规标准:通用设计是第一个节点为“填写表单” UserTask

在大多数需要人工参与的审批流程中,通用的最佳实践是将第一个节点设计为一个 UserTask,即“填写/修改申请”任务。

这样做有以下优势:

  • 支持“驳回后重新提交”:当审批不通过时,流程可以自然地回退到该任务,由发起人修改后再次提交。
  • 流程可审计:Camunda 会记录该任务的创建、完成时间,便于追踪整个流程生命周期。
  • 用户体验一致:无论是首次提交还是驳回修改,用户都在同一个任务中操作。
<userTask
  id="initiatorTask"
  name="填写/修改申请"
  camunda:assignee="${applicant}"
  _nodeType="INITIATOR_FORM_TASK" />

注意: 这段 xml 这是伪代码,表示增加的节点类型设置,实际应用中你可能在属性中设置,也可能是在 extensionElements 中设置,请根据实际情况配置。

2. 特殊场景:系统自动发起的流程不应设置“表单节点”

对于由系统(如定时任务、消息触发、数据同步等)自动发起的流程,情况有所不同。

这类流程的“发起人”是系统本身,显然无法被“驳回”去修改数据。如果在这种流程中仍然设计一个“填写表单”的 UserTask,并允许流程回退到该节点,就会出现逻辑问题:

问题示例
系统每天凌晨自动生成10万条财务结算单并启动审批流程。
若某条记录审批失败被“驳回”,流程试图回到“系统填写表单”节点——但系统无法交互,也无法重新“填写”,导致流程卡死或行为异常。

因此,在系统自动发起的流程中,应避免设置“发起人 UserTask”节点,直接进入审批或其他处理环节。

3. 高并发场景:避免不必要的节点以提升性能

在高并发、大数据量的系统中,每一个 taskService.complete() 操作都会带来显著的数据库开销,包括:

  • 历史表写入(ACT_HI_TASKINST, ACT_HI_VARINST

  • 运行时任务表的增删改(ACT_RU_TASK

  • 变量表更新(ACT_RU_VARIABLE

  • 执行流推进(ACT_RU_EXECUTION

  • 可能得更多相关表

关于 Camunda 表 ACT_RUN_TASKPARENT_TASK_ID_这个字段缺少索引,在大数据量场景下的性能问题,我给官方提交了 issue:https://github.com/camunda/camunda/issues/36824,请根据实际需要自行为这个字段添加索引 CREATE INDEX ACT_IDX_TASK_PARENT_TASK_ID ON ACT_RU_TASK(PARENT_TASK_ID_);

如果为每一个流程实例都增加一个“自动完成”的 UserTask,即使只是启动时的一次性操作,在百万级流程实例的场景下也会造成:

  • 额外的数据库压力
  • 更长的流程启动时间
  • 更高的资源消耗

因此,在不需要“回退发起人”的场景下,省略第一个 UserTask 节点,不仅能简化流程模型,还能显著提升系统吞吐量。

最佳实践总结

决策树:如何设计第一个节点?

是否需要"驳回给发起人"?

是 → 设计第一个节点为:发起人 UserTask

  • 启动流程后自动 complete 该任务
  • 支持驳回后重新修改表单
  • 提供完整的流程审计记录

否 → 第一个节点应为:审批人或处理人 UserTask

  • 启动流程时直接传入表单数据
  • 无额外 complete 操作
  • 提升系统性能和吞吐量

补充判断:是否为系统自动发起?

在“不需要回退”的前提下,进一步判断:

  • 如果是 人工发起 的流程,且未来可能扩展“回退”功能,可预留扩展性。
  • 如果是 系统自动发起 的流程,坚决不要添加“表单节点”,避免逻辑混乱和性能浪费。
  • 推荐的系统设计逻辑:为流程级增加一个全局变量,在流程发起时可以动态设置这个变量,由这个变量来控制后面的节点进行“回退”时,能否回退到发起人。

重要实践注意事项

⚠️ 发起人节点标识的最佳实践

**关键原则:**永远不要依赖"第一个任务"或"firstTask"来识别发起人节点,而应该为发起人节点设置固定的属性标识。

为什么这样做?
  • **流程稳定性:**流程定义可能会变更,第一个任务的位置可能发生变化
  • **代码可维护性:**明确的标识使代码更易理解和维护
  • **避免歧义:**防止在复杂流程中误判节点类型
  • **扩展性:**便于后续添加更多发起人相关的业务逻辑
推荐的实现方式:
  • 在BPMN流程定义中,为发起人节点添加自定义属性来标识节点类型,如_nodeType="INITIATOR_FORM_TASK"
错误示例 vs 正确示例:
// ❌ 错误:依赖firstTask或任务顺序
Task firstTask = taskService.createTaskQuery()
    .processInstanceId(instance.getId())
    .list().get(0);  // 依赖顺序,容易出错

// ✅ 正确:使用节点类型属性
Task initiatorTask = taskService.createTaskQuery()
    .processInstanceId(instance.getId())
    .taskAttributeEquals("_nodeType", "INITIATOR_FORM_TASK")  // 基于业务语义的标识
    .singleResult();

代码示例对比

场景一:需要"回退发起人" —— 保留第一个 UserTask

在需要支持"驳回给发起人"的场景下,我们不仅需要启动流程时自动完成发起人任务,还需要提供驳回功能。以下是完整的实现:

/**
 * 示例代码
 * @author 单红宇
 */
@Service
@Transactional
public class ApprovalService {

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private TaskService taskService;

    /**
     * 提交表单并发起流程
     */
    public String submitAndStart(String applicant, ApplyForm form) {
        Map vars = new HashMap<>();
        vars.put("applicant", applicant);
        vars.put("approver", "admin");
        vars.put("reason", form.getReason());
        vars.put("amount", form.getAmount());

        ProcessInstance instance = runtimeService.startProcessInstanceByKey("approvalProcess", vars);

        // 通过节点类型属性来获取发起人任务,而不是依赖firstTask
        Task initiatorTask = taskService.createTaskQuery()
            .processInstanceId(instance.getId())
            .taskAttributeEquals("_nodeType", "INITIATOR_FORM_TASK")  // 基于业务语义的标识
            .singleResult();

        if (initiatorTask != null) {
            taskService.complete(initiatorTask.getId(), vars);
        }

        return instance.getId();
    }
    
    /**
     * 退回给发起人重新修改
     * @param processInstanceId 流程实例ID
     * @param currentTaskId 当前任务ID
     * @param rejectReason 驳回原因
     * @return 退回后的任务ID
     */
    public String rejectToInitiatorFormTask(String processInstanceId, String currentTaskId, String rejectReason) {
        // 获取当前任务
        Task currentTask = taskService.createTaskQuery()
            .taskId(currentTaskId)
            .singleResult();
        
        if (currentTask == null) {
            throw new RuntimeException("任务不存在: " + currentTaskId);
        }
        
        // 设置驳回原因变量
        Map variables = new HashMap<>();
        variables.put("rejectReason", rejectReason);
        variables.put("rejectTime", new Date());
        
        // 完成当前任务,流程会回退到发起人节点
        taskService.complete(currentTaskId, variables);
        
        // 获取退回后的发起人任务
        Task initiatorTask = getInitiatorFormTask(processInstanceId);
        
        if (initiatorTask == null) {
            throw new RuntimeException("未找到发起人任务");
        }
        
        return initiatorTask.getId();
    }
    
    /**
     * 获取流程的发起人任务
     * @param processInstanceId 流程实例ID
     * @return 发起人任务,如果不存在则返回null
     */
    public Task getInitiatorFormTask(String processInstanceId) {
        return taskService.createTaskQuery()
            .processInstanceId(processInstanceId)
            .taskAttributeEquals("_nodeType", "INITIATOR_FORM_TASK")
            .singleResult();
    }
}

场景二:系统自动发起 + 无需回退 —— 直接进入审批

public String startAutoApproval(AutoFormData data) {
    Map variables = new HashMap<>();
    variables.put("sourceSystem", data.getSource());
    variables.put("batchId", data.getBatchId());
    variables.put("totalAmount", data.getTotalAmount());
    variables.put("approver", "finance-team");

    ProcessInstance instance = runtimeService.startProcessInstanceByKey(
        "autoSettlementProcess", variables
    );

    return instance.getId();
    // ✅ 无多余 taskService.complete() 调用
}

结论

通过以上分析,我们可以得出以下核心结论:

  1. 常规标准:对于需要人工发起且支持“驳回修改”的流程,第一个节点应为“填写表单” UserTask,这是通用且推荐的做法。
  2. 系统发起场景:由系统自动触发的流程,不应设计“表单节点”,否则“回退到系统”会导致逻辑错误。
  3. 性能考量:在高并发、大数据量场景下,避免不必要的 UserTask 节点,尤其是那些仅用于"启动即完成"的节点,可显著降低数据库压力,提升系统性能。
  4. 节点标识规范:为发起人节点设置固定的属性标识(如_nodeType="INITIATOR_FORM_TASK"),避免依赖任务顺序或firstTask,确保流程的稳定性和代码的可维护性。

最终的设计决策应基于业务需求而非“通用模板”。灵活运用 Camunda 的建模能力,在满足功能的前提下,追求简洁、高效、可维护的流程架构。


(END)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

catoop

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值