WorkflowTask 是某个 Execution 上的人工工作项。待办资格可能来自直接 Assignee,也可能来自物化候选表。业务详情权限仍由业务模块决定;看到待办不等于能绕过业务对象授权。
1. 任务生成
默认解析先保留显式 UserIds,再通过 IParticipantProvider 展开 Roles、Positions 和 OrgUnits;自定义 Resolver 命中后仍合并 UserIds。角色型来源会把候选用户物化,供高频待办查询。
2. 待办查询
Platform QueryWorkflowTodo 从可信 CurrentUser 取 UserId/TenantId,把 PageSize 规范到 1..200,并设置 ApplyDataScope=true。
var query = new TodoQuery{ // 用户与租户来自 CurrentUser,不接受前端代填。 UserId = currentUserId, TenantId = currentTenantId, BusinessType = "MatterIntake", OfficeId = officeId, Keyword = keyword, ApplyDataScope = true, Page = page <= 0 ? 1 : page, // 服务端再次约束页大小,防止放大读取。 PageSize = pageSize is <= 0 or > 200 ? 20 : pageSize};
TodoPage result = await taskService.QueryTodoAsync( query, cancellationToken);没有高级筛选时,IWorkflowQueryStore 在数据库完成 Assigned/Candidate UNION、count 与分页。只要 ApplyDataScope、Keyword、OfficeId 或 FlowStates 存在,当前实现就回退 IPersistenceStore,先用 int.MaxValue 拉取全部任务,再逐任务权限检查、内存筛选和分页。
3. DataScope 的真实执行
TaskService 对每个任务调用 IFlowDataPermissionChecker(View)。平台实现把 userId 解析为 long,构造 workflow/task/{taskId} ResourceDescriptor,再请求统一授权;无法解析或非显式 Allow 时拒绝。
节点 FlowDataScopeConfig 当前没有被传给 checker。它是定义模型而不是已执行的规则引擎。真正的业务数据权限仍需业务详情 Endpoint 再校验。
4. 待办数量
GetTodoCount 使用 IWorkflowCache,优先 QueryStore.CountTodoAsync。接口注释明确数量徽标不应用 DataScope,容忍过包含而不容忍遗漏。因此列表为 8 条、角标为 10 条可能是设计结果,不应强求完全一致。
缓存键包含 tenant、user、businessType;任务完成、转办、候选变化时要覆盖所有相关用户失效。Redis/backplane 缺失时跨节点只靠短 TTL。
5. 已办查询
有 QueryStore 时数据库分页;没有时 IPersistenceStore 查询当前页,再额外用 int.MaxValue 查全量计算 TotalCount。大数据量独立宿主必须实现 QueryStore。
已办由任务完成事实构造,不等价于实例已完成。多实例中某用户已办时流程可能仍在等待其他人。
6. 任务详情的当前缺陷
GetTaskAsync 读取 Task 后,调用权限检查时传入空字符串 userId。Platform FlowDataPermissionChecker 无法把空值解析为 long,因此 fail-closed 返回 false;正常组合下详情会得到 null。
var hasPermission = await permissionChecker.HasPermissionAsync( string.Empty, // 当前源码没有从方法参数获得调用者。 taskId, task.TenantId, FlowDataScopeCheck.View, cancellationToken);正确方案是把 trusted userId 作为 ITaskService.GetTaskAsync 参数,或引入可信 CurrentUser 端口,并增加防 IDOR 测试。文档不能把详情描述为已正常交付。
7. 标记已读
MarkRead 校验调用者是 Assignee 或物化候选。重复标记直接成功且不推进 Version;首次更新带 task.Version,DBConcurrencyException 映射为 Task.ConcurrencyConflict。完成后失效 Assignee 与实际候选调用者的待办缓存。
Result marked = await taskService.MarkReadAsync( taskId, currentUserId, cancellationToken);
// 重复调用成功;并发第一次调用可能返回 ConcurrencyConflict。EF Core 之外的并发异常类型也要验证;当前 catch 只显式捕获 DBConcurrencyException。
8. 候选刷新
RefreshCandidates 依赖 IParticipantProvider,读取任务、实例和定义节点,删除旧候选后重新展开。删除与重建没有显式事务;中途失败会留下空候选。ResolveParticipantRuleAsync 通过 ListDefinitionsAsync 后取 versions[0],没有按实例 DefinitionId 读取,可能用错版本规则。
RefreshCandidatesByRole 逐任务调用,忽略单项 Result 并最终返回成功;大批量没有 checkpoint、失败清单或限流。这些都应进入候选刷新 GA 清单。
9. 候选人与任务责任
CandidateBuilder 为角色/岗位/组织规则按每个已解析用户写候选;显式 UserIds 仅在没有这些来源时写 Direct。Assignee 本身仍是解析后的具体用户,不是 role ID。
人员离职需要同时考虑:
- 现有 Task.Assignee;
- WorkflowTaskCandidate;
- 当前申请人;
- 委派/转办关系;
- 待办缓存;
- 通知接收人;
- 在途批量转交报告。
10. 查询返回模型
TodoItem 包含 Task/Instance/BusinessKey/Definition/Node/FlowState/Status/Assignee/Priority/DueTime。当前 DefinitionName 实际回填 instance.DefinitionKey,不读取定义名称;UI 不应依赖它展示友好名称,直到查询模型补齐。
11. 性能改造目标
把 DataScope、Keyword、OfficeId 和 FlowStates 编译进 QueryStore DSL;一次 SQL/Provider query 完成租户过滤、Assigned/Candidate 去重、授权范围、排序、count 和 page。禁止先数据库分页再应用过滤,否则会漏项;也禁止全量拉回。
需要基准覆盖 1 万、10 万、100 万 Pending Task,角色成员变更、热点用户和多租户并发,并记录 P50/P95、数据库行扫描、分配量与缓存命中。
12. 验证命令
# 查询分支、全量回退与详情用户 ID。rg -n "HasAdvancedFilters|int.MaxValue|GetTaskAsync|string.Empty" src/Framework/BitzOrcas.Workflow/BitzOrcas.Workflow.Engine/Services/TaskService.cs
# 候选刷新与版本选择。rg -n "RefreshCandidates|ResolveParticipantRule|versions\[0\]" src/Framework/BitzOrcas.Workflow -g '*.cs'