当前位置: 首页 > news >正文

从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 判断维护。这导致两个问题:

  1. 哪些状态转换是合法的?散落在各处,没有统一约束
  2. 状态转换时应该触发什么动作?比如"审批通过"要发通知、要生成下游单据——这些逻辑混在 Controller 里

重构方向:用State 模式为每个聚合根实现显式状态机,状态转换规则和副作用集中管理。

2.4 单号生成:规则不同但模式一致

申请单号、询价单号、订单号、工单号——每个模块都有自己的单号生成类。我们读了其中一个:

格式:(年)公司码-部门码-业务码-流水号-船舶ID-船岸标识 示例:(2026)HKMW-TEC-RP-001-SHIP001-S

每个单号生成器做的事情一样:拼前缀 → 查当前最大号 → +1 → 拼后缀。区别只在前缀各段的取值来源。

重构方向:用Factory 模式+ 配置化规则,统一单号生成器,前缀各段通过配置或策略注入。

2.5 多币种金额:值对象的天然候选

备件采购和物料采购的实体上都有这些字段:CurrencyCurRateUsdRateTotalPrice。费用实体甚至有三套币种:报价币种(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 # 纯工具层(不包含业务逻辑)

分层的核心原则:

  1. 领域层零外部依赖——不引用 EF Core、不引用 ASP.NET Core,只依赖 .NET BCL
  2. 应用层依赖领域层抽象——通过接口调用仓储和工作流,不关心实现
  3. 基础设施层依赖领域层——实现领域层定义的仓储接口、工作流接口
  4. 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所有模块都依赖,是后续重构的基础
P0Result 模式 + 统一异常改善 API 一致性,减少异常滥用
P1审批策略 + 模板方法解决最大的重复代码问题
P1状态机防止非法状态转换,集中业务规则
P1Money 值对象金额计算出错代价高,值得抽象
P2单号生成工厂不影响业务逻辑,但减少样板代码
P2领域事件 + Mediator解耦效果好,但引入了异步复杂度
P2附件/通知通用化需要和基础设施改造配合
P3船岸同步策略复杂度高,建议在核心模块稳定后推进

7. 写在最后

从15个模块中抽取共性,本质上是在回答一个问题:什么是"系统真正的核心",什么只是"技术实现的重复"?

审批逻辑不是某个模块的核心——它是所有需要审批的模块共享的能力。单号生成不是业务逻辑——它是基础设施。多币种金额不是采购的专利——它是一个通用的值对象。

DDD 的价值在于:它迫使你区分"领域逻辑"和"技术细节",把领域逻辑放在领域层,把技术细节推到基础设施层。当你做对了这个分类,高内聚低耦合就是自然的结果。

💬 最后一个互动:如果让你给自己的系统画一张"共性矩阵"——哪些能力被复制了三次以上?你觉得哪个最值得先抽出来?欢迎留言讨论。

http://www.jsqmd.com/news/1408522/

相关文章:

  • 炒股盯盘、考试搜题、外卖抢单、随手记账——鸿蒙闪控球让4件事从3步变1步
  • AI期刊论文生成靠谱吗?2026年研究生实测全流程记录
  • 生物信息学分析的可重复性实践与Git进阶应用
  • 很多技术团队悄悄迁移的大模型!DMXAPI 聚合平台 qwen3.7‑plus7.9 折,国产模型内卷出真正硬实力!
  • Windows系统文件SynCOM.dll丢失找不到问题解决
  • 2026 年柞水专业的AI全域获客机构哪家专业,实体店月入翻3倍,全靠这没人教的获客狠招,它到底藏着啥门道?-抖盈电子商务 - 企业信息推荐-2
  • OC游戏:用纸笔规则重构互动叙事,从桌面RPG到创意引擎
  • AI时代程序员的保命技能是什么
  • 2019-2026年城市人口迁徙数据集
  • 谷歌Gboard Rambler:设备端AI如何重塑个性化输入体验
  • H3K27ac:从组蛋白修饰到超级增强子鉴定的核心技术与实践
  • C++开发者如何理性评估Qt框架的学习价值与就业前景
  • 路口红灯还剩几秒不用盯着看——鸿蒙版高德地图实况窗直接帮你读秒
  • Windows资源管理器预览功能扩展:为代码文件添加文本预览
  • MES与QMS协同联动:以全流程质量数据筑牢产品品质防线
  • 多智能体协作编程:从单文件生成到仓库级代码开发的AI进化
  • Agent 评测沙箱:AgentENV 如何用 Firecracker + overlaybd + ublk 把环境启动压到 50ms
  • PDF转二维码:高效分享行业研究报告的实用指南
  • 划线分享选中文字自动出海报二维码——5款应用已适配
  • 求职招聘大厂Java面试实录:JDK17、Redis分布式锁、Kafka投递削峰、Seata分布式事务、Spring AI+RAG智能匹配,谢飞机三轮被虐哭(附完整答案解析)
  • Scratch 4K 高清复刻《植物大战僵尸》交互界面:从事件驱动到克隆技术的实战解析
  • EVE-NG环境下的RSTP协议实验与网络优化实践
  • 花旗骰:从概率沙盘到决策思维,在随机性中寻找确定性
  • 你的内容多久更新一次?GEO给的答案是:一个月
  • DeepSeek Harness论文——它想让AI学会安全地“自我升级“
  • 鸿蒙实况窗锁屏就能看进度——外卖、骑行、充电、航班不用开App
  • AI智能体对抗性评估:构建鲁棒性测试框架与实战指南
  • 一键下单软件靠谱吗?细数隐形收费与工具陷阱,合规工具选择参考 - 抖掌柜一键下单
  • 智能排版拯救强迫症:2026年论文格式一键规范指南
  • 现在才懂啊,儿子的家呀根本就不是我的家。昨天中午呢我孙子就跑到厨房啊,扒着门框喊说,爷爷饭好了没有我饿了。我把最后一道啊排骨端出来,我说好了,快喊你爸妈出来吃饭吧。儿媳妇呢就跟着从卧室啊走出来就往桌旁