在Flowable开源流程引擎里,待办、已办、在办、我发起、抄送我的,数据分别从哪里查询?

先给结论:待办主要查运行时任务,已办主要查历史任务,我发起主要查历史流程实例;在办必须先定义产品口径;抄送我的不是 Flowable OSS 的标准任务类型,最好使用平台扩展表,身份关联只能作为兼容方案。

很多团队一开始会设计五条 SQL,分别查询五个菜单。真正上线后才发现,同一条流程会重复出现、转办后的已办找不到、候选组任务漏掉、流程结束后抄送记录消失。问题不在 SQL 写得不够复杂,而在于没有先区分运行数据、历史数据、身份关系和业务读模型。

一、先用一张表看清五个列表

列表 推荐 Flowable API 核心引擎表或扩展表 关键条件
待办 TaskService.createTaskQuery ACT_RU_TASK、ACT_RU_IDENTITYLINK 当前用户是 assignee 或候选人,任务有效
已办 HistoryService.createHistoricTaskInstanceQuery ACT_HI_TASKINST 实际由当前用户完成,任务已结束
在办 TaskQuery 或 HistoricTaskInstanceQuery ACT_RU_TASK 或 ACT_HI_TASKINST + ACT_HI_PROCINST 取决于“正在处理”还是“参与且未结束”
我发起 HistoryService.createHistoricProcessInstanceQuery ACT_HI_PROCINST START_USER_ID_ 为当前用户
抄送我的 平台 CC Query Service 推荐 wf_cc_record;兼容 ACT_RU/HI_IDENTITYLINK 接收人为当前用户,区分已读、撤销和租户

在这里插入图片描述

图 1:五个菜单并不对应五张引擎表。待办看运行态,已办和我发起主要看历史态,在办取决于产品定义,抄送通常需要平台扩展。

二、先理解 Flowable 的四组数据

Flowable 的表前缀已经提示了数据用途:

  • ACT_RU_*:当前仍在运行的数据,例如任务、执行实例、变量和身份关联;
  • ACT_HI_*:历史与审计数据,既可能包含已经结束的实例,也可能包含仍在运行的历史镜像;
  • ACT_RE_*:流程定义、部署等静态资源;
  • ACT_ID_*:Flowable 自带身份数据;企业项目也可能接入 LDAP、AD 或独立组织中心。

HistoryService 官方说明特别强调:历史信息用于查询持续中和已经结束的流程,运行数据只保存当前执行状态并为执行性能优化。因此,“历史表”不等于“只有结束数据”。ACT_HI_PROCINST 中 END_TIME_ 为空通常表示流程仍未结束,ACT_HI_TASKINST 中 END_TIME_ 为空表示任务仍未结束。

工作台列表还需要标题、单据金额、组织、紧急程度、当前节点、业务状态和数据权限。这些字段不一定都适合塞进流程变量,更不应该每页从几十张业务表临时拼接。成熟平台通常会增加流程摘要表或搜索索引。

三、待办:从运行时任务和候选身份关系查询

待办是用户当前可以办理或签收的任务。推荐查询链为:

TaskService → createTaskQuery → active → taskCandidateOrAssigned(userId) → 排序与 listPage。

taskCandidateOrAssigned 会合并两类任务:

  • 已经签收或直接分配给当前用户的任务,assignee 等于 userId;
  • 尚未签收,但当前用户或其所属组是候选人的任务。

底层主要涉及:

数据 主要位置 作用
当前任务 ACT_RU_TASK 任务名称、办理人、Owner、创建时间、到期时间、流程实例等
候选用户和候选组 ACT_RU_IDENTITYLINK candidate user、candidate group 与任务的关系
当前执行实例 ACT_RU_EXECUTION 流程令牌、父子作用域和运行状态
当前变量 ACT_RU_VARIABLE 流程变量和任务局部变量
流程定义 ACT_RE_PROCDEF 定义名称、Key、版本和部署信息

