架构设计 · DDD · .NET Core · 高内聚低耦合 · 2026-08
你有没有接手过这样的系统——十几个业务模块,每个模块都有自己的 Controller、Service、Entity,看似各自独立,但当你读了三五个模块的代码后,会发现它们在反复做同样的事情:审批、附件、单号生成、状态流转、多币种金额、船岸数据同步……
这些共性能力散落在各个模块中,被复制、被微调、被逐渐写出差异。结果是:一个审批逻辑的 bug 要在四个模块里各修一遍,一个新模块要从零搭一遍基础设施。
本文记录我们如何从一个船舶管理系统(PMS)的 15 个业务模块中,系统性地抽取出共性业务,并基于 .NET Core 和 DDD 重新设计架构。
1. 先看全貌:15 个模块在做什么
| 模块 | 核心能力 | 有没有审批 | 有没有附件 | 有没有单号 |
|---|---|---|---|---|
| 系统基础与公共模块 | 登录、工作台、任务中心 | — | ✅ | — |
| 设备管理 | 设备台账、计数器、保养触发 | ✅ 设备卡片 | ✅ | — |
| 维修保养计划与工单 | 计划、工单、完工报告 | ✅ 工单审批 | ✅ | ✅ 工单号 |
| 备件管理 | 备件主数据、采购、库存 | ✅ 四级审批 | ✅ | ✅ 申请/订单号 |
| 物料/物资管理 | 申请、询价、采购、库存、费用 | ✅ 三级审批 | ✅ | ✅ 申请/询价/订单号 |
| 船舶修理 | 工程单、询价、完工、结算 | ✅ 审批 | ✅ | ✅ 修理工程号 |
| 预算管理 | 预算编制、执行、控制 | ✅ | — | — |
| 安全管理 | 检查、隐患、整改 | ✅ | ✅ | — |
| 供应商管理 | 供应商准入、评估 | ✅ 准入审批 | ✅ | — |
| 台账/手册管理 | 手册、图纸、证书 | — | ✅ | — |
| 字典管理 | 各类基础数据 | — | — | — |
| 工作流集成 | 流程引擎、任务流转 | ✅ | — | — |
| 微信端 | 移动审批、查询 | ✅ | ✅ | — |
| 报表与报告 | 统计报表 | — | — | — |
| 系统管理 | 用户、角色、权限、配置 | — | — | — |
看到这张表,你发现了什么?
💬 互动一下:在继续往下读之前,你能从这张表中圈出几个反复出现的共性能力?把你想到的列出来,然后和我们的分析对比一下。
2. 共性能力的七个维度
我们把 15 个模块的代码逐一读过之后,归纳出七个维度的共性:
2.1 数据基类:每个实体都在重复的字段
读代码的第一个发现是:几乎所有业务实体都继承自同一个基类PmsBaseEntity,它包含五个字段:
publicclassPmsBaseEntity{publicint?IsDelete{get;set;}// 软删除标志publicstringDataFile{get;set;}// 数据来源/文件标识publicint?SendFlag{get;set;}// 船岸同步标志publicstringOpuser{get;set;}// 最后操作人publicDateTime?Opdate{get;set;}// 最后操作时间}这五个字段揭示了四个横切关注点:软删除、审计追踪、数据溯源、船岸同步。但在原始实现中,这些字段的赋值逻辑散落在各个 Service 中——有的模块在 Update 时设了 Opuser/Opdate,有的忘了设;SendFlag 的重置逻辑也不一致。
重构方向:在 EF Core 的SaveChanges重写中统一审计字段赋值,业务代码不再手动设置。
2.2 审批流:同一个模式,不同的级别
这是最突出的共性。备件采购是四级审批,物料申请是三级审批,修理工程是二级审批,设备卡片是一级审批。它们的代码结构几乎一样:
Manager1 + ApproveNum1 + AppMemo1 → Manager2 + ApproveNum2 + AppMemo2 → ...每加一个模块,就复制一套审批字段、一套审批 Controller、一套审批逻辑。审批级别的差异是唯一的变量,但这个变量被硬编码在了每个模块中。
重构方向:
- 用Strategy 模式抽象审批策略(不同级别、不同审批人解析规则)
- 用Template Method 模式固化审批流程骨架(提交→校验→逐级审批→通知→后置处理)
- 用Mediator 模式对接工作流引擎(参考原系统 WF5 的 NodeMediator 设计)
- 审批结果通过领域事件通知下游模块
2.3 状态机:被 switch-case 淹没的业务规则
申请单有状态(草稿→审批中→已批准→已驳回→已转订单),采购订单有状态(草稿→已审核→已发送→部分到货→全部到货→已验收→已结算→已付款),工单有状态,设备卡片有状态。
原始代码中,状态流转靠Status字段的整数和 Controller 中的 switch/if 判断维护。这导致两个问题:
- 哪些状态转换是合法的?散落在各处,没有统一约束
- 状态转换时应该触发什么动作?比如"审批通过"要发通知、要生成下游单据——这些逻辑混在 Controller 里
重构方向:用State 模式为每个聚合根实现显式状态机,状态转换规则和副作用集中管理。
2.4 单号生成:规则不同但模式一致
申请单号、询价单号、订单号、工单号——每个模块都有自己的单号生成类。我们读了其中一个:
格式:(年)公司码-部门码-业务码-流水号-船舶ID-船岸标识 示例:(2026)HKMW-TEC-RP-001-SHIP001-S每个单号生成器做的事情一样:拼前缀 → 查当前最大号 → +1 → 拼后缀。区别只在前缀各段的取值来源。
重构方向:用Factory 模式+ 配置化规则,统一单号生成器,前缀各段通过配置或策略注入。
2.5 多币种金额:值对象的天然候选
备件采购和物料采购的实体上都有这些字段:Currency、CurRate、UsdRate、TotalPrice。费用实体甚至有三套币种:报价币种(QCurrency)、结算币种(Currency)、承运币种(CarCurrency)。
这些字段在原始实现中是平铺的,金额计算(汇率转换、汇总)散落在 Service 中,容易出错。
重构方向:设计Money值对象,封装金额、币种和汇率转换逻辑。
2.6 船岸同步:每个实体上的 SendFlag
SendFlag出现在几乎所有实体上。船端操作后标记为"待同步",岸端接收后标记为"已同步"。但同步策略(哪些数据双向同步、冲突怎么解决)在代码中没有统一设计。
重构方向:用Strategy 模式抽象同步策略,结合Outbox 模式保证可靠同步,SendFlag的状态转换由基础设施统一管理。
2.7 附件:全系统共享但各自关联
附件通过attachment_config配置与业务单据关联,每个业务模块都要调附件上传/下载接口,但关联关系的维护是各模块自己做的。
重构方向:附件作为通用基础设施,通过领域事件(如OrderCreated)自动建立关联,业务模块不需要直接操作附件表。
3. 共性矩阵:哪些是核心域,哪些是支撑域
用 DDD 的视角,我们把这些共性能力重新分类:
┌─────────────────────────────────────────────────────┐ │ 业务模块(核心域) │ │ 设备管理 │ 维修保养 │ 备件采购 │ 物料供应 │ 船舶修理 │ └──────────────────────┬──────────────────────────────┘ │ 依赖 ┌──────────────────────▼──────────────────────────────┐ │ 领域公共内核(Shared Kernel) │ │ 审计基类 │ 状态机 │ 审批抽象 │ 单号生成 │ Money值对象 │ │ 仓储接口 │ 领域事件 │ 规约模式 │ 结果模式 │ 异常体系 │ └──────────────────────┬──────────────────────────────┘ │ 实现 ┌──────────────────────▼──────────────────────────────┐ │ 基础设施层(Infrastructure) │ │ EF Core仓储 │ WF5工作流适配 │ 附件服务 │ 邮件/消息 │ │ Redis缓存 │ 船岸同步 │ 单号生成器 │ JWT认证 │ 日志 │ └─────────────────────────────────────────────────────┘这里的关键判断是:审批、状态机、单号、多币种这些不是某个模块的私有逻辑,而是所有业务模块共享的"领域内核"。它们应该放在独立的 Shared Kernel 中,而不是复制在每个模块里。
💬 互动一下:你所在的项目中,有没有类似的"被复制的共性"?比如审批流、编号生成、多租户隔离。你们是怎么处理的?是抽到公共库里,还是每个模块各写一套?欢迎在评论区分享你的做法。
4. .NET Core 分层架构
基于以上分析,我们设计了新的 .NET Core 解决方案结构:
Pms.sln ├── Pms.Domain # 领域层(零外部依赖) │ ├── Shared # 公共内核:AggregateRoot, Entity, ValueObject │ ├── Devices # 设备管理限界上下文 │ ├── Parts # 备件管理限界上下文 │ ├── Materials # 物料管理限界上下文 │ └── ... # 其他上下文 ├── Pms.Application # 应用层(用例编排) │ ├── Abstractions # ICommand, IQuery, IEventHandler │ ├── Devices │ ├── Parts │ └── ... ├── Pms.Infrastructure # 基础设施层(技术实现) │ ├── Persistence # EF Core DbContext, Repository │ ├── Workflow # WF5 工作流引擎适配(ACL) │ ├── Attachments # 附件存储 │ ├── Numbering # 单号生成 │ ├── Sync # 船岸同步 │ └── Notifications # 消息通知 ├── Pms.Api # API层(Controller, Middleware, Filter) ├── Pms.Contracts # 跨域契约(集成事件、ACL接口) └── Pms.Common # 纯工具层(不包含业务逻辑)分层的核心原则:
- 领域层零外部依赖——不引用 EF Core、不引用 ASP.NET Core,只依赖 .NET BCL
- 应用层依赖领域层抽象——通过接口调用仓储和工作流,不关心实现
- 基础设施层依赖领域层——实现领域层定义的仓储接口、工作流接口
- API 层只做路由和序列化——业务逻辑在应用层,业务规则在领域层
这就是经典的依赖倒置原则(DIP):高层模块不依赖低层模块,二者都依赖抽象。
5. 设计模式如何落地
架构文档中详细展开了 14 种设计模式的 .NET Core 实现。这里先预览三个最有代表性的。
5.1 Strategy:审批策略
备件采购四级审批、物料三级审批、修理二级审批——审批级别不同,但流程骨架相同。
// 审批策略接口publicinterfaceIApprovalStrategy{stringBusinessType{get;}intMaxLevel{get;}Task<string>ResolveApproverAsync(ApprovalContextcontext,intlevel);Task<ApprovalResult>ApproveAsync(ApprovalContextcontext,ApprovalDecisiondecision);}// 备件四级审批publicclassPartsApprovalStrategy:IApprovalStrategy{publicstringBusinessType=>"Parts";publicintMaxLevel=>4;// ...}// 物料三级审批publicclassMaterialApprovalStrategy:IApprovalStrategy{publicstringBusinessType=>"Material";publicintMaxLevel=>3;// ...}通过 DI 注入所有策略,按业务类型选择:
publicclassApprovalService{privatereadonlyIEnumerable<IApprovalStrategy>_strategies;publicApprovalService(IEnumerable<IApprovalStrategy>strategies)=>_strategies=strategies;publicTask<ApprovalResult>ApproveAsync(stringbusinessType,...){varstrategy=_strategies.First(s=>s.BusinessType==businessType);// 模板方法驱动审批流程returnExecuteApprovalFlowAsync(strategy,context);}}5.2 State 模式:单据状态机
publicabstractclassDocumentState{publicabstractDocumentStatusStatus{get;}publicabstractIReadOnlyCollection<DocumentStatus>AllowedTransitions{get;}publicvirtualResultCanTransitionTo(DocumentStatustarget)=>AllowedTransitions.Contains(target)?Result.Success():Result.Fail($"不允许从{Status}转换到{target}");}publicclassDraftState:DocumentState{publicoverrideDocumentStatusStatus=>DocumentStatus.Draft;publicoverrideIReadOnlyCollection<DocumentStatus>AllowedTransitions=>new[]{DocumentStatus.PendingApproval,DocumentStatus.Cancelled};}publicclassPendingApprovalState:DocumentState{publicoverrideDocumentStatusStatus=>DocumentStatus.PendingApproval;publicoverrideIReadOnlyCollection<DocumentStatus>AllowedTransitions=>new[]{DocumentStatus.Approved,DocumentStatus.Rejected,DocumentStatus.Draft};}新增一个状态?只需要加一个类。修改转换规则?只改一个地方。再也不用在 Controller 里写if (status == 3 && newStatus == 5)。
5.3 Mediator + 领域事件:模块间解耦
原始系统中,审批通过后要做什么?直接在审批 Controller 里调库存、调通知、调报表——编译时依赖,紧耦合。
重构后,审批通过只发布一个领域事件:
publicclassPurchaseRequestApprovedEvent:IDomainEvent{publicstringRequestId{get;init;}publicstringApprovedBy{get;init;}publicDateTimeApprovedAt{get;init;}}谁需要响应这个事件,谁就订阅:
publicclassStockReservationHandler:INotificationHandler<PurchaseRequestApprovedEvent>{publicTaskHandle(PurchaseRequestApprovedEventev,CancellationTokenct){// 库存预留逻辑}}publicclassNotificationHandler:INotificationHandler<PurchaseRequestApprovedEvent>{publicTaskHandle(PurchaseRequestApprovedEventev,CancellationTokenct){// 发送通知}}审批模块完全不知道谁在响应事件。新增一个订阅者不需要改审批模块的代码。
6. 改造的优先级建议
不是所有共性都要在第一天就抽出来。我们建议按以下优先级推进:
| 优先级 | 共性能力 | 理由 |
|---|---|---|
| P0 | 审计基类 + 软删除统一处理 | 改动量小、收益大、零风险 |
| P0 | 仓储 + Unit of Work | 所有模块都依赖,是后续重构的基础 |
| P0 | Result 模式 + 统一异常 | 改善 API 一致性,减少异常滥用 |
| P1 | 审批策略 + 模板方法 | 解决最大的重复代码问题 |
| P1 | 状态机 | 防止非法状态转换,集中业务规则 |
| P1 | Money 值对象 | 金额计算出错代价高,值得抽象 |
| P2 | 单号生成工厂 | 不影响业务逻辑,但减少样板代码 |
| P2 | 领域事件 + Mediator | 解耦效果好,但引入了异步复杂度 |
| P2 | 附件/通知通用化 | 需要和基础设施改造配合 |
| P3 | 船岸同步策略 | 复杂度高,建议在核心模块稳定后推进 |
7. 写在最后
从15个模块中抽取共性,本质上是在回答一个问题:什么是"系统真正的核心",什么只是"技术实现的重复"?
审批逻辑不是某个模块的核心——它是所有需要审批的模块共享的能力。单号生成不是业务逻辑——它是基础设施。多币种金额不是采购的专利——它是一个通用的值对象。
DDD 的价值在于:它迫使你区分"领域逻辑"和"技术细节",把领域逻辑放在领域层,把技术细节推到基础设施层。当你做对了这个分类,高内聚低耦合就是自然的结果。
💬 最后一个互动:如果让你给自己的系统画一张"共性矩阵"——哪些能力被复制了三次以上?你觉得哪个最值得先抽出来?欢迎留言讨论。