XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

定义与发布 ​

这一页讲工作流子系统的前半段:流程定义是什么模型、怎样启用引擎、怎样用构造器或 JSON 写出一份定义、怎样校验与发布、怎样管理版本与列出定义。读完后,我们能把一个请假审批流定义出来并发布到目录,供下一页启动实例、处理待办。前置知识是特性与清单、操作契约与结果与错误处理;换用数据库实现时还要先读持久化与 Provider。

为什么需要 ​

审批、订单履约、对账这类业务要跨越几小时到几天:中间要等人审批、等外部系统回调、等定时到点。把「走到哪一步、在等什么」写成业务表里的几个状态字段,分支一多就失控,服务重启后还要自己想办法接着跑。工作流子系统把这件事抽出来:定义是一张有向图,实例是定义的一次执行,所有状态落在存储里;需要等待的节点以书签挂起,到点或被唤醒时从存储恢复。引擎在抽象包里,开发与测试用内存实现,换成数据库实现就能跨进程重启长时间等待,业务代码不变。

模型 ​

元素类型说明
定义WorkflowDefinition一张有向图,按名字加版本管理,状态是 Draft、Published、Disabled 或 Archived
节点WorkflowNode执行某种活动(ActivityType)
转移WorkflowTransition连接节点,可带条件表达式
变量WorkflowVariableDefinition声明实例的输入
实例WorkflowInstance定义的一次执行,状态全部落在存储里
节点实例WorkflowNodeInstance每执行一个节点留一条
书签WorkflowBookmark等待中的节点(人工审批、定时、信号、子流程、外部任务、重试、超时)挂起的凭据,被消费时恢复执行

值模型是 JSON(规范 RFC-0021《工作流:持久化执行》):变量、节点属性、输入、输出、节点状态与书签载荷都是 JsonObject,存储以 JSON 文本持久化,序列化走源生成,Native AOT 下可用。时间一律是 UTC 的 DateTimeOffset。

引擎在实例锁内按「波次」推进:从队列逐个执行节点,直到队列空或实例不再运行,然后一次写回实例、节点实例与书签。活动在引擎这次操作所在的调用里执行(同一作用域、同一事务),看到的主体、租户与追踪都是这次调用的。Provider 实现三个存储、事务、实例锁和到期书签的轮询驱动。

应用直接使用的四个服务都是作用域服务:

服务用途
IWorkflowDefinitionCatalog定义的生命周期:草稿、发布、新版本、停用、归档、删除、列表
IWorkflowEngine启动、恢复书签、信号,以及挂起、恢复、取消、终止、重试、改变量、跳转、迁移、清理
IWorkflowTaskService用户任务:待办查询、完成、转办、加签、认领与释放、退回、催办
IWorkflowExternalTaskService外部任务:外部工作者按主题领取、完成、失败、延长锁

事务 ​

在一次调用里使用这四个服务时,它们加入调用方的事务:存储写入、活动与结果处理器的写入、事件都随业务一起提交或回滚。「写申请并发起流程」「授予并完成实例」这样的组合是原子的:业务失败、调用回滚,实例、节点实例、书签都不留。

  • 事件在事务里经作用域的 IMessagePublisher 发出。装配了发件箱时随事务提交、回滚即丢弃;没有发件箱时直接发布,可能早于提交、回滚不撤回。要求一致的应用要装配发件箱,见领域事件与发件箱。
  • 引擎取得的实例锁持有到调用的服务作用域结束(事务提交或回滚之后)才释放,别的节点拿到锁时读到的一定是提交后的状态。同一调用里再操作同一个实例直接复用这把锁。
  • 只读调用(规范 RFC-0005「只读工作单元」,如 get / list 操作)里只能查询;写入在取锁、执行活动之前返回 PERSISTENCE_UNEXPECTED_001(操作实现的缺陷,HTTP 500)。内存实现与数据库实现相同:数据库实现看工作单元,内存实现看调用的操作契约,没装持久化也一样拒绝。
  • 在调用之外使用(后台任务、托管服务、轮询器、定时器)时,引擎自己开一次经完整管道的调用(来源协议 workflow,目标是操作名,如 workflow.start),在那次调用的事务里执行;租户取所在作用域的调用,没有即平台。应用不在服务状态时返回 WORKFLOW_UNAVAILABLE_001。
  • 活动失败按重试 / 继续 / 实例故障处理,它在失败之前做的写入留在同一事务里、不单独回滚:会写入的活动与结果处理器先校验、后写入。
  • Workflow.InMemory 没有事务:写入立即生效,调用回滚不撤销。

