# 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/`: ```csharp // 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 目标 1. 定义**与 X6 `toJSON()`/`fromJSON()` 原生兼容**的 `definition_json` schema,作为前后端唯一对接契约。 2. 让 `definition_json` 升级为后端**唯一事实源**,`ProcessEngine` 改读 JSON。 3. 简化接口:废弃逐 Node/Edge CRUD,改为**整图保存**。 4. 前端用 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 显示: ```ts 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}` 两种等价形式,本契约选用前者更易读写): ```json { "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 一致) ```json "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_` | 前端生成(拖入时),后端保存时校验唯一 | | 边 | `edge_` | 前端生成(连线时),后端保存时校验唯一 | | 端口 | `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 完整示例(最小可运行图) ```json { "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 Nodes` + `List 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"`)。**发布期校验必须覆盖引擎运行时会抛异常的所有情形**,避免发布出运行即崩溃的流程。 **结构校验:** 1. **唯一 Start**:有且仅有一个 `shape === "wf-start"` 节点。 2. **至少一个 End**:存在 `shape === "wf-end"` 节点。 3. **无悬空边**:每条边的 `source.cell` / `target.cell` 都能在 `nodes[].id` 中找到。 4. **可达性**:从 Start 节点出发,所有 End 节点可达(DFS/BFS)。 5. **shape 合法**:所有 `shape` 在 5.5 映射表内。 6. **id 唯一**:节点 id、边 id 全局唯一;边的 `source.cell`/`target.cell` 引用有效。 **引擎强约束校验(必须,否则运行时抛 BusinessException):** 7. **Approval 节点出边完备性**:每个 `wf-approval` 节点必须至少有一条 `data.edgeType=Approved` 或 `data.edgeType=Normal` 的出边(通过路径);若业务允许驳回,还必须有一条 `data.edgeType=Rejected` 的出边。否则审批任务完成时引擎找不到匹配边直接异常。 8. **Condition / ExclusiveGateway 节点分支评估**:每个 `wf-condition` / `wf-exclusive` 节点的所有出边必须带 `data.condition` 和 `data.order`;**强烈建议提供一条兜底边**(condition 为 `true` 或空),否则当所有分支都不匹配时引擎抛 `"No condition branch matched"`。 9. **SubProcess 节点引用有效**:`wf-subprocess` 节点的 `data.definitionId` 必须指向一个已发布(`Published`)的流程定义。 > 校验失败统一返回 `ValidationErrorResponse`(沿用现有 `BuildFlow.Shared/Api/` 的校验失败信封)。 --- ## 第 7 章 迁移策略(关系表 → JSON) ### 7.1 核心难点:已发布定义的不可变性与运行实例的版本隔离 > ⚠️ **本节是整个迁移方案的关键,直接决定方案是否可行。** **现有代码的事实缺陷(必须先正视)**: 1. **WorkflowInstance 没有 Version 字段**(基于现有代码 `Domain/Entities/WorkflowInstance.cs`):实例仅存 `DefinitionId`。 2. **ProcessEngine 按 DefinitionId 实时读图**(基于现有代码 `Application/Engine/ProcessEngine.cs` 的 `StartAsync`):`WorkflowNodes.Where(n => n.DefinitionId == instance.DefinitionId)`,无版本隔离。 3. **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)`。 **写入时机(发布流程)**: 1. `PublishWorkflowDefinitionCommandHandler`(基于现有代码)在做 `Status=Published; Version++` **之前**,先做 6.3 全部校验。 2. 校验通过后,把当前 `definition_json` 复制成一条 `WorkflowDefinitionSnapshot` 记录(Version = 新版本号)。 3. 快照写入后,引擎运行实例只读快照表,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` 增加状态校验(基于现有代码,当前缺失): ```csharp // 仅 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.Name` - `edge.SourceNodeId/TargetNodeId` → `edge.source.cell/target.cell` - `edge.EdgeType/Condition/Order` → `edge.data.edgeType/condition/order` - 节点/边 id 统一为 `node_` / `edge_` - 为每个已 `Published` 的定义补写一条 `WorkflowDefinitionSnapshot` 快照(Version 取定义当前 Version)。 - 为每个运行中的 `WorkflowInstance` 回填 `DefinitionVersion`(取其定义当前 Version)。 **第四步(P4):关系表降级 → 最终移除** - 关系表降级为只读快照(过渡期)。 - 新版本:移除 `WorkflowNodes` / `WorkflowEdges` DbSet、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 迁移回滚预案 生产数据迁移必须有回滚方案: 1. **迁移前备份**:迁移脚本执行前,对 `wf_workflow_definitions.definition_json` 原值、`wf_workflow_nodes`、`wf_workflow_edges` 做全表 SQL 备份(`pg_dump` 相关表)。 2. **dry-run 模式**:脚本支持 `--dry-run`,仅输出迁移报告(将生成多少条 JSON、多少条快照、回填多少实例),不写库。 3. **分批 + 可重入**:按 `DefinitionId` 分批处理,每批独立事务;脚本可重复执行(幂等:`definition_json` 已非空则跳过)。 4. **回滚**:若迁移后引擎异常,回滚步骤: - 恢复 `definition_json` 原值(备份还原); - 保留关系表数据(P4 之前关系表不 drop,是安全网); - ProcessEngine 通过特性开关回退到读关系表的旧路径(P1 改造时即应保留双读开关,详见 9.1)。 5. **关系表保留期**:`wf_workflow_nodes` / `wf_workflow_edges` 在 P4 之前**禁止 drop**,作为整个迁移期的最终回滚保障。 --- ## 第 8 章 分阶段实施路线 | 阶段 | 内容 | 交付物 | |---|---|---| | **P0** | 文档评审(本文) | ✅ 本文档定稿 | | **P1** | 后端契约改造 | ✅ 已实现:`WorkflowGraph`/`WorkflowShapeMap` 解析层、`WorkflowDefinitionSnapshot` 快照表(含 Migration)、`WorkflowInstance.DefinitionVersion` 字段、Published 锁定(Draft 校验)、ProcessEngine 适配层切换读快照(双读开关)、整图保存接口、`WorkflowGraphValidator` 发布校验(9 条规则)、`WorkflowGraphMigration` 存量迁移脚本(幂等,接入 DatabaseInitializer)。423 单测全通过。 | | **P2** | 前端设计器骨架 | ✅ 已实现:X6 画布(复刻 AgentFlow 配置)+ Stencil 拖拽面板(3 分组 9 节点)+ 连线 + 端口 hover 显隐 + 整图 toJSON/fromJSON 保存加载闭环。typecheck + build 通过。 | | **P3** | 节点配置弹窗 | ✅ 已实现:节点配置抽屉(双击打开,按 NodeType 动态渲染表单)含审批(approvalMode/assigneeRule/timeoutMinutes/钩子)、抄送(recipients)、子流程(definitionId)、定时(delayMinutes/targetTime)。 | | **P4** | 收尾 | ⏳ 待办:列表/发布页(列表页已做,启停页待补)、关系表 drop、6 套废弃接口移除。注:6 套接口已标 `[Obsolete]`。 | --- ## 第 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 待决项(需评审拍板) 1. **节点/边 ID 生成方**:当前建议前端生成 `node_` / `edge_`,后端透传。是否允许后端保存时重写 id?(影响 `source.cell` 引用稳定性) 2. **Published 流程的派生编辑**:7.3 锁定 Published 后,是否提供「复制已发布流程 → 新 Draft」的派生编辑能力?(建议提供,作为修改已发布流程的标准路径) 3. **shape ↔ NodeType 映射表存放位置**:建议放 Domain 层 `WorkflowShapeMap` 静态类。 4. **`WorkflowGraph` 是否强类型化**:建议实现 `WorkflowGraph`/`GraphNode`/`GraphEdge` 强类型模型(record),避免裸 `JsonDocument` 散落。是否纳入本次标准?(建议纳入 P1) 5. **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`(**待新增**) — 发布快照实体,参照 `FormDefinitionVersion` - `src/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.cs` - `src/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowDefinitionDetailDto.cs` - `src/modules/workflow/Application/Features/WorkflowDefinitions/DTOs/WorkflowNodeDto.cs` - `src/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.cs` - `src/modules/workflow/Infrastructure/Persistence/Configurations/WorkflowNodeConfiguration.cs` - `src/modules/workflow/Infrastructure/Persistence/Configurations/WorkflowEdgeConfiguration.cs` - `src/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 管理端前端开发约定