只使用 taskAssignee(userId) 会漏掉所有尚未签收的候选任务;只使用 taskCandidateUser(userId) 又可能漏掉已经签收给当前用户的任务。OA 工作台通常应使用 candidateOrAssigned 语义。

active() 在 Flowable 中主要排除暂停任务。运行表中的任务本来就尚未完成,因此“active”不能替代业务上的有效性校验。平台还要过滤被业务撤销、逻辑删除、越权或不应在当前渠道展示的任务。

1. 候选组查询为什么经常漏数据

候选组待办依赖用户与组的解析方式。如果使用 Flowable 自带 IDM,taskCandidateOrAssigned 可以结合组成员关系;如果组织架构来自企业统一身份中心,应配置自定义 GroupIdentityManager,或先取得当前用户的岗位、角色和部门组,再使用 taskCandidateGroupIn。

不要把“部门 ID、角色 ID、岗位 ID”全部拼成一个无边界的 IN 条件。应统一组编码,带上租户和组织范围,并限制集合大小。用户调岗后是否立即获得旧任务,也要形成明确策略。

2. 待办列表不要直接加载全部变量

includeProcessVariables 使用方便,但列表一页几十条任务时,可能读取大量变量和二进制内容。更稳妥的方式是:

  • 首屏只取任务主键、流程实例、标题、时间和轻量摘要;
  • 业务标题、金额、申请人快照来自流程摘要表;
  • 打开详情时再按需加载变量和表单数据;
  • 文件、富文本和大 JSON 不参与普通列表查询。

四、已办:从历史任务查询实际完成记录

已办表示“当前用户实际办理并完成过的任务”,推荐查询链为:

HistoryService → createHistoricTaskInstanceQuery → taskCompletedBy(userId) → finished → 按结束时间倒序分页。

当前 Flowable Javadoc 已提供 taskCompletedBy,它比 taskAssignee 更接近“谁实际点击了完成”。两者不能简单互换:

  • 任务可能先分配给 A,后转办给 B,最终由 B 完成;
  • 委托任务同时存在 owner、assignee 和实际完成人;
  • 管理员可能代办、强制完成或通过服务账号完成;
  • 会签节点会产生多个历史任务实例。

如果使用 taskAssignee(userId).finished(),查询到的是历史任务最终记录的受理人,不一定等于实际完成人。旧版本若没有可靠的 completedBy 数据,平台应在任务完成监听器中写入独立的办理记录表,而不是猜测。

1. finished 不一定等于正常办理完成

历史任务结束可能有多种原因:正常完成、流程终止、会签提前结束、边界事件取消、节点跳转或管理员删除。ACT_HI_TASKINST 的 DELETE_REASON_ 可以帮助识别取消原因。

如果“已办”只展示用户真正提交过意见的任务,应同时满足:

  • completedBy 为当前用户;
  • END_TIME_ 不为空;
  • 属于平台认可的正常完成或代办完成类型;
  • 排除仅因流程终止、会签提前结束而被动取消的任务。

Flowable 的 taskWithoutDeleteReason 可筛选没有删除原因的历史任务,但中国式审批平台通常还会保存 operationType、decision、commentId 和 delegationSource,用自己的办理日志形成更稳定的口径。

在这里插入图片描述

图 2:候选关系帮助形成待办;任务签收、办理并完成后,历史任务保留实际完成人和结束结果。运行记录会消失,历史记录继续存在。

五、在办:这个词必须先定义,再决定查哪里

“在办”是五个列表中歧义最大的一个词,常见有两种含义。

1. 含义一:正在我手里处理的任务

它是待办的一个子集,数据仍来自 ACT_RU_TASK。最简单的条件是 assignee=userId,表示任务已经由当前用户签收或分配给当前用户。

新版本 Flowable 的任务对象还提供 IN_PROGRESS 状态、inProgressStartTime 和 inProgressStartedBy 等字段。如果平台明确调用“开始处理”动作,可以查询 taskState(IN_PROGRESS) 或 taskInProgressStartedBy(userId),把“已签收但未开始”和“正在处理”区分开。

