# Leaderboard Service 开发指导

## 项目概述

Leaderboard Service 是 RWA 榴莲皇后平台的龙虎榜微服务，负责管理用户排名计算、虚拟账户管理、榜单数据聚合与缓存、日/周/月榜单管理等功能。

### 核心职责 ✅
- 龙虎榜排名计算（日榜、周榜、月榜）
- 虚拟排名功能（系统生成虚拟账户参与排名）
- 榜单开关管理（日榜/周榜/月榜独立开关）
- 榜单显示数量设置
- 虚拟账户管理（系统省公司、系统市公司、总部社区）
- 榜单数据缓存与定时刷新

### 不负责 ❌
- 实际业务数据存储（从其他上下文读取）
- 权限验证（Authorization Context）
- 用户基本信息管理（Identity Context）
- 团队统计计算（Referral Context）

---

## 核心业务规则

### 1. 龙虎榜分值计算

```
龙虎榜分值 = 团队总认种量 - 最大单个直推团队认种量

目的：
- 鼓励均衡发展团队，而不是只依赖单个大团队
- 防止"烧伤"现象（单腿发展）

示例：
用户A的团队数据：
- 直推B的团队认种: 100棵
- 直推C的团队认种: 80棵
- 直推D的团队认种: 50棵
- 团队总认种: 230棵
- 最大单个直推团队: 100棵 (B)
- 龙虎榜分值: 230 - 100 = 130
```

### 2. 榜单周期

| 榜单类型 | 统计周期 | 重置时间 |
|---------|---------|---------|
| 日榜 | 当日 00:00 - 23:59 | 每日 00:00 重置 |
| 周榜 | 周一 00:00 - 周日 23:59 | 每周一 00:00 重置 |
| 月榜 | 每月1日 00:00 - 月末 23:59 | 每月1日 00:00 重置 |

### 3. 虚拟排名功能

```
虚拟账户数量设置：
- 设置为 30：真实排名第1的用户显示在第31名
- 设置为 29：真实排名第1的用户显示在第1名（虚拟账户占29位）
- 设置为 0：关闭虚拟排名，完全显示真实排名

虚拟账户特点：
- 系统自动生成
- 显示随机用户名和头像
- 分值在合理范围内随机生成
- 不参与实际奖励分配
```

### 4. 榜单开关功能

| 开关 | 状态 | 前端显示 |
|------|------|---------|
| 日榜开关 | 关闭 | "日榜待开启" |
| 周榜开关 | 关闭 | "周榜待开启" |
| 月榜开关 | 关闭 | "月榜待开启" |

### 5. 显示数量设置

- 前端排名数量可配置（如：10、20、30、50、100）
- 默认显示前30名
- 超出设置数量的排名不在前端展示

---

## 技术栈

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

---

## 架构设计

```
leaderboard-service/
├── prisma/
│   ├── schema.prisma              # 数据库模型定义
│   └── migrations/                # 数据库迁移文件
│
├── src/
│   ├── api/                       # 🔵 Presentation Layer (表现层)
│   │   ├── controllers/
│   │   │   ├── health.controller.ts
│   │   │   ├── leaderboard.controller.ts
│   │   │   ├── leaderboard-config.controller.ts
│   │   │   └── virtual-account.controller.ts
│   │   ├── dto/
│   │   │   ├── request/
│   │   │   │   ├── update-leaderboard-config.dto.ts
│   │   │   │   └── query-leaderboard.dto.ts
│   │   │   └── response/
│   │   │       ├── leaderboard-ranking.dto.ts
│   │   │       ├── leaderboard-config.dto.ts
│   │   │       └── virtual-account.dto.ts
│   │   └── api.module.ts
│   │
│   ├── application/               # 🟢 Application Layer (应用层)
│   │   ├── commands/
│   │   │   ├── refresh-leaderboard/
│   │   │   │   ├── refresh-leaderboard.command.ts
│   │   │   │   └── refresh-leaderboard.handler.ts
│   │   │   ├── update-leaderboard-config/
│   │   │   │   ├── update-leaderboard-config.command.ts
│   │   │   │   └── update-leaderboard-config.handler.ts
│   │   │   ├── generate-virtual-accounts/
│   │   │   │   ├── generate-virtual-accounts.command.ts
│   │   │   │   └── generate-virtual-accounts.handler.ts
│   │   │   └── index.ts
│   │   ├── queries/
│   │   │   ├── get-leaderboard/
│   │   │   │   ├── get-leaderboard.query.ts
│   │   │   │   └── get-leaderboard.handler.ts
│   │   │   ├── get-my-ranking/
│   │   │   │   ├── get-my-ranking.query.ts
│   │   │   │   └── get-my-ranking.handler.ts
│   │   │   ├── get-leaderboard-config/
│   │   │   │   ├── get-leaderboard-config.query.ts
│   │   │   │   └── get-leaderboard-config.handler.ts
│   │   │   └── index.ts
│   │   ├── services/
│   │   │   └── leaderboard-application.service.ts
│   │   ├── schedulers/
│   │   │   └── leaderboard-refresh.scheduler.ts
│   │   └── application.module.ts
│   │
│   ├── domain/                    # 🟡 Domain Layer (领域层)
│   │   ├── aggregates/
│   │   │   ├── leaderboard-ranking/
│   │   │   │   ├── leaderboard-ranking.aggregate.ts
│   │   │   │   ├── leaderboard-ranking.spec.ts
│   │   │   │   └── index.ts
│   │   │   └── leaderboard-config/
│   │   │       ├── leaderboard-config.aggregate.ts
│   │   │       ├── leaderboard-config.spec.ts
│   │   │       └── index.ts
│   │   ├── entities/
│   │   │   ├── virtual-account.entity.ts
│   │   │   ├── virtual-ranking-entry.entity.ts
│   │   │   └── index.ts
│   │   ├── value-objects/
│   │   │   ├── leaderboard-type.enum.ts
│   │   │   ├── leaderboard-period.enum.ts
│   │   │   ├── rank-position.vo.ts
│   │   │   ├── ranking-score.vo.ts
│   │   │   ├── user-snapshot.vo.ts
│   │   │   ├── virtual-account-type.enum.ts
│   │   │   └── index.ts
│   │   ├── events/
│   │   │   ├── domain-event.base.ts
│   │   │   ├── leaderboard-refreshed.event.ts
│   │   │   ├── ranking-changed.event.ts
│   │   │   ├── config-updated.event.ts
│   │   │   └── index.ts
│   │   ├── repositories/
│   │   │   ├── leaderboard-ranking.repository.interface.ts
│   │   │   ├── leaderboard-config.repository.interface.ts
│   │   │   ├── virtual-account.repository.interface.ts
│   │   │   └── index.ts
│   │   ├── services/
│   │   │   ├── leaderboard-calculation.service.ts
│   │   │   ├── virtual-ranking-generator.service.ts
│   │   │   ├── ranking-merger.service.ts
│   │   │   └── index.ts
│   │   └── domain.module.ts
│   │
│   ├── infrastructure/            # 🔴 Infrastructure Layer (基础设施层)
│   │   ├── persistence/
│   │   │   ├── prisma/
│   │   │   │   └── prisma.service.ts
│   │   │   ├── mappers/
│   │   │   │   ├── leaderboard-ranking.mapper.ts
│   │   │   │   ├── leaderboard-config.mapper.ts
│   │   │   │   └── virtual-account.mapper.ts
│   │   │   └── repositories/
│   │   │       ├── leaderboard-ranking.repository.impl.ts
│   │   │       ├── leaderboard-config.repository.impl.ts
│   │   │       └── virtual-account.repository.impl.ts
│   │   ├── external/
│   │   │   ├── referral-service/
│   │   │   │   └── referral-service.client.ts
│   │   │   └── identity-service/
│   │   │       └── identity-service.client.ts
│   │   ├── kafka/
│   │   │   ├── event-consumer.controller.ts
│   │   │   ├── event-publisher.service.ts
│   │   │   └── kafka.module.ts
│   │   ├── redis/
│   │   │   ├── redis.service.ts
│   │   │   ├── leaderboard-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
│   │   └── index.ts
│   │
│   ├── app.module.ts
│   └── main.ts
│
├── test/
├── .env.example
├── .env.development
├── package.json
├── tsconfig.json
└── Dockerfile
```

