Gemini 生成

既然你准备好深入了,那我们就拿“优惠券核销(Coupon Redemption)”这个极具代表性的场景,来一场架构师级别的“手术式”拆解。

在传统开发中,你可能直接在 OrderService 里写一段逻辑:校验优惠券、扣减余额、改订单状态。但在 DDD 视角下,我们需要回答一个核心问题:优惠券核销,到底是属于“订单”聚合,还是“优惠券”聚合?


1. 方案权衡:谁才是“聚合根”?

这是 DDD 落地时最经典的争论。我们来看两种建模路径的 Pros & Cons

方案 A:优惠券作为订单(Order)的一部分

  • 思路: 将优惠券信息存放在 Order 聚合内部,作为其实体。
  • Pros: 强一致性。在一个数据库事务里完成订单创建和优惠券使用,绝对不会出现“订单付了钱但优惠券没扣”的情况。
  • Cons: 聚合过于臃肿。如果优惠券还有自己的复杂逻辑(如:过期自动退回、动态调整折扣),订单模型会变得极其复杂。

方案 B:优惠券(Coupon)是独立聚合(推荐)

  • 思路: 订单和优惠券是两个独立的聚合根。
  • Pros: 职责清晰,符合高内聚低耦合。优惠券可以独立演进(比如增加“拉新专项券”逻辑),不影响订单。
  • Cons: 存在分布式一致性挑战。需要处理“订单创建成功但优惠券锁定失败”的回滚逻辑。

2. 战术实现:如何优雅地“核销”?

如果采用方案 B,我们需要引入领域事件(Domain Event)

步骤一:定义聚合的行为

  • Coupon Aggregate: 暴露 lock()redeem() 方法。内部维护状态机(Available -> Locked -> Used)。
  • Order Aggregate: 暴露 create() 方法。内部包含 AppliedCouponId

步骤二:流程编排(Application Service)

应用层不负责业务逻辑,只负责“发号施令”

  1. 调用 CouponRepository 加载优惠券。
  2. 执行 coupon.lock()(内存中状态变更)。
  3. 调用 OrderRepository 保存订单。
  4. 执行 couponRepository.save(coupon) 持久化。

3. 核心挑战:如何应对“并发核销”?

在 10 倍流量下,如果两个请求同时用一张券怎么办?

  • DDIA 思想应用: 我们不依赖数据库的大锁,而是利用乐观锁(Versioning)
  • DDD 处理: 在聚合根保存时校验版本号。如果版本冲突,说明券已被他人抢先锁定,直接抛出领域异常 CouponAlreadyUsedException

4. 关键架构图(描述)

  1. 用户发起请求: 携带 couponId
  2. 应用服务(App Service): 开启事务。
  3. 领域层(Domain): * Coupon 聚合校验有效期和适用范围。
    • Order 聚合计算最终价格。
  4. 基础设施层(Infrastructure): * Repository 执行 SQL:UPDATE coupon SET status = 'LOCKED' WHERE id = ? AND version = ?
  5. 异步补偿: 如果订单支付超时,发布 OrderCancelledEvent,优惠券聚合监听后自动执行 unlock()

5. 演进建议:从“同步”转向“事件驱动”

随着业务量增长,同步锁定优惠券会拖慢订单创建。

  • 10 倍流量策略: 采用 预扣模式
    • 用户下单时,先发一个异步消息去锁定优惠券。
    • 订单系统进入“待确认”状态。
    • 利用 Saga 模式本地消息表 确保两个聚合之间的最终一致性。

最后的一点老兵心得:
DDD 不是为了让代码变多,而是为了让业务规则有地方落脚。如果你发现你的 Service 依然有几千行,而 Entity 只有 Getter/Setter,那么你只是披着 DDD 外壳的传统开发。

这个优惠券核销的逻辑,你是希望我用伪代码展示一下“充血模型”的具体写法,还是想聊聊这种架构下如何做单元测试?