但许多 OA 系统只有“打开详情”和“提交”按钮,没有调用任务的开始处理生命周期。此时不能根据用户打开过页面就认定任务进入 IN_PROGRESS,更不能从数据库猜测“在办”。平台应单独记录 VIEWED、OPENED 或 PROCESSING 事件。

2. 含义二:我已经办理过,但整条流程还没结束

这也是国内 OA 常见的“在办”定义。推荐查询链为:

HistoricTaskInstanceQuery → taskCompletedBy(userId) → finished → processUnfinished。

它的核心数据来自 ACT_HI_TASKINST,并通过流程实例关联 ACT_HI_PROCINST,要求流程 END_TIME_ 仍为空。由于一个人可能在同一流程中办理多次,列表通常按 processInstanceId 去重,展示流程实例而不是重复任务。

两种含义最好在产品上直接改名:

建议菜单名 准确定义 查询对象
正在处理 当前任务在我手里,且已进入处理状态 运行时任务
我参与的未结束 我已经办过至少一个任务,流程仍在运行 历史任务 + 未结束流程实例

如果继续使用“在办”两个字,接口文档必须写明口径,否则前端、后端、测试和用户会各自理解成不同列表。

六、我发起:优先从历史流程实例统一查询

“我发起”要覆盖进行中、已完成、已撤回和已终止的流程,因此最方便的查询不是分别查运行表和历史表,而是:

HistoryService → createHistoricProcessInstanceQuery → startedBy(userId) → 按开始时间倒序分页。

HistoricProcessInstanceQuery 可以继续增加:

  • unfinished():只看仍在运行的申请;
  • finished():只看已经结束的申请;
  • deleted() 或 notDeleted():区分被删除、终止的实例;
  • processDefinitionKey、tenantId、startedAfter、startedBefore:进行业务和时间筛选。

底层核心是 ACT_HI_PROCINST 的 START_USER_ID_、START_TIME_、END_TIME_、BUSINESS_KEY_、PROC_DEF_ID_ 和 DELETE_REASON_ 等字段。

1. 为什么 START_USER_ID_ 经常为空

通过 Java API 启动流程前,应正确设置认证用户,例如在调用链中使用 IdentityService.setAuthenticatedUserId,并在 finally 中清理认证上下文。若定时任务、消息消费者或系统账号启动流程,也要显式记录业务发起人。

不要只把 initiator 写成普通流程变量。startedBy 查询依赖流程实例的启动人字段;变量可能被修改、删除或因历史级别不足而不可查询。平台可以同时保存 startUserId 和 starterSnapshot,前者用于身份查询,后者用于展示当时的姓名、部门和岗位。

调用活动产生的子流程是否算“我发起”也要规定。普通工作台通常只展示根流程实例,避免一个申请因为多个 Call Activity 出现多行。

七、抄送我的:Flowable OSS 没有完整的 OA 抄送模型

Flowable OSS 的 IdentityLinkType 预定义了 assignee、candidate、owner、starter、participant 等类型,但没有完整的“抄送、已读、撤回抄送、再次提醒”模型。

有两种实现方案。

1. 方案一:使用流程实例身份关联

RuntimeService.addUserIdentityLink 可以把用户以指定 identityLinkType 关联到流程实例。平台可使用自定义类型 cc,并通过 involvedUser(userId, "cc") 查询相关流程实例。运行期关系主要位于 ACT_RU_IDENTITYLINK,历史关系位于 ACT_HI_IDENTITYLINK。

这种方案适合简单“可见关系”,但仍有局限:

  • 开源预定义常量没有 CC,升级时需验证自定义类型兼容性;
  • IdentityLink 不负责已读时间、通知渠道和提醒次数;
  • participant 会混入审批人、协作人等其他参与关系,不能直接当抄送;
  • 历史身份关系能否完整保留受 history level 影响;
  • 节点级抄送、撤销抄送和字段权限仍需扩展数据。

2. 方案二:建立平台抄送记录表

