前两天一个做Java后端的朋友跟我吐槽,说他们公司OA系统里的请假审批要加一条规则:请假超过三天需要部门总监加签,开发一评估说要排两天。我听完一点都不意外,因为我知道他们的审批逻辑是硬写在Service里的,状态字段从0加到了7,每个数字代表什么含义全靠一张Excel维护。这种玩法一开始很爽,改到第三年就全是债了。
我这两年经手的项目,做审批、报销、合同会签这类功能,统一用的是SpringBoot + flowable这套组合,从需求确认到流程跑通通常一两天就能完成。Flowable本身是Java生态里非常成熟的开源工作流引擎(源自Activiti 5,后来分叉独立演进),它把审批链路描述成一份BPMN文件,引擎负责状态流转、任务分配、历史归档,业务代码只需要关心“发起”和“完成”这两件事。这篇文章我不打算讲太多理论,就把我实际接入Flowable做请假审批的完整过程、关键代码、以及接入真实业务后躲不开的坑,一次性说清楚。适合正在做OA、订单审核、简历筛选这类功能,想快速引入工作流能力的后端同学。
1. 为什么我把公司审批流从“硬编码状态机”换成了 Flowable
先别急着写代码,得先想清楚一个问题:你现在的审批流到底难在哪里。我自己拆过不少“看似稳定”的自研审批模块,表面看跑了好几年,实际上早就快撑不住了。
1.1 自研状态机的三个隐藏成本
很多团队一开始做审批,走的是“加字段”路线:业务表上放一个leave_status或audit_status,0 待提交、1 直属领导审批、2 总监审批、3 人事备案,以此类推。再加一个状态流转Service方法,里面全是if (status == 1 && result == 2)这种分支判断。
第一个隐藏成本是分支规模膨胀。审批链一旦出现会签、加签、驳回、撤回,排列组合会迅速爆炸。我见过一张审批表的状态流转代码有四百多行,一个状态值是“部门经理审批中”,另一个值“部门经理审批不通过待重新提交”,中间隔了好多层逻辑,新来的同事看半天不敢动。
第二个成本是审计缺失。自研状态机往往只记录“最终结果”,过程记录靠业务日志表自己拼。一旦有人问“这个单子在谁手上卡了两天?谁改了什么意见?”,你会发现数据根本对不上。
第三个成本是流程复用基本为零。公司既要请假审批,又要报销审批,又要合同盖章审批。自研状态机写一套,第二套还要复制改动,三套之后维护量翻倍。
1.2 Flowable真正帮你解决的问题(以及不解决的问题)
Flowable最大的价值不是“省掉两个状态字段”,而是把流程定义和业务代码解耦。流程长什么样,画在BPMN文件里;节点审批人怎么取,写在流程变量里;业务代码只发起流程、查询待办、完成审批,不再去写状态流转。
但有个边界必须搞清楚:Flowable不帮你判断业务规则。比如“请假超过三天要总监加签”这个规则,它体现在排他网关上,条件还是要你自己用表达式写。另外消息通知、超时提醒、表单渲染这些能力,Flowable有基础支持但通常不会直接满足你公司的交互要求,还是要自己做。
也别迷信工作流引擎。如果你们就是单表单、单节点、两个状态的“审批”,比如一个“启用/停用”开关,那我建议别上Flowable,自己写个状态字段更轻。Flowable真正适合的是:链路超过三级、存在驳回会签、需要完整历史追溯、以及审批流程经常变化的场景。
我把选型依据整理成了一张表,方便你按实际情况对号入座:
| 对比维度 | 自研状态机 | Flowable | 轻量级脚本引擎(如Drools) |
|---|---|---|---|
| 流程可视化 | 无,靠文档 | BPMN文件本身可画图 | 无 |
| 审批链变更 | 改代码,测试回归 | 改BPMN文件,重新部署 | 改规则脚本 |
| 会签/多实例 | 自研复杂 | 原生支持并行会签 | 不支持 |
| 完整历史追踪 | 需自建日志 | ACT_HI_*系列表完整记录 | 不擅长 |
| 学习成本 | 低 | 中等偏高 | 中等 |
| 适合场景 | 状态极少、流程固定 | 长链路、多变、需追溯 | 规则判断密集型 |
2. SpringBoot 工程接入 Flowable:依赖、配置与先搞懂的核心概念
确定要用Flowable之后,先把工程搭起来。这步看起来简单,实际上版本匹配是第一道坎。
2.1 版本匹配是第一道坎
Flowable的版本历史和SpringBoot版本强相关。我当前用的组合是SpringBoot 2.7.x + Flowable 6.8.1,这是目前最稳妥的组合之一。如果你项目已经上了SpringBoot 3.x,那就得选Flowable 7.x,API上差异不大,但依赖坐标和自动配置类有不少改动。
pom依赖就引入一个starter:
<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>6.8.1</version> </dependency>这个starter会自动帮你配置好RepositoryService、RuntimeService、TaskService、HistoryService这些核心Bean,不需要你再手动new。
application.yml这边需要注意几个点:
spring: datasource: url: jdbc:mysql://localhost:3306/flowable_demo?nullCatalogMeansCurrent=true&useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root flowable: database-schema-update: true async-executor-activate: true process-definition-location-prefix: classpath:/processes/database-schema-update: true表示首次启动时自动创建Flowable所需的ACT_*系列表。注意MySQL 8的连接串里我加了nullCatalogMeansCurrent=true,这个参数不加,Flowable在初始化时可能扫描到错误catalog,导致表建到别的库去,这是我踩过的坑,后面排坑章节再细说。
2.2 从一条请假审批来看核心对象
很多新手看Flowable源码看得一头雾水,主要是没搞懂这几个对象的关系。我用一条请假审批来串一下:
- ProcessDefinition(流程定义):就是那张BPMN“图纸”。请假审批流程定义了一次,一个公司所有请假人都用这张图纸。
- ProcessInstance(流程实例):某一次具体的请假。张三7月1号发起请假,引擎就为这次请假创建一个流程实例。流程定义和流程实例的关系,相当于“类”和“对象”。
- Execution(执行实例):一条流程实例内部可能有多条执行路径,特别是遇到并行网关时。你日常用到的场景里,把Execution理解成流程实例内部的执行指针就行。
- Task(任务):当前轮到谁处理了。比如“直属领导审批”这个节点被激活后,引擎会创建一条Task记录,处理人通过TaskService查到它并执行complete操作。
用生活化的类比:流程定义是施工图纸,流程实例是正在盖的这栋楼,Task是当前工位上贴着的待办单。
2.3 你需要记住的五个Service
Flowable提供的Service很多,但日常开发高频用到的只有这几个:
| Service | 核心作用 | 常见操作 |
|---|---|---|
| RepositoryService | 部署流程定义、查询流程定义、暂停/激活 | createDeployment()、createProcessDefinitionQuery() |
| RuntimeService | 发起流程实例、操作执行实例 | startProcessInstanceByKey()、createProcessInstanceQuery() |
| TaskService | 查待办、完成审批、设置处理人 | createTaskQuery()、complete()、setAssignee() |
| HistoryService | 查询历史流程实例、历史活动、历史任务 | createHistoricActivityInstanceQuery()、createHistoricProcessInstanceQuery() |
| IdentityService | 用户和组管理(引擎内置,通常不用) | newUser()、saveUser() |
大多数项目里,IdentityService基本用不上,因为审批人一般直接用你们公司内部员工ID,存在流程变量里就行,不需要同步到Flowable的用户表。
2.4 数据库表别害怕
Flowable自动建出来的表看着铺天盖地,实际上按前缀分成几类:
ACT_RE_*:Repository,流程定义和模型相关。比如ACT_RE_PROCDEF存放流程定义,部署新版本会同一条key新增版本号。ACT_RU_*:Runtime,运行时数据。比如ACT_RU_TASK就是当前待办表,ACT_RU_EXECUTION是执行实例表。ACT_HI_*:History,历史数据。比如ACT_HI_PROCINST是历史流程实例,ACT_HI_ACTINST是每一个节点的执行历史,ACT_HI_TASKINST是每一条任务的历史。ACT_ID_*:Identity,用户组表,一般不用。ACT_GE_*:通用数据,比如ACT_GE_BYTEARRAY存BPMN文件二进制内容。
你只需要重点理解ACT_RU_TASK和ACT_HI_ACTINST这两张,日常业务百分之八十都在跟它们打交道。
3. 用 BPMN 画一个请假审批流:从建模到部署
工程环境准备好之后,核心任务就是建模。我建议你用一张最简单的请假审批流跑通全链路:发起申请 → 直属领导审批 → 排他网关判断天数 → 三天以上加总监审批 → 人事备案 → 结束。
3.1 两种建模方式
Flowable支持手写BPMN XML,也支持可视化建模。可视化工具有IDEA的Flowable BPMN插件,也有flowable-ui应用。我的建议是:学习阶段手写XML,这样能逼自己搞懂每个节点的含义;流程落地之后再用可视化工具维护,方便给业务同事看流程图。
手写XML最核心的就是<process>节点,它定义一个流程。流程里每个节点有自己的id和name,id是引擎内部用的,name是给人看的。
3.2 请假审批BPMN核心片段
我直接给一份简化但完整的请假审批BPMN文件,把关键元素标出来:
<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:flowable="http://flowable.org/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" targetNamespace="http://flowable.org/bpmn"> <process id="leaveApproval" name="请假审批流程" isExecutable="true"> <startEvent id="startEvent" name="发起申请"/> <userTask id="submitTask" name="提交申请" flowable:formKey="leaveForm"> <extensionElements> <flowable:startFormDataFormKey>leaveForm</flowable:startFormDataFormKey> </extensionElements> </userTask> <userTask id="leaderTask" name="直属领导审批" flowable:assignee="${leaderId}"/> <exclusiveGateway id="daysGateway" name="请假天数判断"/> <userTask id="directorTask" name="总监审批" flowable:assignee="${directorId}"/> <userTask id="hrTask" name="人事备案" flowable:assignee="${hrUserId}"/> <endEvent id="endEvent" name="结束"/> <sequenceFlow id="flow_start_submit" sourceRef="startEvent" targetRef="submitTask"/> <sequenceFlow id="flow_submit_leader" sourceRef="submitTask" targetRef="leaderTask"/> <sequenceFlow id="flow_leader_gateway" sourceRef="leaderTask" targetRef="daysGateway"/> <sequenceFlow id="flow_gateway_hr" sourceRef="daysGateway" targetRef="hrTask"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${days <= 3}]]> </conditionExpression> </sequenceFlow> <sequenceFlow id="flow_director" sourceRef="daysGateway" targetRef="directorTask"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${days > 3}]]> </conditionExpression> </sequenceFlow> <sequenceFlow id="flow_director_hr" sourceRef="directorTask" targetRef="hrTask"/> <sequenceFlow id="flow_hr_end" sourceRef="hrTask" targetRef="endEvent"/> </process> </definitions>简单解释几个关键点:
flowable:assignee="${leaderId}"表示这个用户任务的处理人从流程变量leaderId里取。所以发起流程时,你需要在流程变量里塞入leaderId、directorId、hrUserId这些值。exclusiveGateway是排他网关,它的作用是从分支里选一条满足条件的路径走。上面我先判断days <= 3,不满足就走另一个分支。注意${days > 3}这个表达式里的days也是流程变量,靠它判断。formKey字段并不是Flowable强制的,它只是给外部表单一个标识。真实项目中你大概率还是用自建的前端表单,这里的formKey可以当成关联业务表的标识来用。
3.3 deploy:让引擎认识这张图纸
BPMN文件放哪、怎么加载,有两种常用方式。
第一种是手动部署,适合动态上传流程的场景:
@Autowired private RepositoryService repositoryService; public void deployProcess() { Deployment deployment = repositoryService.createDeployment() .addClasspathResource("processes/leaveApproval.bpmn20.xml") .name("请假审批流程") .deploy(); System.out.println("部署ID: " + deployment.getId()); }第二种是自动部署。Flowable starter默认会扫描classpath*:/processes/目录下的BPMN文件,只要你的文件放在src/main/resources/processes/下,应用启动时就会自动部署。不需要写任何部署代码。
部署成功后,你可以查ACT_RE_PROCDEF表,或者写查询接口确认:
repositoryService.createProcessDefinitionQuery() .processDefinitionKey("leaveApproval") .latestVersion() .singleResult();同一个key再次部署,Flowable会自动递增版本号。这在实际项目中非常重要——流程改完部署上去,老流程实例继续走老版本,新流程用新版本,互不干扰。
4. 发起流程、查待办、做审批:核心代码一次说清
部署完了,接下来就是业务开发里最常写的几段代码:发起流程、查待办、完成审批、查历史。
4.1 发起流程:流程变量就是上下文
先看发起流程的代码。假设前端提交了一个请假表单,后端需要把请假人、天数、原因、审批人这些数据传进流程:
@Autowired private RuntimeService runtimeService; public String startLeaveProcess(LeaveApplyRequest request) { // 确定各级审批人 String leaderId = userService.findLeaderIdByUserId(request.getUserId()); String directorId = userService.findDirectorIdByUserIdAndDept(request.getUserId()); String hrUserId = "hr001"; // 流程变量就是整个流程运行期的上下文 Map<String, Object> variables = new HashMap<>(); variables.put("applyUserId", request.getUserId()); variables.put("days", request.getDays()); variables.put("reason", request.getReason()); variables.put("leaderId", leaderId); variables.put("directorId", directorId); variables.put("hrUserId", hrUserId); variables.put("approved", false); // businessKey一般是业务单据号,方便业务表关联 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey( "leaveApproval", request.getBizNo(), variables ); return processInstance.getId(); }这里有几个设计点要理解。第一,流程变量不是临时数据,它跟着整个流程实例存活,审批过程中每个节点的表达式都是从变量里取值。第二,startProcessInstanceByKey用的是流程定义的key,不是id,这样部署新版本不影响发起入口。第三,businessKey是业务单号,建议和业务表主键对应,后面查流程实例非常方便。
流程发起后,第一个用户任务submitTask(提交申请)会自动生成。很多项目会把审批表单填写和“提交”合在一起做,也可以手动调用complete把这个任务处理掉。
4.2 待办查询与审批完成
查询某个用户当前有哪些待办任务,是审批系统的核心操作:
@Autowired private TaskService taskService; public List<TaskVO> queryTodoList(String userId) { List<Task> tasks = taskService.createTaskQuery() .taskAssignee(userId) .orderByTaskCreateTime() .desc() .list(); return tasks.stream().map(task -> { ProcessInstance processInstance = runtimeService .createProcessInstanceQuery() .processInstanceId(task.getProcessInstanceId()) .singleResult(); TaskVO vo = new TaskVO(); vo.setTaskId(task.getId()); vo.setTaskName(task.getName()); vo.setProcessInstanceId(task.getProcessInstanceId()); vo.setApplicationNo(processInstance.getBusinessKey()); return vo; }).collect(Collectors.toList()); }如果你的审批场景是“几个人抢同一张单子”,也就是候选组模式,可以在BPMN里用flowable:candidateGroups指定候选组,然后查待办时用taskCandidateGroup(userId)。区别在于:assignee是“已分配给我处理”,candidate是“我候选但还没认领”,认领用taskService.claim(taskId, userId)。
完成审批的代码特别简单:
public void completeTask(String taskId, String comment, boolean approved) { Map<String, Object> variables = new HashMap<>(); variables.put("comment", comment); variables.put("approved", approved); taskService.complete(taskId, variables); }看到没有,审批动作本质上就是“给流程变量赋值 + 把当前任务标记完成”。approved、comment这些变量传入后,后续节点和网关可以通过它们判断走向。
4.3 驳回是怎么实现的
驳回可能是新手最懵的地方。Flowable并没有一个专门的“驳回”API,但实现思路很清晰:画BPMN时,在需要驳回的节点上画一条回到上文节点的连线,用条件表达式控制。
比如上面请假流程里,如果直属领导审批不通过,我希望流程回到“提交申请”节点让员工修改后再提交。那就需要在leaderTask和submitTask之间加一条sequenceFlow,并在leaderTask完成后通过排他网关判断。换句话说,“驳回”在BPMN里就是一条特殊的流转路径。
不过实际项目里驳回往往需要跳到任意指定节点,BPMN画线反而不灵活。Flowable 6.5+ 提供了ChangeActivityStateBuilder,可以动态移动执行到指定节点:
@Autowired private RuntimeService runtimeService; public void rejectToTask(String processInstanceId, String targetActivityId) { runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveActivityIdTo("leaderTask", targetActivityId) .changeState(); }这个方法非常有用。审批不通过,想退到哪个节点就退到哪个节点,不用重新画流程图。但要注意,移动节点后记得把相关流程变量重置,否则旧的审批意见会影响下一次流转。
4.4 用历史服务还原审批全链路
审批系统还有一项硬需求:给业务方展示“这个单子流转过哪些节点、谁在什么时间批的”。这个用HistoryService查:
@Autowired private HistoryService historyService; public List<HistoricActivityInstance> queryProcessHistory(String processInstanceId) { return historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list(); }返回结果里就有每个节点activityId、activityName、assignee(处理人)、startTime、endTime。配合流程变量里存的comment,一个完整的审批时间线就出来了。
5. 接入真实业务后躲不开的三个硬问题:业务关联、驳回会签、组织映射
跑通请假审批之后,你会面临三个“真实系统才有的问题”。这三点做不好,生产环境一定出乱子。
5.1 业务表与流程实例关联
工作流引擎只管流程,你的业务数据(请假单明细、金额、附件)仍然存在业务表里。两边怎么关联?两种主流方案:
方案A:业务表新增一列process_instance_id,启动流程后写入返回的流程实例ID。查询时直接用业务单号查流程实例,或者用流程实例ID查业务单号。
方案B:把业务表主键作为businessKey传入流程。需要的时候用runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(bizNo).singleResult()反向查出流程实例。
我在真实项目里通常是双管齐下:businessKey存业务单号用于引擎侧检索,业务表冗余process_instance_id用于快速查历史。冗余字段确实脏一点,但换来的是查询代码简单直接,不用每次都通过businessKey反查。
5.2 驳回、撤回、会签怎么做
驳回刚才用ChangeActivityStateBuilder说了,撤回本质上也是移动节点,只是目标是“上一个节点”。实现时可以在流程实例里维护一个“当前活动”的栈,或者直接记录上一步节点ID,撤回时移动回去。
会签是另一个高频需求。比如合同审批需要多个部门负责人同时同意。Flowable解决方式是“多实例(multi-instance)”节点。我给出并行会签的BPMN片段:
<userTask id="multiApproveTask" name="多部门会签" flowable:assignee="${assignee}"> <extensionElements> <flowable:formKey>multiApproveForm</flowable:formKey> </extensionElements> <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="${assigneeList}" flowable:elementVariable="assignee"> <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition> </multiInstanceLoopCharacteristics> </userTask>关键点在flowable:collection="${assigneeList}":这个流程变量是一个List,引擎会为列表里的每个元素生成一条子任务,elementVariable指定每个子任务的处理人。isSequential="false"表示并行审批,completionCondition表示全部完成后才往下一步走。如果想实现“过半通过即可”,可以调整completionCondition为${nrOfCompletedInstances >= 2}这种形式。
我第一次用会签时犯过错,以为assigneeList可以硬编码在XML里,结果每次审批人不同,必须通过流程变量传入。
5.3 组织架构与多租户
Flowable内置了IdentityService和用户组概念,但大多数公司的组织架构维护在自研的权限系统里。我的建议是:不要试图把公司人员全量同步进Flowable的用户表,直接用业务员工ID作为assignee和流程变量即可。引擎只认字符串ID,不关心你背后是哪个系统。
多租户场景稍微复杂一点。如果你们是SaaS,多个租户共用一套引擎,那就要用tenantId隔离。部署流程时可以指定:
repositoryService.createDeployment() .addClasspathResource("processes/leaveApproval.bpmn20.xml") .tenantId("tenant_001") .deploy();发起流程时也带上租户上下文。查询待办时,引擎会自动按租户过滤。这一块不复杂,但要在项目第一天就设计好,后面补会非常痛苦。
6. 实战排坑:自动建表、事务边界、性能与长流程的取舍
最后这章全是血泪经验。任何一个坑在测试环境都看不出来,上生产必炸。
6.1 自动建表与MySQL字符集坑
先说nullCatalogMeansCurrent=true。Flowable初始化时要扫描数据库catalog来创建ACT_*表,如果你MySQL连接串没加这个参数,它可能扫描出多个catalog,导致表建到其他库去,或者在已存在的库里报“表已存在”的错。这个参数的含义是告诉JDBC驱动“返回的catalog只用当前连接指定的库”,我加上之后问题立刻消失。
字符集也要注意。审批意见、驳回理由这种字段必然是中文,MySQL建库时务必用utf8mb4,连接串上加characterEncoding=utf8mb4。别用默认的latin1,否则等你上线后写入中文变问号,再改字符集就是一场灾难。
6.2 事务边界:别把HTTP回调放在事务里
Flowable默认和Spring事务是打通的。也就是说,你在一个@Transactional方法里调用taskService.complete(),事务提交时流程状态变更一起提交,事务回滚时流程状态也一起回滚。这一点非常好,但也容易让人踩坑。
常见错误是在complete之后马上发MQ、调外部HTTP接口通知审批人。如果外部接口超时或者MQ不可用,整个事务会被拖着,流程推进变慢,严重时数据库连接耗尽。我的做法是:事务方法里只改流程和业务数据,外部通知通过Spring的事件机制发布出去,事务提交后再异步执行;或者直接把通知写入本地消息表,用定时任务/消息中间件慢慢消费。
6.3 性能:运行时表一定要“瘦”
Flowable性能大头不在引擎本身,而在数据表设计。你要记住一个原则:*ACT_RU_系列是运行时表,流程结束后里面的数据必须清空。如果你发现ACT_RU_TASK表数据量涨到几十万,那说明大量流程没有走到正常结束节点,是流程设计或业务逻辑有问题。
正常结束后由运行库搬进历史库,Flowable自动完成。但历史库会无限涨,所以需要定期归档。我的做法是用定时任务把ACT_HI_PROCINST按结束时间搬进历史归档表,保留近一年热数据。查询历史页时先走Flowable原生HistoryService,数据量大了之后建议按业务单号+时间范围分页,不要list()一把梭。
6.4 长流程的模型设计:别把所有节点画在一条线上
有些人上手之后会画出一条三十个节点的“超级流程”,上下游串联,看着很完整,实际上运维噩梦。但凡中间某个节点返回数据格式变了,排查需要翻遍整张流程图。
我的经验是:超过七八个节点的流程,优先拆成子流程。Flowable支持callActivity调用子流程,也支持事件子流程、定时边界事件。比如审批通过后需要自动通知多个系统的场景,就用异步子流程,让主审批流快速结束,后续动作慢慢跑。
定时提醒用边界定时事件做。比如“财务审批超时2小时自动提醒”,可以在财务审批节点上加一个定时边界事件,触发后给指定人发提醒,同时流程继续等。这种需求用自研状态机实现很痛苦,但在BPMN里就是一个节点参数。
6.5 我现在的验收清单
每次上线新流程,我都会拿着下面这个清单过一遍:
- 流程定义是否有版本管理,能否回滚?没有版本管理的部署迟早出事。
- 待办查询是否走索引?
ACT_RU_TASK按 assignee 查询要建组合索引。 - 历史查询有没有分页?生产环境
list()查询必炸。 - 驳回和撤回路径是否都测试过?特别是驳回后变量重置。
- 是否有监控?可以定时扫
ACT_RU_TASK表找出超过N天未处理的积压任务并告警。 - 通知是否异步?有没有可能把外部依赖拖进流程事务里。
说个最让我印象深刻的生产事故:某次午夜,同事发版本,改动了一个流程定义。由于没有指定版本,老流程实例全部被新版本“吸走”了,正在审批的人发现界面上的流程图变了,历史节点显示错乱。从那之后我要求所有流程部署必须打version标签,禁止裸部署。
工作流引擎这东西,用起来不难,难的是把边界理解清楚。Flowable替你扛住了状态流转、历史归档、并发控制这些底层脏活,但业务规则、通知、权限、数据一致性仍然是你自己的责任。你只需要画好流程图、管理好流程变量、设计好业务关联,就能把审批类功能做得又稳又灵活。至少现在,朋友再跟我说“审批要加签”,我第一反应不会再是“排期两天”,而是“改一下BPMN,半小时搞定”。