---

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

### 1.1 创建 NestJS 项目

```bash
cd backend/services/leaderboard-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 -D prisma @types/uuid @types/passport-jwt
npm install -D @nestjs/testing jest ts-jest @types/jest supertest @types/supertest
```

### 1.3 环境变量配置

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

# 数据库
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_leaderboard?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=leaderboard-service-group
KAFKA_CLIENT_ID=leaderboard-service

# 外部服务
IDENTITY_SERVICE_URL=http://localhost:3001
REFERRAL_SERVICE_URL=http://localhost:3004

# 榜单刷新间隔（毫秒）
LEADERBOARD_REFRESH_INTERVAL=300000

# 榜单缓存过期时间（秒）
LEADERBOARD_CACHE_TTL=300
```

---

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

### 2.1 创建 prisma/schema.prisma

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

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

// ============================================
// 龙虎榜排名表 (聚合根1)
// 存储各周期榜单的实际排名数据
// ============================================
model LeaderboardRanking {
  id              BigInt   @id @default(autoincrement()) @map("ranking_id")

  // === 榜单信息 ===
  leaderboardType String   @map("leaderboard_type") @db.VarChar(30)  // DAILY/WEEKLY/MONTHLY
  periodKey       String   @map("period_key") @db.VarChar(20)       // 2024-01-15 / 2024-W03 / 2024-01

  // === 用户信息 ===
  userId          BigInt   @map("user_id")
  isVirtual       Boolean  @default(false) @map("is_virtual")       // 是否虚拟账户

  // === 排名信息 ===
  rankPosition    Int      @map("rank_position")                    // 实际排名
  displayPosition Int      @map("display_position")                 // 显示排名（含虚拟）
  previousRank    Int?     @map("previous_rank")                    // 上次排名

  // === 分值信息 ===
  totalTeamPlanting      Int     @default(0) @map("total_team_planting")        // 团队总认种
  maxDirectTeamPlanting  Int     @default(0) @map("max_direct_team_planting")   // 最大直推团队认种
  effectiveScore         Int     @default(0) @map("effective_score")            // 有效分值

  // === 用户快照 ===
  userSnapshot    Json     @map("user_snapshot")  // { nickname, avatar, accountNo }

  // === 时间戳 ===
  periodStartAt   DateTime @map("period_start_at")
  periodEndAt     DateTime @map("period_end_at")
  calculatedAt    DateTime @default(now()) @map("calculated_at")
  createdAt       DateTime @default(now()) @map("created_at")

  @@unique([leaderboardType, periodKey, userId], name: "uk_type_period_user")
  @@map("leaderboard_rankings")
  @@index([leaderboardType, periodKey, displayPosition], name: "idx_display_rank")
  @@index([leaderboardType, periodKey, effectiveScore(sort: Desc)], name: "idx_score")
  @@index([userId], name: "idx_ranking_user")
  @@index([periodKey], name: "idx_period")
  @@index([isVirtual], name: "idx_virtual")
}

// ============================================
// 龙虎榜配置表 (聚合根2)
// 管理榜单开关、虚拟数量、显示设置
// ============================================
model LeaderboardConfig {
  id              BigInt   @id @default(autoincrement()) @map("config_id")
  configKey       String   @unique @map("config_key") @db.VarChar(50)  // GLOBAL / DAILY / WEEKLY / MONTHLY

  // === 榜单开关 ===
  dailyEnabled    Boolean  @default(true) @map("daily_enabled")
  weeklyEnabled   Boolean  @default(true) @map("weekly_enabled")
  monthlyEnabled  Boolean  @default(true) @map("monthly_enabled")

  // === 虚拟排名设置 ===
  virtualRankingEnabled   Boolean @default(false) @map("virtual_ranking_enabled")
  virtualAccountCount     Int     @default(0) @map("virtual_account_count")     // 虚拟账户数量

  // === 显示设置 ===
  displayLimit    Int      @default(30) @map("display_limit")       // 前端显示数量

  // === 刷新设置 ===
  refreshIntervalMinutes  Int @default(5) @map("refresh_interval_minutes")

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

  @@map("leaderboard_configs")
}

// ============================================
// 虚拟账户表
// 存储系统生成的虚拟排名账户
// ============================================
model VirtualAccount {
  id              BigInt   @id @default(autoincrement()) @map("virtual_account_id")

  // === 账户信息 ===
  accountType     String   @map("account_type") @db.VarChar(30)     // RANKING_VIRTUAL / SYSTEM_PROVINCE / SYSTEM_CITY / HEADQUARTERS
  displayName     String   @map("display_name") @db.VarChar(100)
  avatar          String?  @map("avatar") @db.VarChar(255)

  // === 区域信息（省市公司用）===
  provinceCode    String?  @map("province_code") @db.VarChar(10)
  cityCode        String?  @map("city_code") @db.VarChar(10)

  // === 虚拟分值范围（排名虚拟账户用）===
  minScore        Int?     @map("min_score")
  maxScore        Int?     @map("max_score")
  currentScore    Int      @default(0) @map("current_score")

  // === 账户余额（省市公司用）===
  usdtBalance     Decimal  @default(0) @map("usdt_balance") @db.Decimal(20, 8)
  hashpowerBalance Decimal @default(0) @map("hashpower_balance") @db.Decimal(20, 8)

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

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

  @@map("virtual_accounts")
  @@index([accountType], name: "idx_va_type")
  @@index([provinceCode], name: "idx_va_province")
  @@index([cityCode], name: "idx_va_city")
  @@index([isActive], name: "idx_va_active")
}

// ============================================
// 虚拟排名条目表
// 每个周期的虚拟排名数据
// ============================================
model VirtualRankingEntry {
  id              BigInt   @id @default(autoincrement()) @map("entry_id")

  // === 关联虚拟账户 ===
  virtualAccountId BigInt  @map("virtual_account_id")

  // === 榜单信息 ===
  leaderboardType String   @map("leaderboard_type") @db.VarChar(30)
  periodKey       String   @map("period_key") @db.VarChar(20)

  // === 排名信息 ===
  displayPosition Int      @map("display_position")                 // 占据的显示位置
  generatedScore  Int      @map("generated_score")                  // 生成的分值

  // === 显示信息 ===
  displayName     String   @map("display_name") @db.VarChar(100)
  avatar          String?  @map("avatar") @db.VarChar(255)

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

  @@unique([leaderboardType, periodKey, displayPosition], name: "uk_vr_type_period_pos")
  @@map("virtual_ranking_entries")
  @@index([virtualAccountId], name: "idx_vr_va")
  @@index([leaderboardType, periodKey], name: "idx_vr_type_period")
}

// ============================================
// 榜单历史快照表
// 保存每个周期结束时的完整榜单数据
// ============================================
model LeaderboardSnapshot {
  id              BigInt   @id @default(autoincrement()) @map("snapshot_id")

  // === 榜单信息 ===
  leaderboardType String   @map("leaderboard_type") @db.VarChar(30)
  periodKey       String   @map("period_key") @db.VarChar(20)

  // === 快照数据 ===
  rankingsData    Json     @map("rankings_data")                    // 完整排名数据

  // === 统计信息 ===
  totalParticipants Int    @map("total_participants")              // 参与人数
  topScore        Int      @map("top_score")                       // 最高分
  averageScore    Int      @map("average_score")                   // 平均分

  // === 时间戳 ===
  periodStartAt   DateTime @map("period_start_at")
  periodEndAt     DateTime @map("period_end_at")
  snapshotAt      DateTime @default(now()) @map("snapshot_at")

  @@unique([leaderboardType, periodKey], name: "uk_snapshot_type_period")
  @@map("leaderboard_snapshots")
  @@index([leaderboardType], name: "idx_snapshot_type")
  @@index([periodKey], name: "idx_snapshot_period")
}

// ============================================
// 虚拟账户交易记录表
// 记录省市公司账户的资金变动
// ============================================
model VirtualAccountTransaction {
  id              BigInt   @id @default(autoincrement()) @map("transaction_id")
  virtualAccountId BigInt  @map("virtual_account_id")

  // === 交易信息 ===
  transactionType String   @map("transaction_type") @db.VarChar(30)  // INCOME / EXPENSE
  amount          Decimal  @map("amount") @db.Decimal(20, 8)
  currency        String   @map("currency") @db.VarChar(10)          // USDT / HASHPOWER

  // === 来源信息 ===
  sourceType      String?  @map("source_type") @db.VarChar(50)       // PLANTING_REWARD / MANUAL
  sourceId        String?  @map("source_id") @db.VarChar(100)
  sourceUserId    BigInt?  @map("source_user_id")

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

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

  @@map("virtual_account_transactions")
  @@index([virtualAccountId], name: "idx_vat_account")
  @@index([transactionType], name: "idx_vat_type")
  @@index([createdAt(sort: Desc)], name: "idx_vat_created")
}

// ============================================
// 龙虎榜事件表
// ============================================
model LeaderboardEvent {
  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("leaderboard_events")
  @@index([aggregateType, aggregateId], name: "idx_lb_event_aggregate")
  @@index([eventType], name: "idx_lb_event_type")
  @@index([occurredAt], name: "idx_lb_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() {
  // 初始化全局配置
  await prisma.leaderboardConfig.upsert({
    where: { configKey: 'GLOBAL' },
    update: {},
    create: {
      configKey: 'GLOBAL',
      dailyEnabled: true,
      weeklyEnabled: true,
      monthlyEnabled: true,
      virtualRankingEnabled: false,
      virtualAccountCount: 0,
      displayLimit: 30,
      refreshIntervalMinutes: 5,
    },
  });

  // 初始化总部社区虚拟账户
  await prisma.virtualAccount.upsert({
    where: { id: 1n },
    update: {},
    create: {
      accountType: 'HEADQUARTERS',
      displayName: '总部社区',
      isActive: true,
    },
  });

  console.log('Seed completed: Leaderboard config and headquarters account 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/leaderboard-type.enum.ts
```typescript
export enum LeaderboardType {
  DAILY = 'DAILY',      // 日榜
  WEEKLY = 'WEEKLY',    // 周榜
  MONTHLY = 'MONTHLY',  // 月榜
}