分区 ​

工作流的数据分成平台分区与每个租户一个分区。一次调用只读写自己的分区:分区是当前调用的租户,没有租户的调用(平台)是平台分区。平台与租户互不可见:租户看不到平台和别的租户的定义、实例与任务,平台也看不到任何租户的。看不到的一律按不存在处理(定义 WORKFLOW_NOTFOUND_001、实例 _002、书签 _003)。平台要管理某个租户的流程,以那个租户的身份发起调用。用 Workflow.Database 时租户的记录随租户分库,见租户分片与分库。

启用 ​

实现存储实例锁适用
Workflow.InMemory(默认实现,Profiles.Full 已包含)有界内存表进程内信号量单实例、开发、测试;重启后实例丢失
Workflow.Database应用的关系库(persistence.relational@1)数据库租约行多实例、需要持久化
bash
dotnet add package XiHan.Framework.Workflow.InMemory --prerelease
csharp
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Workflow.InMemory;

var builder = WebApplication.CreateBuilder(args);
builder.AddXiHan(xihan => xihan.UseFeature<InMemoryWorkflowFeature>());

var app = builder.Build();
app.UseXiHan();
app.Run();

换成数据库实现时装配一个持久化 Provider 和 DatabaseWorkflowFeature,连接串来自绑定 db(见资源绑定);装配后顶替内存实现,业务代码注入的四个服务不变:

csharp
using SqlSugar;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Persistence.SqlSugar;
using XiHan.Framework.Workflow.Database;

builder.AddXiHan(xihan => xihan
    .UseFeature(new SqlSugarPersistenceFeature(static (binding, config) =>
    {
        config.DbType = DbType.PostgreSQL;
        config.ConnectionString = binding.ValueOrThrow().Value;
    }))
    .UseFeature<DatabaseWorkflowFeature>());

特性启动时起到期书签的轮询(缺省每秒一次),定时、超时、重试、提醒、定时启动都靠它推进。每个周期先看应用是否在服务状态:启动中与排空中管道不接受调用,这时的周期什么也不做、不记告警,进入服务状态后照常轮询与清理。数据库实现的轮询领导权也只在服务状态拿与续:排空中的节点在下一个周期就释放,另一个在服务的实例随即接手。

最小示例 ​

下面定义一个请假审批:主管审批,超过 3 天的再由人事三人会签、60% 通过即可,主管 3 天不处理视为超时。先用构造器写出定义:

csharp
using XiHan.Framework.Workflow.Abstractions.Activities;
using XiHan.Framework.Workflow.Abstractions.Assignees;
using XiHan.Framework.Workflow.Abstractions.Definitions;
using XiHan.Framework.Workflow.Abstractions.Tasks;

public static class LeaveWorkflow
{
    public static WorkflowDefinition Build() => WorkflowDefinitionBuilder.Create("hr.leave", "请假审批")
        .WithCategory("hr")
        .WithTimeout(TimeSpan.FromDays(7), WorkflowTimeoutAction.Terminate)
        .AddVariable("applicant", required: true)
        .AddVariable("days", required: true)
        .AddVariable("approvers", required: true, description: "主管,逗号分隔")
        .AddStart()
        .AddUserTask("manager", "主管审批", node => node
            .WithAssigneeRule(AssigneeRuleTypes.Expression, "approvers")
            .WithProperty(UserTaskActivity.TitleProperty, "{{ applicant }} 请假 {{ days }} 天")
            .WithProperty(UserTaskActivity.DueAfterSecondsProperty, 86400)
            .WithTimeout(TimeSpan.FromDays(3)))
        .AddDecision("route")
        .AddUserTask("hr", "人事会签", node => node
            .WithAssigneeRule(AssigneeRuleTypes.User, "hr-1", "hr-2", "hr-3")
            .WithProperty(UserTaskActivity.CompletionPolicyProperty, "All")
            .WithProperty(UserTaskActivity.ApprovalThresholdProperty, "60%"))
        .AddEnd("approved", outcome: UserTaskOutcomes.Approved)
        .AddEnd("rejected", outcome: UserTaskOutcomes.Rejected)
        .AddTransition("start", "manager")
        .AddTransition("manager", "route")
        // 决策节点按优先级(小者先)取第一条成立的出边
        .AddTransition("route", "hr", "outcome == 'approved' && days > 3")
        .AddTransition("route", "approved", "outcome == 'approved'", priority: 1)
        .AddDefaultTransition("route", "rejected")
        .AddTransition("hr", "approved", "outcome == 'approved'")
        .AddDefaultTransition("hr", "rejected")
        .Build();
}

