2. 报表领域模型设计:报表实体定义、报表值对象设计、报表聚合根建模、报表领域服务

领域模型是报表系统的核心骨架。在 Go 语言实现的 DDD 架构中,报表领域模型需要精确区分实体、值对象、聚合根与领域服务的职责边界。本节从这四个维度展开,建立可落地的报表领域模型设计范式。

2.1 报表实体定义

实体(Entity)拥有唯一标识,且其状态会随业务操作发生变化。在报表领域,报表定义(ReportDefinition) 是最核心的实体。

属性 类型 说明
ReportID string (UUID) 全局唯一标识,实体身份锚点
Name string 报表名称,业务可读
DataSourceID string 关联的数据源标识
Status ReportStatus 草稿/已发布/已归档
Version int 乐观锁版本号,用于并发控制
CreatedAt time.Time 创建时间
UpdatedAt time.Time 最后修改时间
实体设计原则: 报表实体的 ReportID 在系统内不可变,即使报表名称或数据源变更,标识依然保持不变。Go 中建议使用 uuid.New() 生成,避免自增 ID 在分布式环境下的冲突风险。
// ReportDefinition 是报表领域的核心实体
type ReportDefinition struct {
    ReportID     string       `json:"report_id"`
    Name         string       `json:"name"`
    DataSourceID string       `json:"data_source_id"`
    Status       ReportStatus `json:"status"`
    Version      int          `json:"version"`
    CreatedAt    time.Time    `json:"created_at"`
    UpdatedAt    time.Time    `json:"updated_at"`
}

// ReportStatus 表示报表生命周期状态
type ReportStatus string

const (
    ReportStatusDraft     ReportStatus = "draft"
    ReportStatusPublished ReportStatus = "published"
    ReportStatusArchived  ReportStatus = "archived"
)

2.2 报表值对象设计

值对象(Value Object)没有唯一标识,通过属性值来定义自身,且不可变。在报表模型中,查询参数(QueryParam) 和 报表样式(ReportStyle) 是典型的值对象。

值对象 属性 不可变规则
QueryParam StartDate, EndDate, Filters, SortBy 创建后不可修改,替换即新建
ReportStyle Theme, FontSize, ColorScheme 通过 With* 方法返回新实例
ColumnDef FieldName, DisplayName, Width, Align 值相等即视为相同
Go 实现技巧: 值对象应实现 Equals() 方法用于比较,且所有字段导出为只读(首字母小写或使用 getter 方法)。避免暴露指针,防止外部修改。
// QueryParam 是不可变值对象
type QueryParam struct {
    startDate time.Time
    endDate   time.Time
    filters   map[string]string
    sortBy    string
}

func NewQueryParam(start, end time.Time, filters map[string]string, sortBy string) QueryParam {
    return QueryParam{
        startDate: start,
        endDate:   end,
        filters:   filters,
        sortBy:    sortBy,
    }
}

func (q QueryParam) Equals(other QueryParam) bool {
    return q.startDate.Equal(other.startDate) &&
        q.endDate.Equal(other.endDate) &&
        reflect.DeepEqual(q.filters, other.filters) &&
        q.sortBy == other.sortBy
}

2.3 报表聚合根建模

聚合根(Aggregate Root)是领域模型的边界控制点。在报表上下文中,报表聚合(ReportAggregate) 将实体与值对象组合为一个一致性单元,外部只能通过聚合根访问内部对象。

报表聚合根结构 聚合根边界(Aggregate Root Boundary) ReportAggregate ReportDefinition (实体) QueryParam (值对象) ReportStyle (值对象) ColumnDef[] (值对象集合) 外部只能通过 ReportAggregate 访问内部实体和值对象,保证数据一致性
// ReportAggregate 是报表聚合根
type ReportAggregate struct {
    report   *ReportDefinition  // 实体
    query    QueryParam         // 值对象
    style    ReportStyle        // 值对象
    columns  []ColumnDef        // 值对象集合
}