export const LeaderboardTypeLabels: Record<LeaderboardType, string> = {
  [LeaderboardType.DAILY]: '日榜',
  [LeaderboardType.WEEKLY]: '周榜',
  [LeaderboardType.MONTHLY]: '月榜',
};
```

#### 3.1.2 src/domain/value-objects/leaderboard-period.vo.ts
```typescript
export class LeaderboardPeriod {
  private constructor(
    public readonly type: LeaderboardType,
    public readonly key: string,           // 2024-01-15 / 2024-W03 / 2024-01
    public readonly startAt: Date,
    public readonly endAt: Date,
  ) {}

  /**
   * 创建当前日榜周期
   */
  static currentDaily(): LeaderboardPeriod {
    const now = new Date();
    const startAt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
    const endAt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
    const key = this.formatDate(now);

    return new LeaderboardPeriod(LeaderboardType.DAILY, key, startAt, endAt);
  }

  /**
   * 创建当前周榜周期
   */
  static currentWeekly(): LeaderboardPeriod {
    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);

    const weekNumber = this.getWeekNumber(now);
    const key = `${now.getFullYear()}-W${weekNumber.toString().padStart(2, '0')}`;

    return new LeaderboardPeriod(LeaderboardType.WEEKLY, key, monday, sunday);
  }

  /**
   * 创建当前月榜周期
   */
  static currentMonthly(): LeaderboardPeriod {
    const now = new Date();
    const startAt = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
    const endAt = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
    const key = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}`;

    return new LeaderboardPeriod(LeaderboardType.MONTHLY, key, startAt, endAt);
  }

  /**
   * 根据类型创建当前周期
   */
  static current(type: LeaderboardType): LeaderboardPeriod {
    switch (type) {
      case LeaderboardType.DAILY:
        return this.currentDaily();
      case LeaderboardType.WEEKLY:
        return this.currentWeekly();
      case LeaderboardType.MONTHLY:
        return this.currentMonthly();
    }
  }

  /**
   * 检查是否在当前周期内
   */
  isCurrentPeriod(): boolean {
    const now = new Date();
    return now >= this.startAt && now <= this.endAt;
  }

  private static formatDate(date: Date): string {
    return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
  }

  private static 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);
  }
}
```

#### 3.1.3 src/domain/value-objects/ranking-score.vo.ts
```typescript
/**
 * 龙虎榜分值值对象
 *
 * 计算公式: 团队总认种量 - 最大单个直推团队认种量
 */
export class RankingScore {
  private constructor(
    public readonly totalTeamPlanting: number,      // 团队总认种量
    public readonly maxDirectTeamPlanting: number,  // 最大单个直推团队认种量
    public readonly effectiveScore: number,         // 有效分值（龙虎榜分值）
  ) {}

  static calculate(
    totalTeamPlanting: number,
    maxDirectTeamPlanting: number,
  ): RankingScore {
    const effectiveScore = Math.max(0, totalTeamPlanting - maxDirectTeamPlanting);
    return new RankingScore(totalTeamPlanting, maxDirectTeamPlanting, effectiveScore);
  }

  static zero(): RankingScore {
    return new RankingScore(0, 0, 0);
  }

  static fromRaw(
    totalTeamPlanting: number,
    maxDirectTeamPlanting: number,
    effectiveScore: number,
  ): RankingScore {
    return new RankingScore(totalTeamPlanting, maxDirectTeamPlanting, effectiveScore);
  }

  /**
   * 比较分值（用于排序）
   */
  compareTo(other: RankingScore): number {
    return other.effectiveScore - this.effectiveScore;
  }

  equals(other: RankingScore): boolean {
    return this.effectiveScore === other.effectiveScore;
  }
}
```

#### 3.1.4 src/domain/value-objects/rank-position.vo.ts
```typescript
export class RankPosition {
  private constructor(
    public readonly value: number,
  ) {
    if (value < 1) {
      throw new Error('排名必须大于0');
    }
  }

  static create(value: number): RankPosition {
    return new RankPosition(value);
  }

  /**
   * 是否在前N名
   */
  isTop(n: number): boolean {
    return this.value <= n;
  }

  /**
   * 计算排名变化
   */
  calculateChange(previousRank: RankPosition | null): number {
    if (!previousRank) return 0;
    return previousRank.value - this.value;
  }

  equals(other: RankPosition): boolean {
    return this.value === other.value;
  }
}
```

#### 3.1.5 src/domain/value-objects/user-snapshot.vo.ts
```typescript
export class UserSnapshot {
  private constructor(
    public readonly userId: bigint,
    public readonly nickname: string,
    public readonly avatar: string | null,
    public readonly accountNo: string | null,
  ) {}

  static create(params: {
    userId: bigint;
    nickname: string;
    avatar?: string | null;
    accountNo?: string | null;
  }): UserSnapshot {
    return new UserSnapshot(
      params.userId,
      params.nickname,
      params.avatar || null,
      params.accountNo || null,
    );
  }

  static fromJson(json: Record<string, any>): UserSnapshot {
    return new UserSnapshot(
      BigInt(json.userId),
      json.nickname,
      json.avatar || null,
      json.accountNo || null,
    );
  }

  toJson(): Record<string, any> {
    return {
      userId: this.userId.toString(),
      nickname: this.nickname,
      avatar: this.avatar,
      accountNo: this.accountNo,
    };
  }
}
```

