基于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 事务。

推荐执行顺序:

  1. 查询任务并校验 assignee、租户、版本和操作权限;
  2. 生成 operationId,检查幂等键;
  3. 插入 wf_approval_record,记录当前任务快照;
  4. 可选调用 addComment,保存简化文本和类型;
  5. 调用 taskService.complete,传入路由所需变量;
  6. 更新流程摘要并写入 Outbox 事件;
  7. 事务统一提交。

在这里插入图片描述

图 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 中,迁移就必须复制内部历史表或重新构造评论,成本更高。

posted @ 2026-08-18 14:51  大龄码农有梦想  阅读(8)  评论(0)    收藏  举报