func NewReportAggregate(report *ReportDefinition, query QueryParam, style ReportStyle, columns []ColumnDef) *ReportAggregate {
    return &ReportAggregate{
        report:  report,
        query:   query,
        style:   style,
        columns: columns,
    }
}

// 聚合根提供业务方法,外部不能直接修改内部对象
func (a *ReportAggregate) Publish() error {
    if a.report.Status != ReportStatusDraft {
        return fmt.Errorf("only draft report can be published")
    }
    a.report.Status = ReportStatusPublished
    a.report.UpdatedAt = time.Now()
    return nil
}
聚合根设计避坑: 不要将数据库操作(如 Repository 调用)放在聚合根方法内。聚合根只负责业务规则校验和状态变更,持久化由领域服务或应用服务协调完成。

2.4 报表领域服务

领域服务(Domain Service)用于处理那些不适合放在实体或值对象中的业务逻辑,通常涉及多个聚合根协作或外部系统交互。

领域服务 职责 协作对象
ReportExecutionService 执行报表查询,组装结果集 ReportAggregate, DataSource
ReportValidationService 校验报表参数合法性、数据源可用性 ReportAggregate, DataSourceRepository
ReportExportService 将报表结果导出为 PDF/Excel/CSV ReportAggregate, ExportAdapter
报表领域服务协作流程 ReportExecutionService 执行查询 ReportValidationService 校验参数与数据源 ReportExportService 导出结果 ReportAggregate DataSourceRepository ExportAdapter 领域服务无状态,通过接口依赖倒置(依赖 Repository/Adapter 接口) 避免领域服务直接依赖具体实现,保持可测试性
// ReportExecutionService 是报表执行领域服务
type ReportExecutionService struct {
    dataSourceRepo DataSourceRepository
    resultBuilder  *ReportResultBuilder
}

func NewReportExecutionService(dsRepo DataSourceRepository, builder *ReportResultBuilder) *ReportExecutionService {
    return &ReportExecutionService{
        dataSourceRepo: dsRepo,
        resultBuilder:  builder,
    }
}

// Execute 执行报表查询,返回结果集
func (s *ReportExecutionService) Execute(ctx context.Context, agg *ReportAggregate) (*ReportResult, error) {
    // 1. 获取数据源
    ds, err := s.dataSourceRepo.FindByID(ctx, agg.report.DataSourceID)
    if err != nil {
        return nil, fmt.Errorf("data source not found: %w", err)
    }

    // 2. 构建查询
    query := buildQuery(agg.query, agg.columns)

    // 3. 执行查询并组装结果
    rawData, err := ds.Query(ctx, query)
    if err != nil {
        return nil, fmt.Errorf("query execution failed: %w", err)
    }

    return s.resultBuilder.Build(rawData, agg.style), nil
}
领域服务 vs 应用服务: 领域服务包含业务规则(如“报表执行前必须校验数据源状态”),而应用服务只负责编排领域服务、事务管理和权限校验。保持领域服务纯净,不引入 HTTP、gRPC 等基础设施概念。

2.5 模型设计总结

报表领域模型的分层设计遵循 DDD 战术模式,各元素职责清晰:

  • 实体: ReportDefinition 拥有唯一标识,状态可变,是业务操作的主体。
  • 值对象: QueryParam、ReportStyle、ColumnDef 不可变,通过值相等判断,降低系统复杂度。
  • 聚合根: ReportAggregate 作为边界,封装实体与值对象的业务一致性,外部只能通过聚合根方法操作。
  • 领域服务: 处理跨聚合根或依赖外部资源的业务逻辑,保持无状态,通过接口与基础设施解耦。

在 Go 实现中,建议将每个领域元素放在独立的 package 内(如 entity/、valueobject/、aggregate/、service/),通过接口定义依赖关系,确保领域层的独立性和可测试性。