#### 3.1.6 src/domain/value-objects/virtual-account-type.enum.ts
```typescript
export enum VirtualAccountType {
  RANKING_VIRTUAL = 'RANKING_VIRTUAL',      // 排名虚拟账户
  SYSTEM_PROVINCE = 'SYSTEM_PROVINCE',      // 系统省公司
  SYSTEM_CITY = 'SYSTEM_CITY',              // 系统市公司
  HEADQUARTERS = 'HEADQUARTERS',            // 总部社区
}
```

#### 3.1.7 src/domain/value-objects/index.ts
```typescript
export * from './leaderboard-type.enum';
export * from './leaderboard-period.vo';
export * from './ranking-score.vo';
export * from './rank-position.vo';
export * from './user-snapshot.vo';
export * from './virtual-account-type.enum';
```

### 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/leaderboard-refreshed.event.ts
```typescript
import { DomainEvent } from './domain-event.base';
import { LeaderboardType } from '../value-objects/leaderboard-type.enum';

export interface LeaderboardRefreshedPayload {
  leaderboardType: LeaderboardType;
  periodKey: string;
  totalParticipants: number;
  topScore: number;
  refreshedAt: Date;
}

export class LeaderboardRefreshedEvent extends DomainEvent {
  constructor(private readonly payload: LeaderboardRefreshedPayload) {
    super();
  }

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

  get aggregateId(): string {
    return `${this.payload.leaderboardType}_${this.payload.periodKey}`;
  }

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

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

#### 3.2.3 src/domain/events/config-updated.event.ts
```typescript
import { DomainEvent } from './domain-event.base';

export interface ConfigUpdatedPayload {
  configKey: string;
  changes: Record<string, any>;
  updatedBy: string;
}

