# Reporting & Analytics Service 开发指导

## 项目概述

Reporting & Analytics Service 是 RWA 榴莲皇后平台的报表分析微服务，负责多维报表生成、数据统计与分析、报表导出、数据大屏支持等功能。

### 核心职责 ✅
- 多维报表生成（按时间/地域/权益类型等维度）
- 龙虎榜数据统计（日榜、周榜、月榜排名导出）
- 榴莲树认种报表（日/周/月/季度/年度）
- 各省/市认种报表统计
- 社区数据统计（支持模糊查询、日期范围筛选）
- 系统省/市公司账户数据统计
- 收益来源细分统计与时间轴
- 报表导出（Excel/PDF/CSV/JSON）
- 数据大屏支持

### 不负责 ❌
- 实时业务数据存储（从其他上下文聚合）
- 权限控制（Authorization Context）
- 复杂计算（由源上下文提供）
- 业务逻辑处理（由各业务服务负责）

---

## 核心业务需求

### 1. 龙虎榜数据统计
```
报表类型：日榜、周榜、月榜
统计内容：排名、用户、分值、团队数据
功能要求：
- 可指定统计范围（时间段）
- 支持导出表格（Excel/CSV）
- 历史榜单数据查询
```

### 2. 榴莲树认种报表
```
报表周期：
- 日报表：按天统计
- 周报表：按周统计
- 月报表：按月统计
- 季度报表：按季度统计
- 年度报表：按年统计

统计内容：
- 认种数量（棵）
- 认种金额（USDT）
- 新增用户数
- 活跃用户数
- 同比/环比增长率
```

### 3. 各省/市认种报表
```
报表类型：
- 按省统计的认种报表（日/周/月/季度/年度）
- 按市统计的认种报表（日/周/月/季度/年度）

统计内容：
- 区域认种数量
- 区域认种金额
- 区域用户数
- 区域排名
```

### 4. 授权省/市公司第1名统计
```
统计内容：
- 各授权省公司的第1名用户及其完成数据
- 各授权市公司的第1名用户及其完成数据
- 包含认种量、团队数、收益等指标
```

### 5. 社区数据统计
```
统计维度：
- 本社区名称
- 上级社区
- 下级社区列表
- 社区认种总量

查询功能：
- 日新增认种量
- 周新增认种量
- 月新增认种量
- 指定日期范围查询
- 模糊查询（社区名、用户名）
```

### 6. 系统省/市公司账户月度统计
```
统计指标：
① 每月算力
② 累计算力
③ 每月挖矿量
④ 累计挖矿量
⑤ 每月佣金
⑥ 累计佣金
⑦ 每月累计认种提成
⑧ 累计佣金总额

输出格式：表格报表，支持导出
```

### 7. 系统账户收益来源细分统计
```
统计内容：
- 每笔收益的来源类型
- 每笔收益的金额
- 每笔收益的时间轴

筛选条件：
- 时间范围筛选
- 关键词筛选（地址、交易流水号等）
- 账户类型筛选（省公司/市公司）
```

---

## 技术栈

| 组件 | 技术选型 |
|------|----------|
| **框架** | NestJS 10.x |
| **数据库** | PostgreSQL + Prisma ORM |
| **架构** | DDD + Hexagonal Architecture (六边形架构) |
| **语言** | TypeScript 5.x |
| **消息队列** | Kafka (kafkajs) |
| **缓存** | Redis (ioredis) |
| **定时任务** | @nestjs/schedule |
| **报表导出** | ExcelJS (Excel), PDFKit (PDF) |
| **API文档** | Swagger (@nestjs/swagger) |

---

## 架构设计

```
reporting-service/
├── prisma/
│   ├── schema.prisma              # 数据库模型定义
│   └── migrations/                # 数据库迁移文件
│
├── src/
│   ├── api/                       # 🔵 Presentation Layer (表现层)
│   │   ├── controllers/
│   │   │   ├── health.controller.ts
│   │   │   ├── leaderboard-report.controller.ts
│   │   │   ├── planting-report.controller.ts
│   │   │   ├── regional-report.controller.ts
│   │   │   ├── community-report.controller.ts
│   │   │   ├── system-account-report.controller.ts
│   │   │   └── export.controller.ts
│   │   ├── dto/
│   │   │   ├── request/
│   │   │   │   ├── generate-report.dto.ts
│   │   │   │   ├── query-report.dto.ts
│   │   │   │   ├── export-report.dto.ts
│   │   │   │   └── date-range.dto.ts
│   │   │   └── response/
│   │   │       ├── report-snapshot.dto.ts
│   │   │       ├── planting-report.dto.ts
│   │   │       ├── regional-report.dto.ts
│   │   │       ├── community-report.dto.ts
│   │   │       └── system-account-report.dto.ts
│   │   └── api.module.ts
│   │
│   ├── application/               # 🟢 Application Layer (应用层)
│   │   ├── commands/
│   │   │   ├── generate-report/
│   │   │   │   ├── generate-report.command.ts
│   │   │   │   └── generate-report.handler.ts
│   │   │   ├── export-report/
│   │   │   │   ├── export-report.command.ts
│   │   │   │   └── export-report.handler.ts
│   │   │   ├── schedule-report/
│   │   │   │   ├── schedule-report.command.ts
│   │   │   │   └── schedule-report.handler.ts
│   │   │   └── index.ts
│   │   ├── queries/
│   │   │   ├── get-leaderboard-report/
│   │   │   │   ├── get-leaderboard-report.query.ts
│   │   │   │   └── get-leaderboard-report.handler.ts
│   │   │   ├── get-planting-report/
│   │   │   │   ├── get-planting-report.query.ts
│   │   │   │   └── get-planting-report.handler.ts
│   │   │   ├── get-regional-report/
│   │   │   │   ├── get-regional-report.query.ts
│   │   │   │   └── get-regional-report.handler.ts
│   │   │   ├── get-community-report/
│   │   │   │   ├── get-community-report.query.ts
│   │   │   │   └── get-community-report.handler.ts
│   │   │   ├── get-system-account-report/
│   │   │   │   ├── get-system-account-report.query.ts
│   │   │   │   └── get-system-account-report.handler.ts
│   │   │   └── index.ts
│   │   ├── services/
│   │   │   └── reporting-application.service.ts
│   │   ├── schedulers/
│   │   │   ├── report-generation.scheduler.ts
│   │   │   └── snapshot-cleanup.scheduler.ts
│   │   └── application.module.ts
│   │
│   ├── domain/                    # 🟡 Domain Layer (领域层)
│   │   ├── aggregates/
│   │   │   ├── report-definition/
│   │   │   │   ├── report-definition.aggregate.ts
│   │   │   │   ├── report-definition.spec.ts
│   │   │   │   └── index.ts
│   │   │   └── report-snapshot/
│   │   │       ├── report-snapshot.aggregate.ts
│   │   │       ├── report-snapshot.spec.ts
│   │   │       └── index.ts
│   │   ├── entities/
│   │   │   ├── analytics-metric.entity.ts
│   │   │   ├── report-file.entity.ts
│   │   │   └── index.ts
│   │   ├── value-objects/
│   │   │   ├── report-type.enum.ts
│   │   │   ├── report-period.enum.ts
│   │   │   ├── report-dimension.enum.ts
│   │   │   ├── report-parameters.vo.ts
│   │   │   ├── report-schedule.vo.ts
│   │   │   ├── output-format.enum.ts
│   │   │   ├── snapshot-data.vo.ts
│   │   │   ├── data-source.vo.ts
│   │   │   ├── date-range.vo.ts
│   │   │   └── index.ts
│   │   ├── events/
│   │   │   ├── domain-event.base.ts
│   │   │   ├── report-generated.event.ts
│   │   │   ├── report-exported.event.ts
│   │   │   ├── snapshot-created.event.ts
│   │   │   └── index.ts
│   │   ├── repositories/
│   │   │   ├── report-definition.repository.interface.ts
│   │   │   ├── report-snapshot.repository.interface.ts
│   │   │   ├── analytics-metric.repository.interface.ts
│   │   │   └── index.ts
│   │   ├── services/
│   │   │   ├── report-generation.service.ts
│   │   │   ├── report-export.service.ts
│   │   │   ├── data-aggregation.service.ts
│   │   │   └── index.ts
│   │   └── domain.module.ts
│   │
│   ├── infrastructure/            # 🔴 Infrastructure Layer (基础设施层)
│   │   ├── persistence/
│   │   │   ├── prisma/
│   │   │   │   └── prisma.service.ts
│   │   │   ├── mappers/
│   │   │   │   ├── report-definition.mapper.ts
│   │   │   │   ├── report-snapshot.mapper.ts
│   │   │   │   └── analytics-metric.mapper.ts
│   │   │   └── repositories/
│   │   │       ├── report-definition.repository.impl.ts
│   │   │       ├── report-snapshot.repository.impl.ts
│   │   │       └── analytics-metric.repository.impl.ts
│   │   ├── external/
│   │   │   ├── planting-service/
│   │   │   │   └── planting-service.client.ts
│   │   │   ├── reward-service/
│   │   │   │   └── reward-service.client.ts
│   │   │   ├── referral-service/
│   │   │   │   └── referral-service.client.ts
│   │   │   ├── leaderboard-service/
│   │   │   │   └── leaderboard-service.client.ts
│   │   │   ├── wallet-service/
│   │   │   │   └── wallet-service.client.ts
│   │   │   └── identity-service/
│   │   │       └── identity-service.client.ts
│   │   ├── export/
│   │   │   ├── excel-export.service.ts
│   │   │   ├── pdf-export.service.ts
│   │   │   ├── csv-export.service.ts
│   │   │   └── export.module.ts
│   │   ├── storage/
│   │   │   ├── file-storage.service.ts
│   │   │   └── storage.module.ts
│   │   ├── kafka/
│   │   │   ├── event-consumer.controller.ts
│   │   │   ├── event-publisher.service.ts
│   │   │   └── kafka.module.ts
│   │   ├── redis/
│   │   │   ├── redis.service.ts
│   │   │   ├── report-cache.service.ts
│   │   │   └── redis.module.ts
│   │   └── infrastructure.module.ts
│   │
│   ├── shared/                    # 共享模块
│   │   ├── decorators/
│   │   │   ├── current-user.decorator.ts
│   │   │   ├── public.decorator.ts
│   │   │   └── index.ts
│   │   ├── exceptions/
│   │   │   ├── domain.exception.ts
│   │   │   ├── application.exception.ts
│   │   │   └── index.ts
│   │   ├── filters/
│   │   │   ├── global-exception.filter.ts
│   │   │   └── domain-exception.filter.ts
│   │   ├── guards/
│   │   │   ├── jwt-auth.guard.ts
│   │   │   └── admin.guard.ts
│   │   ├── interceptors/
│   │   │   └── transform.interceptor.ts
│   │   └── strategies/
│   │       └── jwt.strategy.ts
│   │
│   ├── config/
│   │   ├── app.config.ts
│   │   ├── database.config.ts
│   │   ├── jwt.config.ts
│   │   ├── redis.config.ts
│   │   ├── kafka.config.ts
│   │   ├── storage.config.ts
│   │   └── index.ts
│   │
│   ├── app.module.ts
│   └── main.ts
│
├── test/
├── .env.example
├── .env.development
├── package.json
├── tsconfig.json
└── Dockerfile
```

---

## 第一阶段：项目初始化

### 1.1 创建 NestJS 项目

```bash
cd backend/services/reporting-service
npx @nestjs/cli new . --skip-git --package-manager npm
```

### 1.2 安装依赖