再写一个作用域服务,把定义建成草稿并发布:

csharp
using XiHan.Framework.Primitives;
using XiHan.Framework.Workflow.Abstractions.Definitions;

public sealed class LeaveSetup(IWorkflowDefinitionCatalog catalog)
{
    public async ValueTask<Result<WorkflowDefinition>> PublishAsync(CancellationToken cancellationToken)
    {
        var created = await catalog.CreateAsync(LeaveWorkflow.Build(), cancellationToken);
        if (!created.TryGetValue(out var draft))
        {
            return created;
        }

        // 发布前先校验;不合法返回 WORKFLOW_VALIDATION_001,details.errors 列出全部问题
        return await catalog.PublishAsync(draft.Id, cancellationToken);
    }
}

在上面「启用」的 Program.cs 里登记 builder.Services.AddScoped<LeaveSetup>(),在一次调用里(例如一个操作或启动种子)调用 PublishAsync。运行后:CreateAsync 返回版本 1 的 Draft,PublishAsync 返回同一条记录、状态变为 Published,FindPublishedAsync("hr.leave") 能找到它;再调一次 CreateAsync 得到版本 2 的草稿,旧版本不受影响。下一页用 IWorkflowEngine.StartAsync 按名字 hr.leave 启动它。

用构造器定义 ​

WorkflowDefinitionBuilder.Create(name, displayName) 以流式 API 生成定义。构造器为 Start、End、Decision、Parallel、Join、Delay、UserTask、EventGateway、Http、ExternalTask、Compensate 提供专用方法(AddStart、AddEnd、AddDecision、AddUserTask、AddExternalTask 等),其余活动用 AddNode(id, activityType, displayName, configure) 加 WithProperty 设置属性。

层级方法作用
节点WithProperty(name, value)设置活动属性,字符串值支持模板
节点WithAssigneeRule(type, values...)用户任务的指派规则
节点WithRetry(maxAttempts, firstDelay, backoffFactor)失败重试
节点WithTimeout(timeout)挂起后多久超时
节点WithContinueOnError()失败不让实例故障,以 error 标签流出
节点WithOutputMapping(variable, expression)节点输出映射到变量
转移AddTransition(source, target, condition, priority) / AddDefaultTransition(source, target)带条件或默认的出边
定义WithCompensation() / WithCompensateOnFault()取消时补偿、故障时也补偿
定义WithTimeout(timeout, action)实例级超时,Fault 或 Terminate
定义WithCategory、WithDescription、WithProperties分类、说明与扩展属性

用 JSON 定义 ​

流程设计器交来的是 JSON。WorkflowJson.Serialize 与 WorkflowJson.DeserializeDefinition 做往返,属性名是 camelCase,枚举是字符串,时长是 TimeSpan 的常量格式:

json
{
  "id": "draft",
  "name": "hr.expense",
  "version": 1,
  "displayName": "报销",
  "variables": [{ "name": "amount", "required": true }],
  "nodes": [
    { "id": "start", "activityType": "Start" },
    {
      "id": "approve",
      "activityType": "UserTask",
      "displayName": "财务审批",
      "properties": {
        "AssigneeRules": [{ "type": "User", "values": ["finance-1"] }],
        "Title": "报销 {{ amount }} 元"
      },
      "timeout": "2.00:00:00"
    },
    {
      "id": "notify",
      "activityType": "Http",
      "properties": { "Url": "https://erp.localhost/expense", "Method": "POST", "Body": "{\"amount\": {{ amount }}}" },
      "retryPolicy": { "maxAttempts": 3, "firstDelay": "00:00:10", "backoffFactor": 2 }
    },
    { "id": "end", "activityType": "End" }
  ],
  "transitions": [
    { "id": "t1", "sourceNodeId": "start", "targetNodeId": "approve" },
    { "id": "t2", "sourceNodeId": "approve", "targetNodeId": "notify", "condition": "outcome == 'approved'" },
    { "id": "t3", "sourceNodeId": "approve", "targetNodeId": "end", "isDefault": true },
    { "id": "t4", "sourceNodeId": "notify", "targetNodeId": "end" }
  ]
}
csharp
using XiHan.Framework.Workflow.Abstractions.Serialization;