export class ConfigUpdatedEvent extends DomainEvent {
  constructor(private readonly payload: ConfigUpdatedPayload) {
    super();
  }

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

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

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

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

### 3.3 聚合根 (Aggregates)

#### 3.3.1 src/domain/aggregates/leaderboard-ranking/leaderboard-ranking.aggregate.ts
```typescript
import { DomainEvent } from '../../events/domain-event.base';
import { LeaderboardType } from '../../value-objects/leaderboard-type.enum';
import { LeaderboardPeriod } from '../../value-objects/leaderboard-period.vo';
import { RankingScore } from '../../value-objects/ranking-score.vo';
import { RankPosition } from '../../value-objects/rank-position.vo';
import { UserSnapshot } from '../../value-objects/user-snapshot.vo';

/**
 * 龙虎榜排名聚合根
 *
 * 不变式:
 * 1. 同一榜单内排名必须唯一且连续
 * 2. 虚拟账户不参与真实排名计算
 * 3. 有效分值 = 团队总认种 - 最大单个直推团队认种
 */
export class LeaderboardRanking {
  private _id: bigint | null = null;
  private readonly _leaderboardType: LeaderboardType;
  private readonly _period: LeaderboardPeriod;
  private readonly _userId: bigint;
  private readonly _isVirtual: boolean;
  private _rankPosition: RankPosition;
  private _displayPosition: RankPosition;
  private _previousRank: RankPosition | null;
  private _score: RankingScore;
  private readonly _userSnapshot: UserSnapshot;
  private readonly _calculatedAt: Date;

  private _domainEvents: DomainEvent[] = [];

  private constructor(
    leaderboardType: LeaderboardType,
    period: LeaderboardPeriod,
    userId: bigint,
    isVirtual: boolean,
    rankPosition: RankPosition,
    displayPosition: RankPosition,
    previousRank: RankPosition | null,
    score: RankingScore,
    userSnapshot: UserSnapshot,
    calculatedAt: Date,
  ) {
    this._leaderboardType = leaderboardType;
    this._period = period;
    this._userId = userId;
    this._isVirtual = isVirtual;
    this._rankPosition = rankPosition;
    this._displayPosition = displayPosition;
    this._previousRank = previousRank;
    this._score = score;
    this._userSnapshot = userSnapshot;
    this._calculatedAt = calculatedAt;
  }

  // ============ Getters ============
  get id(): bigint | null { return this._id; }
  get leaderboardType(): LeaderboardType { return this._leaderboardType; }
  get period(): LeaderboardPeriod { return this._period; }
  get periodKey(): string { return this._period.key; }
  get userId(): bigint { return this._userId; }
  get isVirtual(): boolean { return this._isVirtual; }
  get rankPosition(): RankPosition { return this._rankPosition; }
  get displayPosition(): RankPosition { return this._displayPosition; }
  get previousRank(): RankPosition | null { return this._previousRank; }
  get score(): RankingScore { return this._score; }
  get userSnapshot(): UserSnapshot { return this._userSnapshot; }
  get calculatedAt(): Date { return this._calculatedAt; }
  get domainEvents(): DomainEvent[] { return [...this._domainEvents]; }

  get rankChange(): number {
    return this._displayPosition.calculateChange(this._previousRank);
  }

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

  /**
   * 创建真实用户排名
   */
  static createRealRanking(params: {
    leaderboardType: LeaderboardType;
    period: LeaderboardPeriod;
    userId: bigint;
    rankPosition: number;
    displayPosition: number;
    previousRank: number | null;
    totalTeamPlanting: number;
    maxDirectTeamPlanting: number;
    userSnapshot: UserSnapshot;
  }): LeaderboardRanking {
    const score = RankingScore.calculate(
      params.totalTeamPlanting,
      params.maxDirectTeamPlanting,
    );

    return new LeaderboardRanking(
      params.leaderboardType,
      params.period,
      params.userId,
      false,
      RankPosition.create(params.rankPosition),
      RankPosition.create(params.displayPosition),
      params.previousRank ? RankPosition.create(params.previousRank) : null,
      score,
      params.userSnapshot,
      new Date(),
    );
  }

  /**
   * 创建虚拟用户排名
   */
  static createVirtualRanking(params: {
    leaderboardType: LeaderboardType;
    period: LeaderboardPeriod;
    virtualAccountId: bigint;
    displayPosition: number;
    generatedScore: number;
    displayName: string;
    avatar: string | null;
  }): LeaderboardRanking {
    const userSnapshot = UserSnapshot.create({
      userId: params.virtualAccountId,
      nickname: params.displayName,
      avatar: params.avatar,
    });

    return new LeaderboardRanking(
      params.leaderboardType,
      params.period,
      params.virtualAccountId,
      true,
      RankPosition.create(params.displayPosition),  // 虚拟账户的实际排名等于显示排名
      RankPosition.create(params.displayPosition),
      null,
      RankingScore.fromRaw(params.generatedScore, 0, params.generatedScore),
      userSnapshot,
      new Date(),
    );
  }

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

  /**
   * 更新显示排名（虚拟排名插入后调整）
   */
  updateDisplayPosition(newDisplayPosition: number): void {
    this._displayPosition = RankPosition.create(newDisplayPosition);
  }

  /**
   * 是否在显示范围内
   */
  isWithinDisplayLimit(limit: number): boolean {
    return this._displayPosition.isTop(limit);
  }

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

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

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

  static reconstitute(data: {
    id: bigint;
    leaderboardType: LeaderboardType;
    periodKey: string;
    periodStartAt: Date;
    periodEndAt: Date;
    userId: bigint;
    isVirtual: boolean;
    rankPosition: number;
    displayPosition: number;
    previousRank: number | null;
    totalTeamPlanting: number;
    maxDirectTeamPlanting: number;
    effectiveScore: number;
    userSnapshot: Record<string, any>;
    calculatedAt: Date;
  }): LeaderboardRanking {
    const period = new LeaderboardPeriod(
      data.leaderboardType,
      data.periodKey,
      data.periodStartAt,
      data.periodEndAt,
    );

    const ranking = new LeaderboardRanking(
      data.leaderboardType,
      period,
      data.userId,
      data.isVirtual,
      RankPosition.create(data.rankPosition),
      RankPosition.create(data.displayPosition),
      data.previousRank ? RankPosition.create(data.previousRank) : null,
      RankingScore.fromRaw(
        data.totalTeamPlanting,
        data.maxDirectTeamPlanting,
        data.effectiveScore,
      ),
      UserSnapshot.fromJson(data.userSnapshot),
      data.calculatedAt,
    );
    ranking._id = data.id;
    return ranking;
  }
}
```

#### 3.3.2 src/domain/aggregates/leaderboard-config/leaderboard-config.aggregate.ts
```typescript
import { DomainEvent } from '../../events/domain-event.base';
import { ConfigUpdatedEvent } from '../../events/config-updated.event';

/**
 * 龙虎榜配置聚合根
 *
 * 不变式:
 * 1. 虚拟账户数量不能为负数
 * 2. 显示数量必须大于0
 * 3. 刷新间隔必须大于0
 */
export class LeaderboardConfig {
  private _id: bigint | null = null;
  private readonly _configKey: string;

  // 榜单开关
  private _dailyEnabled: boolean;
  private _weeklyEnabled: boolean;
  private _monthlyEnabled: boolean;

  // 虚拟排名设置
  private _virtualRankingEnabled: boolean;
  private _virtualAccountCount: number;

  // 显示设置
  private _displayLimit: number;

  // 刷新设置
  private _refreshIntervalMinutes: number;

  private readonly _createdAt: Date;

  private _domainEvents: DomainEvent[] = [];

  private constructor(
    configKey: string,
    dailyEnabled: boolean,
    weeklyEnabled: boolean,
    monthlyEnabled: boolean,
    virtualRankingEnabled: boolean,
    virtualAccountCount: number,
    displayLimit: number,
    refreshIntervalMinutes: number,
  ) {
    this._configKey = configKey;
    this._dailyEnabled = dailyEnabled;
    this._weeklyEnabled = weeklyEnabled;
    this._monthlyEnabled = monthlyEnabled;
    this._virtualRankingEnabled = virtualRankingEnabled;
    this._virtualAccountCount = virtualAccountCount;
    this._displayLimit = displayLimit;
    this._refreshIntervalMinutes = refreshIntervalMinutes;
    this._createdAt = new Date();
  }

  // ============ Getters ============
  get id(): bigint | null { return this._id; }
  get configKey(): string { return this._configKey; }
  get dailyEnabled(): boolean { return this._dailyEnabled; }
  get weeklyEnabled(): boolean { return this._weeklyEnabled; }
  get monthlyEnabled(): boolean { return this._monthlyEnabled; }
  get virtualRankingEnabled(): boolean { return this._virtualRankingEnabled; }
  get virtualAccountCount(): number { return this._virtualAccountCount; }
  get displayLimit(): number { return this._displayLimit; }
  get refreshIntervalMinutes(): number { return this._refreshIntervalMinutes; }
  get createdAt(): Date { return this._createdAt; }
  get domainEvents(): DomainEvent[] { return [...this._domainEvents]; }

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

  static createDefault(): LeaderboardConfig {
    return new LeaderboardConfig(
      'GLOBAL',
      true,   // dailyEnabled
      true,   // weeklyEnabled
      true,   // monthlyEnabled
      false,  // virtualRankingEnabled
      0,      // virtualAccountCount
      30,     // displayLimit
      5,      // refreshIntervalMinutes
    );
  }

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

  /**
   * 更新榜单开关
   */
  updateLeaderboardSwitch(
    type: 'daily' | 'weekly' | 'monthly',
    enabled: boolean,
    updatedBy: string,
  ): void {
    const changes: Record<string, any> = {};

    switch (type) {
      case 'daily':
        this._dailyEnabled = enabled;
        changes.dailyEnabled = enabled;
        break;
      case 'weekly':
        this._weeklyEnabled = enabled;
        changes.weeklyEnabled = enabled;
        break;
      case 'monthly':
        this._monthlyEnabled = enabled;
        changes.monthlyEnabled = enabled;
        break;
    }

    this._domainEvents.push(new ConfigUpdatedEvent({
      configKey: this._configKey,
      changes,
      updatedBy,
    }));
  }

  /**
   * 更新虚拟排名设置
   */
  updateVirtualRankingSettings(
    enabled: boolean,
    accountCount: number,
    updatedBy: string,
  ): void {
    if (accountCount < 0) {
      throw new Error('虚拟账户数量不能为负数');
    }

    this._virtualRankingEnabled = enabled;
    this._virtualAccountCount = accountCount;

    this._domainEvents.push(new ConfigUpdatedEvent({
      configKey: this._configKey,
      changes: {
        virtualRankingEnabled: enabled,
        virtualAccountCount: accountCount,
      },
      updatedBy,
    }));
  }

  /**
   * 更新显示数量
   */
  updateDisplayLimit(limit: number, updatedBy: string): void {
    if (limit <= 0) {
      throw new Error('显示数量必须大于0');
    }

    this._displayLimit = limit;

    this._domainEvents.push(new ConfigUpdatedEvent({
      configKey: this._configKey,
      changes: { displayLimit: limit },
      updatedBy,
    }));
  }

  /**
   * 更新刷新间隔
   */
  updateRefreshInterval(minutes: number, updatedBy: string): void {
    if (minutes <= 0) {
      throw new Error('刷新间隔必须大于0');
    }

    this._refreshIntervalMinutes = minutes;

    this._domainEvents.push(new ConfigUpdatedEvent({
      configKey: this._configKey,
      changes: { refreshIntervalMinutes: minutes },
      updatedBy,
    }));
  }

  /**
   * 检查榜单是否启用
   */
  isLeaderboardEnabled(type: LeaderboardType): boolean {
    switch (type) {
      case LeaderboardType.DAILY:
        return this._dailyEnabled;
      case LeaderboardType.WEEKLY:
        return this._weeklyEnabled;
      case LeaderboardType.MONTHLY:
        return this._monthlyEnabled;
      default:
        return false;
    }
  }

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

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

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

  static reconstitute(data: {
    id: bigint;
    configKey: string;
    dailyEnabled: boolean;
    weeklyEnabled: boolean;
    monthlyEnabled: boolean;
    virtualRankingEnabled: boolean;
    virtualAccountCount: number;
    displayLimit: number;
    refreshIntervalMinutes: number;
  }): LeaderboardConfig {
    const config = new LeaderboardConfig(
      data.configKey,
      data.dailyEnabled,
      data.weeklyEnabled,
      data.monthlyEnabled,
      data.virtualRankingEnabled,
      data.virtualAccountCount,
      data.displayLimit,
      data.refreshIntervalMinutes,
    );
    config._id = data.id;
    return config;
  }
}

import { LeaderboardType } from '../../value-objects/leaderboard-type.enum';
```

### 3.4 实体 (Entities)

#### 3.4.1 src/domain/entities/virtual-account.entity.ts
```typescript
import { VirtualAccountType } from '../value-objects/virtual-account-type.enum';

/**
 * 虚拟账户实体
 */
export class VirtualAccount {
  private _id: bigint | null = null;
  private readonly _accountType: VirtualAccountType;
  private _displayName: string;
  private _avatar: string | null;
  private readonly _provinceCode: string | null;
  private readonly _cityCode: string | null;
  private _minScore: number | null;
  private _maxScore: number | null;
  private _currentScore: number;
  private _usdtBalance: number;
  private _hashpowerBalance: number;
  private _isActive: boolean;
  private readonly _createdAt: Date;

  private constructor(
    accountType: VirtualAccountType,
    displayName: string,
    avatar: string | null,
    provinceCode: string | null,
    cityCode: string | null,
    minScore: number | null,
    maxScore: number | null,
  ) {
    this._accountType = accountType;
    this._displayName = displayName;
    this._avatar = avatar;
    this._provinceCode = provinceCode;
    this._cityCode = cityCode;
    this._minScore = minScore;
    this._maxScore = maxScore;
    this._currentScore = 0;
    this._usdtBalance = 0;
    this._hashpowerBalance = 0;
    this._isActive = true;
    this._createdAt = new Date();
  }

  // ============ Getters ============
  get id(): bigint | null { return this._id; }
  get accountType(): VirtualAccountType { return this._accountType; }
  get displayName(): string { return this._displayName; }
  get avatar(): string | null { return this._avatar; }
  get provinceCode(): string | null { return this._provinceCode; }
  get cityCode(): string | null { return this._cityCode; }
  get minScore(): number | null { return this._minScore; }
  get maxScore(): number | null { return this._maxScore; }
  get currentScore(): number { return this._currentScore; }
  get usdtBalance(): number { return this._usdtBalance; }
  get hashpowerBalance(): number { return this._hashpowerBalance; }
  get isActive(): boolean { return this._isActive; }
  get createdAt(): Date { return this._createdAt; }

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

  /**
   * 创建排名虚拟账户
   */
  static createRankingVirtual(params: {
    displayName: string;
    avatar?: string;
    minScore: number;
    maxScore: number;
  }): VirtualAccount {
    return new VirtualAccount(
      VirtualAccountType.RANKING_VIRTUAL,
      params.displayName,
      params.avatar || null,
      null,
      null,
      params.minScore,
      params.maxScore,
    );
  }

  /**
   * 创建系统省公司账户
   */
  static createSystemProvince(provinceCode: string, provinceName: string): VirtualAccount {
    return new VirtualAccount(
      VirtualAccountType.SYSTEM_PROVINCE,
      `系统省公司-${provinceName}`,
      null,
      provinceCode,
      null,
      null,
      null,
    );
  }

  /**
   * 创建系统市公司账户
   */
  static createSystemCity(cityCode: string, cityName: string): VirtualAccount {
    return new VirtualAccount(
      VirtualAccountType.SYSTEM_CITY,
      `系统市公司-${cityName}`,
      null,
      null,
      cityCode,
      null,
      null,
    );
  }

  /**
   * 创建总部社区账户
   */
  static createHeadquarters(): VirtualAccount {
    return new VirtualAccount(
      VirtualAccountType.HEADQUARTERS,
      '总部社区',
      null,
      null,
      null,
      null,
      null,
    );
  }

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

  /**
   * 生成随机分值
   */
  generateRandomScore(): number {
    if (this._minScore === null || this._maxScore === null) {
      return 0;
    }
    this._currentScore = Math.floor(
      Math.random() * (this._maxScore - this._minScore + 1) + this._minScore
    );
    return this._currentScore;
  }

  /**
   * 增加余额
   */
  addBalance(usdtAmount: number, hashpowerAmount: number): void {
    this._usdtBalance += usdtAmount;
    this._hashpowerBalance += hashpowerAmount;
  }

  /**
   * 扣减余额
   */
  deductBalance(usdtAmount: number, hashpowerAmount: number): void {
    if (this._usdtBalance < usdtAmount) {
      throw new Error('USDT余额不足');
    }
    if (this._hashpowerBalance < hashpowerAmount) {
      throw new Error('算力余额不足');
    }
    this._usdtBalance -= usdtAmount;
    this._hashpowerBalance -= hashpowerAmount;
  }

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

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

  /**
   * 更新显示信息
   */
  updateDisplayInfo(displayName: string, avatar?: string): void {
    this._displayName = displayName;
    if (avatar !== undefined) {
      this._avatar = avatar;
    }
  }

  /**
   * 更新分值范围
   */
  updateScoreRange(minScore: number, maxScore: number): void {
    if (minScore > maxScore) {
      throw new Error('最小分值不能大于最大分值');
    }
    this._minScore = minScore;
    this._maxScore = maxScore;
  }

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

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

  static reconstitute(data: {
    id: bigint;
    accountType: VirtualAccountType;
    displayName: string;
    avatar: string | null;
    provinceCode: string | null;
    cityCode: string | null;
    minScore: number | null;
    maxScore: number | null;
    currentScore: number;
    usdtBalance: number;
    hashpowerBalance: number;
    isActive: boolean;
    createdAt: Date;
  }): VirtualAccount {
    const account = new VirtualAccount(
      data.accountType,
      data.displayName,
      data.avatar,
      data.provinceCode,
      data.cityCode,
      data.minScore,
      data.maxScore,
    );
    account._id = data.id;
    account._currentScore = data.currentScore;
    account._usdtBalance = data.usdtBalance;
    account._hashpowerBalance = data.hashpowerBalance;
    account._isActive = data.isActive;
    return account;
  }
}
```

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

#### 3.5.1 src/domain/repositories/leaderboard-ranking.repository.interface.ts
```typescript
import { LeaderboardRanking } from '../aggregates/leaderboard-ranking/leaderboard-ranking.aggregate';
import { LeaderboardType } from '../value-objects/leaderboard-type.enum';

export interface ILeaderboardRankingRepository {
  save(ranking: LeaderboardRanking): Promise<void>;
  saveAll(rankings: LeaderboardRanking[]): Promise<void>;
  findById(id: bigint): Promise<LeaderboardRanking | null>;

  findByTypeAndPeriod(
    type: LeaderboardType,
    periodKey: string,
    options?: {
      limit?: number;
      includeVirtual?: boolean;
    },
  ): Promise<LeaderboardRanking[]>;

  findUserRanking(
    type: LeaderboardType,
    periodKey: string,
    userId: bigint,
  ): Promise<LeaderboardRanking | null>;

  findUserPreviousRanking(
    type: LeaderboardType,
    userId: bigint,
  ): Promise<LeaderboardRanking | null>;

  deleteByTypeAndPeriod(
    type: LeaderboardType,
    periodKey: string,
  ): Promise<void>;

  countByTypeAndPeriod(
    type: LeaderboardType,
    periodKey: string,
  ): Promise<number>;
}

export const LEADERBOARD_RANKING_REPOSITORY = Symbol('ILeaderboardRankingRepository');
```

#### 3.5.2 src/domain/repositories/leaderboard-config.repository.interface.ts
```typescript
import { LeaderboardConfig } from '../aggregates/leaderboard-config/leaderboard-config.aggregate';

export interface ILeaderboardConfigRepository {
  save(config: LeaderboardConfig): Promise<void>;
  findByKey(configKey: string): Promise<LeaderboardConfig | null>;
  getGlobalConfig(): Promise<LeaderboardConfig>;
}

export const LEADERBOARD_CONFIG_REPOSITORY = Symbol('ILeaderboardConfigRepository');
```

#### 3.5.3 src/domain/repositories/virtual-account.repository.interface.ts
```typescript
import { VirtualAccount } from '../entities/virtual-account.entity';
import { VirtualAccountType } from '../value-objects/virtual-account-type.enum';

export interface IVirtualAccountRepository {
  save(account: VirtualAccount): Promise<void>;
  saveAll(accounts: VirtualAccount[]): Promise<void>;
  findById(id: bigint): Promise<VirtualAccount | null>;
  findByType(type: VirtualAccountType): Promise<VirtualAccount[]>;
  findActiveRankingVirtuals(limit: number): Promise<VirtualAccount[]>;
  findByProvinceCode(provinceCode: string): Promise<VirtualAccount | null>;
  findByCityCode(cityCode: string): Promise<VirtualAccount | null>;
  findHeadquarters(): Promise<VirtualAccount | null>;
  countByType(type: VirtualAccountType): Promise<number>;
  deleteById(id: bigint): Promise<void>;
}

export const VIRTUAL_ACCOUNT_REPOSITORY = Symbol('IVirtualAccountRepository');
```

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

#### 3.6.1 src/domain/services/leaderboard-calculation.service.ts
```typescript
import { Injectable, Inject } from '@nestjs/common';
import { LeaderboardRanking } from '../aggregates/leaderboard-ranking/leaderboard-ranking.aggregate';
import { LeaderboardType } from '../value-objects/leaderboard-type.enum';
import { LeaderboardPeriod } from '../value-objects/leaderboard-period.vo';
import { UserSnapshot } from '../value-objects/user-snapshot.vo';

// 外部服务接口（防腐层）
export interface IReferralServiceClient {
  getTeamStatisticsForLeaderboard(params: {
    periodStartAt: Date;
    periodEndAt: Date;
    limit: number;
  }): Promise<Array<{
    userId: bigint;
    totalTeamPlanting: number;
    maxDirectTeamPlanting: number;
    effectiveScore: number;
  }>>;
}

export interface IIdentityServiceClient {
  getUserSnapshots(userIds: bigint[]): Promise<Map<string, {
    userId: bigint;
    nickname: string;
    avatar: string | null;
    accountNo: string | null;
  }>>;
}

export const REFERRAL_SERVICE_CLIENT = Symbol('IReferralServiceClient');
export const IDENTITY_SERVICE_CLIENT = Symbol('IIdentityServiceClient');

@Injectable()
export class LeaderboardCalculationService {
  constructor(
    @Inject(REFERRAL_SERVICE_CLIENT)
    private readonly referralService: IReferralServiceClient,
    @Inject(IDENTITY_SERVICE_CLIENT)
    private readonly identityService: IIdentityServiceClient,
  ) {}

  /**
   * 计算龙虎榜排名
   */
  async calculateRankings(
    type: LeaderboardType,
    limit: number = 100,
  ): Promise<LeaderboardRanking[]> {
    const period = LeaderboardPeriod.current(type);

    // 1. 从 Referral Service 获取团队统计数据
    const teamStats = await this.referralService.getTeamStatisticsForLeaderboard({
      periodStartAt: period.startAt,
      periodEndAt: period.endAt,
      limit,
    });

    if (teamStats.length === 0) {
      return [];
    }

    // 2. 获取用户信息
    const userIds = teamStats.map(s => s.userId);
    const userSnapshots = await this.identityService.getUserSnapshots(userIds);

    // 3. 构建排名列表
    const rankings: LeaderboardRanking[] = [];

    for (let i = 0; i < teamStats.length; i++) {
      const stat = teamStats[i];
      const userInfo = userSnapshots.get(stat.userId.toString());

      if (!userInfo) continue;

      const ranking = LeaderboardRanking.createRealRanking({
        leaderboardType: type,
        period,
        userId: stat.userId,
        rankPosition: i + 1,
        displayPosition: i + 1,  // 初始显示排名等于实际排名
        previousRank: null,      // TODO: 从历史数据获取
        totalTeamPlanting: stat.totalTeamPlanting,
        maxDirectTeamPlanting: stat.maxDirectTeamPlanting,
        userSnapshot: UserSnapshot.create({
          userId: userInfo.userId,
          nickname: userInfo.nickname,
          avatar: userInfo.avatar,
          accountNo: userInfo.accountNo,
        }),
      });

      rankings.push(ranking);
    }

    return rankings;
  }
}
```

#### 3.6.2 src/domain/services/virtual-ranking-generator.service.ts
```typescript
import { Injectable, Inject } from '@nestjs/common';
import { VirtualAccount } from '../entities/virtual-account.entity';
import { LeaderboardRanking } from '../aggregates/leaderboard-ranking/leaderboard-ranking.aggregate';
import { LeaderboardType } from '../value-objects/leaderboard-type.enum';
import { LeaderboardPeriod } from '../value-objects/leaderboard-period.vo';
import {
  IVirtualAccountRepository,
  VIRTUAL_ACCOUNT_REPOSITORY,
} from '../repositories/virtual-account.repository.interface';

// 随机中文名字库
const CHINESE_SURNAMES = ['王', '李', '张', '刘', '陈', '杨', '赵', '黄', '周', '吴'];
const CHINESE_NAMES = ['伟', '芳', '娜', '敏', '静', '丽', '强', '磊', '洋', '勇'];

@Injectable()
export class VirtualRankingGeneratorService {
  constructor(
    @Inject(VIRTUAL_ACCOUNT_REPOSITORY)
    private readonly virtualAccountRepository: IVirtualAccountRepository,
  ) {}

  /**
   * 生成虚拟排名条目
   */
  async generateVirtualRankings(params: {
    type: LeaderboardType;
    count: number;
    topRealScore: number;
  }): Promise<LeaderboardRanking[]> {
    if (params.count <= 0) {
      return [];
    }

    const period = LeaderboardPeriod.current(params.type);

    // 1. 获取或创建虚拟账户
    let virtualAccounts = await this.virtualAccountRepository.findActiveRankingVirtuals(params.count);

    // 如果虚拟账户不足，创建新的
    if (virtualAccounts.length < params.count) {
      const needed = params.count - virtualAccounts.length;
      const newAccounts = await this.createVirtualAccounts(needed, params.topRealScore);
      await this.virtualAccountRepository.saveAll(newAccounts);
      virtualAccounts = [...virtualAccounts, ...newAccounts];
    }

    // 2. 生成虚拟排名
    const virtualRankings: LeaderboardRanking[] = [];

    // 虚拟账户的分值应该高于真实用户最高分
    const scoreBase = params.topRealScore + 100;

    for (let i = 0; i < params.count; i++) {
      const account = virtualAccounts[i];

      // 生成递减的分值
      const generatedScore = scoreBase + (params.count - i) * 50 + Math.floor(Math.random() * 30);

      const ranking = LeaderboardRanking.createVirtualRanking({
        leaderboardType: params.type,
        period,
        virtualAccountId: account.id!,
        displayPosition: i + 1,  // 虚拟账户占据前面的位置
        generatedScore,
        displayName: account.displayName,
        avatar: account.avatar,
      });

      virtualRankings.push(ranking);
    }

    return virtualRankings;
  }

  /**
   * 创建虚拟账户
   */
  private async createVirtualAccounts(count: number, baseScore: number): Promise<VirtualAccount[]> {
    const accounts: VirtualAccount[] = [];

    for (let i = 0; i < count; i++) {
      const displayName = this.generateRandomName();
      const avatar = this.generateRandomAvatar();

      const account = VirtualAccount.createRankingVirtual({
        displayName,
        avatar,
        minScore: baseScore,
        maxScore: baseScore + 500,
      });

      accounts.push(account);
    }

    return accounts;
  }

  /**
   * 生成随机中文名
   */
  private generateRandomName(): string {
    const surname = CHINESE_SURNAMES[Math.floor(Math.random() * CHINESE_SURNAMES.length)];
    const name1 = CHINESE_NAMES[Math.floor(Math.random() * CHINESE_NAMES.length)];
    const name2 = Math.random() > 0.5
      ? CHINESE_NAMES[Math.floor(Math.random() * CHINESE_NAMES.length)]
      : '';

    // 部分名字用 * 遮挡
    const maskedName = surname + '*' + (name2 || '*');
    return maskedName;
  }

  /**
   * 生成随机头像URL
   */
  private generateRandomAvatar(): string {
    const avatarId = Math.floor(Math.random() * 100) + 1;
    return `https://api.dicebear.com/7.x/avataaars/svg?seed=${avatarId}`;
  }
}
```

#### 3.6.3 src/domain/services/ranking-merger.service.ts
```typescript
import { Injectable } from '@nestjs/common';
import { LeaderboardRanking } from '../aggregates/leaderboard-ranking/leaderboard-ranking.aggregate';

