1. 流程驳回到底在解决什么问题
1.1 从一个真实场景说起
做过审批流的人大概率都遇到过这种需求:一个请假单提交上来,部门主管批了,到了总监那里发现金额或者天数不对,总监不想直接拒绝,而是希望把单子退回到发起人那里改一改再重新走。这个“退回去”的动作,就是流程驳回。
听起来简单,但真到实现层面,坑比想象的多。驳回和拒绝是两码事——拒绝是流程终止,驳回是流程回退到某个节点继续跑。驳回和撤回也不一样——撤回是发起人自己把单子拉回来,驳回是审批人把单子打回去。这三个概念在实际项目里经常被混着用,导致需求评审的时候大家说的根本不是一回事。
我见过不少项目,一开始需求方只说“加个驳回按钮”,开发同学随手写了个跳转,结果上线之后发现:驳回到发起人之后,发起人改完重新提交,流程直接跳到了总监那里,中间那个部门主管的审批记录凭空消失了。这就是典型的驳回目标节点没选对,或者说回退策略没设计好。
1.2 驳回的核心诉求拆解
把驳回这件事拆开看,本质上要回答三个问题:
第一个问题:退到哪去。是退到上一个审批节点,还是退到发起人,还是退到任意一个已经走过的节点?这三种策略在不同业务场景下都有用。退到上一节点适合“小修小补”,退到发起人适合“大改重来”,退到任意节点适合“谁的问题谁负责”。
第二个问题:退回去之后怎么回来。发起人改完重新提交,是直接回到驳回他的那个人那里,还是把中间所有节点重新走一遍?前者效率高但可能跳过必要的审核,后者严谨但流程冗长。这个选择直接决定了流程引擎的配置方式。
第三个问题:历史数据怎么处理。驳回之后,原来那条审批记录是标记为“已驳回”还是直接删掉?重新提交之后是生成新的任务还是复用旧任务?这些细节决定了后续的审批历史查询、流程追溯能不能做干净。
这三个问题想清楚了,技术选型和实现路径基本就定了。想不清楚,后面就是无穷无尽的改bug。
1.3 为什么流程引擎的驳回这么难做
有人可能会说,不就是改个状态吗,能有多难。问题在于,流程引擎的核心是一张有向图,节点之间的连线代表了流转规则。驳回本质上是让流程沿着已有的连线反向走,但引擎默认只支持正向流转。
更麻烦的是并行网关。假设一个流程在某个节点分出了三条并行分支,其中一条分支的审批人点了驳回,这时候另外两条分支怎么办?是全部回退还是只回退这一条?如果只回退这一条,另外两条已经审批通过的数据要不要保留?这些问题在单线流程里不存在,一旦上了并行网关就全冒出来了。
还有一个容易被忽略的点:驳回之后的重新提交,在引擎看来是一次全新的流程实例启动,还是同一个实例的继续执行?这两种处理方式对业务数据的影响完全不同。前者会生成新的流程实例ID,后者保持原ID不变。如果业务系统里用流程实例ID做了关联查询,选错了方案就是灾难。
2. 驳回策略的选型与设计思路
2.1 三种主流驳回模式对比
在实际项目里,驳回策略基本可以归为三类,我把它整理成了一张表,方便对照选择:
| 驳回模式 | 回退目标 | 重新提交后的路径 | 适用场景 | 实现复杂度 |
|---|---|---|---|---|
| 退到上一节点 | 当前节点的前一个审批节点 | 从上一节点继续往下走 | 小修改,不需要发起人介入 | 低 |
| 退到发起人 | 流程的第一个用户任务节点 | 从发起人节点重新走完整流程 | 大改,需要重新审批 | 中 |
| 退到任意节点 | 用户指定的已走过节点 | 从指定节点继续往下走 | 责任明确,谁的问题谁改 | 高 |
退到上一节点是最简单的,引擎一般都有现成的API支持。但它的局限也很明显:如果上一节点是系统自动节点而不是人工审批节点,退回去之后没人处理,流程就卡死了。所以用这个模式之前,一定要确认目标节点是用户任务。
退到发起人是业务上最常用的。发起人改完重新提交,整个审批链重新走一遍,逻辑清晰,不容易出问题。缺点是效率低,如果流程有七八个审批节点,每次驳回都要从头走一遍,审批人会有怨言。
退到任意节点最灵活,但实现也最复杂。需要解决两个问题:一是怎么确定哪些节点可以被选为回退目标,二是回退之后那些被跳过的节点怎么处理。我的经验是,只允许回退到用户任务节点,系统自动节点不允许作为回退目标,这样能避免大部分问题。
2.2 回退目标的确定逻辑
不管选哪种模式,都需要一个算法来确定回退目标。最直接的做法是查历史任务表,找到当前节点之前最近的一个用户任务节点。但这里有个坑:如果流程走过并行网关,历史任务表里会有多条记录,时间戳可能非常接近,单纯按时间排序不一定能拿到正确的节点。
更稳妥的做法是结合流程定义图来算。流程定义里每个节点都有唯一的ID,节点之间的连线定义了流转方向。从当前节点出发,沿着入边反向遍历,遇到的第一个用户任务节点就是回退目标。这个算法不依赖历史数据,结果更可靠。
如果要做“退到任意节点”,就需要把当前节点之前所有走过的用户任务节点都列出来让用户选。这时候要注意去重——并行分支上可能有多个同名节点,列表里要能区分开。我的做法是在节点名称后面加上执行实例的标识,比如“部门主管审批(分支A)”,这样用户能看清楚。
2.3 重新提交后的流转策略
驳回之后重新提交,流转策略有两种:一种是“原路返回”,直接回到驳回的那个节点;另一种是“重新走一遍”,从回退目标节点开始按正常流程往下走。
原路返回的效率高,但有个前提:中间被跳过的节点必须都是自动通过的,或者业务上允许跳过。如果中间有必须人工审批的节点,原路返回就等于绕过了这些审批,合规上可能有问题。
重新走一遍更安全,但需要处理一个细节:那些之前已经审批通过的节点,重新走的时候是自动通过还是重新审批?如果自动通过,那审批记录怎么展示?如果重新审批,审批人会不会觉得莫名其妙——这个单子我不是已经批过了吗?
我的建议是:在流程变量里记录一个“驳回次数”和“驳回节点列表”,重新走到这些节点时,根据配置决定是自动通过还是重新审批。如果业务要求严格,就重新审批,但在任务标题上标注“驳回重审”,让审批人知道来龙去脉。
3. 核心实现细节与代码要点
3.1 流程定义的关键配置
要实现驳回,流程定义文件里需要做一些特殊配置。以常见的BPMN 2.0规范为例,关键点在于用户任务节点的属性设置。
首先,每个用户任务节点需要有一个明确的标识,方便回退时定位。通常用id属性就够了,但如果有并行分支,同名节点会有多个实例,这时候需要结合executionId来区分。
其次,需要在流程定义里预留回退的连线。标准BPMN不直接支持反向流转,所以常见的做法是在每个用户任务节点上加一个边界事件或者自定义属性,标记这个节点是否允许被回退到。比如可以加一个扩展属性:
<userTask id="deptManagerApprove" name="部门主管审批"> <extensionElements> <custom:allowReject>true</custom:allowReject> <custom:rejectTarget>initiator</custom:rejectTarget> </extensionElements> </userTask>这样在运行时就能根据这些配置来判断回退策略,而不是把逻辑硬编码在代码里。
3.2 驳回操作的API调用链路
在代码层面,一次驳回操作通常涉及以下几个步骤:
第一步,查询当前任务和流程实例信息。需要拿到当前任务ID、流程实例ID、当前节点ID、流程定义ID。
第二步,计算回退目标节点。根据前面说的策略,从流程定义图或者历史任务表里找到目标节点。
第三步,执行回退。这一步是最关键的,不同引擎的API不一样。以Activiti为例,常见的做法是先用runtimeService.createChangeActivityStateBuilder()来改变流程状态:
runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveActivityIdTo(currentActivityId, targetActivityId) .changeState();这段代码的意思是,把流程实例从当前节点移动到目标节点。执行完之后,目标节点会生成一个新的待办任务。
第四步,更新业务数据。把当前任务的审批意见、驳回原因、驳回时间等信息记录下来,同时更新业务表里的流程状态。
第五步,发送通知。给被驳回的人发消息,告诉他单子被退回来了,原因是什么。
这五步里,第三步最容易出问题。moveActivityIdTo这个方法在并行网关场景下行为比较微妙,如果当前节点在并行分支上,移动之后其他分支的状态可能不符合预期。我的经验是,在执行回退之前,先检查当前节点是否在并行分支上,如果是,要么把整个并行网关回退,要么只回退当前分支并记录其他分支的状态。
3.3 并行网关下的驳回处理
并行网关是驳回功能的最大挑战。假设一个流程在某个节点分出三条并行分支,分别由A、B、C三个人审批。现在A点了驳回,应该怎么处理?
方案一:整个并行网关回退。三条分支全部回退到网关之前的节点,B和C已经审批通过的任务作废。这个方案逻辑最清晰,但B和C的工作白做了,用户体验不好。
方案二:只回退A所在的分支。B和C的审批结果保留,A改完之后重新提交,只需要重新走A这条分支。这个方案效率高,但需要引擎支持单分支回退。Activiti的moveActivityIdTo在某些版本里支持这种操作,但需要仔细测试。
方案三:把并行网关改成包容网关。包容网关的特点是,已经完成的分支不会因为其他分支的回退而作废。但这个方案需要改流程定义,影响面比较大。
我的建议是,如果业务上并行审批的分支之间没有强依赖关系,优先用方案二。如果分支之间有依赖,比如必须三个人都批完才能进入下一步,那就用方案一,虽然效率低但不会出逻辑错误。
3.4 驳回历史的数据结构设计
驳回操作会产生一些额外的数据,需要设计好存储结构。我通常会在审批记录表里加几个字段:
action_type:操作类型,区分“审批通过”“驳回”“撤回”等reject_reason:驳回原因,文本类型reject_target:驳回目标节点IDreject_count:这条流程被驳回的次数original_task_id:被驳回的原始任务ID,方便追溯
另外,建议单独建一张驳回记录表,记录每次驳回的详细信息,包括驳回人、驳回时间、驳回前后的节点、重新提交时间等。这张表在后续做流程效率分析的时候非常有用,比如可以统计哪个节点最容易被驳回,从而优化流程设计。
4. 实操过程中踩过的坑与排查技巧
4.1 驳回后任务重复生成的问题
这个问题我遇到过不止一次。现象是:驳回之后,目标节点生成了一个待办任务,但过一会儿又生成了一个一模一样的任务,两个任务都能审批,审批其中一个之后流程状态变得很奇怪。
排查下来,原因通常是回退操作触发了两次。一次是代码里显式调用的回退API,另一次是流程引擎的监听器或者事件机制自动触发的。比如在TaskListener的complete事件里又调了一次回退逻辑,导致重复执行。
解决办法有两个:一是在回退操作前加一个幂等判断,检查目标节点是否已经有待办任务,如果有就跳过;二是检查监听器配置,确保回退操作不会触发额外的流程流转。
4.2 驳回目标节点找不到的异常
有时候执行驳回会报“目标节点不存在”或者“无法找到回退路径”的错误。这种情况多半是因为流程定义版本变了。比如流程实例启动的时候用的是版本1的定义,后来流程定义更新到了版本2,版本2里删掉了某个节点,这时候按版本2的定义去找回退路径就找不到了。
处理办法是:回退操作要基于流程实例启动时的定义版本来计算路径,而不是用最新版本。在Activiti里可以通过repositoryService.getProcessDefinition(processDefinitionId)拿到实例对应的定义,而不是用getLatestProcessDefinition。
4.3 驳回后流程变量丢失的问题
流程变量在驳回过程中容易丢失,尤其是用moveActivityIdTo做回退的时候。原因是这个操作会改变执行实例的状态,某些作用域内的变量会被清理掉。
我的做法是:在执行回退之前,先把关键流程变量备份到一个临时存储里,回退完成之后再恢复回去。或者干脆把关键变量存在业务表里,不依赖流程引擎的变量存储。后者更稳妥,但需要业务代码配合。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 驳回后任务重复生成 | 回退操作触发两次 | 检查监听器和事件配置 | 加幂等判断,检查监听器 |
| 目标节点找不到 | 流程定义版本不一致 | 确认实例的定义版本 | 用实例对应的定义版本计算路径 |
| 流程变量丢失 | 回退操作清理了变量 | 检查变量作用域 | 备份变量或存业务表 |
| 并行分支状态异常 | 回退策略不匹配 | 检查并行网关配置 | 调整回退策略或改网关类型 |
| 驳回后无法重新提交 | 任务状态未正确更新 | 检查任务表和业务表状态 | 手动更新状态或修复回退逻辑 |
4.5 几个实用的避坑技巧
第一个技巧:在流程定义里给每个用户任务节点加一个“是否允许驳回”的配置项,默认关闭,需要的时候再打开。这样能避免一些不该驳回的节点被误操作。
第二个技巧:驳回原因做成必填项。我见过太多项目,驳回原因可以不填,结果发起人收到驳回通知之后完全不知道哪里有问题,只能去问审批人,沟通成本很高。
第三个技巧:驳回之后给发起人发通知的时候,把驳回原因和需要修改的内容一起带上。如果能在通知里直接附上表单的修改链接就更好了,减少发起人的操作步骤。
第四个技巧:在流程历史查询页面,把驳回记录用不同的颜色或者图标标出来,让查看流程轨迹的人一眼就能看到哪些节点被驳回过。这个细节对提升用户体验很有帮助。
第五个技巧:如果项目里用的是Activiti 5.x版本,注意moveActivityIdTo方法在并行网关下的行为跟6.x版本有差异。5.x版本在某些场景下会把整个并行网关的状态重置,6.x版本则更精细一些。升级或者选型的时候要留意这个区别。
5. 驳回功能的扩展与优化方向
5.1 支持批量驳回
有些业务场景下,一个审批人可能同时收到多条待办任务,这些任务属于同一个流程的不同分支,或者属于不同的流程实例。如果每条都要单独点驳回,操作效率很低。
批量驳回的实现思路是:先选中多条任务,然后统一指定驳回目标和驳回原因,最后批量执行回退操作。技术上的难点在于,不同任务可能属于不同的流程实例,回退操作需要逐个执行,而且要保证事务一致性——要么全部成功,要么全部回滚。
我的做法是用一个异步任务来处理批量驳回,每条任务单独执行回退,记录执行结果,最后汇总反馈给用户。这样即使中间某条失败,也不会影响其他任务。
5.2 驳回后的自动提醒与超时处理
驳回之后,发起人可能因为各种原因没有及时修改和重新提交,导致流程一直挂在那里。这时候需要一个超时提醒机制。
可以在驳回操作完成之后,启动一个定时任务,比如24小时之后检查流程是否还在驳回状态,如果是就发提醒通知。如果超过48小时还没动静,可以自动升级通知给发起人的上级,或者自动作废流程。
这个机制的实现依赖定时任务框架和流程状态查询,技术难度不大,但对提升流程效率很有帮助。
5.3 驳回数据的统计分析
前面提到过,驳回记录表可以用来做流程效率分析。具体能分析出什么呢?
比如可以统计每个节点的驳回率,驳回率高的节点说明审批标准不清晰,或者上游节点的质量有问题。还可以统计驳回原因的分类分布,看看大部分驳回是因为材料不全、金额错误还是其他原因,从而有针对性地优化表单设计。
再进一步,可以分析驳回后的平均处理时长,看看发起人一般需要多久才能重新提交。如果这个时长很长,说明驳回通知的方式或者修改流程有问题,需要优化。
这些分析结果对流程优化非常有价值,但前提是驳回数据要记录得足够细。所以在设计驳回功能的时候,不要只记录“驳回了”这个事实,要把驳回原因、驳回目标、驳回时间、重新提交时间都记下来。
5.4 与消息通知系统的集成
驳回操作天然需要通知机制。被驳回的人需要知道单子被退回来了,驳回原因是什么,需要改什么。如果通知不到位,整个驳回功能的价值就大打折扣。
集成消息通知系统的时候,有几个细节要注意:
通知内容要包含关键信息:流程名称、驳回人、驳回原因、驳回时间、修改链接。这些信息缺一不可,少了任何一个都会增加沟通成本。
通知渠道要支持多种:站内信、邮件、即时通讯工具。不同用户习惯不同渠道,最好让用户能自己配置。
通知要支持重试:消息发送可能失败,需要有重试机制,确保通知能送达。
通知要避免骚扰:如果同一个流程被多次驳回,不要每次都发全量通知,可以合并或者只发变更部分。
6. 不同流程引擎的驳回实现差异
6.1 Activiti与Flowable的对比
Activiti和Flowable同源,很多API是相似的,但在驳回相关的功能上有一些差异。
Activiti的moveActivityIdTo方法在6.x版本里比较稳定,支持单节点回退和并行分支回退。Flowable在这个基础上做了一些增强,比如支持更灵活的回退目标选择,以及回退后的变量处理策略配置。
如果项目里用的是Activiti 5.x,回退功能需要自己实现的地方比较多,因为5.x的API对回退的支持不够完善。很多团队在5.x上做驳回都是通过自定义命令或者直接操作数据库来实现的,维护成本比较高。
6.2 自研流程引擎的驳回设计
有些项目不用开源引擎,而是自研了一套简单的流程引擎。这种情况下,驳回功能反而更容易实现,因为没有历史包袱,可以按最理想的模型来设计。
自研引擎做驳回,核心是设计好流程状态机。每个节点有明确的状态(待处理、已通过、已驳回、已跳过),节点之间的流转规则用配置或者代码定义。驳回操作就是把当前节点状态改为“已驳回”,把目标节点状态改为“待处理”,然后触发相应的通知和后续动作。
自研引擎的挑战在于,随着业务复杂度增加,状态机会变得越来越庞大,维护成本会上升。所以如果业务场景不是特别特殊,我还是建议用成熟的开源引擎,把精力放在业务逻辑上。
6.3 选型建议
如果项目刚启动,流程引擎还没定,我的建议是:
- 流程简单、驳回需求不复杂的,用Activiti 6.x或Flowable,开箱即用的功能基本够用。
- 流程复杂、有大量并行网关和驳回场景的,考虑Flowable,它的回退API更灵活。
- 有特殊需求、开源引擎满足不了的,再考虑自研,但要做好长期维护的准备。
不管选哪个,都建议在项目早期就把驳回功能跑通,不要等到上线前才临时加。驳回涉及流程引擎的核心机制,改起来牵一发动全身,早做早踏实。
7. 一些个人体会
驳回这个功能,看起来是流程引擎的一个小特性,但真正做好并不容易。我做过好几个审批流项目,几乎每个项目都在驳回上花了不少时间。最开始觉得不就是个回退吗,后来发现要考虑的东西越来越多:并行网关、流程变量、历史数据、通知机制、版本兼容,每一个点都能挖出坑来。
我的经验是,在需求阶段就把驳回策略定清楚,不要等到开发的时候再讨论。具体来说,要明确三个问题:退到哪、怎么回来、历史数据怎么处理。这三个问题有了答案,技术方案基本就定了。
另外,驳回功能的测试用例要覆盖全面,尤其是并行网关和多次驳回的场景。我见过一个项目,单次驳回测试没问题,连续驳回两次就出bug了,原因是第二次驳回的时候流程状态跟第一次不一样,代码没处理这种情况。所以测试的时候一定要把各种边界情况都考虑到。
最后,驳回数据的记录和分析不要忽视。这些数据不仅能帮助排查问题,还能为流程优化提供依据。一个流程如果某个节点经常被驳回,说明这个节点的审批标准或者上游节点的质量有问题,值得深入分析。
驳回功能做得好不好,直接影响审批流的用户体验。做得好,用户觉得流程顺畅;做得不好,用户觉得系统难用。所以在实现的时候多花点心思,把细节打磨好,是值得的。