电商系统设计(六):计价系统 DDD 实践
电商系统设计系列(与(一)推荐阅读顺序一致)
- (一)全景概览与领域划分
- (二)商品中心系统
- (三)库存系统
- (四)营销系统深度解析
- (五)计价引擎
- (六)计价系统 DDD 实践(本文)
- (七)订单系统
- (八)支付系统深度解析
- (九)商品上架系统
- (十)B 端运营系统
本文是电商系统设计系列的第六篇,是(五)计价引擎的姊妹篇,从 DDD 视角重新审视计价系统的建模。
本文是计价引擎系列的方法论篇,聚焦 DDD 在计价系统中的战略/战术设计实践。系统架构与实现细节详见:电商系统设计(五):计价引擎。
一、背景与挑战
在构建电商计价系统的过程中,价格计算并非简单的”标价”,而是由基础价格、营销折扣、平台费用、用户抵扣等多层因素叠加而成。随着业务规模扩大,我们面临着三大核心挑战:
1. 隐晦性(Obscurity)
抽象层面的隐晦:同一个”价格”概念,在不同场景下含义不同
- 商品详情页展示价:用户看到的价格
- 订单价:创建订单时的价格快照
- 支付价:最终扣款价格
实现层面的隐晦:代码中的术语混乱
- 有人叫
originalPrice,有人叫marketPrice - 有人叫
salePrice,有人叫discountPrice - 业务人员和技术人员理解不一致
- 有人叫
2. 耦合性(Coupling)
代码层面:价格计算逻辑散落在各处
- 商品服务有一套计算
- 订单服务又重复计算
- 支付服务再计算一次
模块层面:计价依赖多个外部服务
- 促销服务(获取活动信息)
- 商品服务(获取基础价格)
- 用户服务(判断用户类型)
系统层面:前后端价格不一致导致资损
3. 变化性(Variability)
业务需求频繁变化
- 促销规则每周调整(双11期间优先级变化)
- 新增促销类型(买赠、满减、阶梯价)
- 不同地区有不同定价策略
品类扩展需求
- 实物商品、虚拟商品、服务类商品
- 每种品类有特殊的计价规则
1.3 初期设计的问题
最初的实现方式是面向过程的事务脚本:
1 | // ❌ 问题代码示例 |
核心问题:
- ❌ 业务逻辑分散在各个函数中,难以理解整体流程
- ❌ 缺乏业务概念的抽象,只有数据获取和计算
- ❌ 新增促销类型需要修改核心计算逻辑
- ❌ 无法支持复杂的业务规则(如买N件享M折)
- ❌ 测试困难,需要Mock大量外部依赖
二、DDD核心概念
在介绍计价系统的DDD实践之前,先回顾一下DDD的核心概念。
2.1 什么是领域?
领域由三部分组成:
1 | ┌─────────────────────────────────────────┐ |
计价领域示例:
- 涉众域:商家(定价)、运营(促销)、消费者(购买)、财务(结算)
- 问题域:如何准确计算价格?如何支持多种促销?如何保证一致性?
- 解决方案域:统一的计价模型、规则引擎、价格快照
2.2 什么是领域驱动设计?
针对特定业务领域,用户在面对业务问题时有对应的解决方案,这些问题与方案构成了领域知识。领域驱动设计就是围绕这些知识来设计系统。
计价领域知识:
- 流程:商品展示 → 加入购物车 → 创建订单 → 支付结算
- 规则:促销优先级、费用计算规则、优惠抵扣规则
- 方法:四层计价模型、价格快照机制
三、战略设计实践
3.1 确定用例
我们使用用例图来表达用户与系统的交互:
1 | ┌─────────────────────────────────────────────────────┐ |
3.2 统一语言(Ubiquitous Language)
从用例中抽取概念,建立统一语言。这是DDD最关键的一步。
基础价格术语
| 中文术语 | 英文术语 | Term | 含义 |
|---|---|---|---|
| 市场原价 | Market Price | market_price |
商品的市场标价,来自供应商 |
| 折扣价 | Discount Price | discount_price |
平台日常销售价 |
| 划线价 | Listed Price | listed_price |
用于展示的对比价格 |
促销术语
| 中文术语 | 英文术语 | Term | 含义 |
|---|---|---|---|
| 促销价 | Promotion Price | promotion_price |
参与促销活动后的价格 |
| 秒杀价 | Flash Sale Price | flash_sale_price |
限时秒杀活动价格 |
| 新用户价 | New User Price | new_user_price |
新用户专享价格 |
| 满减价 | Threshold Price | threshold_price |
满XX减XX后的价格 |
费用术语
| 中文术语 | 英文术语 | Term | 含义 |
|---|---|---|---|
| 平台服务费 | Platform Fee | platform_fee |
平台收取的服务费 |
| 配送费 | Delivery Fee | delivery_fee |
物流配送费用 |
| 手续费 | Handling Fee | handling_fee |
支付渠道手续费 |
最终价格术语
| 中文术语 | 英文术语 | Term | 含义 |
|---|---|---|---|
| 计价金额 | Pricing Amount | pricing_amount |
单个SKU的计价金额 |
| 最终价格 | Final Price | final_price |
用户最终支付价格 |
| 结算金额 | Settlement Amount | settlement_amount |
商家结算金额 |
统一语言的重要性:
1 | 案例:某电商平台的混乱 |
3.3 概念模型(Concept Model)
基于统一语言,建立概念模型,明确概念之间的关系:
1 | ┌───────────────────────────────────────────────────────────────┐ |
概念关系说明:
- PriceEntity 包含 (1:1) BasePrice - 每个商品有且只有一个基础价格
- PriceEntity 可能有 (1:0..N) Promotion - 可以参与多个促销(但只能选一个)
- Order 可能有 (1:0..N) Fee - 订单可能产生多种费用
- Order 可能有 (1:0..N) Discount - 订单可能使用多种优惠
- Order 产生 (1:1) FinalPrice - 最终计算出唯一的支付价格
3.4 子域划分(Subdomain)
将复杂问题拆解为多个简单问题,我们基于问题域进行拆分:
1 | ┌─────────────────────────────────────────────────────────┐ |
拆分原则:
- 定价域(核心):专注价格计算逻辑,这是业务的核心竞争力
- 促销域(支撑):管理促销规则,为定价提供数据支持
- 商品域(支撑):提供商品基础信息
- 用户域(支撑):提供用户信息和分群数据
- 支付域(支撑):处理支付和优惠券
- 缓存/配置域(通用):技术基础设施
为什么这样拆分?
1 | 问题:为什么不把所有逻辑都放在一个"定价域"? |
3.5 上下文映射(Context Mapping)
定义子域之间的协作关系:
1 | ┌─────────────────────────────────────────────────────────────┐ |
防腐层的作用:
防腐层(Anti-Corruption Layer)保护领域模型不被外部系统污染。
1 | // ❌ 错误:直接依赖外部服务的数据结构 |
收益:
- ✅ 外部系统变化不影响领域模型
- ✅ 保持领域模型的纯粹性
- ✅ 易于切换外部服务实现
四、战术设计实践
战略设计得到了概念模型和子域划分,战术设计则是将概念模型映射为代码模型。
4.1 实体(Entity)与值对象(Value Object)
实体:有唯一标识和生命周期
1 | // ✅ 实体:PriceEntity(价格实体) |
值对象:无唯一标识,不可变
1 | // ✅ 值对象:Price(价格) |
实体 vs 值对象的判断标准:
1 | 问题:某个概念应该是实体还是值对象? |
4.2 聚合根(Aggregate Root)
聚合根是一组相关对象的入口,保证业务规则的一致性。
聚合根设计原则:
- 满足业务一致性(促销、费用、价格必须一致)
- 满足数据完整性(不存在没有基础价格的价格实体)
- 考虑技术限制(避免加载过大数据)
1 | // ✅ 聚合根:PricingAggregate |
聚合根边界的确定:
1 | 问题:什么应该放在聚合根内?什么应该放在聚合根外? |
4.3 领域服务(Domain Service)
什么时候使用领域服务?
不适合放在聚合根里的领域逻辑,可以放在领域服务中:
1 | // ❌ 不适合放在聚合根:跨聚合根的逻辑 |
领域服务 vs 应用服务:
1 | 领域服务(Domain Service): |
4.4 贫血模型 vs 充血模型
我们的选择:混合模式
1 | // ✅ 核心领域逻辑:充血模型 |
选择标准:
1 | 充血模型(适用场景): |
4.5 体现业务语义的代码
代码应该体现业务含义,让非技术人员也能理解:
1 | // ❌ 错误:没有业务含义 |
收益:
- ✅ 代码即文档(看方法名就知道做什么)
- ✅ 业务规则显式化(不需要深入代码才能理解)
- ✅ 易于沟通(产品和技术可以用同样的语言)
4.6 价格快照与一致性保障
4.6.1 业务场景与挑战
在电商系统中,用户从浏览商品(PDP)到最终下单,价格可能发生变化,这是一个非常常见且重要的问题。
典型场景:
1 | 用户路径: |
核心挑战:
- ❌ 价格不一致导致用户投诉
- ❌ 可能造成资损风险
- ❌ 影响用户购买体验
- ❌ 需要平衡准确性和用户体验
4.6.2 价格快照机制设计
核心思路:在PDP阶段生成价格快照,用户加购/创单时验证快照有效性。
1 | // 价格快照值对象 |
4.6.3 完整实现方案
方案1:PDP阶段生成快照
1 | // 领域服务:价格快照管理 |
方案2:活动有效期前置校验
1 | // 领域服务:促销有效期管理 |
方案3:库存预锁定
1 | // 领域服务:促销库存管理 |
4.6.4 用户体验优化
价格变动提示策略:
1 | // 应用服务:订单创建(集成价格验证) |
前端UI交互示例:
1 | // 前端处理价格变动 |
4.6.5 监控与告警
1 | // 领域服务:价格一致性监控 |
4.6.6 最佳实践总结
| 措施 | 说明 | 优先级 |
|---|---|---|
| 价格快照 | PDP生成快照(10分钟),加购/创单时验证 | P0 |
| 价格校验 | 前端传入期望价格,后端验证(容忍度±1元) | P0 |
| 活动预警 | 活动剩余时间<15分钟时前置提示 | P1 |
| 库存预锁 | 加购时预锁定促销库存(5分钟) | P1 |
| 降级策略 | 促销失效时自动降级到原价 | P0 |
| 用户提示 | 价格变动时明确告知原因并二次确认 | P0 |
| 监控告警 | 价格差异率、快照过期率监控 | P1 |
关键设计原则:
- 快照不可变:价格快照创建后不可修改,保证一致性
- 短期有效:快照有效期10-15分钟,平衡准确性和体验
- 容忍小差异:±1元差异可接受,避免频繁提示
- 明确告知:价格变动时必须告知原因和差异金额
- 用户确认:价格上涨时必须用户二次确认
- 降级保护:促销失效时自动降级到原价,不阻断流程
五、代码架构实践
5.1 六边形架构(Hexagonal Architecture)
我们采用六边形架构(也称为端口和适配器架构):
1 | ┌────────────────────────────────────────────────────────┐ |
核心原则:
- 领域层是核心,不依赖外部
- 外层依赖内层(依赖倒置)
- 通过端口(接口)隔离
5.2 实际代码结构
1 | pricing-service/ |
5.3 依赖方向
1 | 核心原则:依赖倒置原则(Dependency Inversion Principle) |
5.4 实际代码示例
领域层:定义接口
1 | // domain/repository/pricing_repository.go |
基础设施层:实现接口
1 | // infrastructure/persistence/mysql/pricing_repo_impl.go |
应用层:协调各层
1 | // application/service/pricing_service.go |
适配器层:处理HTTP请求
1 | // adapter/http/handler.go |
六、DDD带来的收益
6.1 量化收益
根据实际项目经验,DDD带来的量化收益:
| 指标 | 改造前 | 改造后 | 改善 |
|---|---|---|---|
| 代码重复率 | 40% | 15% | -62% |
| 新功能开发时间 | 2周 | 3天 | -86% |
| 单元测试覆盖率 | 45% | 90% | +100% |
| Bug密度 | 3.5/KLOC | 0.8/KLOC | -77% |
| 需求变更响应时间 | 3天 | 0.5天 | -83% |
| 新人上手时间 | 2周 | 3天 | -85% |
| 价格不一致投诉 | 2-3起/月 | <0.01% | -99% |
| 价格变动处理时间 | 人工处理2小时 | 自动处理秒级 | -99.9% |
6.2 质量收益
1. 概念清晰,沟通顺畅
1 | 改造前: |
2. 业务规则显式化
1 | // ❌ 改造前:隐藏在代码中 |
3. 易于测试
1 | // ✅ 聚合根可以独立测试 |
4. 价格一致性保障
通过价格快照机制,DDD帮助我们实现了价格一致性保障:
1 | 改造前: |
具体案例:
1 | 案例1:促销活动结束 |
6.3 维护性收益
1. 修改影响范围可控
1 | 需求变更:秒杀优先级临时提升 |
2. 业务变化适应性强
1 | 新需求:增加VIP专享价 |
七、常见误区与最佳实践
7.1 常见误区
误区1:深陷DDD概念
1 | // ❌ 过度设计:生搬硬套概念 |
误区2:试图一次性设计完美
1 | ❌ 错误做法: |
误区3:忽视统一语言
1 | ❌ 问题: |
误区4:过度使用领域事件
1 | // ❌ 过度使用:为事件而事件 |
7.2 最佳实践
1. 从业务出发
1 | ✅ 正确顺序: |
2. 持续迭代
1 | 领域模型不是一次性设计出来的 |
3. 团队协作
1 | ┌──────────────────────────────────────┐ |
4. 适度抽象
1 | // ✅ 简单场景:简单实现 |
5. 价格快照机制
价格快照是DDD在计价系统中的重要实践:
1 | 核心设计: |
八、总结
8.1 DDD的核心价值
- 统一语言:消除沟通障碍,提升协作效率
- 领域模型:业务知识显式化,代码即文档
- 分层架构:职责清晰,易于维护和扩展
- 持续迭代:适应业务变化,拥抱需求变更
8.2 实施要点
1 | 战略设计: |
8.3 给后来者的建议
1. 不要害怕DDD的概念体系
从简单开始,逐步深入:
- 第1周:建立统一语言
- 第2周:绘制概念模型
- 第3周:实现简单聚合根
- 第4周:逐步完善
2. 重视统一语言
没有统一语言就没有概念模型,没有概念模型就没有好的代码
投入时间在统一语言上,回报率最高
3. 持续迭代
领域模型是演进出来的,不是设计出来的:
- 业务理解加深 → 模型调整
- 抽象角度变化 → 模型重构
- 业务需求变化 → 模型扩展
4. 团队协作
DDD是团队工作,不是个人英雄主义:
- 产品提供业务视角
- 开发提供技术实现
- 测试提供边界场景
- 运营提供实际反馈
5. 重视价格一致性
价格一致性是电商系统的生命线:
- 第一时间建立价格快照机制
- 明确定义价格变动处理策略
- 充分的用户提示和二次确认
- 实时监控价格差异率
- 建立价格变动审计日志
1 | 价格一致性的三个关键: |
8.4 适用场景
✅ 适合使用DDD的场景:
- 业务逻辑复杂(计价、促销、订单、风控)
- 需求频繁变化
- 需要长期维护
- 团队规模较大(5人以上)
❌ 不适合使用DDD的场景:
- 简单CRUD系统
- 技术型系统(日志、监控)
- 短期项目(< 3个月)
- 单人开发
8.5 最后的话
DDD不是银弹,但它是应对业务复杂性的有效方法。
最重要的不是掌握所有DDD概念,而是学会从业务出发,建立清晰的领域模型,让代码真正反映业务。
九、参考资料
经典书籍
- 《领域驱动设计》 Eric Evans
- 《实现领域驱动设计》 Vaughn Vernon
- 《企业应用架构模式》 Martin Fowler
在线资源
- Martin Fowler 的博客:https://martinfowler.com/
- Domain-Driven Design Community:https://dddcommunity.org/
- DDD Reference:http://domainlanguage.com/ddd/reference/
相关文章
- 电商系统价格计算引擎设计与实现 — 系统架构、场景分析、核心实现
- The Clean Architecture - Robert C. Martin
- Hexagonal Architecture - Alistair Cockburn
- Bounded Context - Martin Fowler
写于 2026年3月14日
作者:后端架构师
领域驱动设计:让软件真正反映业务
系列导航
计价引擎的工程实现细节(多级缓存、降级策略等),详见(五)计价引擎。