@Injectable()
export class RankingMergerService {
  /**
   * 合并虚拟排名和真实排名
   *
   * 规则：
   * - 虚拟账户占据前面的位置
   * - 真实用户排名从虚拟账户数量+1开始
   */
  mergeRankings(
    virtualRankings: LeaderboardRanking[],
    realRankings: LeaderboardRanking[],
    displayLimit: number,
  ): LeaderboardRanking[] {
    const merged: LeaderboardRanking[] = [];
    const virtualCount = virtualRankings.length;

    // 1. 添加虚拟排名
    for (const virtual of virtualRankings) {
      if (virtual.displayPosition.value <= displayLimit) {
        merged.push(virtual);
      }
    }

    // 2. 调整真实用户的显示排名并添加
    for (const real of realRankings) {
      const newDisplayPosition = real.rankPosition.value + virtualCount;

      if (newDisplayPosition <= displayLimit) {
        real.updateDisplayPosition(newDisplayPosition);
        merged.push(real);
      }
    }

    // 3. 按显示排名排序
    merged.sort((a, b) => a.displayPosition.value - b.displayPosition.value);

    return merged;
  }

  /**
   * 仅获取真实用户排名（不含虚拟）
   */
  getRealRankingsOnly(
    rankings: LeaderboardRanking[],
    displayLimit: number,
  ): LeaderboardRanking[] {
    return rankings
      .filter(r => !r.isVirtual)
      .slice(0, displayLimit);
  }
}
```

---

## 领域不变式 (Domain Invariants)

```typescript
class LeaderboardContextInvariants {
  // 1. 同一榜单内排名必须唯一且连续
  static RANK_MUST_BE_UNIQUE_AND_CONTINUOUS =
    "同一榜单内排名必须唯一且连续(1,2,3...)"

