大家好,我是十三。
从本篇开始,我们进入架构思维篇。
前面 29 篇文章,我们用业务视角理解了采购、销售、库存、财务、对账的完整流转,也用产品视角拆解了模块划分、状态机、权限和审批流的设计思路。现在,我们换一个视角:如果你是一位架构师,面对这套覆盖了企业核心命脉的庞大系统,你会怎么切分它。
我的结论是:ERP 之所以复杂,不是因为代码多,而是因为业务概念在不同场景下有不同的含义。 同一个"订单",采购部、销售部、仓储部、财务部各自理解都不一样。如果架构师不给这些概念划定边界,系统很快就会变成一锅谁都改不动的粥。
今天这篇文章,我会用 DDD 的战略设计思想,帮你理解架构师如何给复杂业务划边界。
DDD 解决什么问题?
业务定义
DDD(Domain-Driven Design,领域驱动设计) 是一种应对复杂业务系统的软件设计方法。它的核心思想不是从数据库表或技术框架出发,而是从业务领域本身出发,让代码结构跟业务结构对齐。
DDD 把设计分成两个层面:战略设计 和 战术设计。战略设计回答"系统该切成几块",战术设计回答"每一块内部该怎么写代码"。本文只聚焦战略设计。
为什么 ERP 特别适合 DDD
业财系统是典型的复杂业务系统。它的复杂性不是来自技术,而是来自业务规则本身的交错和重叠。
举个例子。在前面第 1 到第 4 篇文章里,我们走过完整的 P2P(采购到付款)流程。采购部说的"订单"是采购订单(PO),关注的是供应商是谁、单价多少、交货期哪天。
在第 8 到第 11 篇文章里,我们走过 O2C(订单到收款)流程。销售部说的"订单"是销售订单(SO),关注的是客户是谁、售价多少、信用额度够不够。
到了第 15 到第 18 篇的库存管理环节,"订单"又变成了出库指令,关注的是从哪个库位、出哪个批次、数量对不对。
三个部门都说"订单",但内涵完全不同。如果没有清晰的边界划分,一个需求改动可能牵一发而动全身。
通俗类比
DDD 战略设计,就像给公司划分部门。
公司小的时候,所有人坐一个大平层,喊一嗓子就能对齐。公司大了以后,销售部、采购部、财务部、仓储部各自有自己的术语、流程和考核指标。部门之间通过规范接口协作,而不是所有人都挤在同一个房间里互相干扰。
限界上下文,就是软件世界里的"部门墙"。它不是阻碍协作,而是让协作有章可循。
限界上下文:给业务概念划定边界
业务定义
限界上下文(Bounded Context) 是 DDD 战略设计的核心概念。它指的是一个明确的业务边界,在这个边界内部,有一套统一的业务语言和一致的概念模型。
同一个词,在不同的限界上下文里可以有不同的含义,这是被允许的,也是必要的。
关键要理解:限界上下文不是技术模块,而是业务边界。 一个限界上下文可以对应一个微服务,也可以对应一个代码包,甚至可以只对应一个领域层。它的本质是"业务一致性的范围"。
ERP 的四大限界上下文
用我们前面 29 篇文章讲过的业务来举例,一套典型的 ERP 系统至少可以划分出以下四个核心限界上下文。
采购上下文(Procurement Context)
内部统一语言包括:采购申请、采购订单(PO)、供应商、询价、比价、三单匹配。在这个上下文里,"订单"专指采购订单,关注的核心是"以什么价格、从谁那里、买到什么"。
在前面第 1 篇到第 4 篇文章里,我们完整走过了从采购申请到发票校验再到付款的 P2P 全流程。这个过程里的所有概念,都在采购上下文里有统一的定义。
销售上下文(Sales Context)
内部统一语言包括:销售机会、销售订单(SO)、客户、报价单、信用额度、渠道。在这个上下文里,"订单"专指销售订单,关注的核心是"卖给谁、卖多少钱、能不能赊账"。
在第 8 篇到第 11 篇里,从销售机会到回款的 O2C 流程,就是销售上下文的核心业务流程。第 12 篇讨论的 CRM 客户主数据,则是销售上下文与财务上下文之间的重要桥梁。
库存上下文(Inventory Context)
内部统一语言包括:入库、出库、盘点、调拨、可用库存、安全库存、成本计价。在这个上下文里,"订单"指的是出入库指令,关注的核心是"货在哪、有多少、能不能动"。
第 15 篇到第 21 篇讨论过的入库、出库、盘点、调拨、可用库存、安全库存和成本计价,都属于库存上下文的范畴。在第 19 篇里,我们专门讨论过"为什么账上有货却不能卖",这正是库存上下文内部的业务规则在起作用。
财务上下文(Finance Context)
内部统一语言包括:会计科目、凭证、总账、应收账款、应付账款、暂估入账、冲销。在这个上下文里没有"订单"这个概念,取而代之的是"凭证"和"分录",关注的核心是"钱怎么记、账怎么平"。
我们在第 7 篇详细讲过的暂估入账,就是财务上下文里的典型业务。货到了但发票没到,财务需要用自己的语言(暂估、冲销、正式入账)来处理这个业务事件。
这种"领域复杂性"的体现,恰恰是财务上下文必须独立存在的原因。
为什么需要划分这四个上下文
你可能会问:为什么不能把所有东西放在一个系统里,用同一张订单表?
答案是:因为不同部门对"同一件事"的关注点完全不同。
采购员关心供应商交期和单价,销售员关心客户信用和折扣,库管员关心库位和批次,财务关心的是会计科目和借贷平衡。如果硬塞进同一个模型,这张表会越来越宽,每个人都要在一大堆无关字段里找自己关心的那几个。
限界上下文的作用,就是让每个"部门"用自己的语言做事,只在必要的时候通过明确的接口和其他"部门"协作。
上下文映射:它们之间如何协作
限界上下文划分完了,下一步要回答的问题是:它们之间怎么打交道?
DDD 给出了几种典型的上下文关系模式,叫做 上下文映射(Context Mapping)。
五种关系类型
| 关系类型 | 通俗理解 | ERP 场景示例 |
|---|---|---|
| 合作关系 | 双方密切协作,共同演进 | 销售上下文与库存上下文,销售订单需要实时查可用库存 |
| 客户-供应商 | 一方提供接口,另一方消费 | 库存上下文向财务上下文提供入库数据,财务据此生成凭证 |
| 防腐层 | 消费方做一层转换,保护自己不被对方模型污染 | 财务上下文消费采购数据时,把采购订单转成本地理解的凭证模型 |
| 共享内核 | 多个上下文共享一部分公共模型 | 商品基础信息(SKU、名称、规格)被采购、销售、库存共同使用 |
| 开放主机服务 | 一方以标准化接口对外开放 | 主数据服务(如 CRM 客户信息)通过 API 向所有上下文提供数据 |
ERP 上下文映射图
graph LR
subgraph 采购上下文
PO[采购订单]
SUP[供应商]
end
subgraph 销售上下文
SO[销售订单]
CUS[客户]
end
subgraph 库存上下文
INV[库存收发]
LOC[库位]
end
subgraph 财务上下文
GL[总账]
ACC[应收应付]
end
subgraph 共享内核
SKU[商品主数据]
end
PO -->|入库通知| INV
SO -->|出库指令| INV
INV -->|库存变动| ACC
ACC -->|凭证| GL
SKU -.->|共享| PO
SKU -.->|共享| SO
SKU -.->|共享| INV
CUS -->|信用查询| SO
这张图展示了 ERP 各模块之间的典型协作关系。
采购上下文和销售上下文各自向库存上下文下发出入库指令,库存上下文把库存变动事件同步给财务上下文生成凭证,商品主数据作为共享内核被多个上下文共用。
注意这里的箭头方向:它不是简单的数据流向,而是依赖关系。谁调用谁、谁依赖谁,决定了系统的耦合方式和演进自由度。比如采购上下文改了供应商模型,如果库存上下文没有直接依赖它,就不会被波及。
子域分类:哪里该重兵投入
限界上下文回答"怎么切",子域分类回答"切完后怎么投资源"。
DDD 把业务领域分成三类:核心域、支撑域、通用域。这个分类直接决定了团队的技术投入策略。
业务定义
- 核心域(Core Domain):企业的核心竞争力所在,业务逻辑独特且复杂,是必须投入最优资源自研的领域。
- 支撑域(Supporting Domain):不是核心竞争力,但为核心域服务,业务有一定特殊性,通常自研或重度定制。
- 通用域(Generic Domain):业务通用,无特殊性,市场上已有成熟方案,应优先考虑采购或集成。
ERP 子域分类决策表
| 子域 | 分类 | 投入策略 | ERP 示例 |
|---|---|---|---|
| 订单管理 | 核心域 | 重兵自研,深度定制 | 销售订单、采购订单的生命周期与规则引擎 |
| 库存管理 | 核心域 | 重兵自研,深度定制 | 可用库存计算、安全库存策略、成本计价 |
| 财务管理 | 核心域 | 重兵自研,深度定制 | 总账、应收应付、暂估入账、凭证生成 |
| 审批流 | 支撑域 | 中等投入,可配置 | 不同单据的审批节点、审批人、流转条件 |
| 权限管理 | 支撑域 | 中等投入,可配置 | RBAC 模型、数据权限、操作权限 |
| 报表分析 | 支撑域 | 中等投入,可扩展 | 经营看板、多维查询、BI 对接 |
| 消息通知 | 通用域 | 轻量投入,优先采购 | 短信、邮件、站内信,可集成第三方 |
| 操作日志 | 通用域 | 轻量投入,优先采购 | 用户行为记录、审计追踪 |
| 文件存储 | 通用域 | 轻量投入,优先采购 | 发票影像、合同附件,可用云存储服务 |
分类背后的判断逻辑
判断一个子域是不是核心域,关键看两个问题。
第一,这个领域是否直接决定企业的竞争优势。 对于零售企业,订单和库存就是命脉,必须自己掌控。消息通知做得再好,也不会让用户多下一单。
第二,这个领域的业务规则是否具有独特性。 不同企业的财务核算规则可能差异很大,这是无法直接买现成的。但操作日志的格式和存储方式,各家企业都差不多,没有必要自己造轮子。
这个分类的本质是资源分配的优先级。核心域投入 80%的架构精力,通用域能买就买,不要把团队战斗力浪费在重复造轮子上。
统一语言:为什么同一个词在不同上下文里含义不同
业务定义
统一语言(Ubiquitous Language) 是 DDD 的另一个核心概念。它指的是在一个限界上下文内部,业务人员、产品经理、开发工程师使用同一套语言来描述业务,并且这套语言要直接反映在代码里。
但这里有一个常见的误解:统一语言不等于"全系统统一词汇表"。统一语言的边界是限界上下文,不是整个系统。
"订单"在四个上下文中的含义差异
| 上下文 | "订单"指什么 | 关注的核心属性 | 对应代码概念 |
|---|---|---|---|
| 采购上下文 | 采购订单(PO) | 供应商、单价、交货期、采购数量 | PurchaseOrder |
| 销售上下文 | 销售订单(SO) | 客户、售价、信用额度、付款条件 | SalesOrder |
| 库存上下文 | 出入库指令 | 库位、批次、操作类型、数量 | StockMovement |
| 财务上下文 | 无"订单"概念 | 会计科目、借贷方向、金额 | JournalEntry |
如果在全系统里强制使用同一个 Order 类来表示以上所有概念,代码很快就会失控。采购订单的"供应商"字段对销售毫无意义,销售订单的"信用额度"对库存也不相干。
正确的做法是:在每个限界上下文里,用各自的语言定义各自的模型。 采购上下文里有 PurchaseOrder,销售上下文里有 SalesOrder,库存上下文里有 StockMovement。它们之间不直接共享对象,而是通过防腐层进行转换。
防腐层的伪代码示例
假设财务上下文需要处理销售订单的收款事件,但它不应该直接理解 SalesOrder 的结构。这时候就需要一层转换:
// 销售上下文发布的事件
class SalesOrderPaidEvent {
salesOrderId; customerId; amount; paidAt;
}
// 财务上下文的防腐层:把销售事件转成本地理解的模型
class FinanceAntiCorruptionLayer {
translateToReceivable(event) {
return {
documentType: "应收账款",
sourceId: event.salesOrderId,
subjectCode: "1122",
customerCode: this.mapCustomer(event.customerId),
debitAmount: event.amount,
creditSubject: "6001",
occurredAt: event.paidAt
};
}
mapCustomer(salesCustomerId) {
return customerCodeMap.get(salesCustomerId);
}
}
// 财务上下文消费事件
function onSalesOrderPaid(event) {
const acl = new FinanceAntiCorruptionLayer();
const receivable = acl.translateToReceivable(event);
generalLedger.createEntry(receivable);
}
这段代码展示了防腐层的核心作用:外部上下文的事件进来后,先翻译成自己理解的语言,再处理。 这样,财务上下文的代码不会被销售上下文的模型污染,两边可以独立演进。
这就像公司里的翻译岗位。销售部和外资供应商开会,需要翻译把双方的语言转换成彼此能理解的表达。防腐层就是这个翻译。
总结与下一篇预告
今天我们用 DDD 的战略设计视角,重新理解了 ERP 系统的切分逻辑。
第一,限界上下文给业务概念划定边界。采购、销售、库存、财务各自有自己的语言,各自对自己的模型负责。
第二,上下文映射定义了它们之间的协作方式。合作、客户-供应商、防腐层、共享内核、开放主机服务,每种关系对应不同的耦合策略。
第三,子域分类决定了资源投入优先级。核心域重兵自研,支撑域适度投入,通用域能买就买。
第四,统一语言在上下文内部消除歧义,而防腐层在上下文之间保护各自模型的独立性。
DDD 战略设计的本质,不是把系统拆得越碎越好,而是让每一块边界内的业务逻辑尽可能自洽,边界之间的交互尽可能清晰。 拆得不好,系统会变成一盘散沙;完全不拆,系统会变成一潭死水。
在下一篇文章中,我们会进入 DDD 的战术设计:限界上下文内部,聚合、实体、值对象、领域服务该怎么组织。如果说战略设计是"怎么切蛋糕",战术设计就是"怎么把每一块蛋糕做得好吃"。
往期回顾
- 业财通识29:审批流设计——从纸质签字到电子化流转
- 业财通识28:权限设计——谁能看、谁能改、谁能批
- 业财通识27:状态机设计——用状态驱动业务流转
- 业财通识26:功能模块划分——如何把业务流程变成系统菜单
- 业财通识25:业财对账——打通业务与财务的最后一公里
- 业财通识24:银行对账——银行流水与账本的逐笔核对
- 业财通识23:应付对账——管住每一笔该付的钱
- 业财通识22:应收对账——确保每一笔钱都收回来
- 业财通识21:库存成本计价——FIFO、加权平均与标准成本
- 业财通识20:安全库存——科学补货的数学基础
- 业财通识19:可用库存——为什么账上有货却不能卖
- 业财通识18:调拨——多仓协同的物流调度
- 业财通识17:盘点——账实一致的最后防线
- 业财通识16:出库——从销售发货到领料消耗
- 业财通识15:入库——四种场景下的库存增加
- 业财通识14:应收账款——从开票到回款的风险管控
- 业财通识13:价格策略——多维定价与动态调整
- 业财通识12:一个客户,为什么会有三条记录?——CRM 作为主数据底座的三层模型
- 业财通识11:从开票到收款,企业如何收回每一分钱?
- 业财通识10:当货物发出,系统里发生了什么?
- 业财通识09:订单确认前,系统如何防止坏账风险?
- 业财通识08:企业赚钱的第一步,从"潜在客户"到"销售合同"
- 业财通识07:业财难点之"暂估入账"与冲销
- 业财通识06:什么是采购在途?它对库存预测的价值
- 业财通识05:商品世界的基石——SPU 与 SKU
- 业财通识04:万事俱备,如何优雅地"打款"给供应商?
- 业财通识03:收到供应商账单,能直接付款吗?
- 业财通识02:当货物上门,系统里发生了什么?
- 业财通识01:企业花钱的第一步,从"购物清单"到"法律合同"
关于十三Tech
资深服务端研发工程师、架构师、AI 编程实践者。
专注分享真实的技术实践经验,持续记录企业系统、架构设计与 AI 编程实践。