var parsed = WorkflowJson.DeserializeDefinition(File.ReadAllText("expense.json"));
// 不是合法 JSON 或名字不合文法时返回 WORKFLOW_VALIDATION_001
var definition = parsed.ValueOrThrow();

id、version、status 在 CreateAsync 时由目录重新分配,JSON 里的值只是占位。必填的成员只有:定义的 id、name,节点的 id、activityType,转移的 id、sourceNodeId、targetNodeId,变量的 name;没写报 workflow.definition.member_missing。其余成员都可省略,取模型的初值:version 1、status Draft、nodes / transitions / variables 空列表、节点的 properties 空对象、重试策略的 maxAttempts 1、firstDelay 10 秒、backoffFactor 2,其余为空、false 或 0;写成 null 与不写相同。WorkflowJson.Serialize 写出全部成员(只省略值为 null 的),等于初值的也写出,读回来的值不变。

节点、转移与变量 ​

元素字段
WorkflowNodeId(^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$,定义内唯一)、ActivityType、DisplayName、Properties(字符串值支持 {{ 表达式 }} 模板)、RetryPolicy、Timeout(挂起后多久超时)、ContinueOnError、OutputMappings(变量名 → 表达式)
WorkflowTransitionId、SourceNodeId、TargetNodeId、Condition(表达式,空为无条件)、Priority(小者先)、IsDefault
WorkflowVariableDefinitionName、Required、DefaultValue、Description

定义名 WorkflowName 用主题文法 ^[a-z0-9][a-z0-9._-]{0,127}$,一个名字下有多个版本。活动类型 ActivityType 的文法是 ^[A-Za-z][A-Za-z0-9._-]{0,63}$,内置活动用 PascalCase 名。Category 最多 128 个字符(WorkflowDefinition.MaxCategoryLength),超长构造即抛 ArgumentException,从 JSON 读入时返回 WORKFLOW_VALIDATION_001。表达式的文法见活动与扩展。

出边与结果标签 ​

节点以结果标签完成时(用户任务的 approved / rejected / timeout、事件网关的信号名、ContinueOnError 的 error、活动自定的标签),标签写进节点输出,也写进实例变量 outcome;当前节点与后继决策节点的出边条件都按它选边。没有标签的节点不覆盖 outcome。

出边选择由活动登记时的 ActivityOutgoing 决定:

模式行为使用者
AllMatched(缺省)取全部无条件或条件成立的出边;一条都没有时取默认出边大多数活动
Exclusive按优先级取第一条无条件或条件成立的出边;没有时取默认出边Decision
All取全部出边Parallel
None不流出End、Terminate、Fault

AllMatched 与 Exclusive 一条也选不到时实例故障。默认出边只作兜底,有条件成立时不走它。条件引用了未知变量是表达式错误,实例故障,不会悄悄当成假。

校验 ​

WorkflowDefinitionValidator 在发布与启动前执行,失败返回 WORKFLOW_VALIDATION_001,details.errors 列出全部问题(每条的路径、消息键与参数见设计期诊断)。检查项:名字与版本合法;至少一个节点;节点 ID 唯一;活动类型都已登记;恰好一个 Start 且没有入边;转移两端存在;条件与输出映射的表达式语法合法;每个节点最多一条默认出边;Join 的 Mode、UserTask 的 CompletionPolicy、EmptyAssigneeStrategy 与 ApprovalThreshold 取值合法;End 的 Outcome 不是空串;每个 UserTask 至少一个指派来源且规则类型已登记;Start.Cron 是合法 cron;EventGateway 至少一个信号;Http 的 Url 非空、Method 合法;所有节点从 Start 可达。

发布与版本 ​

目录按版本管理定义。同名定义的每个版本是一条独立记录:

方法允许的状态作用
CreateAsync(definition)—新建草稿:新 ID,租户取当前调用的租户(写了别的租户 → WORKFLOW_NOTFOUND_001),版本号 = 该分区里同名最大版本 + 1
UpdateAsync(definition)Draft整体替换草稿,名字、版本、租户不变
PublishAsync(ids)Draft先校验再发布;带 Start.Cron 的定义同时排一个定时启动书签
NewVersionAsync(name)—以当前分区里的最新版本为底复制出一个草稿
DisableAsync(ids)Published停用,删除定时启动书签
ArchiveAsync(ids)任意归档,删除定时启动书签
DeleteAsync(ids)Draft删除草稿
FindAsync(id) / FindPublishedAsync(name, version)—查询;version 为空取最新已发布版本,只在当前分区里找
QueryAsync(WorkflowDefinitionQuery)—定义列表:摘要的一页与总数(WorkflowDefinitionPage)
ActivityTypes()—已登记活动的描述符(类型、显示名、分类、输入属性),供设计器列出可用节点;显示名、分类显示名、说明与输入说明按调用方的区域渲染