  // 2. 虚拟账户不参与真实排名计算
  static VIRTUAL_ACCOUNTS_NOT_IN_REAL_RANKING =
    "虚拟账户仅用于显示，不参与真实排名计算"

  // 3. 龙虎榜分值计算规则
  static LEADERBOARD_SCORE_FORMULA =
    "龙虎榜分值 = 团队总认种量 - 最大单个直推团队认种量"

  // 4. 显示排名规则
  static DISPLAY_RANKING_RULE =
    "显示排名 = 真实排名 + 虚拟账户数量"

  // 5. 榜单周期规则
  static PERIOD_RULES =
    "日榜每日00:00重置，周榜每周一00:00重置，月榜每月1日00:00重置"
}
```

---

## API 端点设计

| 方法 | 路径 | 描述 | 认证 | 权限 |
|------|------|------|------|------|
| GET | `/health` | 健康检查 | 否 | - |
| GET | `/leaderboard` | 获取龙虎榜列表 | JWT | 用户 |
| GET | `/leaderboard/my-ranking` | 获取我的排名 | JWT | 用户 |
| GET | `/leaderboard/config` | 获取榜单配置 | JWT | 管理员 |
| PUT | `/leaderboard/config/switch` | 更新榜单开关 | JWT | 管理员 |
| PUT | `/leaderboard/config/virtual` | 更新虚拟排名设置 | JWT | 管理员 |
| PUT | `/leaderboard/config/display` | 更新显示设置 | JWT | 管理员 |
| POST | `/leaderboard/refresh` | 手动刷新榜单 | JWT | 管理员 |
| GET | `/virtual-accounts` | 获取虚拟账户列表 | JWT | 管理员 |
| POST | `/virtual-accounts/generate` | 生成虚拟账户 | JWT | 管理员 |

---

## 定时任务

### 榜单刷新调度器

```typescript
// application/schedulers/leaderboard-refresh.scheduler.ts