企业 OA 更推荐独立表 wf_cc_record,至少保存:

字段 作用
process_instance_id、business_key 关联流程实例和业务单据
recipient_user_id 抄送接收人
source_activity_id、source_task_id 由哪个节点或任务触发
cc_time、first_read_time、read_time 抄送与阅读状态
revoked_time、status 撤销和有效状态
permission_snapshot 当时允许查看的表单和附件范围
tenant_id、org_id 多租户和组织隔离
channel、message_id 站内信、短信、邮件等通知追踪

“抄送我的”直接查询这张表,再关联流程摘要和业务标题。必要时同步一条自定义 cc IdentityLink,用于 Flowable 层的 involvedUser 权限判断,但不要把 IdentityLink 当成完整抄送记录。

八、一套推荐的查询口径

列表 展示粒度 排序时间 是否去重 推荐数据源
待办 一条任务一行 CREATE_TIME_ 或到期时间 不按流程去重 TaskQuery + 任务摘要
已办 一次办理一行 END_TIME_ 默认不去重 HistoricTaskQuery + 办理日志
正在处理 一条当前任务一行 inProgressStartTime 不去重 TaskQuery
我参与的未结束 一个流程实例一行 最近办理时间 按 processInstanceId 去重 HistoricTaskQuery + HistoricProcessQuery
我发起 一个流程实例一行 START_TIME_ 根实例去重 HistoricProcessQuery
抄送我的 一次抄送一行或一个实例一行 CC_TIME 按产品策略 wf_cc_record + 流程摘要

列表粒度决定分页和总数。先查历史任务再在 Java 内存中按流程去重,会导致“每页 20 条却只剩 7 条”。如果菜单按流程实例展示,应在数据库读模型或搜索索引中提前形成实例级记录。

九、查询 API 与引擎表应该怎样分工

推荐原则是:引擎行为查询使用 Flowable Query API,工作台复杂检索使用平台读模型,任何情况下都不要直接修改引擎表。

Query API 的优势是:

  • 兼容 Flowable 的版本和数据库差异;
  • 正确处理候选人、候选组、租户和状态语义;
  • 能使用 listPage、排序和变量条件;
  • 避免业务代码依赖内部字段变化。

自定义 SQL 或读模型适合:

  • 跨待办、历史、抄送的统一搜索;
  • 标题、金额、组织、业务状态等多维筛选;
  • 大数据量下的稳定分页和聚合统计;
  • 多引擎、多业务库和搜索引擎接入;
  • 已读、置顶、关注、催办等非 BPMN 数据。

如果必须写 NativeQuery,应封装在 Engine Adapter 中,增加版本回归测试,不要让 Controller 或业务模块直接引用 ACT_* 表。

十、为什么工作台还需要流程摘要表

单靠引擎表可以回答“流程在什么位置”,却不一定能高效回答“这是什么业务、金额多少、属于哪个组织、用户能看哪些字段”。推荐增加 wf_process_summary:

  • processInstanceId、rootProcessInstanceId、businessKey;
  • processDefinitionKey、version、tenantId;
  • title、businessType、businessStatus;
  • starterId、starterNameSnapshot、starterOrgSnapshot;
  • currentActivityNames、currentAssignees;
  • priority、dueTime、lastOperateTime;
  • searchableText 和少量业务摘要字段;
  • dataScopeKey、securityLabel 和逻辑删除状态。

摘要表由流程事件监听器或事务 Outbox 更新。列表先查摘要与用户关系,详情再访问 Flowable 和业务系统。这样既避免 includeProcessVariables 造成的大查询,也能支持流程结束后的统一检索。

在这里插入图片描述

图 3:统一查询服务屏蔽运行表、历史表、抄送表和业务摘要的差异,对门户、PC、uniAPP 输出稳定列表模型。

十一、性能、分页和一致性怎么处理

1. 使用稳定排序

只按 CREATE_TIME_ 或 END_TIME_ 排序,时间相同可能造成翻页重复。建议增加 ID_ 作为第二排序键;大表可采用“时间 + ID”的游标分页。

