在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。
十四、上线前检查清单
- 待办是否同时包含 assignee、候选用户和候选组;
- 已办使用实际完成人还是最终 assignee;
- 被终止、跳转和会签提前取消的任务是否排除;
- “在办”究竟是当前任务还是参与且流程未结束;
- 启动流程时是否可靠写入 startUserId;
- 我发起是否只显示根流程实例;
- 抄送是否需要已读、撤销、提醒和字段权限;
- history level 是否足以支持已办和身份关联;
- 列表是否存在逐行查询变量和业务表的 N+1;
- 分页是否稳定,是否带租户、组织和时间范围;
- 历史清理后已办数据是否需要归档;
- 业务代码是否只读引擎表、从不直接更新 ACT_* 表。
十五、如果只记住六句话
- 待办查 TaskQuery,核心是 taskCandidateOrAssigned,而不是只查 assignee;
- 已办查 HistoricTaskInstanceQuery,优先使用 taskCompletedBy;
- 在办有两种含义,必须先定义再查询;
- 我发起可以统一查 HistoricProcessInstanceQuery.startedBy;
- 抄送不是 OSS 原生工作列表,推荐独立抄送表;
- 引擎表负责流程状态,摘要表和查询服务负责企业级工作台体验。

浙公网安备 33010602011771号