基于 Clean Architecture + DDD 的轻量级工作流系统实践
本文介绍在一个 .NET 10 + Vue 3 的后台管理系统(Ncp.Admin)中,如何基于现有的 Clean Architecture + DDD 架构,从零构建一套轻量级审批工作流系统,涵盖后端领域建模、CQRS 命令查询、领域事件驱动的业务自动化,以及前端可视化流程节点设计器的完整实现。
一、项目背景与技术栈
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数据库默认 SQLite,MySQL 支持需额外配置复用现有 EF Core + MySQL.NET 版本Elsa 3.x 对 .NET 10 的兼容性需验证无兼容性风险最终选择了 自建方案 —— 对于审批类工作流,核心逻辑并不复杂,而自建方案可以完美融入现有 DDD 架构,代码风格统一,维护成本更低。
三、领域模型设计
3.1 聚合根划分
工作流系统划分为两个聚合:- WorkflowDefinition (流程定义聚合)
- ├── WorkflowDefinitionId // 强类型 ID
- ├── Name / Description / Category
- ├── Status (Draft → Published → Archived)
- ├── Version
- ├── Nodes: ICollection<WorkflowNode> // 流程节点(值对象集合)
- └── 领域方法: Publish(), Archive(), GetFirstApprovalNode(), GetNextApprovalNode()
- WorkflowInstance (流程实例聚合)
- ├── WorkflowInstanceId // 强类型 ID
- ├── WorkflowDefinitionId // 关联定义
- ├── BusinessKey / BusinessType
- ├── Status (Running → Completed/Rejected/Cancelled)
- ├── Variables // 业务数据 JSON
- ├── Tasks: ICollection<WorkflowTask> // 审批任务集合
- └── 领域方法: CreateTask(), ApproveTask(), RejectTask(), TransferTask(), Complete()
复制代码 3.2 强类型 ID
与项目现有模式一致,所有聚合根使用强类型 ID:- public partial record WorkflowDefinitionId : IGuidStronglyTypedId;
- public partial record WorkflowInstanceId : IGuidStronglyTypedId;
复制代码 3.3 流程定义聚合根
WorkflowDefinition 是流程模板的聚合根,封装了状态管理和 流程流转的领域逻辑:- public class WorkflowDefinition : Entity<WorkflowDefinitionId>, IAggregateRoot
- {
- public WorkflowDefinitionStatus Status { get; private set; }
- public virtual ICollection<WorkflowNode> 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 : Entity<WorkflowInstanceId>, 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)
- : ICommandHandler<StartWorkflowCommand, WorkflowInstanceId>
- {
- public async Task<WorkflowInstanceId> 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) : ICommandHandler
- {
- 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)
- : IDomainEventHandler<WorkflowInstanceCompletedDomainEvent>
- {
- 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.Deserialize<CreateUserVariables>(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 二次确认)
- 上下移动节点(调整审批顺序)
- 配置节点属性(名称、类型、处理人类型、处理人)
- 已发布流程只读,不可编辑
视觉设计:
- 开始/结束节点使用渐变色圆形标识
- 节点卡片顶部彩色色条标识类型(蓝色=审批、绿色=抄送、橙色=通知)
- 连接线带有方向箭头
- 悬浮动效(卡片微浮、操作按钮渐显、添加按钮缩放高亮)
- 表单双列布局节省空间
核心代码片段:
[code]// 节点类型视觉配置const nodeTypeConfig: Record = { 1: { color: '#1677ff', bg: '#e6f4ff', icon: '✓' }, // 审批 2: { color: '#52c41a', bg: '#f6ffed', icon: '
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |