- 为多个端点添加权限控制,包括文件操作、工作流任务、群组管理和表单相关功能 - 重构ChatAgentService以支持从数据库读取AI模型配置,并提供回退到应用设置的功能 - 在DatabaseInitializer中添加ai_models表的建表脚本,确保表结构存在 - 更新Directory.Build.props以抑制EF1002和NU1605警告 - 为FileDbContext添加Settings实体集合和文件设置实体 - 优化FileDbContextDesignTimeFactory的配置文件查找逻辑,支持多种目录结构 - 修复EF迁移快照中的命名空间引用错误 - 为多个工作流和IM相关端点添加相应的权限验证
40 KiB
X6 工作流前后端对接标准
| 项目 | 内容 |
|---|---|
| 文档状态 | 评审稿(P0),未实现 |
| 适用范围 | BuildFlow 审批流自定义流程设计器(前端 + 后端) |
| 技术栈 | 前端 Vue 3 + Vben Admin v5 + AntV X6;后端 .NET 10 + FastEndpoints + MediatR + EF Core |
| 关键决策 | ① 传统审批流(非 AI 编排)② 后端 X6 JSON 单一事实源 ③ X6 自建路线 |
本文是标准/契约文档,不是实现教程。所有事实陈述标注「基于现有代码:
路径」。实现细节另起任务。
第 1 章 背景与现状
1.1 目标场景
BuildFlow 需要一个审批流自定义流程设计器:业务用户在前端通过拖拽节点、连线的方式编排审批流程(开始 → 审批 → 抄送 → 条件分支 → 并行 → 子流程 → 定时 → 结束),保存后由后端 ProcessEngine 执行。
明确不做:AI 智能体编排(LLM/代码/MCP/知识库节点)。
1.2 前端现状
基于现有代码:build-flow.web/apps/web-antdv-next/src/
- 技术栈:Vue 3 + TypeScript + Vue Vben Admin v5 + antdv-next + pnpm workspace。
- 当前无任何流程相关页面(
src/views/下只有 dashboard / system / file-manager / im / share / demos / _core,无 workflow 目录)。 - 当前无 X6 依赖,需全新引入
@antv/x6+@antv/x6-vue-shape。 - API 客户端约定:基于现有代码
src/api/request.ts,统一requestClient(responseReturn: 'data'),后端响应信封由前端层自动解包。
1.3 后端现状(关系表事实源)
基于现有代码:build-flow.api/src/modules/workflow/
1.3.1 三张关系表为唯一事实源
| 表名 | 实体 | 配置文件 |
|---|---|---|
wf_workflow_definitions |
WorkflowDefinition |
Infrastructure/Persistence/Configurations/WorkflowDefinitionConfiguration.cs |
wf_workflow_nodes |
WorkflowNode |
WorkflowNodeConfiguration.cs |
wf_workflow_edges |
WorkflowEdge |
WorkflowEdgeConfiguration.cs |
DefinitionJson 是冗余空壳字段:
- 基于现有代码
Domain/Entities/WorkflowDefinition.cs:21:字段注释为「完整流程图的 JSON 快照(节点+边),便于导出/导入与前端渲染」。 - 基于现有代码
UpdateWorkflowDefinitionCommandHandler.cs:Handler 仅做entity.DefinitionJson = request.DefinitionJson;字符串覆盖,不与 nodes/edges 表同步,无任何一致性校验。 - 基于
SeedData.cs:种子数据只构造 Node/Edge 实体,未写 DefinitionJson。
1.3.2 ProcessEngine 运行时只读关系表
基于现有代码 Application/Engine/ProcessEngine.cs(约 1191 行,方法名定位,行号会漂移):
- 节点查找
FindNodeAsync:_dbContext.WorkflowNodes.FindAsync(nodeId)。 - 出边查找
GetOutgoingEdgesAsync:_dbContext.WorkflowEdges.Where(e => e.SourceNodeId == nodeId)。 - 入边查找
GetIncomingEdgesAsync:_dbContext.WorkflowEdges.Where(e => e.TargetNodeId == nodeId)。 - 启动时加载定义全部节点(
StartAsync内):_dbContext.WorkflowNodes.Where(n => n.DefinitionId == ...)。 - Config 解析统一走
NodeConfigParser.Parse(node.Config)。 DefinitionJson在引擎路径中一次都未被读取(grep 零命中)。
1.3.3 引擎对实体字段的精确依赖(迁移影响面)
| 实体 | 引擎使用的字段 | 是否影响迁移 |
|---|---|---|
WorkflowNode |
Id, DefinitionId, NodeType, Name, Config |
✅ 必须从 JSON 等价读取 |
WorkflowNode |
PositionX, PositionY |
❌ 引擎不读(纯画布字段) |
WorkflowNode |
FormDefinitionId |
⚠️ 引擎不直接读,但 DTO 层有 |
WorkflowEdge |
SourceNodeId, TargetNodeId |
✅ 拓扑遍历核心 |
WorkflowEdge |
EdgeType |
✅ CompleteTaskAsync 选边(Approved 优先 / Normal 回退 / Rejected 独立) |
WorkflowEdge |
Condition |
✅ Condition/ExclusiveGateway 节点条件评估 |
WorkflowEdge |
Order |
✅ Condition/ExclusiveGateway 分支评估顺序(first-match-wins) |
WorkflowEdge |
Label |
❌ 引擎不读 |
1.3.4 现有接口割裂(7 套)
基于现有代码 src/BuildFlow.Host/Endpoints/workflow/WorkflowDefinition/(24 个文件):
- 定义级:Create / Update / Delete / Publish / Disable / GetById / GetList(7 个)
- 节点级:
CreateNode/UpdateNode/DeleteNode(3 个,路由/workflow-definitions/{DefinitionId}/nodes[/{NodeId}]) - 边级:
CreateEdge/UpdateEdge/DeleteEdge(3 个)
问题:前端维护画布需对每个节点/边单独发请求,与 X6「整图 toJSON() 保存」模型不匹配。
1.3.5 响应信封
基于现有代码 shared/BuildFlow.Shared/Api/ApiResponse.cs + ApiResponseMiddleware.cs:
{ "code": 0, "data": <业务对象>, "message": "ok" }
code = 0表示成功;前端requestClient配responseReturn: 'data'自动解包。- 时间统一序列化为 UTC 毫秒时间戳(
TimestampDateTimeConverter)。 - 命名 CamelCase。
1.3.6 现有枚举(本契约复用,不改)
基于现有代码 Domain/Enums/:
// NodeType(int):Start=0, End=1, Approval=2, Cc=3, Condition=4,
// Parallel=5, SubProcess=6, Timer=7, ExclusiveGateway=8
// EdgeType(int):Normal=0, Approved=1, Rejected=2
// DefinitionStatus(int):Draft=0, Published=1, Disabled=2
第 2 章 目标与边界
2.1 目标
- 定义与 X6
toJSON()/fromJSON()原生兼容的definition_jsonschema,作为前后端唯一对接契约。 - 让
definition_json升级为后端唯一事实源,ProcessEngine改读 JSON。 - 简化接口:废弃逐 Node/Edge CRUD,改为整图保存。
- 前端用 X6 实现可视化设计器,
graph.toJSON()→ 直接落库,graph.fromJSON()→ 直接渲染。
2.2 边界
- 本文档只定标准,不实现。后续实现按第 8 章阶段推进。
- 不改变现有
NodeType/EdgeType/DefinitionStatus枚举语义。 - 不改变响应信封、认证、权限体系。
- 不涉及流程实例运行时(
WorkflowInstance/WorkflowTask/WorkflowToken)的存储改造——它们引用的是已发布流程定义的快照,迁移策略见第 7 章。
第 3 章 组件选型结论
3.1 结论:X6 自建路线
| 候选 | 结论 | 依据 |
|---|---|---|
@antv/xflow(X6 官方应用级方案) |
❌ 不可用 | 仓库已声明停止维护;仅支持 React,Vben 是 Vue3 无法集成 |
| X6 官方审批流成品 | ❌ 不存在 | 官方 showcase 无审批流成品,无审批/抄送/条件分支节点预制模板 |
| Vue3 审批流成品(StavinLi/Workflow-Vue3 等) | ❌ 偏离方向 | 均为仿钉钉 DOM 树渲染,不用 X6 画布,与「引入 X6」目标冲突 |
| X6 自建(BPMN/流程图示例 + x6-vue-shape) | ✅ 采纳 | 贴合「引入 X6」目标;官方有 BPMN/流程图示例可参照;审批节点需自写但有参照 |
3.2 依赖包
| 包 | 用途 | 版本约束 |
|---|---|---|
@antv/x6 |
X6 主包(3.x 起插件已合并进主包,Snapline/Stencil/Transform 等无需单独安装) | ^3.x(X6 当前稳定主版本,遵循仓库 pnpm catalog 规则) |
@antv/x6-vue-shape |
Vue 组件渲染节点注册 | 与 @antv/x6 3.x 对齐(同主版本) |
⚠️ 必须用 X6 3.x,2.x 的插件需单独安装且 API 有差异。实现时以
@antv/x6当前发布的稳定主版本为准,并加入仓库 catalog,不要固定死小版本。安装走build-flow.web的 pnpm workspace,禁止 npm/yarn。
3.3 参照起点
- X6 官方 BPMN 示例、流程图示例:源码位于 antvis/X6 仓库
site/examples/showcase/practices/demo/。 - X6 Vue 节点文档:
https://x6.antv.antgroup.com/tutorial/intermediate/vue。 - 连接桩文档:
https://x6.antv.antgroup.com/tutorial/basic/port。 - 旧版审批 demo(请假/人事审批,X6 1.x,仅作节点样式参考):
https://x6.antv.vision/zh/examples/showcase/practices/。
第 4 章 前端方案(设计层面)
4.1 目录落点
遵循 rag-coding-web 技能约定,全部落在 apps/web-antdv-next/src/:
src/
├── api/core/workflow.ts # API 定义
├── router/routes/modules/workflow.ts # 路由 module
└── views/workflow/
├── list/index.vue # 流程定义列表/发布/启停
└── designer/
├── index.vue # 设计器主页面(容器)
├── graph/ # X6 Graph 初始化与配置
│ ├── createGraph.ts # Graph 工厂(grid/snapline/connecting)
│ └── ports.ts # 端口 groups/items 预设
├── shapes/ # 节点注册(每个 NodeType 一个)
│ ├── register.ts # 集中 register 所有 shape
│ ├── StartNode.vue
│ ├── ApprovalNode.vue
│ ├── ConditionNode.vue
│ └── ... # 详见 4.4
├── components/ # 设计器 UI 组件
│ ├── StencilPanel.vue # 拖拽节点面板
│ ├── Toolbar.vue # 保存/撤销/缩放/校验
│ └── NodeConfigDrawer.vue # 节点配置抽屉
└── composables/
├── useGraph.ts # Graph 生命周期管理
└── useGraphPersistence.ts # 加载/保存(toJSON/fromJSON)
上述是设计目标结构,实现时可在遵循「模块内聚」前提下调整。
packages/@core、packages/effects不改。
4.2 设计器能力清单
| 能力 | X6 机制 | 说明 |
|---|---|---|
| 拖拽创建节点 | Stencil 插件 |
分组面板,拖入后 addNode |
| 画布缩放 | mousewheel + minScale/maxScale |
0.5 ~ 3 |
| 对齐线 | Snapline 插件 |
X6 3.x 内置 |
| 连线 | connecting 配置 |
connector: 'smooth'、allowLoop: false、validateConnection |
| 端口显隐 | node:mouseenter/mouseleave 事件 |
默认隐藏,hover 显示,连接后常显 |
| 节点删除 | 卡片内删除按钮 + node.remove() |
Start/End 节点禁止删除 |
| 边删除 | edge:mouseenter 显示 button-remove 工具 |
|
| 整图保存 | graph.toJSON() |
一次请求覆盖整张图 |
| 整图加载 | graph.fromJSON({nodes, edges}) |
详情接口返回的 definitionJson 直接喂入 |
4.3 端口预设(所有节点统一)
四向端口,默认隐藏,hover 显示:
const PORT_GROUPS = {
top: { position: 'top', attrs: { circle: { r: 3, magnet: true, fill: '#C2C8D5', stroke: '#C2C8D5' } } },
right: { position: 'right', attrs: { circle: { r: 3, magnet: true, fill: '#C2C8D5', stroke: '#C2C8D5' } } },
bottom: { position: 'bottom', attrs: { circle: { r: 3, magnet: true, fill: '#C2C8D5', stroke: '#C2C8D5' } } },
left: { position: 'left', attrs: { circle: { r: 3, magnet: true, fill: '#C2C8D5', stroke: '#C2C8D5' } } },
};
const PORT_ITEMS = [
{ id: 'top', group: 'top' }, { id: 'right', group: 'right' },
{ id: 'bottom', group: 'bottom' }, { id: 'left', group: 'left' },
];
4.4 节点形态
用 @antv/x6-vue-shape 注册 Vue 组件渲染。每个 NodeType 对应一个 shape 名和一个 .vue 组件,业务配置通过 node.data 承载,组件内 node.getData() 读取。详见第 5 章 5.5 的 shape 映射表。
交互复杂的节点配置(审批人规则编辑器、条件表达式编辑器)需独立子方案,本文不展开。
第 5 章 后端数据契约(★ 核心)
5.1 顶层结构
definition_json 列存 X6 原生格式。后端统一接收/返回 {nodes, edges} 形式(X6 的 fromJSON/parseJSON 同时支持 {cells} 和 {nodes, edges} 两种等价形式,本契约选用前者更易读写):
{
"nodes": [ /* Node 对象数组 */ ],
"edges": [ /* Edge 对象数组 */ ]
}
空图(仅有定义壳、无节点)合法,值为
{"nodes":[],"edges":[]}。
5.2 节点标准字段表
| 字段 | 类型 | 必填 | 用途 | 后端是否消费 |
|---|---|---|---|---|
id |
string | ✅ | 节点唯一标识,格式见 5.7 | ✅ 拓扑遍历 |
shape |
string | ✅ | 节点形状名,决定渲染器,见 5.5 | ✅ 反推 NodeType |
x |
number | ✅ | 画布 X 坐标(左上角) | ❌ 仅渲染 |
y |
number | ✅ | 画布 Y 坐标(左上角) | ❌ 仅渲染 |
width |
number | ❌ | 宽度(注册时默认) | ❌ 仅渲染 |
height |
number | ❌ | 高度(注册时默认) | ❌ 仅渲染 |
angle |
number | ❌ | 旋转角度 | ❌ 仅渲染 |
visible |
boolean | ❌ | 是否可见,默认 true | ❌ 仅渲染 |
data |
object | ✅ | 业务数据,见 5.5 | ✅ 引擎读取 |
ports |
object | ❌ | 连接桩,见 5.4 | ❌ 仅渲染(边引用 port) |
attrs |
object | ❌ | SVG 样式 | ❌ 仅渲染 |
markup |
array | ❌ | SVG 结构 | ❌ 仅渲染 |
label |
string|object | ❌ | 文本标签 | ❌ 仅渲染 |
后端消费原则:引擎只读 id / shape(反推 NodeType)/ data;几何/样式字段透传不解释。
5.3 边标准字段表
| 字段 | 类型 | 必填 | 用途 | 后端是否消费 |
|---|---|---|---|---|
id |
string | ✅ | 边唯一标识 | ✅ 标识 |
shape |
string | ❌ | 边类型,默认 wf-edge |
❌ |
source |
{cell, port} |
✅ | 起点 {cell: nodeId, port: portId} |
✅ 取 cell 作 SourceNodeId |
target |
{cell, port} |
✅ | 终点 {cell: nodeId, port: portId} |
✅ 取 cell 作 TargetNodeId |
data |
object | ❌ | 业务数据,见 5.6 | ✅ 引擎读取 |
label |
string|object | ❌ | 边标签 | ❌ 仅渲染(引擎不读) |
vertices |
[{x,y}] |
❌ | 路径控制点(弯折点) | ❌ 仅渲染 |
router |
string|object | ❌ | 路由算法(normal/orth/manhattan) | ❌ 仅渲染 |
connector |
string|object | ❌ | 连接器(normal/smooth/rounded) | ❌ 仅渲染 |
attrs |
object | ❌ | 样式 | ❌ 仅渲染 |
zIndex |
number | ❌ | 层级 | ❌ 仅渲染 |
source/target 规则:必须用 {cell, port} 结构;cell 是节点 id(对应 5.2 的 node.id),port 是端口 id(默认 top/right/bottom/left)。后端只取 cell 做拓扑,port 仅前端连线校验用。
5.4 端口 port 结构(与 4.3 一致)
"ports": {
"groups": { /* top/right/bottom/left 组定义,见 4.3 */ },
"items": [
{ "id": "top", "group": "top" },
{ "id": "right", "group": "right" },
{ "id": "bottom", "group": "bottom" },
{ "id": "left", "group": "left" }
]
}
后端不解释 ports 结构,但边的
source.port/target.port必须命中节点ports.items[].id。校验在发布时做(见第 6 章)。
5.5 节点 data 业务契约(关键映射)
node.shape 决定渲染器,同时反推 NodeType 枚举。业务配置统一进 node.data,替代旧的 WorkflowNode.Config(jsonb 字符串)。
| NodeType | 枚举值 | shape 名 | data 业务字段(替代旧 Config) |
|---|---|---|---|
Start |
0 | wf-start |
无(data 可存 title) |
End |
1 | wf-end |
无(data 可存 title) |
Approval |
2 | wf-approval |
approvalMode(single/countersign/any), assigneeRule, assigneeRules(数组), minApprovals, timeoutMinutes, onEnter, onApproved, onRejected, formDefinitionId |
Cc |
3 | wf-cc |
recipients(数组), formDefinitionId |
Condition |
4 | wf-condition |
无(条件表达式与顺序放在出边的 data 上,见 5.6) |
Parallel |
5 | wf-parallel |
无 |
SubProcess |
6 | wf-subprocess |
definitionId(子流程 GUID 字符串) |
Timer |
7 | wf-timer |
delayMinutes, targetTime(ISO8601) |
ExclusiveGateway |
8 | wf-exclusive |
无(同 Condition,分支信息在出边) |
data.title(节点显示名,对应旧WorkflowNode.Name)所有节点通用,建议必填。
shape → NodeType 反推规则:后端维护一张常量映射表,shape 缺失时拒绝发布。
5.6 边 data 业务契约
data 字段 |
类型 | 对应旧字段 | 用途 |
|---|---|---|---|
edgeType |
int (枚举值 0/1/2) | WorkflowEdge.EdgeType |
Normal=0 / Approved=1 / Rejected=2;影响 Approval 节点选边 |
condition |
string | WorkflowEdge.Condition |
Condition/ExclusiveGateway 出边的条件表达式(简单文本或 JSON 树) |
order |
int | WorkflowEdge.Order |
分支评估顺序,值小优先(first-match-wins) |
几何信息(
vertices/router/connector)放边顶层,不放data;这些字段后端透传不读。
5.7 ID 规则
| 元素 | id 格式 | 生成方 |
|---|---|---|
| 节点 | node_<guid> |
前端生成(拖入时),后端保存时校验唯一 |
| 边 | edge_<guid> |
前端生成(连线时),后端保存时校验唯一 |
| 端口 | top / right / bottom / left(固定) |
由 shape 注册预设,不单独生成 |
待决项(见第 9 章):是否允许后端在保存时重写 id。当前建议前端生成、后端透传,保证
source.cell/target.cell引用稳定。
5.8 引擎字段对应表(迁移映射)
ProcessEngine 当前读实体字段 → 改造后从 JSON 路径读取:
| 当前实体字段 | JSON 路径 | 说明 |
|---|---|---|
node.Id |
nodes[].id |
字符串 ↔ Guid(需类型转换) |
node.NodeType |
由 nodes[].shape 反推 |
见 5.5 映射 |
node.Name |
nodes[].data.title |
|
node.Config(jsonb 字符串) |
nodes[].data(对象) |
直接是对象,免去 NodeConfigParser 字符串反序列化 |
node.DefinitionId |
外层(图所属定义) | 加载时由 Definition 上下文注入 |
node.PositionX/Y |
nodes[].x/y |
引擎不读,仅迁移脚本用 |
edge.SourceNodeId |
edges[].source.cell |
|
edge.TargetNodeId |
edges[].target.cell |
|
edge.EdgeType |
edges[].data.edgeType |
|
edge.Condition |
edges[].data.condition |
|
edge.Order |
edges[].data.order |
|
edge.Label |
edges[].label |
引擎不读 |
5.9 完整示例(最小可运行图)
{
"nodes": [
{
"id": "node_start_001",
"shape": "wf-start",
"x": 120, "y": 120,
"data": { "title": "开始" },
"ports": { "groups": { "...": "见 5.4" }, "items": [ "..." ] }
},
{
"id": "node_approval_001",
"shape": "wf-approval",
"x": 120, "y": 280,
"data": {
"title": "部门经理审批",
"approvalMode": "single",
"assigneeRule": "role:dept-manager",
"timeoutMinutes": 1440,
"formDefinitionId": null
},
"ports": { "...": "..." }
},
{
"id": "node_end_001",
"shape": "wf-end",
"x": 120, "y": 440,
"data": { "title": "结束" },
"ports": { "...": "..." }
}
],
"edges": [
{
"id": "edge_001",
"shape": "wf-edge",
"source": { "cell": "node_start_001", "port": "bottom" },
"target": { "cell": "node_approval_001", "port": "top" },
"data": { "edgeType": 0 }
},
{
"id": "edge_002",
"shape": "wf-edge",
"source": { "cell": "node_approval_001", "port": "bottom" },
"target": { "cell": "node_end_001", "port": "top" },
"data": { "edgeType": 1 }
}
]
}
第 6 章 API 契约(简化接口)
6.1 改造后接口(最小集)
遵循现有 FastEndpoints + MediatR + 资源式路由风格(基于现有代码 Endpoints/workflow/WorkflowDefinition/):
| 方法 | 路由 | 说明 | 请求要点 | 响应 |
|---|---|---|---|---|
GET |
/workflow-definitions |
列表 | 分页/筛选 | WorkflowDefinitionDto[](不含 nodes/edges,含 definitionJson 摘要状态) |
GET |
/workflow-definitions/{Id} |
详情 | — | WorkflowDefinitionDetailDto(含完整 definitionJson,前端 graph.fromJSON 直接渲染) |
POST |
/workflow-definitions |
创建 | name, code, formDefinitionId | 初始空图 {"nodes":[],"edges":[]} |
PUT |
/workflow-definitions/{Id} |
整图保存 | name, description, definitionJson, formDefinitionId | WorkflowDefinitionDto |
POST |
/workflow-definitions/{Id}/publish |
发布 | — | 发布时做图校验(见 6.3) |
DELETE |
/workflow-definitions/{Id} |
删除 | — | 空对象(沿用现有写回 "{}" 约定) |
关键变化:PUT 整图保存——请求体 definitionJson 即 X6 graph.toJSON() 输出,一次请求覆盖整张图,后端不再做增量 Node/Edge 同步。
DTO 字段去向(基于现有代码 DTOs/WorkflowDefinitionDetailDto.cs):
现有 WorkflowDefinitionDetailDto 含 List<WorkflowNodeDto> Nodes + List<WorkflowEdgeDto> Edges 两个集合字段,是「关系表事实源」时代的产物。JSON 单一事实源后这两个集合变为冗余,处理方式:
Nodes/Edges集合:置为null(或空集合),不再由后端填充。前端设计器统一从DefinitionJson经graph.fromJSON()渲染,不读这两个集合。DefinitionJson:成为详情接口的唯一权威负载,前端拿它直接喂graph.fromJSON。- 是否删除
Nodes/Edges字段:为避免破坏存量前端调用,P1 阶段保留字段置空,P4 关系表清理时一并从 DTO 移除。 WorkflowNodeDto/WorkflowEdgeDto:P4 阶段随关系表废弃而废弃。
6.2 废弃接口(过渡期保留)
以下 6 套接口标 [Obsolete],过渡期内不删(避免破坏存量调用),最终在 P4 阶段移除:
POST/PUT/DELETE /workflow-definitions/{DefinitionId}/nodes[/{NodeId}]POST/PUT/DELETE /workflow-definitions/{DefinitionId}/edges[/{EdgeId}]
过渡期:这些接口只操作关系表(旧事实源),不写
definition_json。一旦 P1 完成 ProcessEngine 切换,关系表不再是事实源,这些接口失去意义。
6.3 发布时图校验规则
POST /publish 在状态机 Draft → Published 前,对 definition_json 做以下校验(失败返回 ValidationErrorResponse)。
校验规则依据现有
ProcessEngine的运行时强约束(基于现有代码Application/Engine/ProcessEngine.cs:边选择在CompleteTaskAsync内、Condition 评估在ProcessConditionNodeAsync、无匹配分支抛"No condition branch matched")。发布期校验必须覆盖引擎运行时会抛异常的所有情形,避免发布出运行即崩溃的流程。
结构校验:
- 唯一 Start:有且仅有一个
shape === "wf-start"节点。 - 至少一个 End:存在
shape === "wf-end"节点。 - 无悬空边:每条边的
source.cell/target.cell都能在nodes[].id中找到。 - 可达性:从 Start 节点出发,所有 End 节点可达(DFS/BFS)。
- shape 合法:所有
shape在 5.5 映射表内。 - id 唯一:节点 id、边 id 全局唯一;边的
source.cell/target.cell引用有效。
引擎强约束校验(必须,否则运行时抛 BusinessException):
- Approval 节点出边完备性:每个
wf-approval节点必须至少有一条data.edgeType=Approved或data.edgeType=Normal的出边(通过路径);若业务允许驳回,还必须有一条data.edgeType=Rejected的出边。否则审批任务完成时引擎找不到匹配边直接异常。 - Condition / ExclusiveGateway 节点分支评估:每个
wf-condition/wf-exclusive节点的所有出边必须带data.condition和data.order;强烈建议提供一条兜底边(condition 为true或空),否则当所有分支都不匹配时引擎抛"No condition branch matched"。 - SubProcess 节点引用有效:
wf-subprocess节点的data.definitionId必须指向一个已发布(Published)的流程定义。
校验失败统一返回
ValidationErrorResponse(沿用现有BuildFlow.Shared/Api/的校验失败信封)。
第 7 章 迁移策略(关系表 → JSON)
7.1 核心难点:已发布定义的不可变性与运行实例的版本隔离
⚠️ 本节是整个迁移方案的关键,直接决定方案是否可行。
现有代码的事实缺陷(必须先正视):
- WorkflowInstance 没有 Version 字段(基于现有代码
Domain/Entities/WorkflowInstance.cs):实例仅存DefinitionId。 - ProcessEngine 按 DefinitionId 实时读图(基于现有代码
Application/Engine/ProcessEngine.cs的StartAsync):WorkflowNodes.Where(n => n.DefinitionId == instance.DefinitionId),无版本隔离。 - Update 没有状态校验(基于现有代码
UpdateWorkflowDefinitionCommandHandler.cs):注释说「仅 Draft 可改」,但 Handler 未强制——Published状态的定义仍可被 PUT 覆盖。
后果:在关系表时代,这三点就已构成隐患(发布后改节点会污染运行中实例);迁移到 JSON 单一事实源、且 PUT 改为整图覆盖后,隐患被放大——一个 PUT 即可让所有运行中实例读到不一致的图。
结论:JSON 单一事实源方案必须配套解决版本隔离,否则不可上线。
7.2 版本隔离方案:发布快照表(参照仓库已有先例 FormDefinitionVersion)
本方案沿用仓库已有的
FormDefinitionVersion(wf_form_definition_versions)版本快照范式,命名/分层/审计接口完全对齐,不引入新模式。
新增实体 WorkflowDefinitionSnapshot(基于现有先例 Domain/Entities/FormDefinitionVersion.cs 的结构):
| 字段 | 类型 | 说明 |
|---|---|---|
Id |
Guid | 主键 |
DefinitionId |
Guid | 所属流程定义 ID |
Version |
int | 版本号(与发布时 WorkflowDefinition.Version 对应) |
DefinitionJson |
string? (jsonb) | 该版本的完整 X6 graph JSON 快照(不可变) |
Source |
string | 触发来源:Publish(仅发布写入,区别于 FormDefinitionVersion 的 Update/Publish 双源) |
ChangeSummary |
string? | 变更摘要(可选) |
| 审计 | IAuditable | CreatedBy/CreatedAt/UpdatedBy/UpdatedAt/IsDeleted/OperatorIP |
EF 配置(基于现有先例 FormDefinitionVersionConfiguration.cs):
- 表名
wf_workflow_definition_snapshots(对齐wf_form_definition_versions命名)。 DefinitionJson→HasColumnType("jsonb")。- 复合唯一索引
HasIndex(e => new { e.DefinitionId, e.Version })。 - 索引
HasIndex(e => e.DefinitionId)。
写入时机(发布流程):
PublishWorkflowDefinitionCommandHandler(基于现有代码)在做Status=Published; Version++之前,先做 6.3 全部校验。- 校验通过后,把当前
definition_json复制成一条WorkflowDefinitionSnapshot记录(Version = 新版本号)。 - 快照写入后,引擎运行实例只读快照表,Draft 编辑主表的
definition_json不影响已发布版本。
读取规则(ProcessEngine 改造后):
- 引擎不再用
instance.DefinitionId查wf_workflow_definitions.definition_json。 - 改为:用
instance.DefinitionId+ 实例绑定的 Version 定位wf_workflow_definition_snapshots的不可变快照。 - ⚠️ 这要求
WorkflowInstance新增一个DefinitionVersion(int)字段,在StartInstance时从当时的WorkflowDefinition.Version拷贝写入实例(实例一旦启动版本即冻结)。
实体字段新增属侵入式改动,但
WorkflowInstance.DefinitionVersion是方案成立的最小必要条件,无法绕开。EF Migration 需加此列并回填存量实例(用对应定义当前 Version)。
7.3 Draft 编辑的 Published 锁定(修复现有 Update 漏洞)
在 UpdateWorkflowDefinitionCommandHandler 增加状态校验(基于现有代码,当前缺失):
// 仅 Draft 可编辑定义内容;Published/Disabled 状态拒绝整图 PUT
if (entity.Status != DefinitionStatus.Draft)
{
throw new BusinessException("Only draft workflow definitions can be edited.");
}
已发布的定义如需修改,走「复制为新 Draft → 编辑 → 重新发布」流程(产出新 Version + 新快照),原 Version 快照不变,存量实例不受影响。
7.4 四步迁移路线
第一步(P1 核心):实现 JSON 读取层 + 快照机制
- 新建
WorkflowGraph领域值对象 + 解析器(Application/Engine/WorkflowGraphParser.cs),从 X6 JSON 解析出:节点列表、边列表、出边拓扑、节点 data。 - 新建
WorkflowDefinitionSnapshot实体 + EF 配置 + Migration。 WorkflowInstance新增DefinitionVersion字段 + Migration + 回填。PublishWorkflowDefinitionCommandHandler增加 6.3 校验 + 写快照。UpdateWorkflowDefinitionCommandHandler增加 Draft 锁定(7.3)。
第二步(P1):ProcessEngine 切换为读快照
- 引擎启动实例时:按
instance.DefinitionId + instance.DefinitionVersion从wf_workflow_definition_snapshots加载不可变 JSON 到内存WorkflowGraph。 - 改造影响面见 7.5。
- 此阶段起,Draft 编辑主表
definition_json与运行实例彻底解耦。
第三步(P1):存量数据迁移脚本(支持 dry-run + 回滚,见 7.6)
- 遍历
wf_workflow_definitions,对definition_json为空的记录,从wf_workflow_nodes+wf_workflow_edges序列化生成 X6 JSON:PositionX/Y→x/y- 补默认
width/height/ports(按 shape 预设) node.Config(字符串)→node.data(对象),并按 NodeType 补data.title = node.Nameedge.SourceNodeId/TargetNodeId→edge.source.cell/target.celledge.EdgeType/Condition/Order→edge.data.edgeType/condition/order- 节点/边 id 统一为
node_<guid>/edge_<guid>
- 为每个已
Published的定义补写一条WorkflowDefinitionSnapshot快照(Version 取定义当前 Version)。 - 为每个运行中的
WorkflowInstance回填DefinitionVersion(取其定义当前 Version)。
第四步(P4):关系表降级 → 最终移除
- 关系表降级为只读快照(过渡期)。
- 新版本:移除
WorkflowNodes/WorkflowEdgesDbSet、EF Configuration,加 Migration drop 表。
7.5 ProcessEngine 改造影响面(以方法名为准)
基于现有代码 Application/Engine/ProcessEngine.cs。下表以方法名为准(行号会随代码漂移,仅作辅助定位):
| 当前方法 | 当前实现(关系表) | 改造后(读 WorkflowGraph) |
|---|---|---|
StartAsync 加载节点 |
_dbContext.WorkflowNodes.Where(DefinitionId==instance.DefinitionId) |
从快照 JSON 加载 WorkflowGraph,找 shape==="wf-start" |
FindNodeAsync |
_dbContext.WorkflowNodes.FindAsync(nodeId) |
从内存 WorkflowGraph.Nodes 按 id 查 |
GetOutgoingEdgesAsync |
_dbContext.WorkflowEdges.Where(SourceNodeId==) |
从内存 WorkflowGraph.Edges 按 source.cell 过滤 |
GetIncomingEdgesAsync |
_dbContext.WorkflowEdges.Where(TargetNodeId==) |
从内存 WorkflowGraph.Edges 按 target.cell 过滤 |
ParseConfig 包装 |
入参 node.Config(字符串) |
入参改为 node.data(对象),NodeConfigParser 适配 |
Timer 节点内联查边(位于 ProcessTimerNodeAsync 内) |
db.WorkflowEdges.Where(SourceNodeId==) |
同上,改读 WorkflowGraph |
改造后引擎从「每步查库」变为「启动时一次加载快照 JSON 到内存
WorkflowGraph,后续内存遍历」,性能与一致性都更优;且天然实现版本隔离(实例绑定 Version)。
7.6 迁移回滚预案
生产数据迁移必须有回滚方案:
- 迁移前备份:迁移脚本执行前,对
wf_workflow_definitions.definition_json原值、wf_workflow_nodes、wf_workflow_edges做全表 SQL 备份(pg_dump相关表)。 - dry-run 模式:脚本支持
--dry-run,仅输出迁移报告(将生成多少条 JSON、多少条快照、回填多少实例),不写库。 - 分批 + 可重入:按
DefinitionId分批处理,每批独立事务;脚本可重复执行(幂等:definition_json已非空则跳过)。 - 回滚:若迁移后引擎异常,回滚步骤:
- 恢复
definition_json原值(备份还原); - 保留关系表数据(P4 之前关系表不 drop,是安全网);
- ProcessEngine 通过特性开关回退到读关系表的旧路径(P1 改造时即应保留双读开关,详见 9.1)。
- 恢复
- 关系表保留期:
wf_workflow_nodes/wf_workflow_edges在 P4 之前禁止 drop,作为整个迁移期的最终回滚保障。
第 8 章 分阶段实施路线
| 阶段 | 内容 | 交付物 |
|---|---|---|
| P0 | 文档评审(本文) | 本文档定稿 |
| P1 | 后端契约改造 | WorkflowGraph 解析层、WorkflowDefinitionSnapshot 快照表、WorkflowInstance.DefinitionVersion 字段、Published 锁定、ProcessEngine 切换读快照、整图保存接口、发布校验、存量迁移脚本(含 dry-run + 回滚) |
| P2 | 前端设计器骨架 | X6 画布 + Stencil 拖拽 + 连线 + 整图保存/加载闭环(能跑通「画图→保存→重新加载」) |
| P3 | 节点配置弹窗 | 审批人规则编辑器、条件表达式编辑器、抄送人选择、子流程选择 |
| P4 | 收尾 | 列表/发布/启停页、关系表废弃清理、6 套废弃接口移除 |
第 9 章 风险与待决项
9.1 风险
| 风险 | 说明 | 应对 |
|---|---|---|
| 运行实例无版本隔离(现有缺陷,致命) | WorkflowInstance 无 Version 字段,引擎按 DefinitionId 实时读图;发布后改图会污染运行中实例 |
7.2 方案强制实施:新增 DefinitionVersion 字段 + 发布快照表,引擎读不可变快照 |
| Published 后可被 PUT 覆盖(现有漏洞) | UpdateWorkflowDefinitionCommandHandler 无状态校验,已发布定义可被改 |
7.3 方案强制实施:Update Handler 增加 Draft 状态校验 |
| 双源一致性(迁移期) | P1 期间 JSON 与关系表并存 | ProcessEngine 保留特性开关(双读),默认读快照,异常可切回关系表;Draft 编辑只走 JSON |
| 存量已运行实例的 Version 回填 | 迁移期已运行的实例无 DefinitionVersion,按什么值回填? | 取其定义当前 Version 回填(接受「存量实例绑定到当前版本」的近似;P1 切换前应停止新发起,迁移完再恢复) |
| 整图 JSON 体积 | 复杂图可能较大 | 监控单图大小,必要时拆分子流程(已有 SubProcess 节点) |
| 前端节点交互复杂度 | 多人会签、条件表达式编辑器工作量大 | P3 独立子方案,工作量预期见 9.3 |
NodeConfigParser 兼容 |
当前解析字符串,改造后是对象 | 适配层兼容,或迁移完成后简化 |
9.2 待决项(需评审拍板)
- 节点/边 ID 生成方:当前建议前端生成
node_<guid>/edge_<guid>,后端透传。是否允许后端保存时重写 id?(影响source.cell引用稳定性) - Published 流程的派生编辑:7.3 锁定 Published 后,是否提供「复制已发布流程 → 新 Draft」的派生编辑能力?(建议提供,作为修改已发布流程的标准路径)
- shape ↔ NodeType 映射表存放位置:建议放 Domain 层
WorkflowShapeMap静态类。 WorkflowGraph是否强类型化:建议实现WorkflowGraph/GraphNode/GraphEdge强类型模型(record),避免裸JsonDocument散落。是否纳入本次标准?(建议纳入 P1)- P1 切换窗口:存量实例 Version 回填存在近似(9.1),是否接受「P1 切换前暂停新发起、迁移完恢复」的停机窗口?还是要求零停机(零停机需更复杂的双写过渡)?
9.3 工作量预期(诚实声明,避免评审低估)
「X6 自建」意味着审批相关节点 UI 全部自写(基于现有代码,仓库无任何 X6 前端代码可复用):
- P2 设计器骨架(画布/拖拽/连线/保存):~1-2 周
- P3 节点配置弹窗是工作量重头,尤其:
- 审批人规则编辑器(单人/会签/或签、角色/部门/指定人、加签)—— 复杂,~1-2 周
- 条件表达式编辑器(变量选择 + 比较 + 与/或组合)—— 复杂,~1 周
- 抄送人选择、子流程选择、定时配置 —— 较简单,~3-5 天
- P1 后端改造(含快照表/Version 字段/Migration/引擎切换/迁移脚本):~1-1.5 周
P3 的复杂子项(审批人规则、条件表达式)建议在 P2 闭环验证后,另起详细子方案再实现,不在本文展开。
附录 A:关键文件路径清单
后端(build-flow.api/)
Domain
src/modules/workflow/Domain/Entities/WorkflowDefinition.cs— 定义实体(DefinitionJson 字段在此)src/modules/workflow/Domain/Entities/WorkflowNode.cs— 节点实体(迁移源)src/modules/workflow/Domain/Entities/WorkflowEdge.cs— 边实体(迁移源)src/modules/workflow/Domain/Entities/WorkflowInstance.cs— 实例实体(待新增 DefinitionVersion 字段)src/modules/workflow/Domain/Entities/FormDefinitionVersion.cs— 版本快照先例(wf_workflow_definition_snapshots 照此范式新建)WorkflowDefinitionSnapshot(待新增) — 发布快照实体,参照FormDefinitionVersionsrc/modules/workflow/Domain/Enums/NodeType.cs— 节点类型枚举(复用)src/modules/workflow/Domain/Enums/EdgeType.cs— 边类型枚举(复用)src/modules/workflow/Domain/Enums/DefinitionStatus.cs— 定义状态枚举(复用)
Application
src/modules/workflow/Application/Engine/ProcessEngine.cs— 流程引擎(改造核心,~1191 行)src/modules/workflow/Application/Engine/NodeConfigParser.cs— Config 解析器(需适配)src/modules/workflow/Application/Features/WorkflowDefinitions/Commands/UpdateWorkflowDefinitionCommand.cs— 更新命令src/modules/workflow/Application/Features/WorkflowDefinitions/Handlers/UpdateWorkflowDefinitionCommandHandler.cs— 更新处理器(仅覆盖字符串)src/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowDefinitionDto.cssrc/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowDefinitionDetailDto.cssrc/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowNodeDto.cssrc/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowEdgeDto.cs
Infrastructure
src/modules/workflow/Infrastructure/Persistence/WorkflowDbContext.cs— DbSet 定义(WorkflowNodes/WorkflowEdges)src/modules/workflow/Infrastructure/Persistence/Configurations/WorkflowDefinitionConfiguration.cssrc/modules/workflow/Infrastructure/Persistence/Configurations/WorkflowNodeConfiguration.cssrc/modules/workflow/Infrastructure/Persistence/Configurations/WorkflowEdgeConfiguration.cssrc/modules/workflow/Infrastructure/Persistence/SeedData.cs— 种子数据
Host
src/BuildFlow.Host/Endpoints/workflow/WorkflowDefinition/— 24 个 Endpoint/Request 文件(含 6 套待废弃 Node/Edge 接口)
Shared
src/shared/BuildFlow.Shared/Api/ApiResponse.cs— 响应信封src/shared/BuildFlow.Shared/Api/ApiResponseMiddleware.cs— 响应包裹中间件
前端(build-flow.web/apps/web-antdv-next/)
src/api/request.ts— API 客户端(requestClient)src/router/routes/modules/— 路由 module 目录(待新增workflow.ts)src/views/— 页面目录(待新增workflow/)src/api/core/— API 模块目录(待新增workflow.ts)
参考文档
- X6 官方文档:
https://x6.antv.antgroup.com/ - X6 Vue 节点:
https://x6.antv.antgroup.com/tutorial/intermediate/vue - X6 连接桩:
https://x6.antv.antgroup.com/tutorial/basic/port - rag-coding-web 技能:BuildFlow 管理端前端开发约定