```bash
# 核心依赖
npm install @nestjs/config @nestjs/swagger @nestjs/jwt @nestjs/passport @nestjs/microservices @nestjs/schedule
npm install @prisma/client class-validator class-transformer uuid ioredis kafkajs
npm install passport passport-jwt

# 报表导出依赖
npm install exceljs pdfkit csv-stringify

# 开发依赖
npm install -D prisma @types/uuid @types/passport-jwt @types/pdfkit
npm install -D @nestjs/testing jest ts-jest @types/jest supertest @types/supertest
```

### 1.3 环境变量配置

创建 `.env.development`:
```env
# 应用配置
NODE_ENV=development
PORT=3008
APP_NAME=reporting-service

# 数据库
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_reporting?schema=public"

# JWT (与 identity-service 共享密钥)
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_ACCESS_EXPIRES_IN=2h

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# Kafka
KAFKA_BROKERS=localhost:9092
KAFKA_GROUP_ID=reporting-service-group
KAFKA_CLIENT_ID=reporting-service

# 外部服务
IDENTITY_SERVICE_URL=http://localhost:3001
PLANTING_SERVICE_URL=http://localhost:3003
REFERRAL_SERVICE_URL=http://localhost:3004
REWARD_SERVICE_URL=http://localhost:3005
LEADERBOARD_SERVICE_URL=http://localhost:3007
WALLET_SERVICE_URL=http://localhost:3002

# 文件存储
FILE_STORAGE_PATH=./storage/reports
FILE_STORAGE_URL_PREFIX=http://localhost:3008/files

# 报表缓存过期时间（秒）
REPORT_CACHE_TTL=3600

# 报表快照保留天数
SNAPSHOT_RETENTION_DAYS=90
```

---

## 第二阶段：数据库设计 (Prisma Schema)

### 2.1 创建 prisma/schema.prisma

```prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// ============================================
// 报表定义表 (聚合根1)
// 定义各类报表的配置和调度规则
// ============================================
model ReportDefinition {
  id              BigInt   @id @default(autoincrement()) @map("definition_id")

  // === 报表基本信息 ===
  reportType      String   @map("report_type") @db.VarChar(50)     // 报表类型
  reportName      String   @map("report_name") @db.VarChar(200)    // 报表名称
  reportCode      String   @unique @map("report_code") @db.VarChar(50)  // 报表代码
  description     String?  @map("description") @db.Text            // 报表描述

  // === 报表参数 ===
  parameters      Json     @map("parameters")                      // 报表参数配置

  // === 调度配置 ===
  scheduleCron    String?  @map("schedule_cron") @db.VarChar(100)  // Cron表达式
  scheduleTimezone String? @map("schedule_timezone") @db.VarChar(50) @default("Asia/Shanghai")
  scheduleEnabled Boolean  @default(false) @map("schedule_enabled")

  // === 输出格式 ===
  outputFormats   String[] @map("output_formats")                  // 支持的输出格式

  // === 状态 ===
  isActive        Boolean  @default(true) @map("is_active")

  // === 时间戳 ===
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")
  lastGeneratedAt DateTime? @map("last_generated_at")

  @@map("report_definitions")
  @@index([reportType], name: "idx_def_type")
  @@index([isActive], name: "idx_def_active")
  @@index([scheduleEnabled], name: "idx_def_scheduled")
}

// ============================================
// 报表快照表 (聚合根2 - 读模型)
// 存储已生成的报表数据快照
// ============================================
model ReportSnapshot {
  id              BigInt   @id @default(autoincrement()) @map("snapshot_id")

  // === 报表信息 ===
  reportType      String   @map("report_type") @db.VarChar(50)
  reportCode      String   @map("report_code") @db.VarChar(50)
  reportPeriod    String   @map("report_period") @db.VarChar(20)   // DAILY/WEEKLY/MONTHLY/QUARTERLY/YEARLY
  periodKey       String   @map("period_key") @db.VarChar(30)      // 2024-01-15 / 2024-W03 / 2024-01 / 2024-Q1 / 2024

  // === 快照数据 ===
  snapshotData    Json     @map("snapshot_data")                   // 报表数据
  summaryData     Json?    @map("summary_data")                    // 汇总数据

  // === 数据来源 ===
  dataSources     String[] @map("data_sources")                    // 数据来源服务
  dataFreshness   Int      @default(0) @map("data_freshness")      // 数据新鲜度(秒)

  // === 过滤条件 ===
  filterParams    Json?    @map("filter_params")                   // 筛选参数

  // === 统计信息 ===
  rowCount        Int      @default(0) @map("row_count")           // 数据行数

  // === 时间戳 ===
  periodStartAt   DateTime @map("period_start_at")
  periodEndAt     DateTime @map("period_end_at")
  generatedAt     DateTime @default(now()) @map("generated_at")
  expiresAt       DateTime? @map("expires_at")

  @@map("report_snapshots")
  @@unique([reportCode, periodKey], name: "uk_report_period")
  @@index([reportType], name: "idx_snapshot_type")
  @@index([reportCode], name: "idx_snapshot_code")
  @@index([periodKey], name: "idx_snapshot_period")
  @@index([generatedAt(sort: Desc)], name: "idx_snapshot_generated")
  @@index([expiresAt], name: "idx_snapshot_expires")
}

// ============================================
// 报表文件表
// 存储已导出的报表文件信息
// ============================================
model ReportFile {
  id              BigInt   @id @default(autoincrement()) @map("file_id")
  snapshotId      BigInt   @map("snapshot_id")

  // === 文件信息 ===
  fileName        String   @map("file_name") @db.VarChar(500)
  filePath        String   @map("file_path") @db.VarChar(1000)
  fileUrl         String?  @map("file_url") @db.VarChar(1000)
  fileSize        BigInt   @map("file_size")                       // 文件大小(字节)
  fileFormat      String   @map("file_format") @db.VarChar(20)     // EXCEL/PDF/CSV/JSON
  mimeType        String   @map("mime_type") @db.VarChar(100)

  // === 访问信息 ===
  downloadCount   Int      @default(0) @map("download_count")
  lastDownloadAt  DateTime? @map("last_download_at")

  // === 时间戳 ===
  createdAt       DateTime @default(now()) @map("created_at")
  expiresAt       DateTime? @map("expires_at")

  @@map("report_files")
  @@index([snapshotId], name: "idx_file_snapshot")
  @@index([fileFormat], name: "idx_file_format")
  @@index([createdAt(sort: Desc)], name: "idx_file_created")
}

// ============================================
// 分析指标表 (聚合数据)
// 存储预聚合的分析指标数据
// ============================================
model AnalyticsMetric {
  id              BigInt   @id @default(autoincrement()) @map("metric_id")

  // === 指标信息 ===
  metricType      String   @map("metric_type") @db.VarChar(50)     // 指标类型
  metricCode      String   @map("metric_code") @db.VarChar(50)     // 指标代码

  // === 维度 ===
  dimensionTime   DateTime? @map("dimension_time") @db.Date        // 时间维度
  dimensionRegion String?  @map("dimension_region") @db.VarChar(100) // 地域维度(省/市编码)
  dimensionUserType String? @map("dimension_user_type") @db.VarChar(50) // 用户类型维度
  dimensionRightType String? @map("dimension_right_type") @db.VarChar(50) // 权益类型维度

  // === 指标值 ===
  metricValue     Decimal  @map("metric_value") @db.Decimal(20, 8)
  metricData      Json?    @map("metric_data")                     // 指标详情

  // === 时间戳 ===
  calculatedAt    DateTime @default(now()) @map("calculated_at")

  @@map("analytics_metrics")
  @@unique([metricCode, dimensionTime, dimensionRegion, dimensionUserType, dimensionRightType], name: "uk_metric_dimensions")
  @@index([metricType], name: "idx_metric_type")
  @@index([metricCode], name: "idx_metric_code")
  @@index([dimensionTime], name: "idx_metric_time")
  @@index([dimensionRegion], name: "idx_metric_region")
}

// ============================================
// 认种统计日表 (每日聚合)
// ============================================
model PlantingDailyStat {
  id              BigInt   @id @default(autoincrement()) @map("stat_id")

  // === 统计日期 ===
  statDate        DateTime @map("stat_date") @db.Date

  // === 区域维度 ===
  provinceCode    String?  @map("province_code") @db.VarChar(10)
  cityCode        String?  @map("city_code") @db.VarChar(10)

  // === 统计数据 ===
  orderCount      Int      @default(0) @map("order_count")         // 订单数
  treeCount       Int      @default(0) @map("tree_count")          // 认种棵数
  totalAmount     Decimal  @default(0) @map("total_amount") @db.Decimal(20, 8) // 认种金额
  newUserCount    Int      @default(0) @map("new_user_count")      // 新增用户数
  activeUserCount Int      @default(0) @map("active_user_count")   // 活跃用户数

  // === 时间戳 ===
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")

  @@map("planting_daily_stats")
  @@unique([statDate, provinceCode, cityCode], name: "uk_daily_stat")
  @@index([statDate], name: "idx_pds_date")
  @@index([provinceCode], name: "idx_pds_province")
  @@index([cityCode], name: "idx_pds_city")
}

// ============================================
// 社区统计表
// ============================================
model CommunityStat {
  id              BigInt   @id @default(autoincrement()) @map("stat_id")

  // === 社区信息 ===
  communityId     BigInt   @map("community_id")
  communityName   String   @map("community_name") @db.VarChar(200)
  parentCommunityId BigInt? @map("parent_community_id")

  // === 统计日期 ===
  statDate        DateTime @map("stat_date") @db.Date

  // === 统计数据 ===
  totalPlanting   Int      @default(0) @map("total_planting")      // 累计认种量
  dailyPlanting   Int      @default(0) @map("daily_planting")      // 日新增认种
  weeklyPlanting  Int      @default(0) @map("weekly_planting")     // 周新增认种
  monthlyPlanting Int      @default(0) @map("monthly_planting")    // 月新增认种
  memberCount     Int      @default(0) @map("member_count")        // 社区成员数

  // === 时间戳 ===
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")

  @@map("community_stats")
  @@unique([communityId, statDate], name: "uk_community_stat")
  @@index([communityId], name: "idx_cs_community")
  @@index([communityName], name: "idx_cs_name")
  @@index([statDate], name: "idx_cs_date")
  @@index([parentCommunityId], name: "idx_cs_parent")
}

// ============================================
// 系统账户月度统计表
// 省公司/市公司账户的月度数据
// ============================================
model SystemAccountMonthlyStat {
  id              BigInt   @id @default(autoincrement()) @map("stat_id")

  // === 账户信息 ===
  accountId       BigInt   @map("account_id")
  accountType     String   @map("account_type") @db.VarChar(30)    // PROVINCE/CITY
  accountName     String   @map("account_name") @db.VarChar(200)
  regionCode      String   @map("region_code") @db.VarChar(10)     // 省/市编码

  // === 统计月份 ===
  statMonth       String   @map("stat_month") @db.VarChar(7)       // 2024-01

  // === 月度数据 ===
  monthlyHashpower     Decimal  @default(0) @map("monthly_hashpower") @db.Decimal(20, 8)     // 每月算力
  cumulativeHashpower  Decimal  @default(0) @map("cumulative_hashpower") @db.Decimal(20, 8)  // 累计算力
  monthlyMining        Decimal  @default(0) @map("monthly_mining") @db.Decimal(20, 8)        // 每月挖矿量
  cumulativeMining     Decimal  @default(0) @map("cumulative_mining") @db.Decimal(20, 8)     // 累计挖矿量
  monthlyCommission    Decimal  @default(0) @map("monthly_commission") @db.Decimal(20, 8)    // 每月佣金
  cumulativeCommission Decimal  @default(0) @map("cumulative_commission") @db.Decimal(20, 8) // 累计佣金
  monthlyPlantingBonus Decimal  @default(0) @map("monthly_planting_bonus") @db.Decimal(20, 8) // 每月认种提成
  cumulativePlantingBonus Decimal @default(0) @map("cumulative_planting_bonus") @db.Decimal(20, 8) // 累计认种提成

  // === 时间戳 ===
  createdAt       DateTime @default(now()) @map("created_at")
  updatedAt       DateTime @updatedAt @map("updated_at")

  @@map("system_account_monthly_stats")
  @@unique([accountId, statMonth], name: "uk_account_month")
  @@index([accountType], name: "idx_sams_type")
  @@index([statMonth], name: "idx_sams_month")
  @@index([regionCode], name: "idx_sams_region")
}

// ============================================
// 系统账户收益流水表
// 记录每笔收益的来源和时间
// ============================================
model SystemAccountIncomeRecord {
  id              BigInt   @id @default(autoincrement()) @map("record_id")

  // === 账户信息 ===
  accountId       BigInt   @map("account_id")
  accountType     String   @map("account_type") @db.VarChar(30)

  // === 收益信息 ===
  incomeType      String   @map("income_type") @db.VarChar(50)     // 收益类型
  incomeAmount    Decimal  @map("income_amount") @db.Decimal(20, 8)
  currency        String   @map("currency") @db.VarChar(10)        // USDT/HASHPOWER

  // === 来源信息 ===
  sourceType      String   @map("source_type") @db.VarChar(50)     // 来源类型
  sourceId        String?  @map("source_id") @db.VarChar(100)      // 来源ID
  sourceUserId    BigInt?  @map("source_user_id")                  // 来源用户ID
  sourceAddress   String?  @map("source_address") @db.VarChar(200) // 来源地址
  transactionNo   String?  @map("transaction_no") @db.VarChar(100) // 交易流水号

  // === 备注 ===
  memo            String?  @map("memo") @db.Text

  // === 时间戳 ===
  occurredAt      DateTime @map("occurred_at")
  createdAt       DateTime @default(now()) @map("created_at")

  @@map("system_account_income_records")
  @@index([accountId], name: "idx_sair_account")
  @@index([accountType], name: "idx_sair_type")
  @@index([incomeType], name: "idx_sair_income_type")
  @@index([sourceType], name: "idx_sair_source_type")
  @@index([sourceAddress], name: "idx_sair_address")
  @@index([transactionNo], name: "idx_sair_txno")
  @@index([occurredAt(sort: Desc)], name: "idx_sair_occurred")
}

// ============================================
// 报表事件表
// ============================================
model ReportEvent {
  id            BigInt   @id @default(autoincrement()) @map("event_id")
  eventType     String   @map("event_type") @db.VarChar(50)

  // 聚合根信息
  aggregateId   String   @map("aggregate_id") @db.VarChar(100)
  aggregateType String   @map("aggregate_type") @db.VarChar(50)

  // 事件数据
  eventData     Json     @map("event_data")

  // 元数据
  userId        BigInt?  @map("user_id")
  occurredAt    DateTime @default(now()) @map("occurred_at") @db.Timestamp(6)
  version       Int      @default(1) @map("version")

  @@map("report_events")
  @@index([aggregateType, aggregateId], name: "idx_report_event_aggregate")
  @@index([eventType], name: "idx_report_event_type")
  @@index([occurredAt], name: "idx_report_event_occurred")
}
```