状态不允许时返回 WORKFLOW_CONFLICT_002。按标识的操作都是批量、逐项结果,单条写法是 WorkflowDefinitionCatalogExtensions 里的扩展方法(最小示例里的 PublishAsync(draft.Id, …) 就是)。

发布新版本不会改变旧版本的状态。按名字启动时取最新的已发布版本,已经在跑的实例留在原来的版本上,需要时用 MigrateAsync 迁到新版本(见控制实例)。只想让新实例停用旧版本时,对旧版本调用 DisableAsync。

定义列表 ​

QueryAsync(WorkflowDefinitionQuery) 列当前分区的定义,返回这一页的摘要与同条件的总数。摘要 WorkflowDefinitionSummary 只有列表要的字段:Id、Name、Version、DisplayName、Description、Category、Status、TenantId、CreatedAt、PublishedAt,以及显示名或名字 Title;节点、转移、变量与扩展属性不在里面,要整份定义按 Id 调 FindAsync。

条件说明
Keyword关键字:不区分大小写地包含在名字、显示名或说明里即匹配
Name定义名
Category分类,精确匹配
Status状态
CreatedFrom / CreatedTo创建时间区间:下界含、上界不含
Sorts排序项(字段加方向),有序、可多个;空或没给时名字升序、版本降序
Skip / Limit缺省 0 与 100,Limit 最大 1000(WorkflowDefinitionQuery.MaxLimit)

条件同时给出时取交集。过滤、排序、分页与计数都由存储执行,数据库实现是 SQL。分页越界(Skip 为负、Limit 不在 1~1000)不在构造时抛出,QueryAsync 返回 WORKFLOW_VALIDATION_002(HTTP 400,消息键 workflow.definition.query_page_invalid),不进存储;排序字段越界同样是 400。

csharp
var page = (await catalog.QueryAsync(new WorkflowDefinitionQuery
{
    Keyword = "报销",
    Category = "finance",
    Status = WorkflowDefinitionStatus.Published,
    CreatedFrom = monthStart,
    CreatedTo = monthStart.AddMonths(1),
    // 状态升序(生命周期),同状态的创建时间降序
    Sorts = [new(WorkflowDefinitionSortField.Status), new(WorkflowDefinitionSortField.CreatedAt, Descending: true)],
    Skip = 0,
    Limit = 20,
}, cancellationToken)).ValueOrThrow();
// page.Items:这一页的摘要;page.Total:同条件的总数

列表排序 ​

定义查询与实例查询各有一个有序的排序项列表 Sorts(WorkflowDefinitionSort / WorkflowInstanceSort:白名单里的字段 Field 加方向 Descending,缺省升序),可以多个字段:第一项是主排序字段,第一项相同的按第二项,依此类推。排序在存储里执行(数据库实现是 SQL 的 ORDER BY),全部排序项之后接固定的次序键,同值的行在各页之间不重不漏。

查询排序项的字段Sorts 为空或没给次序键(排序项里已有的跳过)项数上限
WorkflowDefinitionQueryName(名字)、Title(显示名,没有取名字)、Version、Status、CreatedAt、PublishedAt名字升序、版本降序名字升序、版本降序(数据库实现最后再按 ID)WorkflowDefinitionQuery.MaxSorts = 6
WorkflowInstanceQueryCreatedAt、EndedAt、Status、DefinitionName、Title(实例标题)创建时间降序创建时间降序、写入先后降序;排序项里有创建时间时写入先后随它的方向WorkflowInstanceQuery.MaxSorts = 5
  • 空列表与没给一样是缺省排序;每一项的方向缺省升序,实例列表要最新的在前得写 new(WorkflowInstanceSortField.CreatedAt, Descending: true)。
  • 排序项不合法时 QueryAsync / ListInstancesAsync 返回 WORKFLOW_VALIDATION_002(HTTP 400),不进存储;按这个顺序检查、报第一个:项数超过上限(消息键 workflow.definition.query_sorts_too_many / workflow.instance.query_sorts_too_many,参数 count、max),某项为空或字段不在白名单(…query_sort_invalid,参数 sort),同一字段出现两次、不论方向(…query_sort_duplicate,参数 sort)。CountInstancesAsync 不看排序。请求里给不认识的字段名在反序列化时就被拒绝。
  • 文本(名字、标题、定义名)按数据库列的排序规则比较,内存实现按序数:大小写、标点与中文的先后可能随库不同。
  • 状态按生命周期的先后:定义 Draft → Published → Disabled → Archived;实例 Running → Suspended → Completed → Canceled → Faulted → Terminated。升序是这个顺序、降序反过来;状态变了位置随之变化。
  • 没发布过的定义 PublishedAt 为空、没结束的实例 EndedAt 为空:空值排在哪头照数据库(PostgreSQL、Oracle 当作最大,SQLite、MySQL、SQL Server 当作最小),内存实现当作最小。
  • 标题按前 256 个字符比较(WorkflowSortKeys.Title)。

