基于Flowable开发业务审批,审批意见应该存流程引擎还是业务数据库?
先给结论:企业级业务审批,建议把结构化审批记录存入业务数据库,并把它定义为唯一权威数据;Flowable Comment 可以同步保存一份简化文本,供引擎通用接口和流程轨迹展示。
只存 ACT_HI_COMMENT 适合原型和轻量应用,却很难覆盖会签、退回、转办、委托、电子签名、人员快照、数据权限、合规留存和引擎迁移。只存业务库也能工作,但通用 Flowable 管理工具可能看不到意见。最稳妥的方案不是“两边随便存”,而是主次分明的双层模型。
一、Flowable 本身能不能保存审批意见
可以。TaskService 提供以下能力:
- addComment(taskId, processInstanceId, message):给任务或流程实例增加评论;
- addComment(taskId, processInstanceId, type, message):增加带自定义类型的评论;
- getTaskComments:查询某个任务的评论;
- getProcessInstanceComments:查询整个流程实例的评论;
- getCommentsByType:按评论类型查询;
- saveComment、deleteComment、deleteComments:更新或删除评论。
Comment 对象包含 commentId、userId、time、taskId、processInstanceId、type 和 fullMessage。常见数据库实现会把记录保存到 ACT_HI_COMMENT,包含 TYPE_、TIME_、USER_ID_、TASK_ID_、PROC_INST_ID_、MESSAGE_、FULL_MSG_ 等字段。
因此,下面的轻量需求完全可以使用 Comment:
- 给任务留一句说明;
- 在流程时间线展示办理人和文本;
- 按 approve、reject、return 等 type 做简单区分;
- 通过 REST API 查询任务或历史流程的评论。
但 Flowable 对 Comment 的定位是围绕任务形成讨论,并没有承诺它是一套完整的中国式审批意见模型。
二、为什么很多项目一开始选择 ACT_HI_COMMENT
它有四个明显优点:
| 优点 | 说明 |
|---|---|
| 关联天然完整 | taskId、processInstanceId 已经存在 |
| API 开箱即用 | 无需自己写评论增删查接口 |
| 流程结束后仍可查询 | 评论属于历史数据 |
| 通用工具容易展示 | 引擎管理端可以沿流程轨迹读取 |
对内部小工具来说,调用 addComment 后再 complete task,就能完成基本审批轨迹。没有合规要求、没有复杂操作类型、不会更换引擎时,这种做法成本最低。
真正的问题不是“能不能存”,而是“它是否足以成为业务事实的唯一来源”。
三、ACT_HI_COMMENT 为什么不适合承担全部业务语义
1. 它主要表达文本,不是结构化决定
“同意”“退回修改”“不同意但允许重提”不能只是一段字符串。流程路由需要稳定的 decisionCode,统计需要操作类型,权限需要操作者身份,审计需要操作前后状态。
如果系统靠解析“同意,请尽快办理”中的文字决定流程去向,文案稍有变化就会出错。正确做法是:
- decisionCode 决定流程路由;
- operationType 表达批准、驳回、退回、转办等动作;
- opinionText 只保存人的说明;
- 三者可以相关,但不能混成一个 message。
2. 它缺少企业审批需要的上下文
生产系统通常还要记录:
- 实际操作者、任务 owner、原 assignee、代理人和委托人;
- 操作时的姓名、部门、岗位和组织快照;
- 表单版本、关键数据摘要以及操作前后的业务状态;
- 会签批次、加签来源、退回目标和转办目标;
- 电子签名、证书、附件、设备、IP 和客户端;
- 租户、数据权限、保密级别和可见范围;
- 幂等键、操作流水号和外部系统请求号。
这些字段强行拼成 JSON 放进 FULL_MSG_,查询、索引、升级和权限控制都会变得困难。
3. 它的生命周期跟随引擎历史
Flowable 官方文档说明,评论存放在历史数据库;不创建历史表会影响任务评论能力。Flowable 还支持历史清理,按保留期删除已结束流程及其关联数据。
如果审批意见需要保存十年,而引擎历史只保留一年,就不能让 ACT_HI_COMMENT 成为唯一档案。引擎迁移、实例归档、测试数据清理或租户拆分也可能改变它的可用性。
4. 它允许更新和删除
TaskService 提供 saveComment 和 deleteComment。对普通讨论,这是合理能力;对具有审计和法律效力的审批意见,“原地修改”可能不合适。
企业审批记录更适合追加写入:原记录不变,纠错时增加一条更正记录,并保存 correctedRecordId、原因和审批链。是否允许删除应由档案策略决定,而不是直接开放引擎删除评论接口。
四、审批意见也不应该只存流程变量
另一种常见方案是把 opinion、decision、operator 放入流程变量。它适合给网关传递本次决策,却不适合保存完整意见历史。
原因包括:
- 同名变量会被后续节点覆盖;
- 全局变量和任务局部变量作用域容易混淆;
- 多实例会签需要一人一条记录,单个变量无法自然表达;
- 开启 Full history 才可能保留完整变量修改轨迹,查询成本高;
- 附件、签名和大段文本会放大运行表与历史表;
- 流程迁移和历史清理仍会影响数据。
推荐做法是:只把网关真正需要的轻量 decisionCode、approvedCount 等作为变量;完整审批记录写入业务数据库。临时传递给监听器的信息可以使用 transientVariables,避免长期污染流程变量,但临时变量也不能替代持久化记录。
五、三种存储方案应该怎样选择