### 2.2 初始化数据库和种子数据

```bash
npx prisma generate
npx prisma migrate dev --name init
```

创建 `prisma/seed.ts`:
```typescript
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  // 初始化报表定义
  const reportDefinitions = [
    {
      reportType: 'LEADERBOARD_REPORT',
      reportName: '龙虎榜数据报表',
      reportCode: 'RPT_LEADERBOARD',
      description: '龙虎榜日榜/周榜/月榜排名数据统计',
      parameters: {
        dimensions: ['TIME', 'USER'],
        defaultPeriod: 'DAILY',
      },
      outputFormats: ['EXCEL', 'CSV'],
      isActive: true,
    },
    {
      reportType: 'PLANTING_REPORT',
      reportName: '榴莲树认种报表',
      reportCode: 'RPT_PLANTING',
      description: '榴莲树认种日/周/月/季度/年度报表',
      parameters: {
        dimensions: ['TIME', 'REGION'],
        defaultPeriod: 'DAILY',
      },
      scheduleCron: '0 1 * * *', // 每日凌晨1点
      scheduleEnabled: true,
      outputFormats: ['EXCEL', 'CSV', 'PDF'],
      isActive: true,
    },
    {
      reportType: 'REGIONAL_PLANTING_REPORT',
      reportName: '区域认种报表',
      reportCode: 'RPT_REGIONAL_PLANTING',
      description: '按省/市统计的认种报表',
      parameters: {
        dimensions: ['REGION', 'TIME'],
        defaultPeriod: 'DAILY',
      },
      scheduleCron: '0 2 * * *', // 每日凌晨2点
      scheduleEnabled: true,
      outputFormats: ['EXCEL', 'CSV'],
      isActive: true,
    },
    {
      reportType: 'AUTHORIZED_COMPANY_TOP_REPORT',
      reportName: '授权公司第1名统计',
      reportCode: 'RPT_COMPANY_TOP',
      description: '各授权省公司和市公司的第1名及完成数据',
      parameters: {
        dimensions: ['REGION', 'USER'],
        includeProvince: true,
        includeCity: true,
      },
      outputFormats: ['EXCEL', 'CSV'],
      isActive: true,
    },
    {
      reportType: 'COMMUNITY_REPORT',
      reportName: '社区数据统计',
      reportCode: 'RPT_COMMUNITY',
      description: '社区认种总量、日/周/月新增、上下级社区统计',
      parameters: {
        dimensions: ['COMMUNITY', 'TIME'],
        supportFuzzySearch: true,
      },
      outputFormats: ['EXCEL', 'CSV'],
      isActive: true,
    },
    {
      reportType: 'SYSTEM_ACCOUNT_MONTHLY_REPORT',
      reportName: '系统账户月度报表',
      reportCode: 'RPT_SYSTEM_ACCOUNT_MONTHLY',
      description: '系统省/市公司账户每月各项数据统计',
      parameters: {
        dimensions: ['ACCOUNT', 'TIME'],
        metrics: [
          'monthlyHashpower',
          'cumulativeHashpower',
          'monthlyMining',
          'cumulativeMining',
          'monthlyCommission',
          'cumulativeCommission',
          'monthlyPlantingBonus',
          'cumulativePlantingBonus',
        ],
      },
      scheduleCron: '0 0 1 * *', // 每月1日0点
      scheduleEnabled: true,
      outputFormats: ['EXCEL', 'CSV'],
      isActive: true,
    },
    {
      reportType: 'SYSTEM_ACCOUNT_INCOME_REPORT',
      reportName: '系统账户收益来源报表',
      reportCode: 'RPT_SYSTEM_ACCOUNT_INCOME',
      description: '系统省/市公司账户收益来源细分统计及时间轴',
      parameters: {
        dimensions: ['ACCOUNT', 'TIME', 'SOURCE'],
        supportTimeFilter: true,
        supportKeywordSearch: true,
      },
      outputFormats: ['EXCEL', 'CSV'],
      isActive: true,
    },
  ];

  for (const def of reportDefinitions) {
    await prisma.reportDefinition.upsert({
      where: { reportCode: def.reportCode },
      update: def,
      create: def,
    });
  }

  console.log('Seed completed: Report definitions initialized');
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
```

---

## 第三阶段：领域层实现 (Domain Layer)

### 3.1 值对象 (Value Objects)

#### 3.1.1 src/domain/value-objects/report-type.enum.ts
```typescript
export enum ReportType {
  // 龙虎榜报表
  LEADERBOARD_REPORT = 'LEADERBOARD_REPORT',

  // 认种报表
  PLANTING_REPORT = 'PLANTING_REPORT',
  REGIONAL_PLANTING_REPORT = 'REGIONAL_PLANTING_REPORT',

  // 授权公司报表
  AUTHORIZED_COMPANY_TOP_REPORT = 'AUTHORIZED_COMPANY_TOP_REPORT',

  // 社区报表
  COMMUNITY_REPORT = 'COMMUNITY_REPORT',

  // 系统账户报表
  SYSTEM_ACCOUNT_MONTHLY_REPORT = 'SYSTEM_ACCOUNT_MONTHLY_REPORT',
  SYSTEM_ACCOUNT_INCOME_REPORT = 'SYSTEM_ACCOUNT_INCOME_REPORT',
}

export const ReportTypeLabels: Record<ReportType, string> = {
  [ReportType.LEADERBOARD_REPORT]: '龙虎榜数据报表',
  [ReportType.PLANTING_REPORT]: '榴莲树认种报表',
  [ReportType.REGIONAL_PLANTING_REPORT]: '区域认种报表',
  [ReportType.AUTHORIZED_COMPANY_TOP_REPORT]: '授权公司第1名统计',
  [ReportType.COMMUNITY_REPORT]: '社区数据统计',
  [ReportType.SYSTEM_ACCOUNT_MONTHLY_REPORT]: '系统账户月度报表',
  [ReportType.SYSTEM_ACCOUNT_INCOME_REPORT]: '系统账户收益来源报表',
};
```

#### 3.1.2 src/domain/value-objects/report-period.enum.ts
```typescript
export enum ReportPeriod {
  DAILY = 'DAILY',          // 日报表
  WEEKLY = 'WEEKLY',        // 周报表
  MONTHLY = 'MONTHLY',      // 月报表
  QUARTERLY = 'QUARTERLY',  // 季度报表
  YEARLY = 'YEARLY',        // 年度报表
  CUSTOM = 'CUSTOM',        // 自定义周期
}

export const ReportPeriodLabels: Record<ReportPeriod, string> = {
  [ReportPeriod.DAILY]: '日报表',
  [ReportPeriod.WEEKLY]: '周报表',
  [ReportPeriod.MONTHLY]: '月报表',
  [ReportPeriod.QUARTERLY]: '季度报表',
  [ReportPeriod.YEARLY]: '年度报表',
  [ReportPeriod.CUSTOM]: '自定义周期',
};
```

#### 3.1.3 src/domain/value-objects/report-dimension.enum.ts
```typescript
export enum ReportDimension {
  TIME = 'TIME',              // 时间维度
  REGION = 'REGION',          // 地域维度 (省/市)
  USER = 'USER',              // 用户维度
  USER_TYPE = 'USER_TYPE',    // 用户类型维度
  RIGHT_TYPE = 'RIGHT_TYPE',  // 权益类型维度
  COMMUNITY = 'COMMUNITY',    // 社区维度
  ACCOUNT = 'ACCOUNT',        // 账户维度
  SOURCE = 'SOURCE',          // 来源维度
  PRODUCT = 'PRODUCT',        // 产品维度
}
```

#### 3.1.4 src/domain/value-objects/output-format.enum.ts
```typescript
export enum OutputFormat {
  EXCEL = 'EXCEL',
  PDF = 'PDF',
  CSV = 'CSV',
  JSON = 'JSON',
}

export const OutputFormatMimeTypes: Record<OutputFormat, string> = {
  [OutputFormat.EXCEL]: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  [OutputFormat.PDF]: 'application/pdf',
  [OutputFormat.CSV]: 'text/csv',
  [OutputFormat.JSON]: 'application/json',
};

export const OutputFormatExtensions: Record<OutputFormat, string> = {
  [OutputFormat.EXCEL]: 'xlsx',
  [OutputFormat.PDF]: 'pdf',
  [OutputFormat.CSV]: 'csv',
  [OutputFormat.JSON]: 'json',
};
```

#### 3.1.5 src/domain/value-objects/date-range.vo.ts
```typescript
export class DateRange {
  private constructor(
    public readonly startDate: Date,
    public readonly endDate: Date,
  ) {
    if (startDate > endDate) {
      throw new Error('开始日期不能大于结束日期');
    }
  }

  static create(startDate: Date, endDate: Date): DateRange {
    return new DateRange(startDate, endDate);
  }

  /**
   * 创建今日范围
   */
  static today(): DateRange {
    const now = new Date();
    const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
    const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
    return new DateRange(start, end);
  }

  /**
   * 创建本周范围
   */
  static thisWeek(): DateRange {
    const now = new Date();
    const dayOfWeek = now.getDay();
    const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;

    const monday = new Date(now);
    monday.setDate(now.getDate() + diffToMonday);
    monday.setHours(0, 0, 0, 0);

    const sunday = new Date(monday);
    sunday.setDate(monday.getDate() + 6);
    sunday.setHours(23, 59, 59, 999);

    return new DateRange(monday, sunday);
  }

  /**
   * 创建本月范围
   */
  static thisMonth(): DateRange {
    const now = new Date();
    const start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
    const end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
    return new DateRange(start, end);
  }

  /**
   * 创建本季度范围
   */
  static thisQuarter(): DateRange {
    const now = new Date();
    const quarter = Math.floor(now.getMonth() / 3);
    const start = new Date(now.getFullYear(), quarter * 3, 1, 0, 0, 0);
    const end = new Date(now.getFullYear(), quarter * 3 + 3, 0, 23, 59, 59, 999);
    return new DateRange(start, end);
  }

  /**
   * 创建本年范围
   */
  static thisYear(): DateRange {
    const now = new Date();
    const start = new Date(now.getFullYear(), 0, 1, 0, 0, 0);
    const end = new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999);
    return new DateRange(start, end);
  }

  /**
   * 获取天数
   */
  getDays(): number {
    const diff = this.endDate.getTime() - this.startDate.getTime();
    return Math.ceil(diff / (1000 * 60 * 60 * 24));
  }

  /**
   * 检查日期是否在范围内
   */
  contains(date: Date): boolean {
    return date >= this.startDate && date <= this.endDate;
  }

  /**
   * 生成周期Key
   */
  toPeriodKey(period: ReportPeriod): string {
    const year = this.startDate.getFullYear();
    const month = (this.startDate.getMonth() + 1).toString().padStart(2, '0');
    const day = this.startDate.getDate().toString().padStart(2, '0');

    switch (period) {
      case ReportPeriod.DAILY:
        return `${year}-${month}-${day}`;
      case ReportPeriod.WEEKLY:
        const weekNumber = this.getWeekNumber(this.startDate);
        return `${year}-W${weekNumber.toString().padStart(2, '0')}`;
      case ReportPeriod.MONTHLY:
        return `${year}-${month}`;
      case ReportPeriod.QUARTERLY:
        const quarter = Math.floor(this.startDate.getMonth() / 3) + 1;
        return `${year}-Q${quarter}`;
      case ReportPeriod.YEARLY:
        return `${year}`;
      default:
        return `${year}-${month}-${day}_to_${this.endDate.getFullYear()}-${(this.endDate.getMonth() + 1).toString().padStart(2, '0')}-${this.endDate.getDate().toString().padStart(2, '0')}`;
    }
  }

  private getWeekNumber(date: Date): number {
    const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
    const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000;
    return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
  }
}

import { ReportPeriod } from './report-period.enum';
```

#### 3.1.6 src/domain/value-objects/report-parameters.vo.ts
```typescript
import { ReportDimension } from './report-dimension.enum';
import { DateRange } from './date-range.vo';

export class ReportParameters {
  private constructor(
    public readonly dateRange: DateRange,
    public readonly dimensions: ReportDimension[],
    public readonly filters: Record<string, any>,
    public readonly groupBy: string[],
    public readonly orderBy: { field: string; direction: 'ASC' | 'DESC' }[],
    public readonly pagination: { page: number; pageSize: number } | null,
  ) {}

  static create(params: {
    startDate: Date;
    endDate: Date;
    dimensions?: ReportDimension[];
    filters?: Record<string, any>;
    groupBy?: string[];
    orderBy?: { field: string; direction: 'ASC' | 'DESC' }[];
    page?: number;
    pageSize?: number;
  }): ReportParameters {
    return new ReportParameters(
      DateRange.create(params.startDate, params.endDate),
      params.dimensions || [],
      params.filters || {},
      params.groupBy || [],
      params.orderBy || [],
      params.page && params.pageSize
        ? { page: params.page, pageSize: params.pageSize }
        : null,
    );
  }

  /**
   * 添加筛选条件
   */
  withFilter(key: string, value: any): ReportParameters {
    return new ReportParameters(
      this.dateRange,
      this.dimensions,
      { ...this.filters, [key]: value },
      this.groupBy,
      this.orderBy,
      this.pagination,
    );
  }

  /**
   * 添加维度
   */
  withDimension(dimension: ReportDimension): ReportParameters {
    return new ReportParameters(
      this.dateRange,
      [...this.dimensions, dimension],
      this.filters,
      this.groupBy,
      this.orderBy,
      this.pagination,
    );
  }

  /**
   * 检查是否有某个筛选条件
   */
  hasFilter(key: string): boolean {
    return key in this.filters;
  }

  /**
   * 获取筛选条件值
   */
  getFilter<T>(key: string, defaultValue?: T): T | undefined {
    return this.filters[key] ?? defaultValue;
  }
}
```

#### 3.1.7 src/domain/value-objects/report-schedule.vo.ts
```typescript
export class ReportSchedule {
  private constructor(
    public readonly cronExpression: string,
    public readonly timezone: string,
    public readonly enabled: boolean,
  ) {}

  static create(cronExpression: string, timezone: string = 'Asia/Shanghai', enabled: boolean = true): ReportSchedule {
    return new ReportSchedule(cronExpression, timezone, enabled);
  }

  /**
   * 每日指定时间
   */
  static daily(hour: number = 0, minute: number = 0): ReportSchedule {
    return new ReportSchedule(
      `${minute} ${hour} * * *`,
      'Asia/Shanghai',
      true,
    );
  }

  /**
   * 每周指定时间
   */
  static weekly(dayOfWeek: number, hour: number = 0, minute: number = 0): ReportSchedule {
    return new ReportSchedule(
      `${minute} ${hour} * * ${dayOfWeek}`,
      'Asia/Shanghai',
      true,
    );
  }

  /**
   * 每月指定时间
   */
  static monthly(dayOfMonth: number, hour: number = 0, minute: number = 0): ReportSchedule {
    return new ReportSchedule(
      `${minute} ${hour} ${dayOfMonth} * *`,
      'Asia/Shanghai',
      true,
    );
  }

  /**
   * 每季度指定时间
   */
  static quarterly(dayOfQuarter: number = 1, hour: number = 0): ReportSchedule {
    // 每季度第一天的凌晨执行
    return new ReportSchedule(
      `0 ${hour} ${dayOfQuarter} 1,4,7,10 *`,
      'Asia/Shanghai',
      true,
    );
  }

  /**
   * 启用调度
   */
  enable(): ReportSchedule {
    return new ReportSchedule(this.cronExpression, this.timezone, true);
  }

  /**
   * 禁用调度
   */
  disable(): ReportSchedule {
    return new ReportSchedule(this.cronExpression, this.timezone, false);
  }
}
```

#### 3.1.8 src/domain/value-objects/snapshot-data.vo.ts
```typescript
export class SnapshotData {
  private constructor(
    public readonly rows: any[],
    public readonly summary: Record<string, any>,
    public readonly metadata: Record<string, any>,
  ) {}

  static create(params: {
    rows: any[];
    summary?: Record<string, any>;
    metadata?: Record<string, any>;
  }): SnapshotData {
    return new SnapshotData(
      params.rows,
      params.summary || {},
      params.metadata || {},
    );
  }

  /**
   * 获取行数
   */
  getRowCount(): number {
    return this.rows.length;
  }

  /**
   * 是否为空
   */
  isEmpty(): boolean {
    return this.rows.length === 0;
  }

  /**
   * 获取汇总值
   */
  getSummary<T>(key: string, defaultValue?: T): T | undefined {
    return this.summary[key] ?? defaultValue;
  }

  /**
   * 获取元数据值
   */
  getMetadata<T>(key: string, defaultValue?: T): T | undefined {
    return this.metadata[key] ?? defaultValue;
  }

  /**
   * 转换为JSON
   */
  toJSON(): object {
    return {
      rows: this.rows,
      summary: this.summary,
      metadata: this.metadata,
      rowCount: this.getRowCount(),
    };
  }
}
```

#### 3.1.9 src/domain/value-objects/data-source.vo.ts
```typescript
export class DataSource {
  private constructor(
    public readonly sources: string[],
    public readonly queryTime: Date,
    public readonly dataFreshness: number, // 数据新鲜度(秒)
  ) {}

  static create(sources: string[]): DataSource {
    return new DataSource(sources, new Date(), 0);
  }

  static withFreshness(sources: string[], freshnessSeconds: number): DataSource {
    return new DataSource(sources, new Date(), freshnessSeconds);
  }

  /**
   * 检查数据是否新鲜
   */
  isFresh(maxAgeSeconds: number): boolean {
    const age = (Date.now() - this.queryTime.getTime()) / 1000;
    return age <= maxAgeSeconds;
  }

  /**
   * 添加数据源
   */
  addSource(source: string): DataSource {
    return new DataSource(
      [...this.sources, source],
      this.queryTime,
      this.dataFreshness,
    );
  }
}
```

#### 3.1.10 src/domain/value-objects/index.ts
```typescript
export * from './report-type.enum';
export * from './report-period.enum';
export * from './report-dimension.enum';
export * from './output-format.enum';
export * from './date-range.vo';
export * from './report-parameters.vo';
export * from './report-schedule.vo';
export * from './snapshot-data.vo';
export * from './data-source.vo';
```

### 3.2 领域事件 (Domain Events)

#### 3.2.1 src/domain/events/domain-event.base.ts
```typescript
import { v4 as uuidv4 } from 'uuid';

export abstract class DomainEvent {
  public readonly eventId: string;
  public readonly occurredAt: Date;
  public readonly version: number;

  protected constructor(version: number = 1) {
    this.eventId = uuidv4();
    this.occurredAt = new Date();
    this.version = version;
  }

  abstract get eventType(): string;
  abstract get aggregateId(): string;
  abstract get aggregateType(): string;
  abstract toPayload(): Record<string, any>;
}
```

#### 3.2.2 src/domain/events/report-generated.event.ts
```typescript
import { DomainEvent } from './domain-event.base';
import { ReportType } from '../value-objects/report-type.enum';

export interface ReportGeneratedPayload {
  snapshotId: string;
  reportType: ReportType;
  reportCode: string;
  periodKey: string;
  rowCount: number;
  generatedAt: Date;
}

export class ReportGeneratedEvent extends DomainEvent {
  constructor(private readonly payload: ReportGeneratedPayload) {
    super();
  }

  get eventType(): string {
    return 'ReportGenerated';
  }

  get aggregateId(): string {
    return this.payload.snapshotId;
  }

  get aggregateType(): string {
    return 'ReportSnapshot';
  }

  toPayload(): ReportGeneratedPayload {
    return { ...this.payload };
  }
}
```