关键字 ​

列表查询的 Keyword 都按同一套规则匹配(WorkflowKeyword):去掉首尾空白,为空或只有空白不过滤;关键字与被搜的文本先折叠成不变文化的小写,再判断包含,任一被搜字段包含即匹配,不跨字段。关键字里的 %、_、[、\ 按字面匹配,与全角的 %、_、[、\ 是不同的字符;内存实现与各数据库 Provider 的结果一致,按字面由持久化保证(见仓储读写),小写折叠抹平各库大小写规则的差异。数据库的排序规则另有放宽(如 MySQL 缺省排序规则不区分重音)时以排序规则为准。SqlSugar 在兼容库(达梦、人大金仓、openGauss、TiDB 等)上维持原来的翻译,那里关键字里的 %、_ 会当通配,与应用自己实体的查询相同。

三处查询各自匹配的字段:

查询匹配的字段
定义列表 WorkflowDefinitionQuery名字、显示名、说明
实例列表 WorkflowInstanceQuery实例标题、关联 ID
待办 UserTaskQuery任务标题、实例标题、定义名、关联 ID(所属实例的)

关联 ID 是按包含匹配的,搜索框里输单据号的一段就能找到;要按一组单据号精确查进度,用实例查询的 CorrelationIds。

定义的租户 ​

定义属于一个分区(TenantId 是分区的租户,平台为空)。每个分区是一个名字空间:同名定义在不同分区里各自从版本 1 编号,互不占号。调用只看得到、改得动自己分区的定义,按名字解析(启动、FindPublishedAsync、子流程与 ForEach)也只在自己的分区里找。看不到的定义一律按不存在处理,返回 WORKFLOW_NOTFOUND_001,不暴露别的分区有没有这个定义。

没有全局定义:平台建的定义对租户不可见。各租户共用的流程模板要在每个租户里各建一份,由应用的租户初始化(种子)负责。

常见问题 ​

构造器与 JSON 该用哪个 ​

代码里固定的流程用构造器,编译期就能发现属性名写错;设计器产出、要存库、要让运营改的流程用 JSON。两者生成的是同一个 WorkflowDefinition,WorkflowJson.Serialize 可以把构造器的结果导出给设计器当模板。

发布失败只报 WORKFLOW_VALIDATION_001,怎么知道哪里错了 ​

错误的 details.errors 是逐条的违规列表,每条带定义 JSON 里的路径(如 $.nodes[2].properties.Cron)、消息键与参数;经 HTTP 时宿主写成 Problem Details 的字段级 errors,设计器按路径里的下标定位节点或转移。进程内用 validator.Diagnose(definition) 拿同一组问题,详见设计期诊断。

改了已发布的流程,正在跑的实例怎么办 ​

UpdateAsync 只能改草稿。要改已发布的流程,用 NewVersionAsync(name) 复制出新草稿、改好后发布;旧版本状态不变,正在跑的实例留在旧版本上,新启动的取最新已发布版本。要让旧实例也走新版本,对它们调 MigrateAsync;要让旧版本不再被新实例使用,调 DisableAsync。

为什么平台建的定义租户看不到 ​

分区是硬隔离:没有全局定义,平台与租户互不可见。各租户共用的模板由应用在租户初始化时各建一份;平台要管理某租户的流程时,以那个租户的身份发起调用。

决策节点一条出边都没选到,实例为什么故障了 ​

Decision 的出边是 Exclusive:没有条件成立又没有默认出边时实例故障,不会悄悄停下。给每个决策节点加一条 AddDefaultTransition 作兜底;条件引用了启动时没给的变量同样是故障,可选变量在 AddVariable 里给默认值。

相关包与下一步 ​

Released under The MIT License