2. 领域驱动设计基础:实体、值对象、聚合、领域服务、仓储模式在报表场景下的概念映射

领域驱动设计(DDD)为复杂业务系统提供了清晰的建模方法论。在报表模块中,DDD 的核心构造块——实体、值对象、聚合、领域服务与仓储——需要结合报表的“只读分析”与“配置管理”双重特性进行重新映射。本章将逐一拆解这些概念在报表边界内的具体落地形态。

2.1 报表场景下的实体(Entity)

实体拥有唯一标识,且其状态会随时间或业务操作发生变化。在报表模块中,典型的实体包括:

  • 报表定义(ReportDefinition):每个报表模板拥有全局唯一的 reportId,其属性(如 SQL 语句、参数列表、缓存策略)可被运维人员修改。
  • 调度任务(ScheduleTask):定时生成报表的任务,拥有 taskId,状态可在“启用/停用/执行中”之间流转。
  • 数据源配置(DataSourceConfig):数据库连接信息,拥有 dsId,密码轮换或连接池调整会改变其状态。
边界提示: 报表模块中的实体通常集中在“配置管理”子域。纯查询的报表结果(如数据集)不应建模为实体,因为其生命周期由查询触发,不具备独立标识。

2.2 报表场景下的值对象(Value Object)

值对象通过属性值定义自身,无唯一标识,且不可变。在报表模块中,值对象大量用于描述查询条件与结果结构:

值对象 属性 不可变原因
QueryParam name, type, defaultValue, validationRule 参数定义一旦创建,不应被修改;修改应生成新版本
ColumnMeta fieldName, displayName, dataType, aggregation 列元数据由 SQL 解析生成,修改无意义
FilterCondition field, operator, value 条件组合由前端传入,后端只读使用
ReportResult rows, total, page, size 查询结果是一次性快照,不应被修改
// Go 示例:值对象 QueryParam 的不可变设计
type QueryParam struct {
    Name           string
    Type           string // "string", "int", "date"
    DefaultValue   string
    ValidationRule string
}

// 通过构造函数保证完整创建,不提供 Setter
func NewQueryParam(name, typ, defaultVal, rule string) (QueryParam, error) {
    if name == "" {
        return QueryParam{}, errors.New("param name required")
    }
    return QueryParam{Name: name, Type: typ, DefaultValue: defaultVal, ValidationRule: rule}, nil
}

2.3 报表场景下的聚合(Aggregate)

聚合是数据修改的一致性边界。在报表模块中,报表定义聚合是最核心的聚合根:

报表定义聚合(ReportDefinition Aggregate) ReportDefinition(聚合根) QueryParams[](值对象) ColumnMetas[](值对象) FilterConditions(值对象) ScheduleTask(实体) ExportConfig(实体) AccessControl(实体) 聚合边界规则 • 修改报表定义时,必须通过聚合根 ReportDefinition 操作 • 子实体(ScheduleTask)不能脱离聚合根独立被仓储保存
聚合设计原则: 报表模块中,一个聚合内的所有对象应同时加载、同时持久化。避免将“报表定义”与“数据源”放在同一聚合——数据源是独立生命周期,应作为另一个聚合根。

2.4 报表场景下的领域服务(Domain Service)

当某个业务操作无法自然地归属于某个实体或值对象时,应使用领域服务。报表模块中典型的领域服务包括:

  • ReportExecutionService:负责执行报表 SQL、应用参数、处理分页与缓存。该服务协调 ReportDefinition(聚合根)与 DataSource(外部聚合)之间的交互。
  • ReportValidationService:校验 SQL 语法、参数类型匹配、列权限过滤。该服务不修改任何聚合状态,只返回校验结果。
  • ScheduleTriggerService:根据调度策略触发报表生成,涉及时间计算与任务队列分发。
// Go 示例:领域服务 ReportExecutionService
type ReportExecutionService struct {
    reportRepo ReportRepository
    dsRepo     DataSourceRepository
    cache      ReportCache
}

func (s *ReportExecutionService) Execute(ctx context.Context, reportID string, params map[string]string) (*ReportResult, error) {
    // 1. 加载聚合根
    def, err := s.reportRepo.FindByID(ctx, reportID)
    if err != nil {
        return nil, fmt.Errorf("load report def failed: %w", err)
    }
    // 2. 校验参数(值对象)
    validatedParams, err := def.ValidateParams(params)
    if err != nil {
        return nil, err
    }
    // 3. 获取数据源(跨聚合)
    ds, err := s.dsRepo.FindByID(ctx, def.DataSourceID)
    if err != nil {
        return nil, err
    }
    // 4. 执行查询(领域服务协调)
    result, err := s.executeQuery(ctx, ds, def.SQL, validatedParams)
    if err != nil {
        return nil, err
    }
    // 5. 缓存结果
    s.cache.Set(reportID, params, result)
    return result, nil
}

2.5 报表场景下的仓储模式(Repository)

仓储为聚合提供持久化与重建的抽象接口。在报表模块中,仓储应面向聚合根设计,而非每个表对应一个仓储:

仓储接口 对应聚合根 典型方法
ReportRepository ReportDefinition FindByID, Save, Delete, SearchByTags
ScheduleRepository ScheduleTask FindByID, FindDueTasks, Save, Cancel
DataSourceRepository DataSourceConfig FindByID, FindAll, Save, TestConnection
常见误区: 不要为值对象(如 QueryParam)单独创建仓储。值对象应通过聚合根仓储一起持久化。例如,ReportRepository.Save 应同时保存 ReportDefinition 及其内部的 QueryParams 列表。

2.6 概念映射总结

DDD 概念 → 报表模块映射总览 DDD 概念 实体 值对象 聚合 领域服务 仓储 报表模块映射 ReportDefinition, ScheduleTask, DataSourceConfig QueryParam, ColumnMeta, FilterCondition, ReportResult ReportDefinition 聚合(含子实体与值对象) ReportExecutionService, ReportValidationService ReportRepository, ScheduleRepository, DataSourceRepository
关键原则: 报表模块的 DDD 建模应遵循“配置用实体、查询用值对象、修改用聚合、协调用服务、持久化用仓储”的映射策略。这能有效隔离报表的“读”与“写”两条路径,避免将查询逻辑污染到领域模型中。