#### 3.2.3 src/domain/events/report-exported.event.ts
```typescript
import { DomainEvent } from './domain-event.base';
import { OutputFormat } from '../value-objects/output-format.enum';

export interface ReportExportedPayload {
  fileId: string;
  snapshotId: string;
  format: OutputFormat;
  fileName: string;
  fileSize: number;
  exportedAt: Date;
}

export class ReportExportedEvent extends DomainEvent {
  constructor(private readonly payload: ReportExportedPayload) {
    super();
  }

  get eventType(): string {
    return 'ReportExported';
  }

  get aggregateId(): string {
    return this.payload.fileId;
  }

  get aggregateType(): string {
    return 'ReportFile';
  }

  toPayload(): ReportExportedPayload {
    return { ...this.payload };
  }
}
```

### 3.3 聚合根 (Aggregates)

#### 3.3.1 src/domain/aggregates/report-definition/report-definition.aggregate.ts
```typescript
import { DomainEvent } from '../../events/domain-event.base';
import { ReportType } from '../../value-objects/report-type.enum';
import { ReportSchedule } from '../../value-objects/report-schedule.vo';
import { OutputFormat } from '../../value-objects/output-format.enum';

/**
 * 报表定义聚合根
 *
 * 不变式:
 * 1. reportCode 必须唯一
 * 2. 启用调度时必须有有效的 cron 表达式
 * 3. 至少支持一种输出格式
 */
export class ReportDefinition {
  private _id: bigint | null = null;
  private readonly _reportType: ReportType;
  private _reportName: string;
  private readonly _reportCode: string;
  private _description: string;
  private _parameters: Record<string, any>;
  private _schedule: ReportSchedule | null;
  private _outputFormats: OutputFormat[];
  private _isActive: boolean;
  private readonly _createdAt: Date;
  private _lastGeneratedAt: Date | null;

  private _domainEvents: DomainEvent[] = [];

  private constructor(
    reportType: ReportType,
    reportName: string,
    reportCode: string,
    description: string,
    parameters: Record<string, any>,
    schedule: ReportSchedule | null,
    outputFormats: OutputFormat[],
    isActive: boolean,
  ) {
    this._reportType = reportType;
    this._reportName = reportName;
    this._reportCode = reportCode;
    this._description = description;
    this._parameters = parameters;
    this._schedule = schedule;
    this._outputFormats = outputFormats;
    this._isActive = isActive;
    this._createdAt = new Date();
    this._lastGeneratedAt = null;
  }

  // ============ Getters ============
  get id(): bigint | null { return this._id; }
  get reportType(): ReportType { return this._reportType; }
  get reportName(): string { return this._reportName; }
  get reportCode(): string { return this._reportCode; }
  get description(): string { return this._description; }
  get parameters(): Record<string, any> { return { ...this._parameters }; }
  get schedule(): ReportSchedule | null { return this._schedule; }
  get outputFormats(): OutputFormat[] { return [...this._outputFormats]; }
  get isActive(): boolean { return this._isActive; }
  get createdAt(): Date { return this._createdAt; }
  get lastGeneratedAt(): Date | null { return this._lastGeneratedAt; }
  get domainEvents(): DomainEvent[] { return [...this._domainEvents]; }

  get isScheduled(): boolean {
    return this._schedule !== null && this._schedule.enabled;
  }

  // ============ 工厂方法 ============

  static create(params: {
    reportType: ReportType;
    reportName: string;
    reportCode: string;
    description?: string;
    parameters?: Record<string, any>;
    schedule?: ReportSchedule;
    outputFormats: OutputFormat[];
  }): ReportDefinition {
    if (params.outputFormats.length === 0) {
      throw new Error('至少需要支持一种输出格式');
    }

    return new ReportDefinition(
      params.reportType,
      params.reportName,
      params.reportCode,
      params.description || '',
      params.parameters || {},
      params.schedule || null,
      params.outputFormats,
      true,
    );
  }

  // ============ 领域行为 ============

  /**
   * 更新报表参数
   */
  updateParameters(newParameters: Record<string, any>): void {
    this._parameters = { ...newParameters };
  }

  /**
   * 更新调度配置
   */
  updateSchedule(newSchedule: ReportSchedule): void {
    this._schedule = newSchedule;
  }

  /**
   * 启用调度
   */
  enableSchedule(): void {
    if (!this._schedule) {
      throw new Error('请先配置调度规则');
    }
    this._schedule = this._schedule.enable();
  }

  /**
   * 禁用调度
   */
  disableSchedule(): void {
    if (this._schedule) {
      this._schedule = this._schedule.disable();
    }
  }

  /**
   * 添加输出格式
   */
  addOutputFormat(format: OutputFormat): void {
    if (!this._outputFormats.includes(format)) {
      this._outputFormats.push(format);
    }
  }

  /**
   * 移除输出格式
   */
  removeOutputFormat(format: OutputFormat): void {
    if (this._outputFormats.length <= 1) {
      throw new Error('至少需要保留一种输出格式');
    }
    this._outputFormats = this._outputFormats.filter(f => f !== format);
  }

  /**
   * 激活报表
   */
  activate(): void {
    this._isActive = true;
  }

  /**
   * 停用报表
   */
  deactivate(): void {
    this._isActive = false;
  }

  /**
   * 标记为已生成
   */
  markAsGenerated(): void {
    this._lastGeneratedAt = new Date();
  }

  /**
   * 检查是否支持指定格式
   */
  supportsFormat(format: OutputFormat): boolean {
    return this._outputFormats.includes(format);
  }

  setId(id: bigint): void {
    this._id = id;
  }

  clearDomainEvents(): void {
    this._domainEvents = [];
  }

  // ============ 重建 ============

  static reconstitute(data: {
    id: bigint;
    reportType: ReportType;
    reportName: string;
    reportCode: string;
    description: string;
    parameters: Record<string, any>;
    scheduleCron: string | null;
    scheduleTimezone: string | null;
    scheduleEnabled: boolean;
    outputFormats: string[];
    isActive: boolean;
    createdAt: Date;
    lastGeneratedAt: Date | null;
  }): ReportDefinition {
    const schedule = data.scheduleCron
      ? ReportSchedule.create(
          data.scheduleCron,
          data.scheduleTimezone || 'Asia/Shanghai',
          data.scheduleEnabled,
        )
      : null;

    const definition = new ReportDefinition(
      data.reportType,
      data.reportName,
      data.reportCode,
      data.description,
      data.parameters,
      schedule,
      data.outputFormats as OutputFormat[],
      data.isActive,
    );
    definition._id = data.id;
    definition._lastGeneratedAt = data.lastGeneratedAt;
    return definition;
  }
}
```

#### 3.3.2 src/domain/aggregates/report-snapshot/report-snapshot.aggregate.ts
```typescript
import { DomainEvent } from '../../events/domain-event.base';
import { ReportGeneratedEvent } from '../../events/report-generated.event';
import { ReportType } from '../../value-objects/report-type.enum';
import { ReportPeriod } from '../../value-objects/report-period.enum';
import { SnapshotData } from '../../value-objects/snapshot-data.vo';
import { DataSource } from '../../value-objects/data-source.vo';
import { DateRange } from '../../value-objects/date-range.vo';

/**
 * 报表快照聚合根 (读模型)
 *
 * 不变式:
 * 1. 同一报表同一周期只能有一个快照
 * 2. 快照数据创建后不可修改
 * 3. 过期的快照应被清理
 */
export class ReportSnapshot {
  private _id: bigint | null = null;
  private readonly _reportType: ReportType;
  private readonly _reportCode: string;
  private readonly _reportPeriod: ReportPeriod;
  private readonly _periodKey: string;
  private readonly _snapshotData: SnapshotData;
  private readonly _dataSource: DataSource;
  private readonly _filterParams: Record<string, any> | null;
  private readonly _dateRange: DateRange;
  private readonly _generatedAt: Date;
  private readonly _expiresAt: Date | null;

  private _domainEvents: DomainEvent[] = [];

  private constructor(
    reportType: ReportType,
    reportCode: string,
    reportPeriod: ReportPeriod,
    periodKey: string,
    snapshotData: SnapshotData,
    dataSource: DataSource,
    filterParams: Record<string, any> | null,
    dateRange: DateRange,
    generatedAt: Date,
    expiresAt: Date | null,
  ) {
    this._reportType = reportType;
    this._reportCode = reportCode;
    this._reportPeriod = reportPeriod;
    this._periodKey = periodKey;
    this._snapshotData = snapshotData;
    this._dataSource = dataSource;
    this._filterParams = filterParams;
    this._dateRange = dateRange;
    this._generatedAt = generatedAt;
    this._expiresAt = expiresAt;
  }

  // ============ Getters ============
  get id(): bigint | null { return this._id; }
  get reportType(): ReportType { return this._reportType; }
  get reportCode(): string { return this._reportCode; }
  get reportPeriod(): ReportPeriod { return this._reportPeriod; }
  get periodKey(): string { return this._periodKey; }
  get snapshotData(): SnapshotData { return this._snapshotData; }
  get dataSource(): DataSource { return this._dataSource; }
  get filterParams(): Record<string, any> | null { return this._filterParams; }
  get dateRange(): DateRange { return this._dateRange; }
  get generatedAt(): Date { return this._generatedAt; }
  get expiresAt(): Date | null { return this._expiresAt; }
  get domainEvents(): DomainEvent[] { return [...this._domainEvents]; }

  get rowCount(): number {
    return this._snapshotData.getRowCount();
  }

  get isExpired(): boolean {
    if (!this._expiresAt) return false;
    return new Date() > this._expiresAt;
  }

  // ============ 工厂方法 ============

  static create(params: {
    reportType: ReportType;
    reportCode: string;
    reportPeriod: ReportPeriod;
    snapshotData: SnapshotData;
    dataSource: DataSource;
    dateRange: DateRange;
    filterParams?: Record<string, any>;
    expiresInHours?: number;
  }): ReportSnapshot {
    const now = new Date();
    const periodKey = params.dateRange.toPeriodKey(params.reportPeriod);
    const expiresAt = params.expiresInHours
      ? new Date(now.getTime() + params.expiresInHours * 3600 * 1000)
      : null;

    const snapshot = new ReportSnapshot(
      params.reportType,
      params.reportCode,
      params.reportPeriod,
      periodKey,
      params.snapshotData,
      params.dataSource,
      params.filterParams || null,
      params.dateRange,
      now,
      expiresAt,
    );

    snapshot._domainEvents.push(new ReportGeneratedEvent({
      snapshotId: snapshot._id?.toString() || 'temp',
      reportType: snapshot._reportType,
      reportCode: snapshot._reportCode,
      periodKey: snapshot._periodKey,
      rowCount: snapshot.rowCount,
      generatedAt: snapshot._generatedAt,
    }));

    return snapshot;
  }

  // ============ 领域行为 ============

  /**
   * 检查数据是否新鲜
   */
  isDataFresh(maxAgeSeconds: number): boolean {
    return this._dataSource.isFresh(maxAgeSeconds);
  }

  /**
   * 获取汇总数据
   */
  getSummary(): Record<string, any> {
    return this._snapshotData.summary;
  }

  /**
   * 获取数据行
   */
  getRows(): any[] {
    return this._snapshotData.rows;
  }

  setId(id: bigint): void {
    this._id = id;
  }

  clearDomainEvents(): void {
    this._domainEvents = [];
  }

  // ============ 重建 ============

  static reconstitute(data: {
    id: bigint;
    reportType: ReportType;
    reportCode: string;
    reportPeriod: ReportPeriod;
    periodKey: string;
    snapshotData: any;
    summaryData: any;
    dataSources: string[];
    dataFreshness: number;
    filterParams: any;
    rowCount: number;
    periodStartAt: Date;
    periodEndAt: Date;
    generatedAt: Date;
    expiresAt: Date | null;
  }): ReportSnapshot {
    const snapshot = new ReportSnapshot(
      data.reportType,
      data.reportCode,
      data.reportPeriod,
      data.periodKey,
      SnapshotData.create({
        rows: data.snapshotData.rows || [],
        summary: data.summaryData || {},
        metadata: data.snapshotData.metadata || {},
      }),
      DataSource.withFreshness(data.dataSources, data.dataFreshness),
      data.filterParams,
      DateRange.create(data.periodStartAt, data.periodEndAt),
      data.generatedAt,
      data.expiresAt,
    );
    snapshot._id = data.id;
    return snapshot;
  }
}
```