图 1:引擎单存最简单,业务单存最独立,企业级审批通常选择业务库主存、Flowable Comment 作为可选投影。
| 方案 | 优点 | 主要风险 | 适用场景 |
|---|---|---|---|
| 只存 Flowable Comment | 开发快、天然关联任务 | 字段少、随历史清理、难迁移 | 原型、轻量内部应用 |
| 只存业务数据库 | 模型自由、独立留存、便于统计 | 通用引擎工具看不到意见 | 引擎只做编排、平台 UI 完全自有 |
| 业务主存 + Comment 投影 | 兼顾业务审计和引擎可见性 | 需要同步与对账机制 | 企业 OA、低代码审批平台 |
混合方案的关键不是重复保存,而是定义权威关系:
- wf_approval_record 是 System of Record;
- ACT_HI_COMMENT 是面向引擎工具的只读投影;
- 同一操作使用 operationId 关联;
- 投影失败可以补写,不反向覆盖业务记录;
- 页面时间线优先读取业务记录。
六、推荐的审批记录表应该保存什么
建议把核心记录拆成四组字段。
1. 流程与任务定位
- id、operationId、idempotencyKey;
- processInstanceId、rootProcessInstanceId、taskId;
- processDefinitionKey、processDefinitionVersion;
- activityId、taskDefinitionKey、businessKey;
- tenantId、businessType。
2. 决策与意见
- operationType:APPROVE、REJECT、RETURN、TRANSFER、DELEGATE、ADD_SIGN 等;
- decisionCode:流程路由使用的稳定编码;
- opinionText:自然语言意见;
- opinionTemplateId:使用了哪个意见模板;
- beforeStatus、afterStatus、targetActivityId;
- validStatus、correctedRecordId、correctionReason。
3. 人员与组织快照
- operatorId、operatorNameSnapshot;
- assigneeId、ownerId、principalId、delegateeId;
- departmentId、departmentNameSnapshot;
- positionId、positionNameSnapshot;
- authenticationType、signatureRef。
4. 证据与技术字段
- formVersion、businessDataDigest;
- attachmentCount、evidencePackageId;
- sourceChannel、deviceId、clientIp;
- engineCommentId、engineType、engineVersion;
- createdAt、committedAt、status、failureReason。

图 2:一条审批记录应同时描述流程定位、结构化决定、人员快照和证据,不应只有一段意见文本。
附件最好使用独立的 wf_approval_attachment 表,正文只保存引用。签名图片、证书和大型文件进入对象存储,记录内容哈希、存储地址和权限,不要把 Base64 塞进 Comment 或流程变量。
七、operationType 和 decisionCode 为什么要分开
两者看起来相似,实际回答不同问题:
| 字段 | 回答的问题 | 示例 |
|---|---|---|
| operationType | 用户执行了什么动作 | APPROVE、RETURN、TRANSFER |
| decisionCode | 网关应该走哪条路径 | PASS、REJECT、REWORK |
| opinionText | 用户为什么这样处理 | “合同金额与预算不一致” |
例如“退回经办人补充附件”的 operationType 是 RETURN,decisionCode 可能是 REWORK;“不同意并结束”的 operationType 是 REJECT,decisionCode 是 REJECT_END。转办通常没有审批决定,因此 decisionCode 可以为空。
把这三项拆开后,流程路由、统计报表和人类阅读各自使用稳定数据。
八、同库部署:用一个本地事务保证一致性
Flowable 嵌入 Spring 应用,并与业务表使用同一个 DataSource 和事务管理器时,可以把业务记录、Comment 和 taskService.complete 放进同一个 Spring 事务。
推荐执行顺序:
- 查询任务并校验 assignee、租户、版本和操作权限;
- 生成 operationId,检查幂等键;
- 插入 wf_approval_record,记录当前任务快照;
- 可选调用 addComment,保存简化文本和类型;
- 调用 taskService.complete,传入路由所需变量;
- 更新流程摘要并写入 Outbox 事件;
- 事务统一提交。

