本文目录导读:

- 案例场景:公司差旅费用报销流程
- 第一步:流程定义(BPMN 2.0 XML)
- 第二步:核心 Java 代码实战(Spring Boot 环境)
- 第三步:前端/Controller 交互接口
- 第四步:该类案例的踩坑点与优化建议
Flowable(以及与之齐名的 Activiti、Camunda),最常见的需求是 “如何从零搭建一个请假流程” 或 “如何实现会签/驳回”,为了给你最有价值的案例,我将以 “企业内部报销审批流程” 为例,从 数据库表设计、BPMN建模、核心Java代码、前端流程推进 四个维度进行深度拆解。
这是一个经典的“会签+条件分支+驳回”案例,能够覆盖大部分工作流开发场景。
案例场景:公司差旅费用报销流程
业务规则:
- 员工提交报销单,金额填写。
- 部门经理审批:
- 金额 ≤ 3000元:审批通过后直接结束,打款。
- 金额 > 3000元:需要 财务总监 会签(多人必须全部同意)。
- 任何节点驳回,流程直接结束(或退回发起人,本案例为简单起见直接结束)。
第一步:流程定义(BPMN 2.0 XML)
这是流程图的底层灵魂,通常我们使用 Flowable Modeler 绘制,但为了理解,这里是关键的 XML 片段逻辑:
<process id="expenseProcess" name="报销流程" isExecutable="true">
<!-- 开始事件 -->
<startEvent id="startEvent" name="开始" flowable:initiator="applyUser"/>
<!-- 申请节点(用户任务) -->
<userTask id="applyTask" name="填写报销单" flowable:assignee="${applyUser}"/>
<!-- 部门经理审批(用户任务) -->
<userTask id="managerTask" name="部门经理审批" flowable:assignee="${managerUser}"/>
<!-- 排他网关(判断金额) -->
<exclusiveGateway id="amountGateway" name="金额判断"/>
<!-- 财务总监会签(多实例任务) -->
<userTask id="financeTask" name="财务总监审批" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="financeUsers" flowable:elementVariable="assignee">
<completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<!-- 结束事件 -->
<endEvent id="endEvent" name="结束"/>
<!-- 连线逻辑(Sequence Flow) -->
<sequenceFlow id="flow1" sourceRef="startEvent" targetRef="applyTask"/>
<sequenceFlow id="flow2" sourceRef="applyTask" targetRef="managerTask"/>
<!-- 网关分支:金额小于3000 -->
<sequenceFlow id="flow3" sourceRef="managerTask" targetRef="amountGateway"/>
<sequenceFlow id="flow_le_3000" name="<=3000" sourceRef="amountGateway" targetRef="endEvent">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${amount <= 3000}]]>
</conditionExpression>
</sequenceFlow>
<!-- 网关分支:金额大于3000 -->
<sequenceFlow id="flow_gt_3000" name=">3000" sourceRef="amountGateway" targetRef="financeTask">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${amount > 3000}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow4" sourceRef="financeTask" targetRef="endEvent"/>
</process>
第二步:核心 Java 代码实战(Spring Boot 环境)
这是业务系统对接的关键,我们主要操作 RuntimeService 和 TaskService。
启动流程实例(员工提交申请)
@Service
public class ExpenseService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private IdentityService identityService;
/**
* 启动报销流程
*/
public void startExpenseProcess(String applyUserId, String managerUserId, Double amount) {
// 设置发起人(用于在流程中获取)
identityService.setAuthenticatedUserId(applyUserId);
// 设置流程变量(供BPMN表达式使用)
Map<String, Object> variables = new HashMap<>();
variables.put("applyUser", applyUserId);
variables.put("managerUser", managerUserId);
variables.put("amount", amount);
// 根据流程定义Key启动
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey("expenseProcess", variables);
System.out.println("流程启动成功,流程实例ID:" + processInstance.getId());
}
/**
* 完成部门经理审批任务
*/
public void completeManagerTask(String taskId, Boolean approved) {
Map<String, Object> vars = new HashMap<>();
vars.put("managerApproved", approved);
taskService.complete(taskId, vars);
}
/**
* 查询某人需要处理的任务列表
*/
public List<Task> getTasksForUser(String assignee) {
return taskService.createTaskQuery()
.taskAssignee(assignee)
.orderByTaskCreateTime()
.desc()
.list();
}
}
动态设置会签人(关键难点)
在实际项目中,审批人往往是根据角色动态查出来的,不能写死在流程里,这里在启动流程时,动态注入会签人列表:
public void startExpenseProcess(..., List<String> financeUsers) {
Map<String, Object> variables = new HashMap<>();
// ... 其他变量
// 这里传入List,BPMN中的【flowable:collection】会接收这个变量
variables.put("financeUsers", financeUsers);
runtimeService.startProcessInstanceByKey("expenseProcess", variables);
}
驳回与结束
如果经理审批不通过,我们要终止流程(或者走驳回节点),可以在 completeManagerTask 中处理:
public void completeManagerTask(String taskId, Boolean approved) {
Map<String, Object> vars = new HashMap<>();
vars.put("managerApproved", approved);
// 查找当前任务所属的流程实例
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
String processInstanceId = task.getProcessInstanceId();
if (Boolean.FALSE.equals(approved)) {
// 如果驳回,直接删除流程实例(即终止流程)
runtimeService.deleteProcessInstance(processInstanceId, "经理审批不通过");
} else {
// 正常完成,网关会根据amount变量自动判断走向
taskService.complete(taskId, vars);
}
}
第三步:前端/Controller 交互接口
主要是向业务系统提供 REST API。
@RestController
@RequestMapping("/expense")
public class ExpenseController {
@Autowired
private ExpenseService expenseService;
// 1. 提交审批
@PostMapping("/start")
public Result start(@RequestBody ExpenseDTO dto) {
// 根据部门经理角色查询出实际用户ID,以及财务总监列表
List<String> financeUsers = roleService.getFinanceUsers();
expenseService.startExpenseProcess(dto.getApplyUser(), dto.getManagerUser(),
dto.getAmount(), financeUsers);
return Result.success();
}
// 2. 获取待办任务
@GetMapping("/todos/{userId}")
public Result getTodos(@PathVariable String userId) {
List<Task> tasks = expenseService.getTasksForUser(userId);
// 转换为前端需要的DTO
return Result.success(tasks.stream().map(...).collect(Collectors.toList()));
}
// 3. 审批通过/驳回
@PostMapping("/complete")
public Result complete(@RequestParam String taskId,
@RequestParam Boolean approved) {
expenseService.completeManagerTask(taskId, approved);
return Result.success();
}
}
第四步:该类案例的踩坑点与优化建议
- 并行网关 vs 排他网关:本案例使用的是 排他网关(Exclusive Gateway),如果你是“会签”(多个人同时审批),必须用 多实例(Multi-Instance) 节点,否则流程会在第一个审批人完成后直接流失效。
- 流程变量 (Variables) 大小写:BPMN 表达式
${amount > 3000}中的amount必须与 Java Map 中 Put 的 Key 完全一致。 - 驳回处理:本案例的驳回直接终止了流程,如果业务复杂(比如驳回后需修改重新提交),建议增加 用户任务节点 指向发起人,并设置“驳回原因”变量,而不是直接删除流程实例。
- 性能问题:建议使用 Flowable 自带的
ActivityBehavior进行动态人员解析,尽量避免在 Service 层写大量的 if-else 判断谁来审批,将规则下沉到 BPMN 设计器中。
Flowable 的核心优点是将 动态路由 从 Java 代码中剥离出来,交给了 XML 模型,你的 Java 代码只需要负责:
- 启动流程(塞入业务数据为变量)。
- 监听任务(查询待办)。
- 触发流程(Complete 任务)。
如果你有具体的业务逻辑(比如动态指定审批人、按部门多级审批等),可以告诉我更具体的场景,我可以为你提供更精准的 BPMN 或者代码片段。