import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { LeaderboardApplicationService } from '../services/leaderboard-application.service';
import { LeaderboardType } from '../../domain/value-objects/leaderboard-type.enum';

@Injectable()
export class LeaderboardRefreshScheduler {
  constructor(
    private readonly leaderboardService: LeaderboardApplicationService,
  ) {}

  /**
   * 每5分钟刷新榜单
   */
  @Cron(CronExpression.EVERY_5_MINUTES)
  async refreshAllLeaderboards() {
    console.log('开始刷新龙虎榜...');

    for (const type of Object.values(LeaderboardType)) {
      try {
        await this.leaderboardService.refreshLeaderboard(type);
        console.log(`${type} 榜单刷新完成`);
      } catch (error) {
        console.error(`${type} 榜单刷新失败:`, error);
      }
    }

    console.log('龙虎榜刷新完成');
  }

  /**
   * 每日00:00保存日榜快照并重置
   */
  @Cron('0 0 0 * * *')
  async snapshotDailyLeaderboard() {
    console.log('保存日榜快照...');
    await this.leaderboardService.saveSnapshot(LeaderboardType.DAILY);
  }

  /**
   * 每周一00:00保存周榜快照并重置
   */
  @Cron('0 0 0 * * 1')
  async snapshotWeeklyLeaderboard() {
    console.log('保存周榜快照...');
    await this.leaderboardService.saveSnapshot(LeaderboardType.WEEKLY);
  }

  /**
   * 每月1日00:00保存月榜快照并重置
   */
  @Cron('0 0 0 1 * *')
  async snapshotMonthlyLeaderboard() {
    console.log('保存月榜快照...');
    await this.leaderboardService.saveSnapshot(LeaderboardType.MONTHLY);
  }
}
```

---

## 事件订阅 (Kafka Events)

### 订阅的事件

| Topic | 事件类型 | 触发条件 | 处理逻辑 |
|-------|---------|---------|---------|
| `referral.statistics.updated` | TeamStatisticsUpdatedEvent | 团队统计更新 | 标记榜单需要刷新 |

### 发布的事件

| Topic | 事件类型 | 触发条件 |
|-------|---------|---------|
| `leaderboard.refreshed` | LeaderboardRefreshedEvent | 榜单刷新完成 |
| `leaderboard.config.updated` | ConfigUpdatedEvent | 配置更新 |

---

## 与前端页面对应关系

### 龙虎榜页面

```
页面元素：
- 榜单切换标签: 日榜 | 周榜 | 月榜
- 排名列表: 显示排名、用户头像、用户昵称、龙虎榜分值
- 我的排名: 显示当前用户在各榜单的排名
- 榜单状态: 已启用/待开启

API调用：
- GET /leaderboard?type=DAILY&limit=30
- GET /leaderboard/my-ranking?type=DAILY
```

### 后台管理 - 龙虎榜配置页面

```
页面元素：
- 榜单开关: 日榜开关、周榜开关、月榜开关
- 虚拟排名设置: 启用/禁用、虚拟账户数量输入框
- 显示设置: 前端显示数量输入框
- 操作按钮: 保存配置、手动刷新榜单

API调用：
- GET /leaderboard/config
- PUT /leaderboard/config/switch
- PUT /leaderboard/config/virtual
- PUT /leaderboard/config/display
- POST /leaderboard/refresh
```

---

## 开发顺序建议

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

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

3. **Phase 3: 领域层**
   - 实现所有值对象
   - 实现聚合根 (LeaderboardRanking, LeaderboardConfig)
   - 实现实体 (VirtualAccount)
   - 实现领域事件
   - 实现领域服务
   - 编写单元测试

4. **Phase 4: 基础设施层**
   - 实现仓储 (Repository Implementations)
   - 实现外部服务客户端 (ReferralServiceClient, IdentityServiceClient)
   - 实现Redis缓存服务
   - 实现Kafka消费者和发布者

5. **Phase 5: 应用层**
   - 实现应用服务 (LeaderboardApplicationService)
   - 实现定时任务 (LeaderboardRefreshScheduler)
   - 实现Command/Query handlers

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

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

---

## 注意事项

1. **榜单数据来源**: 龙虎榜分值从 referral-service 的团队统计数据计算得出
2. **虚拟排名功能**: 仅用于显示目的，不影响实际奖励分配
3. **缓存策略**: 榜单数据缓存在 Redis 中，定时刷新
4. **周期计算**: 注意时区问题，统一使用服务器时区
5. **性能优化**: 大量用户时考虑分页和缓存
6. **数据一致性**: 榜单刷新时使用事务确保数据一致性
7. **历史快照**: 每个周期结束时保存完整快照，便于历史查询