### 3.4 仓储接口 (Repository Interfaces)

#### 3.4.1 src/domain/repositories/report-definition.repository.interface.ts
```typescript
import { ReportDefinition } from '../aggregates/report-definition/report-definition.aggregate';
import { ReportType } from '../value-objects/report-type.enum';

export interface IReportDefinitionRepository {
  save(definition: ReportDefinition): Promise<void>;
  findById(id: bigint): Promise<ReportDefinition | null>;
  findByCode(reportCode: string): Promise<ReportDefinition | null>;
  findByType(reportType: ReportType): Promise<ReportDefinition[]>;
  findActive(): Promise<ReportDefinition[]>;
  findScheduled(): Promise<ReportDefinition[]>;
  findAll(): Promise<ReportDefinition[]>;
}

export const REPORT_DEFINITION_REPOSITORY = Symbol('IReportDefinitionRepository');
```

#### 3.4.2 src/domain/repositories/report-snapshot.repository.interface.ts
```typescript
import { ReportSnapshot } from '../aggregates/report-snapshot/report-snapshot.aggregate';
import { ReportType } from '../value-objects/report-type.enum';
import { ReportPeriod } from '../value-objects/report-period.enum';

export interface IReportSnapshotRepository {
  save(snapshot: ReportSnapshot): Promise<void>;
  findById(id: bigint): Promise<ReportSnapshot | null>;
  findByCodeAndPeriod(reportCode: string, periodKey: string): Promise<ReportSnapshot | null>;
  findByType(reportType: ReportType, limit?: number): Promise<ReportSnapshot[]>;
  findLatest(reportCode: string): Promise<ReportSnapshot | null>;
  findByDateRange(
    reportCode: string,
    startDate: Date,
    endDate: Date,
  ): Promise<ReportSnapshot[]>;
  deleteExpired(): Promise<number>;
  deleteOlderThan(date: Date): Promise<number>;
}

export const REPORT_SNAPSHOT_REPOSITORY = Symbol('IReportSnapshotRepository');
```

### 3.5 领域服务 (Domain Services)

#### 3.5.1 src/domain/services/report-generation.service.ts
```typescript
import { Injectable, Inject } from '@nestjs/common';
import { ReportSnapshot } from '../aggregates/report-snapshot/report-snapshot.aggregate';
import { ReportType } from '../value-objects/report-type.enum';
import { ReportPeriod } from '../value-objects/report-period.enum';
import { SnapshotData } from '../value-objects/snapshot-data.vo';
import { DataSource } from '../value-objects/data-source.vo';
import { DateRange } from '../value-objects/date-range.vo';
import { ReportParameters } from '../value-objects/report-parameters.vo';

// 外部服务接口（防腐层）
export interface IPlantingServiceClient {
  getPlantingStatistics(params: {
    startDate: Date;
    endDate: Date;
    groupBy: string[];
    filters?: Record<string, any>;
  }): Promise<any[]>;
}

export interface ILeaderboardServiceClient {
  getLeaderboardData(params: {
    type: string;
    periodKey: string;
    limit: number;
  }): Promise<any[]>;
}

export interface IReferralServiceClient {
  getCommunityStatistics(params: {
    communityId?: bigint;
    communityName?: string;
    startDate: Date;
    endDate: Date;
  }): Promise<any[]>;

  getAuthorizedCompanyTopUsers(params: {
    companyType: 'PROVINCE' | 'CITY';
    regionCode?: string;
  }): Promise<any[]>;
}

export interface IRewardServiceClient {
  getRewardStatistics(params: {
    startDate: Date;
    endDate: Date;
    groupBy: string[];
    filters?: Record<string, any>;
  }): Promise<any[]>;
}

export interface IWalletServiceClient {
  getSystemAccountStatistics(params: {
    accountType: 'PROVINCE' | 'CITY';
    statMonth: string;
  }): Promise<any[]>;

  getSystemAccountIncomeRecords(params: {
    accountId?: bigint;
    accountType?: string;
    startDate: Date;
    endDate: Date;
    keyword?: string;
  }): Promise<any[]>;
}

export const PLANTING_SERVICE_CLIENT = Symbol('IPlantingServiceClient');
export const LEADERBOARD_SERVICE_CLIENT = Symbol('ILeaderboardServiceClient');
export const REFERRAL_SERVICE_CLIENT = Symbol('IReferralServiceClient');
export const REWARD_SERVICE_CLIENT = Symbol('IRewardServiceClient');
export const WALLET_SERVICE_CLIENT = Symbol('IWalletServiceClient');

@Injectable()
export class ReportGenerationService {
  constructor(
    @Inject(PLANTING_SERVICE_CLIENT)
    private readonly plantingService: IPlantingServiceClient,
    @Inject(LEADERBOARD_SERVICE_CLIENT)
    private readonly leaderboardService: ILeaderboardServiceClient,
    @Inject(REFERRAL_SERVICE_CLIENT)
    private readonly referralService: IReferralServiceClient,
    @Inject(REWARD_SERVICE_CLIENT)
    private readonly rewardService: IRewardServiceClient,
    @Inject(WALLET_SERVICE_CLIENT)
    private readonly walletService: IWalletServiceClient,
  ) {}

  /**
   * 生成龙虎榜报表
   */
  async generateLeaderboardReport(
    period: ReportPeriod,
    dateRange: DateRange,
  ): Promise<ReportSnapshot> {
    const periodKey = dateRange.toPeriodKey(period);
    const leaderboardType = this.mapPeriodToLeaderboardType(period);

    // 获取龙虎榜数据
    const leaderboardData = await this.leaderboardService.getLeaderboardData({
      type: leaderboardType,
      periodKey,
      limit: 100,
    });

    const rows = leaderboardData.map((item, index) => ({
      rank: index + 1,
      userId: item.userId,
      nickname: item.userSnapshot?.nickname || '',
      effectiveScore: item.effectiveScore,
      totalTeamPlanting: item.totalTeamPlanting,
      maxDirectTeamPlanting: item.maxDirectTeamPlanting,
      rankChange: item.rankChange || 0,
    }));

    const summary = {
      totalParticipants: rows.length,
      topScore: rows[0]?.effectiveScore || 0,
      averageScore: rows.length > 0
        ? Math.round(rows.reduce((sum, r) => sum + r.effectiveScore, 0) / rows.length)
        : 0,
      periodKey,
      leaderboardType,
    };

    return ReportSnapshot.create({
      reportType: ReportType.LEADERBOARD_REPORT,
      reportCode: 'RPT_LEADERBOARD',
      reportPeriod: period,
      snapshotData: SnapshotData.create({ rows, summary }),
      dataSource: DataSource.create(['leaderboard-service']),
      dateRange,
      expiresInHours: 24,
    });
  }

  /**
   * 生成认种报表
   */
  async generatePlantingReport(
    period: ReportPeriod,
    dateRange: DateRange,
    filters?: { provinceCode?: string; cityCode?: string },
  ): Promise<ReportSnapshot> {
    const groupBy = ['date'];
    if (filters?.provinceCode) groupBy.push('province');
    if (filters?.cityCode) groupBy.push('city');

    const plantingData = await this.plantingService.getPlantingStatistics({
      startDate: dateRange.startDate,
      endDate: dateRange.endDate,
      groupBy,
      filters,
    });

    const rows = plantingData.map(data => ({
      date: data.date,
      province: data.province || '全国',
      city: data.city || '全部',
      orderCount: data.orderCount,
      treeCount: data.treeCount,
      totalAmount: data.totalAmount,
      newUserCount: data.newUserCount,
      activeUserCount: data.activeUserCount,
    }));

    const summary = {
      totalOrders: rows.reduce((sum, r) => sum + r.orderCount, 0),
      totalTrees: rows.reduce((sum, r) => sum + r.treeCount, 0),
      totalAmount: rows.reduce((sum, r) => sum + r.totalAmount, 0),
      totalNewUsers: rows.reduce((sum, r) => sum + r.newUserCount, 0),
      dateRange: {
        start: dateRange.startDate,
        end: dateRange.endDate,
      },
    };

    return ReportSnapshot.create({
      reportType: ReportType.PLANTING_REPORT,
      reportCode: 'RPT_PLANTING',
      reportPeriod: period,
      snapshotData: SnapshotData.create({ rows, summary }),
      dataSource: DataSource.create(['planting-service']),
      dateRange,
      filterParams: filters,
      expiresInHours: 24,
    });
  }

  /**
   * 生成区域认种报表
   */
  async generateRegionalPlantingReport(
    period: ReportPeriod,
    dateRange: DateRange,
    regionType: 'PROVINCE' | 'CITY',
  ): Promise<ReportSnapshot> {
    const groupBy = regionType === 'PROVINCE' ? ['province'] : ['province', 'city'];

    const plantingData = await this.plantingService.getPlantingStatistics({
      startDate: dateRange.startDate,
      endDate: dateRange.endDate,
      groupBy,
    });

    const rows = plantingData.map(data => ({
      province: data.province,
      city: data.city || '',
      orderCount: data.orderCount,
      treeCount: data.treeCount,
      totalAmount: data.totalAmount,
      userCount: data.userCount,
    }));

    // 按认种量排序
    rows.sort((a, b) => b.treeCount - a.treeCount);

    // 添加排名
    rows.forEach((row, index) => {
      (row as any).rank = index + 1;
    });

    const summary = {
      regionCount: rows.length,
      totalTrees: rows.reduce((sum, r) => sum + r.treeCount, 0),
      totalAmount: rows.reduce((sum, r) => sum + r.totalAmount, 0),
      regionType,
    };

    return ReportSnapshot.create({
      reportType: ReportType.REGIONAL_PLANTING_REPORT,
      reportCode: 'RPT_REGIONAL_PLANTING',
      reportPeriod: period,
      snapshotData: SnapshotData.create({ rows, summary }),
      dataSource: DataSource.create(['planting-service']),
      dateRange,
      filterParams: { regionType },
      expiresInHours: 24,
    });
  }

  /**
   * 生成社区数据报表
   */
  async generateCommunityReport(
    dateRange: DateRange,
    filters?: { communityId?: bigint; communityName?: string },
  ): Promise<ReportSnapshot> {
    const communityData = await this.referralService.getCommunityStatistics({
      communityId: filters?.communityId,
      communityName: filters?.communityName,
      startDate: dateRange.startDate,
      endDate: dateRange.endDate,
    });

    const rows = communityData.map(data => ({
      communityId: data.communityId,
      communityName: data.communityName,
      parentCommunityName: data.parentCommunityName || '无',
      childCommunityCount: data.childCommunityCount,
      totalPlanting: data.totalPlanting,
      dailyPlanting: data.dailyPlanting,
      weeklyPlanting: data.weeklyPlanting,
      monthlyPlanting: data.monthlyPlanting,
      memberCount: data.memberCount,
    }));

    const summary = {
      totalCommunities: rows.length,
      totalPlanting: rows.reduce((sum, r) => sum + r.totalPlanting, 0),
      totalMembers: rows.reduce((sum, r) => sum + r.memberCount, 0),
    };

    return ReportSnapshot.create({
      reportType: ReportType.COMMUNITY_REPORT,
      reportCode: 'RPT_COMMUNITY',
      reportPeriod: ReportPeriod.CUSTOM,
      snapshotData: SnapshotData.create({ rows, summary }),
      dataSource: DataSource.create(['referral-service']),
      dateRange,
      filterParams: filters,
      expiresInHours: 1,
    });
  }

  /**
   * 生成授权公司第1名报表
   */
  async generateAuthorizedCompanyTopReport(
    companyType: 'PROVINCE' | 'CITY',
  ): Promise<ReportSnapshot> {
    const topUsers = await this.referralService.getAuthorizedCompanyTopUsers({
      companyType,
    });

    const rows = topUsers.map(data => ({
      companyId: data.companyId,
      companyName: data.companyName,
      regionCode: data.regionCode,
      topUserId: data.topUserId,
      topUserName: data.topUserName,
      topUserPlanting: data.topUserPlanting,
      topUserTeamCount: data.topUserTeamCount,
      topUserTotalReward: data.topUserTotalReward,
    }));

    const summary = {
      companyCount: rows.length,
      companyType,
    };

    return ReportSnapshot.create({
      reportType: ReportType.AUTHORIZED_COMPANY_TOP_REPORT,
      reportCode: 'RPT_COMPANY_TOP',
      reportPeriod: ReportPeriod.CUSTOM,
      snapshotData: SnapshotData.create({ rows, summary }),
      dataSource: DataSource.create(['referral-service']),
      dateRange: DateRange.today(),
      filterParams: { companyType },
      expiresInHours: 1,
    });
  }

  /**
   * 生成系统账户月度报表
   */
  async generateSystemAccountMonthlyReport(
    statMonth: string,
    accountType: 'PROVINCE' | 'CITY',
  ): Promise<ReportSnapshot> {
    const accountStats = await this.walletService.getSystemAccountStatistics({
      accountType,
      statMonth,
    });

    const rows = accountStats.map(data => ({
      accountId: data.accountId,
      accountName: data.accountName,
      regionCode: data.regionCode,
      monthlyHashpower: data.monthlyHashpower,
      cumulativeHashpower: data.cumulativeHashpower,
      monthlyMining: data.monthlyMining,
      cumulativeMining: data.cumulativeMining,
      monthlyCommission: data.monthlyCommission,
      cumulativeCommission: data.cumulativeCommission,
      monthlyPlantingBonus: data.monthlyPlantingBonus,
      cumulativePlantingBonus: data.cumulativePlantingBonus,
    }));

    const summary = {
      accountCount: rows.length,
      totalMonthlyHashpower: rows.reduce((sum, r) => sum + r.monthlyHashpower, 0),
      totalMonthlyMining: rows.reduce((sum, r) => sum + r.monthlyMining, 0),
      totalMonthlyCommission: rows.reduce((sum, r) => sum + r.monthlyCommission, 0),
      statMonth,
      accountType,
    };

    // 解析月份为日期范围
    const [year, month] = statMonth.split('-').map(Number);
    const startDate = new Date(year, month - 1, 1);
    const endDate = new Date(year, month, 0, 23, 59, 59, 999);

    return ReportSnapshot.create({
      reportType: ReportType.SYSTEM_ACCOUNT_MONTHLY_REPORT,
      reportCode: 'RPT_SYSTEM_ACCOUNT_MONTHLY',
      reportPeriod: ReportPeriod.MONTHLY,
      snapshotData: SnapshotData.create({ rows, summary }),
      dataSource: DataSource.create(['wallet-service']),
      dateRange: DateRange.create(startDate, endDate),
      filterParams: { statMonth, accountType },
      expiresInHours: 24,
    });
  }

  /**
   * 生成系统账户收益来源报表
   */
  async generateSystemAccountIncomeReport(
    dateRange: DateRange,
    filters?: {
      accountId?: bigint;
      accountType?: string;
      keyword?: string;
    },
  ): Promise<ReportSnapshot> {
    const incomeRecords = await this.walletService.getSystemAccountIncomeRecords({
      accountId: filters?.accountId,
      accountType: filters?.accountType,
      startDate: dateRange.startDate,
      endDate: dateRange.endDate,
      keyword: filters?.keyword,
    });

    const rows = incomeRecords.map(data => ({
      recordId: data.recordId,
      accountId: data.accountId,
      accountName: data.accountName,
      accountType: data.accountType,
      incomeType: data.incomeType,
      incomeAmount: data.incomeAmount,
      currency: data.currency,
      sourceType: data.sourceType,
      sourceId: data.sourceId,
      sourceUserId: data.sourceUserId,
      sourceAddress: data.sourceAddress,
      transactionNo: data.transactionNo,
      occurredAt: data.occurredAt,
      memo: data.memo,
    }));

    const summary = {
      totalRecords: rows.length,
      totalIncome: rows.reduce((sum, r) => sum + r.incomeAmount, 0),
      byIncomeType: this.groupByField(rows, 'incomeType', 'incomeAmount'),
      bySourceType: this.groupByField(rows, 'sourceType', 'incomeAmount'),
    };

    return ReportSnapshot.create({
      reportType: ReportType.SYSTEM_ACCOUNT_INCOME_REPORT,
      reportCode: 'RPT_SYSTEM_ACCOUNT_INCOME',
      reportPeriod: ReportPeriod.CUSTOM,
      snapshotData: SnapshotData.create({ rows, summary }),
      dataSource: DataSource.create(['wallet-service']),
      dateRange,
      filterParams: filters,
      expiresInHours: 1,
    });
  }

  // ============ 辅助方法 ============

  private mapPeriodToLeaderboardType(period: ReportPeriod): string {
    switch (period) {
      case ReportPeriod.DAILY:
        return 'DAILY';
      case ReportPeriod.WEEKLY:
        return 'WEEKLY';
      case ReportPeriod.MONTHLY:
        return 'MONTHLY';
      default:
        return 'DAILY';
    }
  }

  private groupByField(
    rows: any[],
    groupField: string,
    sumField: string,
  ): Record<string, number> {
    return rows.reduce((acc, row) => {
      const key = row[groupField] || 'unknown';
      acc[key] = (acc[key] || 0) + row[sumField];
      return acc;
    }, {});
  }
}
```

