基于 Clean Architecture + DDD 的轻量级工作流系统实践
基于 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<ApproveTaskCommand>
{
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 二次确认)
- 上下移动节点(调整审批顺序)
- 配置节点属性(名称、类型、处理人类型、处理人)
- 已发布流程只读,不可编辑
视觉设计:
- 开始/结束节点使用渐变色圆形标识
- 节点卡片顶部彩色色条标识类型(蓝色=审批、绿色=抄送、橙色=通知)
- 连接线带有方向箭头
- 悬浮动效(卡片微浮、操作按钮渐显、添加按钮缩放高亮)
- 表单双列布局节省空间
核心代码片段:
// 节点类型视觉配置
const nodeTypeConfig: Record<number, { 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 原则贯穿始终
| 原则 | 实践 |
|---|---|
| 聚合根封装 | 状态变更、流转逻辑、业务规则校验均在聚合根内 |
| 领域事件 | 每个关键状态变更都发布对应领域事件 |
| 强类型 ID | WorkflowDefinitionId、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 定时检查超时任务
- 流程统计看板:审批效率、瓶颈节点分析
- 移动端适配:审批任务推送 + 移动端快速审批
十一、结语
一套好的工作流系统,核心不在于功能有多丰富,而在于 与现有架构的融合度 和 业务扩展的便捷性。
本次实践证明,在 Clean Architecture + DDD 的项目中,自建轻量级工作流是完全可行的。通过聚合根封装流转逻辑、领域事件驱动业务自动化、CQRS 分离读写关注点,我们用不到 2000 行后端代码就实现了一套 可用、可扩展、架构一致 的审批系统。
前端方面,一个 600 行的 Vue 组件就搭建起了直观的可视化节点设计器,配合 Ant Design Vue 的组件库,用户体验也做到了开箱即用。
不是所有场景都需要引入重量级的工作流引擎,适合的才是最好的。

浙公网安备 33010602011771号