2. 先过滤租户和时间范围

所有列表都应带 tenantId 和数据权限条件。历史表持续增长,默认查询最好限制时间窗口,并允许用户主动扩大范围。

3. 不要对每行发起二次查询

典型 N+1 问题是:先查 20 条任务,再逐条查流程定义、启动人、变量、业务表和当前节点。应批量查询或使用摘要表一次返回列表所需字段。

4. 接受运行中数据会变化

用户翻页时任务可能被他人签收或完成,count 与 listPage 不一定属于同一时刻。接口应允许轻微变化,完成任务时再次校验任务版本和 assignee,不能因为列表中出现过就默认仍可办理。

5. 历史清理会影响已办

Flowable 支持历史清理。若企业保留期结束后删除 ACT_HI_* 数据,已办、我发起和参与记录也会受影响。合规要求长期查询时,应在清理前归档到审计库或业务读模型。

十二、权限和多租户不能留给前端过滤

查询接口至少要同时校验:

  • 当前登录用户及代理身份;
  • tenantId、组织范围和数据权限;
  • 表单、附件和敏感字段权限;
  • 任务候选或 assignee 关系;
  • 抄送是否仍有效、是否允许下载;
  • 管理员查询是否有审计原因。

“查得到列表”和“能办理任务”是两件事。抄送人可能能查看但不能审批;历史参与人可能只能查看自己办理时可见的字段;管理员能查询实例也不应自动拥有业务数据全量权限。

十三、低代码平台怎样封装统一工作台

云程低代码开发平台可以设置 Worklist Query Service,对外统一返回 WorkItemDTO,同时在内部拆分五类查询器:

  • TodoQuery:封装 TaskQuery、候选组解析和任务权限;
  • DoneQuery:封装 completedBy、操作类型和历史任务;
  • InProgressQuery:根据租户配置选择两种“在办”口径;
  • StartedByMeQuery:封装历史实例、根流程和业务状态;
  • CcQuery:查询抄送记录、已读状态和字段权限;
  • ProcessSummaryRepository:提供标题、组织、业务摘要和全文检索;
  • Engine Adapter:隔离 Flowable 版本以及未来切换其他引擎的差异。

在这里插入图片描述

PC 门户、移动端和 uniAPP 只面对统一接口,不直接理解 ACT_RU_TASK 或 ACT_HI_TASKINST。列表定义、字段、排序和权限可以在低代码设计器中配置,但查询口径必须由平台预置,不能让每个业务应用重新拼 SQL。

十四、上线前检查清单

  1. 待办是否同时包含 assignee、候选用户和候选组;
  2. 已办使用实际完成人还是最终 assignee;
  3. 被终止、跳转和会签提前取消的任务是否排除;
  4. “在办”究竟是当前任务还是参与且流程未结束;
  5. 启动流程时是否可靠写入 startUserId;
  6. 我发起是否只显示根流程实例;
  7. 抄送是否需要已读、撤销、提醒和字段权限;
  8. history level 是否足以支持已办和身份关联;
  9. 列表是否存在逐行查询变量和业务表的 N+1;
  10. 分页是否稳定,是否带租户、组织和时间范围;
  11. 历史清理后已办数据是否需要归档;
  12. 业务代码是否只读引擎表、从不直接更新 ACT_* 表。

十五、如果只记住六句话

  1. 待办查 TaskQuery,核心是 taskCandidateOrAssigned,而不是只查 assignee;
  2. 已办查 HistoricTaskInstanceQuery,优先使用 taskCompletedBy;
  3. 在办有两种含义,必须先定义再查询;
  4. 我发起可以统一查 HistoricProcessInstanceQuery.startedBy;
  5. 抄送不是 OSS 原生工作列表,推荐独立抄送表;
  6. 引擎表负责流程状态,摘要表和查询服务负责企业级工作台体验。
posted @ 2026-08-18 07:46  大龄码农有梦想  阅读(15)  评论(0)    收藏  举报