实例与用户任务
这一页讲流程跑起来之后的事:怎样启动实例(含信号启动与定时启动)、怎样处理待办(认领、释放、退回、催办、会签阈值、截止与提醒)、怎样写自定义指派规则、实例在等什么又怎样被唤醒(书签、信号、外部任务)、怎样查询与控制实例。读完后,我们能给上一页的请假审批流做出发起、待办与管理界面。前置知识是定义与发布。
为什么需要
定义只是图纸,业务真正关心的是一次次执行:谁发起了、停在谁手上、超时没有、能不能撤回。这些操作有大量边界——重复提交、并发审批、会签人数不够、审批人离职要转办、流程改版后旧实例怎么迁。工作流引擎把它们做成带明确错误码的服务:启动有重用策略,待办有认领与会签语义,等待有书签与信号,控制有状态机与批量逐项结果,应用只写业务界面。
概念
| 概念 | 说明 |
|---|---|
实例状态 WorkflowInstanceStatus | Running、Suspended、Completed、Canceled、Faulted、Terminated;故障信息在 Fault、FaultNodeId、FaultNodeInstanceId,End 节点给出的结果在 Outcome |
书签 WorkflowBookmark | 等待中的节点挂起的凭据,种类见下文「等待与恢复」;用户任务的任务 ID 就是书签 ID |
用户任务 UserTask | 待办视图:Id、InstanceId、NodeId、AssigneeId、ClaimedBy、Title、InstanceTitle、FormKey、FormData、Priority、DueAt、CorrelationId、StarterId、CreatedAt |
外部任务 ExternalTask | 交给外部工作者的工作:Id、Topic、Payload、Variables、Attempt、LockedUntil |
| 分区 | 启动、查询、任务、信号都只在调用方的分区(当前租户,或平台)里进行,见分区 |
最小示例
发起一次请假申请,并给审批人一个待办列表与通过按钮:
using System.Text.Json.Nodes;
using XiHan.Framework.Primitives;
using XiHan.Framework.Workflow.Abstractions.Engine;
using XiHan.Framework.Workflow.Abstractions.Instances;
public sealed class LeaveApplications(IWorkflowEngine engine)
{
public ValueTask<Result<WorkflowInstance>> ApplyAsync(string applicant, int days, IReadOnlyList<string> approvers, string requestNo, CancellationToken cancellationToken)
=> engine.StartAsync(new WorkflowStartRequest
{
Name = "hr.leave",
Variables = new JsonObject
{
["applicant"] = applicant,
["days"] = days,
["approvers"] = string.Join(",", approvers),
},
StarterId = applicant,
CorrelationId = requestNo,
// 同一申请单号已有未结束的实例时拒绝,防止重复提交
ReusePolicy = WorkflowReusePolicy.RejectActive,
}, cancellationToken);
}using XiHan.Framework.Primitives;
using XiHan.Framework.Workflow.Abstractions.Instances;
using XiHan.Framework.Workflow.Abstractions.Tasks;
public sealed class Inbox(IWorkflowTaskService tasks)
{
// 第 page 页(从 0 起),最新的在前;Total 用来算页数
public ValueTask<Result<UserTaskPage>> ListAsync(string userId, int page, CancellationToken cancellationToken)
=> tasks.QueryAsync(new UserTaskQuery { AssigneeId = userId, Descending = true, Skip = page * 20, Limit = 20 }, cancellationToken);
public ValueTask<Result<WorkflowInstance>> ApproveAsync(string taskId, string userId, string? comment, CancellationToken cancellationToken)
=> tasks.CompleteAsync(new UserTaskCompletion(taskId, userId, UserTaskOutcomes.Approved, comment), cancellationToken);
}两个类都登记为作用域服务。运行后:ApplyAsync 返回的实例状态是 Running,停在 manager 节点;approvers 里的每个人在 ListAsync 里看到一条标题为「某人 请假 N 天」的任务。任一主管调用 ApproveAsync,days > 3 的申请流到人事会签,否则实例直接 Completed、Outcome 为 approved;同一单号再调一次 ApplyAsync 得到 WORKFLOW_CONFLICT_005。
启动实例
WorkflowStartRequest 的字段:
| 字段 | 说明 |
|---|---|
Name + Version / DefinitionId | 找定义;Version 为空取最新已发布版本,只在调用方的分区里找。定义不存在、不是 Published 或在别的分区 → WORKFLOW_NOTFOUND_001 |
Variables | 初始变量;声明了的变量缺省时补默认值,缺必填 → WORKFLOW_VALIDATION_002 |
StarterId | 发起人,Starter 指派规则与 ExcludeStarter 用它 |
CorrelationId | 业务关联 ID,信号按它匹配,重用策略按它查重 |
ReusePolicy | AllowDuplicate(缺省)、RejectActive(已有未结束实例 → WORKFLOW_CONFLICT_005)、ReturnActive(返回已有实例);没有 CorrelationId 时忽略。查重只看同一分区的实例;查重与写入在同一把实例锁里,并发的同键启动只建一个实例;等锁超时是 WORKFLOW_TIMEOUT_001 |
StartAt | 延迟启动:开始节点以定时书签挂起到该时刻 |
DisplayName、InstanceId | 实例显示名与指定的实例 ID |
实例属于调用方的分区。返回的是第一个波次结束后的实例:流程一路走到 End 时状态已是 Completed,停在用户任务上时是 Running。定义有实例级超时时,启动时就排好 InstanceTimeout 书签,到期按 TimeoutAction 让实例故障或终止。
信号启动与定时启动
Start 节点的两个属性让定义自己启动:
SignalName:SignalAsync(name, payload, correlationId)在恢复匹配的信号书签之后,为每个Start.SignalName等于该名字的已发布定义(调用方分区的)启动一个实例,载荷写进变量signal,关联 ID 是信号的关联 ID。Cron(可带TimeZone):发布时建一个定义级DefinitionTimer书签,到期由轮询启动一个实例并推算下一次;停用、归档、删除定义时删除书签。文法与调度相同,见作业与计划。
using XiHan.Framework.Workflow.Abstractions.Activities;
using XiHan.Framework.Workflow.Abstractions.Definitions;
var onOrder = WorkflowDefinitionBuilder.Create("orders.fulfil")
.AddNode("start", ActivityTypes.Start, configure: node => node.WithProperty(StartActivity.SignalNameProperty, "order.created"))
.AddExternalTask("ship", "warehouse.ship")
.AddEnd()
.AddTransition("start", "ship").AddTransition("ship", "end")
.Build();
var nightly = WorkflowDefinitionBuilder.Create("finance.reconcile")
.AddNode("start", ActivityTypes.Start, configure: node => node
.WithProperty(StartActivity.CronProperty, "0 0 2 * * *")
.WithProperty(StartActivity.TimeZoneProperty, "Asia/Shanghai"))
.AddNode("log", ActivityTypes.Log, configure: node => node.WithProperty(LogActivity.MessageProperty, "reconcile started"))
.AddEnd()
.AddTransition("start", "log").AddTransition("log", "end")
.Build();处理用户任务
UserTask 节点挂起时为每个指派人建一个用户任务书签,任务 ID 就是书签 ID。指派来源有三个,合并后去重保序:
| 属性 | 说明 |
|---|---|
Assignees | 指派人列表 |
AssigneesExpression | 求值为列表或逗号分隔字符串的表达式 |
AssigneeRules | 规则列表 [{ "type": ..., "values": [...] }];内置类型 User(原样)、Starter(发起人)、Expression(表达式求值) |
其余属性:
| 属性 | 缺省 | 说明 |
|---|---|---|
CompletionPolicy | Any | Any 任一人通过即通过;All 会签;Sequential 依次逐个审批 |
ApprovalThreshold | 全部 | All 下的通过阈值:人数(如 2)或百分比(如 "60%",按指派人数向上取整);剩余人数不可能达到时以 rejected 完成 |
ExcludeStarter | false | 从指派人里去掉发起人 |
EmptyAssigneeStrategy | Fault | 没有指派人时:Fault 节点故障,AutoApprove、AutoReject,Fallback 改用 FallbackAssigneeRules |
Title、FormKey | 节点显示名、空 | 模板,写进任务视图 |
FormData、Priority | 空 | 表单数据、优先级 |
DueAfterSeconds / DueAt | 空 | 截止时长或截止时刻,DueAt 优先;只写进任务视图,不自动完成任务 |
ReminderAfterSeconds | 空 | 每隔该时长发布一次 workflow.task.reminder,直到任务结束 |
CcUserIds / CcRules | 空 | 抄送人,写进任务事件 |
AllowedOutcomes | approved、rejected | 允许的完成结果 |
节点的 Timeout 另插一个超时书签,到期以 outcome = "timeout" 完成节点。Sequential 一次只有当前指派人的一个任务。All 下每个人完成时合并其变量并保留其余人的任务,达到阈值才完成节点;任一人给出非 approved 的结果立即以该结果完成节点。
IWorkflowTaskService 的操作,入参都是记录(UserTaskCompletion、UserTaskTransfer、UserTaskAddition、UserTaskClaim、UserTaskRelease、UserTaskReturn、UserTaskUrge),批量方法接集合、单条写法是重载:
| 方法 | 作用 | 主要错误 |
|---|---|---|
PendingAsync(assigneeId) / PendingByInstanceAsync(instanceId) | 某人或某实例的待办 | — |
QueryAsync(UserTaskQuery) | 指派人或实例必填其一,可按定义名、节点、创建时间与关键字 Keyword 过滤,按创建时间升序或降序(Descending)分页;返回 UserTaskPage(Items 这一页、Total 总数) | WORKFLOW_VALIDATION_002(缺条件;Skip 为负或 Limit 不在 1 到 1000) |
CompleteAsync(completions) | 完成任务,可带结果、意见与变量 | WORKFLOW_DENIED_001(不是指派人)、WORKFLOW_VALIDATION_002(结果不允许)、WORKFLOW_NOTFOUND_003 |
TransferAsync(transfers) | 转办给另一个人 | WORKFLOW_CONFLICT_004(目标已是指派人) |
AddAssigneesAsync(additions) | 加签:Any / All 立即加任务,Sequential 排到队尾 | WORKFLOW_CONFLICT_004 |
ClaimAsync(claims) / ReleaseAsync(releases) | Any 策略下认领(撤下其他人的任务)与释放 | WORKFLOW_CONFLICT_004(已被认领)、WORKFLOW_VALIDATION_002(不是 Any) |
ReturnAsync(returns) | 退回:目标缺省为本节点之前最近完成的用户任务节点,没有则退回 Start 之后的首个节点;以跳转实现 | WORKFLOW_VALIDATION_002 |
UrgeAsync(urges) | 催办:发布 workflow.task.urged,不改状态 | — |
任务标题是节点 Title 模板渲染的结果(缺省节点名),实例标题是启动时给的显示名、没给取定义的显示名、再没有取定义名。查询只返回当前分区、实例处于运行中的任务:实例挂起或故障时它的任务退出结果,恢复后回来。过滤、排序与分页都由存储执行,Total 是同条件的总数;Limit 缺省 100、最大 1000(UserTaskQuery.MaxLimit),越界在查询时返回校验错误,HTTP 是 400。关键字规则见关键字。节点实例的 State 记录 assignees、approved、rejected、claimedBy 与 history(每条含 actorId、outcome、comment、at),审批记录从这里读。
在最小示例的 Inbox 上补搜索、转办与退回:
// 待办搜索框:任务标题、流程标题、定义名或单据号(关联 ID)包含关键字即命中
public ValueTask<Result<UserTaskPage>> SearchAsync(string userId, string keyword, int page, CancellationToken cancellationToken)
=> tasks.QueryAsync(new UserTaskQuery { AssigneeId = userId, Keyword = keyword, Descending = true, Skip = page * 20, Limit = 20 }, cancellationToken);
public ValueTask<Result> HandOverAsync(string taskId, string userId, string colleague, CancellationToken cancellationToken)
=> tasks.TransferAsync(new UserTaskTransfer(taskId, userId, colleague, "出差,转同事处理"), cancellationToken);
public ValueTask<Result<WorkflowInstance>> SendBackAsync(string taskId, string userId, CancellationToken cancellationToken)
=> tasks.ReturnAsync(new UserTaskReturn(taskId, userId, Comment: "材料不全"), cancellationToken);自定义指派规则
组织架构、角色这类规则由应用实现 IAssigneeProvider 并按类型登记,同类型重复登记是编程错误:
using XiHan.Framework.Primitives;
using XiHan.Framework.Workflow.Abstractions;
using XiHan.Framework.Workflow.Abstractions.Assignees;
public interface IOrgChart
{
ValueTask<Result<IReadOnlyList<string>>> ManagersOfAsync(IReadOnlyList<string> departments, CancellationToken cancellationToken);
}
public sealed class DepartmentManagerProvider(IOrgChart org) : IAssigneeProvider
{
public string RuleType => "DepartmentManager";
public Result Validate(AssigneeRule rule) => Result.Success();
// 失败原样返回:不能用空列表掩盖组织数据不可用
public ValueTask<Result<IReadOnlyList<string>>> ResolveAsync(AssigneeResolution resolution, CancellationToken cancellationToken)
=> org.ManagersOfAsync(resolution.Rule.Values, cancellationToken);
}builder.Services.AddAssigneeProvider<DepartmentManagerProvider>() 登记后,节点里写 WithAssigneeRule("DepartmentManager", "finance")。规则类型未登记时校验与执行都返回 WORKFLOW_NOTFOUND_004;Provider 返回失败时节点故障。
等待与恢复
书签的种类(BookmarkKinds):
| 种类 | 由谁建 | 怎样恢复 |
|---|---|---|
UserTask | 用户任务 | IWorkflowTaskService.CompleteAsync |
Timer | Delay、延迟启动 | 到期由轮询恢复 |
Signal | WaitSignal、EventGateway | SignalAsync |
SubWorkflow | SubWorkflow、ForEach | 子实例结束时回调父实例 |
Retry | 节点重试 | 到期由轮询恢复 |
NodeTimeout | 节点 Timeout | 到期由轮询恢复 |
External | ExternalTask | 外部工作者完成;锁到期由轮询解锁 |
Reminder | 用户任务提醒 | 引擎直接处理 |
InstanceTimeout | 定义 Timeout | 引擎直接处理 |
DefinitionTimer | Start.Cron | 引擎直接处理,InstanceId 是定义 ID |
ResumeAsync(bookmarkId, inputs, expectedKey) 消费一个书签并恢复节点;实例不在运行中,或 expectedKey 与书签的 Key 不同时返回 WORKFLOW_CONFLICT_003,轮询器遇到这类书签把到期时间后推 NotResumableBackoff(缺省 5 分钟)。SignalAsync(name, payload, correlationId) 消费当前分区里匹配的全部信号书签(书签的 CorrelationId 为空时接受任意关联),再按信号启动规则启动实例,返回消费数加启动数。
using System.Text.Json.Nodes;
using XiHan.Framework.Workflow.Abstractions.Engine;
public sealed class PaymentCallbacks(IWorkflowEngine engine)
{
// 支付网关回调:唤醒在等 payment.received 信号的、关联 ID 为该订单号的实例
public async ValueTask<int> PaidAsync(string orderNo, decimal amount, CancellationToken cancellationToken)
=> (await engine.SignalAsync("payment.received", new JsonObject { ["amount"] = amount }, orderNo, cancellationToken)).ValueOrThrow();
}外部任务
ExternalTask 活动把一段工作交给外部工作者(另一个进程、另一种语言写的服务),按 Camunda 外部任务的方式工作:
using System.Text.Json.Nodes;
using XiHan.Framework.Workflow.Abstractions.ExternalTasks;
public sealed class ShippingWorker(IWorkflowExternalTaskService external)
{
public async ValueTask RunOnceAsync(CancellationToken cancellationToken)
{
// 领取并加锁 5 分钟:锁内只有本工作者能完成或失败它
var fetched = (await external.FetchAsync("warehouse.ship", "worker-1", TimeSpan.FromMinutes(5), 10, cancellationToken)).ValueOrThrow();
foreach (var task in fetched)
{
var order = task.Payload?["order"]?.GetValue<string>();
if (order is { Length: > 0 })
{
await external.CompleteAsync(new ExternalTaskCompletion(task.Id, "worker-1", new JsonObject { ["shipped"] = true }), cancellationToken);
}
else
{
// 带 RetryAfter:解锁并推迟,任务的 Attempt 加一;不带则节点以该错误故障,按节点重试策略处理
await external.FailAsync(new ExternalTaskFailure(task.Id, "worker-1", "order missing", TimeSpan.FromMinutes(1)), cancellationToken);
}
}
}
}领取只返回当前分区里该主题未锁定(或锁已过期)且已到可领取时刻的任务,按创建时间最多 maxTasks 条。maxTasks 取 1 到 IWorkflowExternalTaskService.MaxTasksPerFetch(100),越界返回 WORKFLOW_VALIDATION_002;条数与分区、可领取时刻一起下推到存储查询。候选在实例锁内重读确认,被别的工作者抢先或实例已不在运行中的跳过,所以一次领到的可能少于 maxTasks,空手而归不代表主题下没有任务。ExtendLockAsync(ExternalTaskLockExtension) 延长锁。没有持有未过期的锁时完成、失败、延长都返回 WORKFLOW_DENIED_002。锁到期的任务由轮询自动解锁,别的工作者可以重新领取。
查询实例
实例与节点实例的查询用引擎的 FindInstanceAsync、ListInstancesAsync、CountInstancesAsync、NodeInstancesAsync、BookmarksAsync。它们只查调用方的分区;按 ID 找别的分区的实例为无值或 WORKFLOW_NOTFOUND_002。
using XiHan.Framework.Workflow.Abstractions.Engine;
using XiHan.Framework.Workflow.Abstractions.Instances;
public sealed class LeaveReports(IWorkflowEngine engine)
{
public async ValueTask<(IReadOnlyList<WorkflowInstance> Items, long Total)> PendingAsync(int page, CancellationToken cancellationToken)
{
// 分区取自当前调用,查询里没有租户条件
var query = new WorkflowInstanceQuery
{
DefinitionName = "hr.leave",
Statuses = [WorkflowInstanceStatus.Running, WorkflowInstanceStatus.Suspended],
Skip = page * 20,
Limit = 20,
};
var items = (await engine.ListInstancesAsync(query, cancellationToken)).ValueOrThrow();
var total = (await engine.CountInstancesAsync(query, cancellationToken)).ValueOrThrow();
return (items, total);
}
// 节点实例按开始时间升序,就是流程走过的轨迹
public async ValueTask<IReadOnlyList<WorkflowNodeInstance>> TrailAsync(string instanceId, CancellationToken cancellationToken)
=> (await engine.NodeInstancesAsync(instanceId, cancellationToken)).ValueOrThrow();
// 实例在等什么:用户任务、定时、信号、子流程、超时……(实例详情的「待恢复」)
public async ValueTask<IReadOnlyList<WorkflowBookmarkSummary>> WaitingAsync(string instanceId, CancellationToken cancellationToken)
=> (await engine.BookmarksAsync(instanceId, cancellationToken)).ValueOrThrow();
}WorkflowInstanceQuery 按状态(Status 或 Statuses)、定义名、关联 ID 集合、父实例、只取顶层、发起人、关键字、创建时间范围(CreatedFrom / CreatedTo)、结束时间上界(EndedBefore)过滤,同时给出的取交集,按排序项 Sorts 排序(可多个,缺省创建时间降序,字段与规则见列表排序);过滤、排序、分页与计数都由存储执行。Skip 缺省 0,Limit 缺省 100、最大 1000(WorkflowInstanceQuery.MaxLimit)。分页越界不在构造时抛出,ListInstancesAsync 返回 WORKFLOW_VALIDATION_002(HTTP 400),不进存储;CountInstancesAsync 不看分页。
| 条件 | 说明 |
|---|---|
CorrelationIds | 一组关联 ID,实例的关联 ID 是其中之一即匹配;重复的只算一个。null 不过滤,空集合不匹配任何实例(列表为空、计数为 0,不进存储)。最多 1000 个(WorkflowInstanceQuery.MaxCorrelationIds),不能有空白的 ID,否则列表与计数都返回 WORKFLOW_VALIDATION_002(消息键 workflow.instance.query_correlations_invalid) |
TopLevelOnly | 只取顶层实例(ParentInstanceId 为空)。子流程继承父实例的发起人与关联 ID,「我发起的」与按单据查进度都要加它 |
Keyword | 不区分大小写地包含在实例标题或关联 ID 里即匹配 |
Sorts | 字段 CreatedAt、EndedAt、Status、DefinitionName、Title 加方向,有序、最多 5 项、每个字段一次;空或没给时创建时间降序 |
// 审批中心「我发起的」:只取顶层,带标题搜索
var mine = new WorkflowInstanceQuery { StarterId = userId, TopLevelOnly = true, Keyword = keyword, Skip = page * 20, Limit = 20 };
// 实例列表点「结束时间」列升序
var finished = new WorkflowInstanceQuery { Sorts = [new(WorkflowInstanceSortField.EndedAt)], Skip = page * 20, Limit = 20 };
// 状态升序(生命周期),同状态的定义名升序,再同的创建时间降序
var grouped = new WorkflowInstanceQuery
{
Sorts =
[
new(WorkflowInstanceSortField.Status),
new(WorkflowInstanceSortField.DefinitionName),
new(WorkflowInstanceSortField.CreatedAt, Descending: true),
],
Skip = page * 20,
Limit = 20,
};
// 一页单据的审批进度:一次查询,按关联 ID 分组
var progress = (await engine.ListInstancesAsync(new WorkflowInstanceQuery
{
DefinitionName = "permission-request",
CorrelationIds = requests.Select(static r => r.Id.ToString()).ToList(),
TopLevelOnly = true,
Limit = WorkflowInstanceQuery.MaxLimit,
}, cancellationToken)).ValueOrThrow().ToLookup(static instance => instance.CorrelationId);控制实例
| 方法 | 允许的状态 | 作用 |
|---|---|---|
SuspendAsync(ids, reason) | Running | 挂起,到期书签暂不推进 |
ResumeInstancesAsync(ids) | Suspended | 恢复;不重新驱动书签,到期书签由轮询接续 |
CancelAsync(ids, reason) | Running、Suspended、Faulted | 取消:清掉未结束节点与全部书签,EnableCompensation 时补偿,级联到未结束的子流程 |
TerminateAsync(ids, reason) | Running、Suspended、Faulted | 终止:同取消,但不补偿 |
RetryAsync(ids) | Faulted 且有故障节点 | 清故障,从故障节点再执行一次(Attempt + 1) |
SetVariablesAsync(updates) | Running、Suspended | WorkflowVariableUpdate(instanceId, variables, remove):合并变量、删除指定键 |
JumpAsync(jumps) | Running、Suspended、Faulted | WorkflowJump(instanceId, targetNodeId, reason, variables, actorId):取消全部未结束节点与书签,从目标节点重新执行 |
MigrateAsync(migrations) | Running、Suspended | WorkflowMigration(instanceId, targetDefinitionId):迁到同名的另一个已发布版本;未结束节点在目标版本里必须存在且活动类型相同 |
PurgeAsync(request) | 已结束 | WorkflowPurgeRequest:删除早于 EndedBefore 结束的实例及其节点实例与书签,可按 Statuses 与 DefinitionName 过滤,一次最多 Limit 条(1~1000,缺省 1000;越界返回 WORKFLOW_VALIDATION_002,不开事务) |
using System.Text.Json.Nodes;
using XiHan.Framework.Workflow.Abstractions.Engine;
// 改一个变量并删掉一个过期的键
await engine.SetVariablesAsync(new WorkflowVariableUpdate(instanceId, new JsonObject { ["days"] = 2 }, Remove: ["draft"]), cancellationToken);
// 管理员把卡住的实例跳回主管审批
await engine.JumpAsync(new WorkflowJump(instanceId, "manager", Reason: "重新审批", ActorId: adminId), cancellationToken);
// 迁到新发布的版本
await engine.MigrateAsync(new WorkflowMigration(instanceId, newVersion.Id), cancellationToken);调用只操作得了自己分区的实例:别的分区的实例按不存在处理,返回 WORKFLOW_NOTFOUND_002,PurgeAsync 只清自己的分区;按书签 ID 的 ResumeAsync 遇到别的分区的书签返回 WORKFLOW_NOTFOUND_003。状态不允许时返回 WORKFLOW_CONFLICT_001;已经结束的实例上取消与终止幂等返回。跳转目标不存在、迁移目标不兼容返回 WORKFLOW_VALIDATION_002;死汇合、波次上限这类引擎级故障没有故障节点,不能重试。所有控制操作都是批量、逐项结果,单条写法在 WorkflowEngineExtensions 里。XiHan:Workflow:RetentionDays 大于 0 时,轮询器每 HousekeepingIntervalSeconds 逐个分区自动清理一次。
常见问题
用户点了两次提交,怎样只建一个实例
给启动请求带业务单号 CorrelationId 并设 ReusePolicy = RejectActive(拒绝)或 ReturnActive(返回已有实例)。查重与写入在同一把实例锁里,并发的同键启动只建一个。
待办里看不到任务了,任务丢了吗
查询只返回实例处于 Running 的任务。实例被挂起或故障时任务暂时退出结果,恢复或重试后回来;用 BookmarksAsync(instanceId) 能看到书签仍在。实例被取消、终止或跳转时任务才真正消失。
会签 60% 怎么算,剩下的人还要审吗
ApprovalThreshold 为百分比时按指派人数向上取整:3 人 60% 是 2 人。达到阈值节点立即完成,其余人的任务撤下;任一人给出非 approved 的结果立即以该结果完成;剩余人数已不可能达到阈值时以 rejected 完成。
审批人离职了,流程卡住怎么办
管理员用 TransferAsync 转办给别人,或 AddAssigneesAsync 加签;没人能操作时用 JumpAsync 跳到别的节点。这些操作都记在节点实例的 history 里。要从源头避免,指派规则写成按部门或角色解析的 IAssigneeProvider,不写死人。
外部工作者领了任务却崩了
锁到期后轮询自动解锁,别的工作者可以重新领取;活着但处理得慢就用 ExtendLockAsync 续锁。工作者报 FailAsync 带 RetryAfter 时任务推迟再领、Attempt 加一,不带时节点故障、按节点的重试策略处理。
怎样把实例的故障原因展示给用户
instance.Fault 存的是错误码、消息键与参数,不是文案:在调用里用 instance.Fault?.Localize() 按调用方区域渲染,要作为错误返回用 ToError(),详见故障原因的展示。
相关包与下一步
- Workflow.Abstractions:引擎、任务服务、外部任务与查询类型
- Workflow.InMemory、Workflow.Database:两种实现的轮询、锁与容量
- 相关章节:发布与订阅(订阅
workflow.task.*做待办通知)、租户解析与隔离 - 下一步:活动与扩展——内置活动、表达式、故障与补偿、自定义活动、事件、存储与配置
