2. 报表领域模型:报表实体定义、报表值对象设计、报表聚合设计、报表仓储接口

领域模型是报表回放系统的核心骨架。在 DDD(领域驱动设计)中,报表领域模型通过实体、值对象、聚合与仓储四个层次,将业务规则与数据持久化解耦。本节将逐一拆解这四个关键构造块,并给出 Go 语言下的典型实现。

2.1 报表实体定义

实体(Entity)拥有唯一标识,且其状态会随时间或业务操作发生变化。在报表回放领域,报表实体(Report) 是聚合根,代表一次完整的报表生成任务。

字段 类型 说明
ReportID string (UUID) 全局唯一标识,不可变
TemplateID string 关联的报表模板标识
Status ReportStatus 枚举:Pending / Running / Completed / Failed
CreatedAt time.Time 创建时间
UpdatedAt time.Time 最后更新时间
// Report 是报表回放领域的聚合根实体
type Report struct {
    ReportID   string       `json:"report_id"`
    TemplateID string       `json:"template_id"`
    Status     ReportStatus `json:"status"`
    CreatedAt  time.Time    `json:"created_at"`
    UpdatedAt  time.Time    `json:"updated_at"`
}

// ReportStatus 表示报表生成状态
type ReportStatus string

const (
    ReportPending   ReportStatus = "pending"
    ReportRunning   ReportStatus = "running"
    ReportCompleted ReportStatus = "completed"
    ReportFailed    ReportStatus = "failed"
)
设计要点:实体必须通过工厂方法创建,确保 ReportID 在构造函数中生成,避免外部篡改。状态变更应封装为领域方法(如 Start()、Complete()),而非直接暴露 Setter。

2.2 报表值对象设计

值对象(Value Object)没有唯一标识,通过属性值来定义自身。在报表领域,报表配置(ReportConfig) 和 数据源连接(DataSourceConnection) 是典型的值对象。

  • 不可变性:值对象一旦创建,属性不可修改。需要变更时,应创建新实例。
  • 相等性:两个值对象的所有属性相等时,视为同一个对象。
  • 自包含:值对象不应引用实体或仓储。
// ReportConfig 是报表配置值对象
type ReportConfig struct {
    OutputFormat string            `json:"output_format"` // pdf, xlsx, html
    Filters      map[string]string `json:"filters"`
    PageSize     int               `json:"page_size"`
}

// DataSourceConnection 是数据源连接值对象
type DataSourceConnection struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    Database string `json:"database"`
    Username string `json:"username"`
    Password string `json:"-"` // 敏感字段,序列化时忽略
}

// Equals 比较两个值对象是否相等
func (c ReportConfig) Equals(other ReportConfig) bool {
    return c.OutputFormat == other.OutputFormat &&
        reflect.DeepEqual(c.Filters, other.Filters) &&
        c.PageSize == other.PageSize
}
常见误区:不要为值对象定义 ID 字段,也不要使用指针接收器修改内部属性。值对象应通过值传递(非指针)在方法间传递,以保证不可变性。

2.3 报表聚合设计

聚合(Aggregate)是领域模型中的一致性边界。在报表回放场景中,Report 聚合 包含一个聚合根(Report 实体)和若干内部实体/值对象。聚合内部的所有变更必须通过聚合根对外暴露的方法完成,确保业务不变量始终成立。

报表聚合结构图 Report(聚合根) ReportID | Status | CreatedAt ReportConfig OutputFormat | Filters ReportTask TaskID | Progress | Status DataSourceConn Host | Port | Database 聚合规则 • 聚合根(Report)是外部访问的唯一入口 • 内部对象(ReportTask)只能通过聚合根方法修改 • 值对象(ReportConfig / DataSourceConn)不可变,替换即创建新实例 • 聚合边界内保证最终一致性,跨聚合使用领域事件
// ReportAggregate 封装聚合操作
type ReportAggregate struct {
    report   *Report
    config   ReportConfig
    tasks    []*ReportTask
    conn     DataSourceConnection
}

// NewReportAggregate 创建新的报表聚合
func NewReportAggregate(templateID string, config ReportConfig, conn DataSourceConnection) *ReportAggregate {
    return &ReportAggregate{
        report: &Report{
            ReportID:   uuid.New().String(),
            TemplateID: templateID,
            Status:     ReportPending,
            CreatedAt:  time.Now(),
            UpdatedAt:  time.Now(),
        },
        config: config,
        tasks:  make([]*ReportTask, 0),
        conn:   conn,
    }
}

// Start 启动报表生成,仅当状态为 Pending 时允许
func (a *ReportAggregate) Start() error {
    if a.report.Status != ReportPending {
        return errors.New("only pending report can be started")
    }
    a.report.Status = ReportRunning
    a.report.UpdatedAt = time.Now()
    // 创建初始任务
    a.tasks = append(a.tasks, NewReportTask(a.report.ReportID, "data_fetch"))
    return nil
}

2.4 报表仓储接口

仓储(Repository)为聚合提供持久化与检索能力,使领域层不依赖具体数据库实现。仓储接口定义在领域层,实现放在基础设施层。

// ReportRepository 是报表聚合的仓储接口
type ReportRepository interface {
    // Save 保存聚合(新增或更新)
    Save(ctx context.Context, aggregate *ReportAggregate) error
    // FindByID 根据聚合根 ID 查找完整聚合
    FindByID(ctx context.Context, reportID string) (*ReportAggregate, error)
    // FindByStatus 根据状态查找聚合列表
    FindByStatus(ctx context.Context, status ReportStatus) ([]*ReportAggregate, error)
    // Delete 删除聚合
    Delete(ctx context.Context, reportID string) error
}
仓储设计原则:
  • 仓储接口只操作聚合根,不直接操作内部实体或值对象。
  • 方法命名使用业务语言(如 Save 而非 InsertOrUpdate)。
  • 返回完整聚合,调用方不应再通过其他仓储补数据。
  • 事务边界由应用服务控制,仓储内部不开启事务。

2.5 领域模型关系总览

下表总结了报表领域模型中各元素的职责与关系:

元素 类型 标识 可变性 持久化方式
Report 实体(聚合根) ReportID 可变 通过仓储整体持久化
ReportTask 实体(内部) TaskID 可变 随聚合一起持久化
ReportConfig 值对象 无 不可变 序列化为聚合的一部分
DataSourceConnection 值对象 无 不可变 序列化为聚合的一部分