2. DDD 基础回顾:实体、值对象、聚合、聚合根、领域服务、仓储模式

在深入支付订单聚合的实战之前,必须重新校准 DDD 的核心战术模式。支付领域对数据一致性、不可变性和边界划分要求极高,任何概念偏差都会导致后续的聚合设计出现结构性缺陷。本节将结合支付场景,逐一拆解六个核心概念。

2.1 实体(Entity)与值对象(Value Object)

实体与值对象的区分是 DDD 建模的起点。实体拥有唯一标识且会经历生命周期变化;值对象描述某个维度的属性,一旦创建便不可变,且没有全局标识。

维度 实体(Entity) 值对象(Value Object)
标识 拥有全局唯一 ID(如 OrderID) 无全局标识,靠属性值相等判断
可变性 属性可变,状态随时间变化 不可变,修改即替换
生命周期 有独立生命周期,可被追踪 无生命周期,依附于实体
支付示例 订单(Order)、支付单(Payment) 金额(Money)、地址(Address)、支付方式(PaymentMethod)
相等判断 ID 相同即相等 所有属性相同即相等
支付实战要点: 金额(Money)必须设计为值对象,包含币种和金额数值,且提供加减乘除方法返回新实例。绝不允许在实体中直接使用 float64 或 decimal 裸字段,否则精度和币种校验将散落在各处。

2.2 聚合(Aggregate)与聚合根(Aggregate Root)

聚合是一组相关对象的集群,外部只能通过聚合根访问内部成员。聚合根是聚合中唯一能被外部引用的实体,负责维护整个聚合的不变规则。

在支付场景中,一个典型的聚合是 支付单聚合:聚合根是 Payment,内部包含支付金额(值对象)、支付方式(值对象)、支付流水记录(实体集合)。外部服务只能通过 Payment 根对象发起支付、退款或查询操作。

支付单聚合边界示意图 聚合边界(Aggregate Boundary) Payment(聚合根) paymentID: string PaymentRecord(实体) recordID, 状态, 时间 Money(值对象) 金额 + 币种(不可变) PaymentMethod(值对象) 渠道编码 + 参数 Address(值对象) 国家/城市/详情 外部只能引用 Payment 根
聚合设计铁律: 一个事务只修改一个聚合。支付单聚合内的所有变更(创建、支付成功、退款)必须通过 Payment 根对象的方法完成,不允许外部直接操作 PaymentRecord 或修改 Money。

2.3 领域服务(Domain Service)

当某个业务逻辑无法自然地归属于某个实体或值对象时,就需要领域服务。领域服务是无状态的,它协调多个聚合或外部资源完成一个领域操作。

支付场景中的典型领域服务:

  • 支付路由服务(PaymentRoutingService):根据金额、币种、渠道权重选择最优支付渠道。
  • 对账服务(ReconciliationService):对比支付单聚合与渠道侧账单,生成差异记录。
  • 风控校验服务(RiskControlService):在支付前检查交易是否触发风控规则。
// 领域服务示例:支付路由选择
type PaymentRoutingService struct {
    channelRepo ChannelRepository // 仓储接口
}

// SelectRoute 根据支付单聚合和上下文选择最优渠道
func (s *PaymentRoutingService) SelectRoute(
    ctx context.Context,
    payment *Payment, // 聚合根
    amount Money,     // 值对象
) (*ChannelRoute, error) {
    // 1. 获取所有可用渠道
    channels, err := s.channelRepo.FindAvailable(ctx)
    if err != nil {
        return nil, fmt.Errorf("获取可用渠道失败: %w", err)
    }
    // 2. 按权重和金额筛选(领域逻辑)
    return rankAndPick(channels, amount)
}

2.4 仓储模式(Repository Pattern)

仓储为聚合提供持久化与重建的接口,使领域层不依赖具体数据库实现。每个聚合根对应一个仓储接口,仓储只负责聚合的完整存取,不暴露内部实体。

仓储原则 说明 支付示例
按聚合根设计 每个聚合根一个仓储接口 PaymentRepository
完整存取 一次加载整个聚合,不允许部分加载 Save(ctx, *Payment) / Load(ctx, paymentID)
接口在领域层 接口定义在 domain 包,实现在 infrastructure domain.PaymentRepository → infra.MySQLPaymentRepo
不暴露内部表结构 仓储返回聚合根,不返回数据库行 FindByOrderID(ctx, orderID) (*Payment, error)
// 领域层仓储接口定义
package domain

type PaymentRepository interface {
    Save(ctx context.Context, payment *Payment) error
    Load(ctx context.Context, paymentID PaymentID) (*Payment, error)
    FindByOrderID(ctx context.Context, orderID string) (*Payment, error)
    Delete(ctx context.Context, paymentID PaymentID) error
}
常见误区: 不要在仓储中暴露内部实体的查询方法,例如 FindPaymentRecordsByStatus()。这会导致领域逻辑泄露到基础设施层,破坏聚合边界。所有对内部实体的操作必须通过聚合根的方法进行。

2.5 六种模式在支付领域的协作关系

下图展示了实体、值对象、聚合、聚合根、领域服务和仓储在支付场景中的完整协作流程:

DDD 战术模式在支付领域的协作关系 应用服务层(Application Service) 编排领域服务与仓储,管理事务边界 领域层(Domain Layer) 领域服务 无状态,协调多个聚合 聚合(含聚合根) 实体 + 值对象集群 仓储接口 定义在领域层 基础设施层(Infrastructure)—— 实现仓储接口、ORM、RPC 调用
支付架构决策: 在 Go 中实现 DDD 时,仓储接口返回聚合根指针(*Payment),内部使用不可变值对象(Money、PaymentMethod)。领域服务通过构造函数注入仓储接口,不依赖具体实现。这种分层确保支付核心逻辑可测试、可替换、可演进。