基于 Clean Architecture + DDD 的轻量级工作流系统实践

发布时间:2026/5/21 3:38:10

基于 Clean Architecture + DDD 的轻量级工作流系统实践 一、项目背景与技术栈Ncp.Admin是一套采用 Clean Architecture 分层架构的后台管理系统技术栈如下层级技术选型前端Vue 3 Vite Ant Design Vue (Vben Admin)API 层ASP.NET Core FastEndpoints应用层MediatR (CQRS)、FluentValidation领域层DDD 聚合根、领域事件、强类型 ID基础设施EF Core Pomelo MySQL、Redis、CAP、Hangfire项目已经有完善的用户、角色、部门、权限管理模块。本次需求是在现有架构基础上增加一套审批工作流系统支持流程定义、流程发起、多级审批、驳回、转办等能力并实现新增用户需走审批流程的业务闭环。二、为什么不用 Elsa Workflows在技术选型阶段我们对比了 Elsa Workflows 和自建方案维度Elsa Workflows自建方案功能丰富度自带可视化设计器、条件分支、定时触发等按需实现功能精简学习成本需理解 Elsa 活动模型、序列化机制复用现有 DDD 模式团队零成本架构耦合引入独立的持久化层和运行时完全融入现有分层架构前端集成自带 Blazor/React 设计器与 Vue 生态不匹配原生 Vue 3 Ant Design Vue数据库默认 SQLiteMySQL 支持需额外配置复用现有 EF Core MySQL.NET 版本Elsa 3.x 对 .NET 10 的兼容性需验证无兼容性风险最终选择了自建方案—— 对于审批类工作流核心逻辑并不复杂而自建方案可以完美融入现有 DDD 架构代码风格统一维护成本更低。三、领域模型设计3.1 聚合根划分工作流系统划分为两个聚合WorkflowDefinition (流程定义聚合) ├── WorkflowDefinitionId // 强类型 ID ├── Name / Description / Category ├── Status (Draft → Published → Archived) ├── Version ├── Nodes: ICollectionWorkflowNode // 流程节点值对象集合 └── 领域方法: Publish(), Archive(), GetFirstApprovalNode(), GetNextApprovalNode() WorkflowInstance (流程实例聚合) ├── WorkflowInstanceId // 强类型 ID ├── WorkflowDefinitionId // 关联定义 ├── BusinessKey / BusinessType ├── Status (Running → Completed/Rejected/Cancelled) ├── Variables // 业务数据 JSON ├── Tasks: ICollectionWorkflowTask // 审批任务集合 └── 领域方法: CreateTask(), ApproveTask(), RejectTask(), TransferTask(), Complete()3.2 强类型 ID与项目现有模式一致所有聚合根使用强类型 IDpublic partial record WorkflowDefinitionId : IGuidStronglyTypedId; public partial record WorkflowInstanceId : IGuidStronglyTypedId;3.3 流程定义聚合根WorkflowDefinition是流程模板的聚合根封装了状态管理和流程流转的领域逻辑public class WorkflowDefinition : EntityWorkflowDefinitionId, IAggregateRoot { public WorkflowDefinitionStatus Status { get; private set; } public virtual ICollectionWorkflowNode Nodes { get; init; } []; // 状态变更 领域事件 public void Publish() { if (Status WorkflowDefinitionStatus.Published) throw new KnownException(流程定义已经发布, ErrorCodes.WorkflowDefinitionAlreadyPublished); Status WorkflowDefinitionStatus.Published; AddDomainEvent(new WorkflowDefinitionPublishedDomainEvent(this)); } // 流程流转逻辑下沉到聚合根而非 Handler public WorkflowNode? GetFirstApprovalNode() GetOrderedApprovalNodes().FirstOrDefault(); public WorkflowNode? GetNextApprovalNode(string currentNodeName) { var orderedNodes GetOrderedApprovalNodes(); var currentIndex orderedNodes.ToList().FindIndex(n n.NodeName currentNodeName); return (currentIndex 0 currentIndex orderedNodes.Count - 1) ? orderedNodes[currentIndex 1] : null; } }DDD 要点流转逻辑获取首节点、下一节点放在WorkflowDefinition聚合根而非 Command Handler 中。Handler 只负责编排调度领域逻辑由聚合根保护。3.4 流程实例聚合根WorkflowInstance管理一次具体的审批流程执行public class WorkflowInstance : EntityWorkflowInstanceId, IAggregateRoot { public string Variables { get; private set; } {}; // 业务数据 JSON public WorkflowTask CreateTask(string nodeName, WorkflowTaskType taskType, UserId assigneeId, string assigneeName) { var task new WorkflowTask(nodeName, taskType, assigneeId, assigneeName); Tasks.Add(task); CurrentNodeName nodeName; AddDomainEvent(new WorkflowTaskCreatedDomainEvent(this, task)); return task; } public void ApproveTask(WorkflowTaskId taskId, UserId operatorId, string comment) { var task Tasks.FirstOrDefault(t t.Id taskId) ?? throw new KnownException(未找到该任务, ErrorCodes.WorkflowTaskNotFound); task.Approve(comment); AddDomainEvent(new WorkflowTaskCompletedDomainEvent(this, task)); } public void Complete() { Status WorkflowInstanceStatus.Completed; CompletedAt DateTimeOffset.UtcNow; AddDomainEvent(new WorkflowInstanceCompletedDomainEvent(this)); } }四、CQRS 命令与查询4.1 发起流程命令StartWorkflowCommand演示了 Handler 如何编排聚合根交互public class StartWorkflowCommandHandler( IWorkflowDefinitionRepository definitionRepository, IWorkflowInstanceRepository instanceRepository) : ICommandHandlerStartWorkflowCommand, WorkflowInstanceId { public async TaskWorkflowInstanceId Handle(StartWorkflowCommand request, CancellationToken ct) { var definition await definitionRepository.GetAsync(request.WorkflowDefinitionId, ct) ?? throw new KnownException(未找到流程定义); // 创建实例 var instance new WorkflowInstance( request.WorkflowDefinitionId, definition.Name, request.BusinessKey, request.BusinessType, request.Title, request.InitiatorId, request.InitiatorName, request.Variables, request.Remark); await instanceRepository.AddAsync(instance, ct); // 通过聚合根领域方法获取第一个审批节点逻辑在 Definition 中 var firstNode definition.GetFirstApprovalNode(); if (firstNode ! null long.TryParse(firstNode.AssigneeValue, out var id)) { instance.CreateTask(firstNode.NodeName, WorkflowTaskType.Approval, new UserId(id), string.Empty); } return instance.Id; } }4.2 审批命令 — 自动流转public class ApproveTaskCommandHandler( IWorkflowInstanceRepository instanceRepository, IWorkflowDefinitionRepository definitionRepository) : ICommandHandlerApproveTaskCommand { public async Task Handle(ApproveTaskCommand request, CancellationToken ct) { var instance await instanceRepository.GetAsync(request.WorkflowInstanceId, ct); instance.ApproveTask(request.TaskId, request.OperatorId, request.Comment); var definition await definitionRepository.GetAsync(instance.WorkflowDefinitionId, ct); var approvedTask instance.Tasks.First(t t.Id request.TaskId); // 领域方法获取下一节点 var nextNode definition.GetNextApprovalNode(approvedTask.NodeName); if (nextNode ! null) { // 创建下一个审批任务 instance.CreateTask(nextNode.NodeName, WorkflowTaskType.Approval, ...); } else { // 所有节点审批完毕流程完成 instance.Complete(); } } }五、领域事件驱动的业务自动化5.1 领域事件定义public record WorkflowDefinitionPublishedDomainEvent(WorkflowDefinition WorkflowDefinition) : IDomainEvent; public record WorkflowInstanceStartedDomainEvent(WorkflowInstance WorkflowInstance) : IDomainEvent; public record WorkflowInstanceCompletedDomainEvent(WorkflowInstance WorkflowInstance) : IDomainEvent; public record WorkflowTaskCreatedDomainEvent(WorkflowInstance WorkflowInstance, WorkflowTask WorkflowTask) : IDomainEvent; public record WorkflowTaskCompletedDomainEvent(WorkflowInstance WorkflowInstance, WorkflowTask WorkflowTask) : IDomainEvent;5.2 审批通过后自动执行业务操作这是整个系统的亮点设计 —— 通过领域事件实现流程与业务的解耦public class WorkflowInstanceCompletedDomainEventHandler(IMediator mediator, RoleQuery roleQuery) : IDomainEventHandlerWorkflowInstanceCompletedDomainEvent { public async Task Handle(WorkflowInstanceCompletedDomainEvent domainEvent, CancellationToken ct) { var instance domainEvent.WorkflowInstance; if (instance.Status ! WorkflowInstanceStatus.Completed) return; switch (instance.BusinessType) { case CreateUser: await HandleCreateUser(instance, ct); break; // 后续可扩展case PurchaseOrder: ... } } private async Task HandleCreateUser(WorkflowInstance instance, CancellationToken ct) { // 从 Variables JSON 中反序列化用户数据 var userData JsonSerializer.DeserializeCreateUserVariables(instance.Variables); // 复用现有的 CreateUserCommand var cmd new CreateUserCommand( userData.Name, userData.Email, userData.Password, ...); await mediator.Send(cmd, ct); } }设计思想前端提交审批时将完整的业务数据如用户信息序列化为 JSON 存入Variables字段。审批通过后领域事件处理器从Variables中反序列化数据调用对应的业务 Command 完成操作。这样工作流引擎本身不需要了解任何业务细节新增业务类型只需在switch中扩展即可。六、前端可视化节点设计器6.1 设计思路传统的做法是让用户编辑 JSON 来配置流程节点这显然不够友好。我们实现了一个基于竖向流程图的可视化节点设计器[▶ 开始] │ ↓ ┌──────────────┐ │ ✓ 主管审批 │ ← 可编辑卡片 │ 类型: 审批 │ │ 处理人: 张三 │ └──────────────┘ │ () ← 点击插入新节点 │ ┌──────────────┐ │ ✓ 总监审批 │ │ 类型: 审批 │ │ 处理人: 李四 │ └──────────────┘ │ ↓ [■ 结束]6.2 组件实现node-designer.vue是一个完整的 Vue 3 组件核心设计如下交互能力添加节点顶部、中间、底部均可插入删除节点带 Popconfirm 二次确认上下移动节点调整审批顺序配置节点属性名称、类型、处理人类型、处理人已发布流程只读不可编辑视觉设计开始/结束节点使用渐变色圆形标识节点卡片顶部彩色色条标识类型蓝色审批、绿色抄送、橙色通知连接线带有方向箭头悬浮动效卡片微浮、操作按钮渐显、添加按钮缩放高亮表单双列布局节省空间核心代码片段// 节点类型视觉配置 const nodeTypeConfig: Recordnumber, { color: string; bg: string; icon: string } { 1: { color: #1677ff, bg: #e6f4ff, icon: ✓ }, // 审批 2: { color: #52c41a, bg: #f6ffed, icon: }, // 抄送 3: { color: #faad14, bg: #fffbe6, icon: }, // 通知 };6.3 分类下拉选择流程定义的「分类」字段从自由文本输入改为下拉选择统一维护枚举值export function useCategoryOptions() { return [ { label: 用户管理, value: UserManagement }, { label: 角色管理, value: RoleManagement }, { label: 请假审批, value: LeaveRequest }, { label: 采购审批, value: PurchaseOrder }, { label: 报销审批, value: Reimbursement }, { label: 通用流程, value: General }, ]; }前端查找对应流程定义时使用枚举值精确匹配不再依赖中文字符串const userCreateDef definitions.find( (d) d.category UserManagement, );七、操作指南如何创建流程与审批下面从使用角度说明如何创建一条自定义工作流程以及审批人在哪里处理待办。7.1 如何创建一个自定义工作流程进入工作流管理 → 流程定义。点击新增打开流程定义表单。填写流程名称、分类从下拉选择如「用户管理」「请假审批」等、描述。在流程节点设计区域配置审批节点点击「 添加节点」或节点之间的「」插入节点为每个节点填写节点名称选择节点类型审批 / 抄送 / 通知选择处理人类型指定用户、指定角色、部门主管、发起人自选若为指定用户或指定角色再选择具体处理人通过上移 / 下移调整节点顺序通过删除移除节点。保存后在列表中找到该流程点击发布。只有已发布的流程才能被发起。流程定义列表可新增、编辑、发布、归档编辑时在下方进行流程节点设计。7.2 在哪里审批审批人的待办任务在工作流管理 → 我的待办中处理登录后进入工作流管理菜单点击我的待办。列表中展示当前用户作为处理人的所有待审批任务流程标题、流程名称、发起人、节点名称等。点击办理进入详情可查看流程信息与业务数据如用户申请内容进行通过、驳回或转办操作。已处理的任务可在我的已办中查看历史记录。我的待办审批人在此处理待审批任务。八、新增用户走审批流程 — 完整链路这是一个典型的端到端示例展示工作流如何与具体业务打通8.1 前端 — 提交审批在系统管理 → 用户管理的新增用户表单中提供「提交审批」按钮。用户填写完账号、姓名、角色等信息后可选择直接保存若有权限或提交审批。点击「提交审批」后验证表单数据查询已发布的流程定义匹配分类为「用户管理」的定义将用户表单数据 JSON 序列化为variables调用startWorkflowAPI 发起审批新增用户时可选择「提交审批」进入已配置的用户管理审批流程。async function onSubmitForApproval() { const { valid } await formApi.validate(); if (!valid) return; const definitions await getPublishedDefinitions(); const userCreateDef definitions.find(d d.category UserManagement); const formValues await formApi.getValues(); const variables JSON.stringify({ name: formValues.name, email: formValues.email, password: formValues.password, realName: formValues.realName, roleIds: formValues.roleIds || [], // ... 其他字段 }); await startWorkflow({ workflowDefinitionId: userCreateDef.id, businessKey: user-create-${Date.now()}, businessType: CreateUser, title: 新增用户申请 - ${formValues.realName}, variables, }); }8.2 后端 — 审批流转提交审批 → StartWorkflowCommand → 创建 WorkflowInstance → Definition.GetFirstApprovalNode() → 创建第一个 Task 审批通过 → ApproveTaskCommand → Instance.ApproveTask() → Definition.GetNextApprovalNode() → 有下一节点 → 创建新 Task → 无下一节点 → Instance.Complete() → 触发 WorkflowInstanceCompletedDomainEvent 领域事件 → WorkflowInstanceCompletedDomainEventHandler → BusinessType CreateUser → 反序列化 Variables → CreateUserCommand → 用户创建成功8.3 流程图[用户填写表单] → [提交审批] → [主管审批] → [总监审批] → [审批通过] ↓ [领域事件触发] ↓ [自动创建用户]九、架构亮点总结9.1 DDD 原则贯穿始终原则实践聚合根封装状态变更、流转逻辑、业务规则校验均在聚合根内领域事件每个关键状态变更都发布对应领域事件强类型 IDWorkflowDefinitionId、WorkflowInstanceId避免 ID 误用值对象WorkflowNode作为WorkflowDefinition的子实体集合9.2 CQRS 分离清晰Command 侧StartWorkflowCommand、ApproveTaskCommand、RejectTaskCommand等Handler 只做编排Query 侧WorkflowDefinitionQuery、WorkflowInstanceQuery使用AsNoTracking()优化性能配合IMemoryCache缓存高频数据9.3 业务与流程解耦工作流引擎通用 业务处理特定 ───────────────── ───────────────── WorkflowInstance ↗ CreateUserCommand .Complete() │ → DomainEvent ──────→ EventHandler (switch BusinessType) │ ↘ 其他业务 Command新增业务类型时前端新增提交入口传入businessType和variables后端在WorkflowInstanceCompletedDomainEventHandler的switch中增加分支流程引擎本身无需任何修改9.4 前端体验优化可视化节点设计器替代 JSON 编辑降低使用门槛分类枚举化避免自由文本带来的匹配错误i18n 国际化支持中英文已发布流程自动锁定为只读模式十、后续规划条件分支节点根据表单字段值走不同审批路径会签/或签一个节点可配置多个审批人审批催办基于 Hangfire 定时检查超时任务流程统计看板审批效率、瓶颈节点分析移动端适配审批任务推送 移动端快速审批

相关新闻