ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从15个业务模块中抽取共性:一次船舶PMS系统的架构复盘

2026/8/17 3:10:35 拓冰建站 浏览量
从15个业务模块中抽取共性:一次船舶PMS系统的架构复盘 架构设计 · 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 船岸同步每个实体上的 SendFlagSendFlag出现在几乎所有实体上。船端操作后标记为待同步岸端接收后标记为已同步。但同步策略哪些数据双向同步、冲突怎么解决在代码中没有统一设计。重构方向用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;}TaskstringResolveApproverAsync(ApprovalContextcontext,intlevel);TaskApprovalResultApproveAsync(ApprovalContextcontext,ApprovalDecisiondecision);}// 备件四级审批publicclassPartsApprovalStrategy:IApprovalStrategy{publicstringBusinessTypeParts;publicintMaxLevel4;// ...}// 物料三级审批publicclassMaterialApprovalStrategy:IApprovalStrategy{publicstringBusinessTypeMaterial;publicintMaxLevel3;// ...}通过 DI 注入所有策略按业务类型选择publicclassApprovalService{privatereadonlyIEnumerableIApprovalStrategy_strategies;publicApprovalService(IEnumerableIApprovalStrategystrategies)_strategiesstrategies;publicTaskApprovalResultApproveAsync(stringbusinessType,...){varstrategy_strategies.First(ss.BusinessTypebusinessType);// 模板方法驱动审批流程returnExecuteApprovalFlowAsync(strategy,context);}}5.2 State 模式单据状态机publicabstractclassDocumentState{publicabstractDocumentStatusStatus{get;}publicabstractIReadOnlyCollectionDocumentStatusAllowedTransitions{get;}publicvirtualResultCanTransitionTo(DocumentStatustarget)AllowedTransitions.Contains(target)?Result.Success():Result.Fail($不允许从{Status}转换到{target});}publicclassDraftState:DocumentState{publicoverrideDocumentStatusStatusDocumentStatus.Draft;publicoverrideIReadOnlyCollectionDocumentStatusAllowedTransitionsnew[]{DocumentStatus.PendingApproval,DocumentStatus.Cancelled};}publicclassPendingApprovalState:DocumentState{publicoverrideDocumentStatusStatusDocumentStatus.PendingApproval;publicoverrideIReadOnlyCollectionDocumentStatusAllowedTransitionsnew[]{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:INotificationHandlerPurchaseRequestApprovedEvent{publicTaskHandle(PurchaseRequestApprovedEventev,CancellationTokenct){// 库存预留逻辑}}publicclassNotificationHandler:INotificationHandlerPurchaseRequestApprovedEvent{publicTaskHandle(PurchaseRequestApprovedEventev,CancellationTokenct){// 发送通知}}审批模块完全不知道谁在响应事件。新增一个订阅者不需要改审批模块的代码。6. 改造的优先级建议不是所有共性都要在第一天就抽出来。我们建议按以下优先级推进优先级共性能力理由P0审计基类 软删除统一处理改动量小、收益大、零风险P0仓储 Unit of Work所有模块都依赖是后续重构的基础P0Result 模式 统一异常改善 API 一致性减少异常滥用P1审批策略 模板方法解决最大的重复代码问题P1状态机防止非法状态转换集中业务规则P1Money 值对象金额计算出错代价高值得抽象P2单号生成工厂不影响业务逻辑但减少样板代码P2领域事件 Mediator解耦效果好但引入了异步复杂度P2附件/通知通用化需要和基础设施改造配合P3船岸同步策略复杂度高建议在核心模块稳定后推进7. 写在最后从15个模块中抽取共性本质上是在回答一个问题什么是系统真正的核心什么只是技术实现的重复审批逻辑不是某个模块的核心——它是所有需要审批的模块共享的能力。单号生成不是业务逻辑——它是基础设施。多币种金额不是采购的专利——它是一个通用的值对象。DDD 的价值在于它迫使你区分领域逻辑和技术细节把领域逻辑放在领域层把技术细节推到基础设施层。当你做对了这个分类高内聚低耦合就是自然的结果。 最后一个互动如果让你给自己的系统画一张共性矩阵——哪些能力被复制了三次以上你觉得哪个最值得先抽出来欢迎留言讨论。