#### 3.5.2 src/domain/services/report-export.service.ts
```typescript
import { Injectable } from '@nestjs/common';
import { ReportSnapshot } from '../aggregates/report-snapshot/report-snapshot.aggregate';
import { OutputFormat } from '../value-objects/output-format.enum';

export interface ExportResult {
  buffer: Buffer;
  fileName: string;
  mimeType: string;
  fileSize: number;
}

export interface IExcelExporter {
  export(snapshot: ReportSnapshot): Promise<Buffer>;
}

export interface IPdfExporter {
  export(snapshot: ReportSnapshot): Promise<Buffer>;
}

export interface ICsvExporter {
  export(snapshot: ReportSnapshot): Promise<Buffer>;
}

export const EXCEL_EXPORTER = Symbol('IExcelExporter');
export const PDF_EXPORTER = Symbol('IPdfExporter');
export const CSV_EXPORTER = Symbol('ICsvExporter');

@Injectable()
export class ReportExportService {
  constructor(
    @Inject(EXCEL_EXPORTER) private readonly excelExporter: IExcelExporter,
    @Inject(PDF_EXPORTER) private readonly pdfExporter: IPdfExporter,
    @Inject(CSV_EXPORTER) private readonly csvExporter: ICsvExporter,
  ) {}

  /**
   * 导出报表
   */
  async export(
    snapshot: ReportSnapshot,
    format: OutputFormat,
  ): Promise<ExportResult> {
    let buffer: Buffer;

    switch (format) {
      case OutputFormat.EXCEL:
        buffer = await this.excelExporter.export(snapshot);
        break;
      case OutputFormat.PDF:
        buffer = await this.pdfExporter.export(snapshot);
        break;
      case OutputFormat.CSV:
        buffer = await this.csvExporter.export(snapshot);
        break;
      case OutputFormat.JSON:
        buffer = Buffer.from(JSON.stringify(snapshot.snapshotData.toJSON(), null, 2));
        break;
      default:
        throw new Error(`不支持的导出格式: ${format}`);
    }

    const fileName = this.generateFileName(snapshot, format);
    const mimeType = this.getMimeType(format);

    return {
      buffer,
      fileName,
      mimeType,
      fileSize: buffer.length,
    };
  }

  private generateFileName(snapshot: ReportSnapshot, format: OutputFormat): string {
    const extension = this.getExtension(format);
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    return `${snapshot.reportCode}_${snapshot.periodKey}_${timestamp}.${extension}`;
  }

  private getMimeType(format: OutputFormat): string {
    switch (format) {
      case OutputFormat.EXCEL:
        return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
      case OutputFormat.PDF:
        return 'application/pdf';
      case OutputFormat.CSV:
        return 'text/csv';
      case OutputFormat.JSON:
        return 'application/json';
      default:
        return 'application/octet-stream';
    }
  }

  private getExtension(format: OutputFormat): string {
    switch (format) {
      case OutputFormat.EXCEL:
        return 'xlsx';
      case OutputFormat.PDF:
        return 'pdf';
      case OutputFormat.CSV:
        return 'csv';
      case OutputFormat.JSON:
        return 'json';
      default:
        return 'bin';
    }
  }
}

import { Inject } from '@nestjs/common';
```

---

## 领域不变式 (Domain Invariants)

```typescript
class ReportingContextInvariants {
  // 1. 报表代码必须唯一
  static REPORT_CODE_MUST_BE_UNIQUE =
    "报表代码在系统中必须唯一";

  // 2. 同一报表同一周期只能有一个快照
  static ONE_SNAPSHOT_PER_PERIOD =
    "同一报表代码同一周期只能生成一个快照";

  // 3. 快照数据创建后不可修改
  static SNAPSHOT_DATA_IMMUTABLE =
    "报表快照数据一旦创建不可修改";

  // 4. 至少支持一种输出格式
  static AT_LEAST_ONE_OUTPUT_FORMAT =
    "报表定义至少需要支持一种输出格式";

  // 5. 启用调度需要有效的cron表达式
  static SCHEDULE_REQUIRES_CRON =
    "启用调度时必须配置有效的cron表达式";

  // 6. 过期快照应被清理
  static EXPIRED_SNAPSHOTS_CLEANUP =
    "过期的报表快照应定期清理以节省存储空间";

  // 7. 数据来源必须明确
  static DATA_SOURCE_REQUIRED =
    "报表快照必须记录数据来源服务";
}
```

---

## API 端点设计