图 3:同库场景在一个事务中完成记录、Comment、任务提交和 Outbox;跨库场景则依靠 operationId、状态机和补偿对账。
为什么先插入记录再完成任务?因为 complete 之后运行时任务会消失,流程还可能同步创建下一节点、触发监听器或结束实例。提前保存快照能让监听器在同一事务中读取本次操作,也便于失败时整体回滚。
接口不能只靠前端防重复。wf_approval_record 应对 tenantId + idempotencyKey 建唯一约束;第二次点击要返回第一次的处理结果,而不是再次完成已经不存在的任务。
九、跨库或远程引擎:不要假装有一个大事务
如果业务数据库与 Flowable 数据库分离,或者 Flowable 通过 REST 服务调用,一个本地事务无法同时覆盖两边。此时不建议使用 XA 强行包住所有系统,而应采用领域命令和最终一致性。
一种可落地的状态机是:
- PREPARED:业务库已记录命令,尚未确认引擎完成;
- EXECUTING:消费者正在调用 Flowable;
- COMMITTED:任务完成已确认;
- RETRYING:网络或临时错误,等待重试;
- FAILED:业务校验失败,需人工处理;
- RECONCILING:响应丢失,正在查询历史任务核对结果。
请求先写审批记录和 Outbox,再由消费者调用引擎。Flowable 成功后回写 COMMITTED,并生成 Comment 投影。如果调用成功但响应丢失,重试前应查询运行任务和历史任务,结合 operationId 日志判断是否已经完成。
taskService.complete 本身不能被当成“随便重放”的幂等接口。任务完成后再次调用通常会找不到运行任务。因此,幂等必须在审批命令服务和引擎适配器中实现。
十、不要把保存意见藏在 TaskListener 里
TaskListener 适合处理任务创建、分配、完成时的通用扩展,但把全部审批记录逻辑放在 complete 监听器中会产生几个问题:
- 监听器未必拿得到完整的页面意见、签名和附件;
- 转办、退回、撤回等操作不一定都经过普通 complete;
- 不同流程重复配置监听器,版本难统一;
- 业务数据库失败会与引擎命令强耦合;
- 测试和幂等处理不直观。
更推荐由 Approval Application Service 统一接收审批命令、保存记录并调用 Flowable。监听器只用于补充通用事件、更新摘要或校验是否存在 operationId。
十一、会签、委托和退回怎样记录意见
1. 会签
每个多实例任务都应有独立 taskId 和审批记录,另存 multiInstanceRootId、roundNo 和汇总批次。节点最终结论由会签规则计算,不能覆盖个人意见。
2. 委托
同时保存 principalId、ownerId、delegateeId 和 actualOperatorId。意见属于实际操作者,责任归属仍可指向原委托人。仅保存 USER_ID_ 无法完整说明代理关系。
3. 转办
转办是一条操作记录,不一定产生 decisionCode。应保存 fromAssignee、toAssignee、原因以及转办前后的权限快照。
4. 退回和驳回
退回保存 targetActivityId 和返工要求;驳回保存否定决定和后续路由。两者不能都只记录 type=reject。
十二、审批时间线应该从哪里读
生产页面推荐以 wf_approval_record 为主,按 operationTime + id 稳定排序;Flowable 历史任务用于补充节点开始、结束和取消原因,Comment 只做兼容展示。
可以把时间线分成三类事件:
- 人工决定:批准、驳回、退回、转办、委托、加签;
- 流程事件:节点到达、会签汇总、超时、终止、撤销;
- 系统事件:通知、外部接口、补偿和归档。
审批意见表不要承担所有系统日志。人工意见、流程运行历史和技术审计各有独立模型,再由 Timeline Query Service 聚合展示。
十三、安全、合规和不可抵赖怎么处理
审批意见可能包含个人信息、商业秘密和法律结论,至少需要:
- 传输加密和数据库敏感字段加密;
- 按流程、表单字段和组织范围控制可见性;
- 附件防盗链、病毒检测和访问审计;
- 对 opinionText、decisionCode、业务摘要和附件清单生成内容摘要;
- 电子签名时保存证书、签名值、可信时间和验签结果;
- 更正采用追加记录,不覆盖原始内容;
- 归档、销毁和脱敏遵循业务保留期,而不是直接跟随引擎默认策略。
只保存 IP 和图片签名并不等于具备法律效力。需要不可抵赖时,应由专业电子签名和可信时间服务形成证据包。
十四、引擎历史清理和迁移前要做什么
如果采用混合方案,历史清理前应确认:
- 所有审批记录已经 COMMITTED;
- Comment 投影是否仍需保留;
- 任务名称、节点名称和办理人快照是否已归档;
- 附件和签名引用是否独立于 Flowable 历史表;
- 新引擎是否能通过 businessKey、operationId 关联旧记录;
- 对账任务不存在未处理差异。
迁移到其他引擎时,业务审批记录无需改写,只需要更新 Engine Adapter 和 engineType。若意见只在 ACT_HI_COMMENT 中,迁移就必须复制内部历史表或重新构造评论,成本更高。

浙公网安备 33010602011771号