| 方法 | 路径 | 描述 | 认证 | 权限 |
|------|------|------|------|------|
| GET | `/health` | 健康检查 | 否 | - |
| **龙虎榜报表** |
| GET | `/reports/leaderboard` | 获取龙虎榜报表 | JWT | 管理员 |
| POST | `/reports/leaderboard/generate` | 生成龙虎榜报表 | JWT | 管理员 |
| GET | `/reports/leaderboard/export` | 导出龙虎榜报表 | JWT | 管理员 |
| **认种报表** |
| GET | `/reports/planting` | 获取认种报表 | JWT | 管理员 |
| POST | `/reports/planting/generate` | 生成认种报表 | JWT | 管理员 |
| GET | `/reports/planting/export` | 导出认种报表 | JWT | 管理员 |
| **区域报表** |
| GET | `/reports/regional/province` | 获取省级认种报表 | JWT | 管理员 |
| GET | `/reports/regional/city` | 获取市级认种报表 | JWT | 管理员 |
| GET | `/reports/regional/export` | 导出区域报表 | JWT | 管理员 |
| **授权公司报表** |
| GET | `/reports/company/top` | 获取授权公司第1名统计 | JWT | 管理员 |
| GET | `/reports/company/top/export` | 导出授权公司报表 | JWT | 管理员 |
| **社区报表** |
| GET | `/reports/community` | 获取社区数据统计 | JWT | 管理员 |
| GET | `/reports/community/search` | 模糊搜索社区数据 | JWT | 管理员 |
| GET | `/reports/community/export` | 导出社区报表 | JWT | 管理员 |
| **系统账户报表** |
| GET | `/reports/system-account/monthly` | 获取系统账户月度报表 | JWT | 管理员 |
| GET | `/reports/system-account/income` | 获取系统账户收益来源报表 | JWT | 管理员 |
| GET | `/reports/system-account/income/timeline` | 获取收益时间轴 | JWT | 管理员 |
| GET | `/reports/system-account/export` | 导出系统账户报表 | JWT | 管理员 |
| **通用导出** |
| POST | `/reports/export` | 通用报表导出 | JWT | 管理员 |
| GET | `/reports/files/{fileId}` | 下载报表文件 | JWT | 管理员 |
| GET | `/reports/snapshots` | 获取报表快照列表 | JWT | 管理员 |
| GET | `/reports/snapshots/{snapshotId}` | 获取报表快照详情 | JWT | 管理员 |

---

## 定时任务

### 报表生成调度器

```typescript
// application/schedulers/report-generation.scheduler.ts

import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { ReportingApplicationService } from '../services/reporting-application.service';
import { ReportPeriod } from '../../domain/value-objects/report-period.enum';

@Injectable()
export class ReportGenerationScheduler {
  constructor(
    private readonly reportingService: ReportingApplicationService,
  ) {}

  /**
   * 每日凌晨1点生成日报表
   */
  @Cron('0 1 * * *')
  async generateDailyReports() {
    console.log('开始生成日报表...');

    try {
      // 生成认种日报表
      await this.reportingService.generatePlantingReport(ReportPeriod.DAILY);

      // 生成区域认种日报表
      await this.reportingService.generateRegionalPlantingReport(ReportPeriod.DAILY, 'PROVINCE');
      await this.reportingService.generateRegionalPlantingReport(ReportPeriod.DAILY, 'CITY');

      console.log('日报表生成完成');
    } catch (error) {
      console.error('日报表生成失败:', error);
    }
  }

  /**
   * 每周一凌晨2点生成周报表
   */
  @Cron('0 2 * * 1')
  async generateWeeklyReports() {
    console.log('开始生成周报表...');

    try {
      await this.reportingService.generatePlantingReport(ReportPeriod.WEEKLY);
      await this.reportingService.generateLeaderboardReport(ReportPeriod.WEEKLY);

      console.log('周报表生成完成');
    } catch (error) {
      console.error('周报表生成失败:', error);
    }
  }

  /**
   * 每月1日凌晨3点生成月报表
   */
  @Cron('0 3 1 * *')
  async generateMonthlyReports() {
    console.log('开始生成月报表...');

    try {
      await this.reportingService.generatePlantingReport(ReportPeriod.MONTHLY);
      await this.reportingService.generateLeaderboardReport(ReportPeriod.MONTHLY);
      await this.reportingService.generateSystemAccountMonthlyReport();

      console.log('月报表生成完成');
    } catch (error) {
      console.error('月报表生成失败:', error);
    }
  }

  /**
   * 每季度第一天凌晨4点生成季度报表
   */
  @Cron('0 4 1 1,4,7,10 *')
  async generateQuarterlyReports() {
    console.log('开始生成季度报表...');

    try {
      await this.reportingService.generatePlantingReport(ReportPeriod.QUARTERLY);

      console.log('季度报表生成完成');
    } catch (error) {
      console.error('季度报表生成失败:', error);
    }
  }

  /**
   * 每年1月1日凌晨5点生成年度报表
   */
  @Cron('0 5 1 1 *')
  async generateYearlyReports() {
    console.log('开始生成年度报表...');

    try {
      await this.reportingService.generatePlantingReport(ReportPeriod.YEARLY);

      console.log('年度报表生成完成');
    } catch (error) {
      console.error('年度报表生成失败:', error);
    }
  }
}
```

### 快照清理调度器

```typescript
// application/schedulers/snapshot-cleanup.scheduler.ts

import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { ReportingApplicationService } from '../services/reporting-application.service';

@Injectable()
export class SnapshotCleanupScheduler {
  constructor(
    private readonly reportingService: ReportingApplicationService,
  ) {}

  /**
   * 每天凌晨0点清理过期快照
   */
  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
  async cleanupExpiredSnapshots() {
    console.log('开始清理过期报表快照...');

    try {
      const deletedCount = await this.reportingService.cleanupExpiredSnapshots();
      console.log(`清理完成，共删除 ${deletedCount} 个过期快照`);
    } catch (error) {
      console.error('快照清理失败:', error);
    }
  }

  /**
   * 每周清理超过保留期的报表文件
   */
  @Cron('0 6 * * 0')
  async cleanupOldReportFiles() {
    console.log('开始清理过期报表文件...');

    try {
      const deletedCount = await this.reportingService.cleanupOldReportFiles();
      console.log(`清理完成，共删除 ${deletedCount} 个过期文件`);
    } catch (error) {
      console.error('报表文件清理失败:', error);
    }
  }
}
```

---

## 事件订阅 (Kafka Events)

### 订阅的事件

| Topic | 事件类型 | 触发条件 | 处理逻辑 |
|-------|---------|---------|---------|
| `planting.order.paid` | PlantingOrderPaidEvent | 认种订单支付成功 | 更新认种日统计 |
| `reward.created` | RewardCreatedEvent | 奖励创建 | 更新收益统计 |
| `leaderboard.refreshed` | LeaderboardRefreshedEvent | 榜单刷新完成 | 缓存最新榜单数据 |

### 发布的事件

| Topic | 事件类型 | 触发条件 |
|-------|---------|---------|
| `report.generated` | ReportGeneratedEvent | 报表生成完成 |
| `report.exported` | ReportExportedEvent | 报表导出完成 |

---

## 与后台页面对应关系

### 1. 龙虎榜数据统计页面
```
页面元素：
- 榜单类型切换: 日榜 | 周榜 | 月榜
- 日期选择器: 选择统计范围
- 数据表格: 排名、用户、分值等
- 导出按钮: Excel/CSV

API调用：
- GET /reports/leaderboard?period=DAILY&startDate=2024-01-01&endDate=2024-01-15
- GET /reports/leaderboard/export?format=EXCEL
```

### 2. 榴莲树认种报表页面
```
页面元素：
- 报表类型切换: 日报 | 周报 | 月报 | 季度报 | 年度报
- 日期范围选择器
- 区域筛选: 省/市下拉
- 数据表格: 日期、认种数、金额、用户数等
- 导出按钮

API调用：
- GET /reports/planting?period=DAILY&startDate=2024-01-01&endDate=2024-01-31
- POST /reports/planting/generate
- GET /reports/planting/export?format=EXCEL
```

### 3. 区域认种报表页面
```
页面元素：
- 维度切换: 按省 | 按市
- 日期范围选择器
- 数据表格: 区域、认种数、金额、排名等
- 导出按钮

API调用：
- GET /reports/regional/province?period=MONTHLY
- GET /reports/regional/city?period=MONTHLY
- GET /reports/regional/export?format=CSV
```

### 4. 社区数据统计页面
```
页面元素：
- 搜索框: 支持社区名模糊搜索
- 日期选择: 日新增 | 周新增 | 月新增 | 自定义范围
- 数据表格: 社区名、上级、下级、认种总量、新增量
- 导出按钮

API调用：
- GET /reports/community?communityName=xxx
- GET /reports/community/search?keyword=xxx&startDate=xxx&endDate=xxx
- GET /reports/community/export?format=EXCEL
```

### 5. 系统账户月度报表页面
```
页面元素：
- 账户类型: 省公司 | 市公司
- 月份选择器
- 数据表格: 账户名、每月算力、累计算力、每月挖矿量、累计挖矿量等
- 导出按钮

API调用：
- GET /reports/system-account/monthly?accountType=PROVINCE&month=2024-01
- GET /reports/system-account/export?format=EXCEL
```

### 6. 系统账户收益来源页面
```
页面元素：
- 账户选择: 省公司/市公司下拉
- 日期范围选择器
- 关键词搜索: 地址、交易流水号
- 收益时间轴: 每笔收益的时间线展示
- 来源细分统计: 饼图/柱状图
- 数据表格: 时间、来源、金额、交易号等
- 导出按钮

API调用：
- GET /reports/system-account/income?accountId=xxx&startDate=xxx&endDate=xxx&keyword=xxx
- GET /reports/system-account/income/timeline?accountId=xxx
- GET /reports/system-account/export?format=EXCEL
```

---

## 开发顺序建议

1. **Phase 1: 项目初始化**
   - 创建NestJS项目
   - 安装依赖
   - 配置环境变量

2. **Phase 2: 数据库层**
   - 创建Prisma Schema
   - 运行迁移和种子数据
   - 创建PrismaService

3. **Phase 3: 领域层**
   - 实现所有值对象
   - 实现聚合根 (ReportDefinition, ReportSnapshot)
   - 实现实体 (AnalyticsMetric, ReportFile)
   - 实现领域事件
   - 实现领域服务 (ReportGenerationService, ReportExportService)
   - 编写单元测试

4. **Phase 4: 基础设施层**
   - 实现仓储 (Repository Implementations)
   - 实现外部服务客户端 (各Service Client)
   - 实现导出服务 (Excel, PDF, CSV)
   - 实现文件存储服务
   - 实现Kafka消费者和发布者
   - 实现Redis缓存服务

5. **Phase 5: 应用层**
   - 实现应用服务 (ReportingApplicationService)
   - 实现定时任务 (ReportGenerationScheduler, SnapshotCleanupScheduler)
   - 实现Command/Query handlers

6. **Phase 6: API层**
   - 实现DTO
   - 实现Controllers
   - 配置Swagger文档
   - 配置JWT认证和管理员权限

7. **Phase 7: 测试和部署**
   - 集成测试
   - E2E测试
   - Docker配置

---

## 注意事项

1. **数据聚合**: 报表数据从多个微服务聚合，需要处理服务间的数据一致性
2. **性能优化**: 大数据量报表生成需要考虑分批处理和异步生成
3. **缓存策略**: 热门报表数据应缓存到Redis，减少重复计算
4. **文件管理**: 导出的报表文件需要定期清理，避免存储空间耗尽
5. **权限控制**: 所有报表接口都需要管理员权限
6. **导出限制**: 导出数据量过大时需要限制或采用异步导出
7. **时区处理**: 统一使用Asia/Shanghai时区进行日期计算
8. **模糊搜索**: 社区搜索支持模糊匹配，需要优化查询性能
9. **时间轴展示**: 收益来源时间轴需要支持分页加载
10. **幂等性**: 报表生成需要保证幂等性，避免重复生成
