rwadurian/backend/services/referral-service/DEVELOPMENT_GUIDE.md

61 KiB
Raw Blame History

Referral Service 开发指导

项目概述

Referral Service 是 RWA 榴莲女皇平台的推荐团队微服务,负责管理推荐关系树、团队统计维护、龙虎榜分值计算、省市团队占比统计等功能。

核心职责

  • 推荐关系树构建(基于序列号/推荐码)
  • 团队统计数据维护
  • 上级查找(最近的授权省/市/社区)
  • 直推用户管理
  • 团队层级关系管理
  • 本省/本市认种占比计算
  • 龙虎榜分值计算

不负责

  • 用户账户管理Identity Context
  • 奖励计算Reward Context
  • 授权考核判定Authorization Context

技术栈

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

架构设计

参考 identity-service 的架构模式,严格保持一致性:

referral-service/
├── prisma/
│   ├── schema.prisma              # 数据库模型定义
│   └── migrations/                # 数据库迁移文件
│
├── src/
│   ├── api/                       # 🔵 Presentation Layer (表现层/API层)
│   │   ├── controllers/
│   │   │   ├── health.controller.ts
│   │   │   ├── referral.controller.ts
│   │   │   ├── team.controller.ts
│   │   │   └── leaderboard.controller.ts
│   │   ├── dto/
│   │   │   ├── request/
│   │   │   │   └── create-referral.dto.ts
│   │   │   └── response/
│   │   │       ├── referral-info.dto.ts
│   │   │       ├── team-statistics.dto.ts
│   │   │       ├── direct-referral.dto.ts
│   │   │       └── leaderboard.dto.ts
│   │   └── api.module.ts
│   │
│   ├── application/               # 🟢 Application Layer (应用层)
│   │   ├── commands/
│   │   │   ├── create-referral-relationship/
│   │   │   │   ├── create-referral-relationship.command.ts
│   │   │   │   └── create-referral-relationship.handler.ts
│   │   │   ├── update-team-statistics/
│   │   │   │   ├── update-team-statistics.command.ts
│   │   │   │   └── update-team-statistics.handler.ts
│   │   │   └── index.ts
│   │   ├── queries/
│   │   │   ├── get-my-referral-info/
│   │   │   │   ├── get-my-referral-info.query.ts
│   │   │   │   └── get-my-referral-info.handler.ts
│   │   │   ├── get-direct-referrals/
│   │   │   │   ├── get-direct-referrals.query.ts
│   │   │   │   └── get-direct-referrals.handler.ts
│   │   │   ├── get-team-statistics/
│   │   │   │   ├── get-team-statistics.query.ts
│   │   │   │   └── get-team-statistics.handler.ts
│   │   │   ├── get-referral-chain/
│   │   │   │   ├── get-referral-chain.query.ts
│   │   │   │   └── get-referral-chain.handler.ts
│   │   │   ├── get-leaderboard/
│   │   │   │   ├── get-leaderboard.query.ts
│   │   │   │   └── get-leaderboard.handler.ts
│   │   │   └── index.ts
│   │   ├── services/
│   │   │   └── referral-application.service.ts
│   │   └── application.module.ts
│   │
│   ├── domain/                    # 🟡 Domain Layer (领域层) - 核心业务逻辑
│   │   ├── aggregates/
│   │   │   ├── referral-relationship/
│   │   │   │   ├── referral-relationship.aggregate.ts
│   │   │   │   ├── referral-relationship.spec.ts
│   │   │   │   └── index.ts
│   │   │   └── team-statistics/
│   │   │       ├── team-statistics.aggregate.ts
│   │   │       ├── team-statistics.spec.ts
│   │   │       └── index.ts
│   │   ├── value-objects/
│   │   │   ├── referral-code.vo.ts
│   │   │   ├── referral-chain.vo.ts
│   │   │   ├── leaderboard-score.vo.ts
│   │   │   ├── province-city-distribution.vo.ts
│   │   │   ├── user-id.vo.ts
│   │   │   └── index.ts
│   │   ├── events/
│   │   │   ├── domain-event.base.ts
│   │   │   ├── referral-relationship-created.event.ts
│   │   │   ├── team-statistics-updated.event.ts
│   │   │   ├── leaderboard-score-changed.event.ts
│   │   │   └── index.ts
│   │   ├── repositories/
│   │   │   ├── referral-relationship.repository.interface.ts
│   │   │   ├── team-statistics.repository.interface.ts
│   │   │   └── index.ts
│   │   ├── services/
│   │   │   ├── referral-chain.service.ts
│   │   │   ├── leaderboard-calculator.service.ts
│   │   │   ├── team-aggregation.service.ts
│   │   │   └── index.ts
│   │   └── domain.module.ts
│   │
│   ├── infrastructure/            # 🔴 Infrastructure Layer (基础设施层)
│   │   ├── persistence/
│   │   │   ├── prisma/
│   │   │   │   └── prisma.service.ts
│   │   │   ├── mappers/
│   │   │   │   ├── referral-relationship.mapper.ts
│   │   │   │   └── team-statistics.mapper.ts
│   │   │   └── repositories/
│   │   │       ├── referral-relationship.repository.impl.ts
│   │   │       └── team-statistics.repository.impl.ts
│   │   ├── external/
│   │   │   ├── identity-service/
│   │   │   │   └── identity-service.client.ts
│   │   │   └── planting-service/
│   │   │       └── planting-service.client.ts
│   │   ├── kafka/
│   │   │   ├── event-consumer.controller.ts
│   │   │   ├── event-publisher.service.ts
│   │   │   └── kafka.module.ts
│   │   ├── redis/
│   │   │   ├── redis.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
│   │   ├── 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/                          # 测试
│   ├── unit/
│   ├── integration/
│   └── e2e/
│
├── .env.example
├── .env.development
├── .env.production
├── .eslintrc.js
├── .prettierrc
├── nest-cli.json
├── package.json
├── tsconfig.json
├── tsconfig.build.json
└── Dockerfile

第一阶段:项目初始化

1.1 创建 NestJS 项目

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

1.2 安装依赖

# 核心依赖
npm install @nestjs/config @nestjs/swagger @nestjs/jwt @nestjs/passport @nestjs/microservices
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 package.json 配置

{
  "name": "referral-service",
  "version": "1.0.0",
  "description": "RWA Referral & Team Context Service",
  "author": "RWA Team",
  "private": true,
  "license": "UNLICENSED",
  "prisma": {
    "schema": "prisma/schema.prisma",
    "seed": "ts-node prisma/seed.ts"
  },
  "scripts": {
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json",
    "prisma:generate": "prisma generate",
    "prisma:migrate": "prisma migrate dev",
    "prisma:migrate:prod": "prisma migrate deploy",
    "prisma:studio": "prisma studio"
  },
  "dependencies": {
    "@nestjs/axios": "^3.0.0",
    "@nestjs/common": "^10.0.0",
    "@nestjs/config": "^3.1.1",
    "@nestjs/core": "^10.0.0",
    "@nestjs/jwt": "^10.2.0",
    "@nestjs/microservices": "^10.0.0",
    "@nestjs/passport": "^10.0.0",
    "@nestjs/platform-express": "^10.0.0",
    "@nestjs/swagger": "^7.1.17",
    "@prisma/client": "^5.7.0",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.0",
    "ioredis": "^5.3.2",
    "kafkajs": "^2.2.4",
    "passport": "^0.7.0",
    "passport-jwt": "^4.0.1",
    "reflect-metadata": "^0.1.13",
    "rxjs": "^7.8.1",
    "uuid": "^9.0.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^10.0.0",
    "@nestjs/schematics": "^10.0.0",
    "@nestjs/testing": "^10.0.0",
    "@types/express": "^4.17.17",
    "@types/jest": "^29.5.2",
    "@types/node": "^20.3.1",
    "@types/passport-jwt": "^4.0.0",
    "@types/supertest": "^6.0.0",
    "@types/uuid": "^9.0.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.42.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.0",
    "jest": "^29.5.0",
    "prettier": "^3.0.0",
    "prisma": "^5.7.0",
    "source-map-support": "^0.5.21",
    "supertest": "^6.3.3",
    "ts-jest": "^29.1.0",
    "ts-loader": "^9.4.3",
    "ts-node": "^10.9.1",
    "tsconfig-paths": "^4.2.0",
    "typescript": "^5.1.3"
  },
  "jest": {
    "moduleFileExtensions": ["js", "json", "ts"],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": ["**/*.(t|j)s"],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node",
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/$1"
    }
  }
}

1.4 tsconfig.json 配置

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "ES2021",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"]
    },
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "strictBindCallApply": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true
  }
}

1.5 环境变量配置

创建 .env.development:

# 应用配置
NODE_ENV=development
PORT=3004
APP_NAME=referral-service

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

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

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

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

# 外部服务
IDENTITY_SERVICE_URL=http://localhost:3001
PLANTING_SERVICE_URL=http://localhost:3003

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

2.1 创建 prisma/schema.prisma

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

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

// ============================================
// 推荐关系表 (聚合根1)
// 记录用户与推荐人的关系,推荐关系一旦建立终生不可修改
// ============================================
model ReferralRelationship {
  id              BigInt   @id @default(autoincrement()) @map("relationship_id")
  userId          BigInt   @unique @map("user_id")

  // 推荐人信息
  referrerId      BigInt?  @map("referrer_id")          // 直接推荐人 (null = 无推荐人/根节点)
  rootUserId      BigInt?  @map("root_user_id")         // 顶级上级用户ID

  // 推荐码
  myReferralCode    String  @unique @map("my_referral_code") @db.VarChar(20)
  usedReferralCode  String? @map("used_referral_code") @db.VarChar(20)

  // 推荐链 (使用PostgreSQL数组类型最多存储10层上级)
  ancestorPath      BigInt[] @map("ancestor_path")      // [父节点, 祖父节点, ...] 从根到父的路径
  depth             Int      @default(0) @map("depth")  // 层级深度 (0=根节点)

  // 直推统计 (快速查询用,冗余存储)
  directReferralCount    Int @default(0) @map("direct_referral_count")
  activeDirectCount      Int @default(0) @map("active_direct_count")  // 已认种的直推人数

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  // 自引用关系 (方便查询推荐人)
  referrer        ReferralRelationship?  @relation("ReferrerToReferral", fields: [referrerId], references: [userId])
  directReferrals ReferralRelationship[] @relation("ReferrerToReferral")

  // 关联团队统计
  teamStatistics TeamStatistics?

  @@map("referral_relationships")
  @@index([referrerId], name: "idx_referrer")
  @@index([myReferralCode], name: "idx_my_referral_code")
  @@index([usedReferralCode], name: "idx_used_referral_code")
  @@index([rootUserId], name: "idx_root_user")
  @@index([depth], name: "idx_depth")
  @@index([createdAt], name: "idx_referral_created")
}

// ============================================
// 团队统计表 (聚合根2)
// 每个用户的团队认种统计数据,需要实时更新
// ============================================
model TeamStatistics {
  id      BigInt @id @default(autoincrement()) @map("statistics_id")
  userId  BigInt @unique @map("user_id")

  // === 注册统计 ===
  directReferralCount Int @default(0) @map("direct_referral_count")   // 直推注册数
  totalTeamCount      Int @default(0) @map("total_team_count")        // 团队总注册数

  // === 个人认种 ===
  selfPlantingCount      Int     @default(0) @map("self_planting_count")      // 自己认种数量
  selfPlantingAmount     Decimal @default(0) @map("self_planting_amount") @db.Decimal(20, 8)

  // === 团队认种 (包含自己和所有下级) ===
  directPlantingCount      Int     @default(0) @map("direct_planting_count")      // 直推认种数
  totalTeamPlantingCount   Int     @default(0) @map("total_team_planting_count")  // 团队总认种数
  totalTeamPlantingAmount  Decimal @default(0) @map("total_team_planting_amount") @db.Decimal(20, 8)

  // === 直推团队数据 (JSON存储每个直推的团队认种量) ===
  // 格式: [{ userId: bigint, personalCount: int, teamCount: int, amount: decimal }, ...]
  directTeamPlantingData Json @default("[]") @map("direct_team_planting_data")

  // === 龙虎榜相关 ===
  // 龙虎榜分值 = 团队总认种量 - 最大单个直推团队认种量
  maxSingleTeamPlantingCount        Int @default(0) @map("max_single_team_planting_count")
  effectivePlantingCountForRanking  Int @default(0) @map("effective_planting_count_for_ranking")

  // === 本省本市统计 (用于省市授权考核) ===
  ownProvinceTeamCount    Int     @default(0) @map("own_province_team_count")   // 自有团队本省认种
  ownCityTeamCount        Int     @default(0) @map("own_city_team_count")       // 自有团队本市认种
  provinceTeamPercentage  Decimal @default(0) @map("province_team_percentage") @db.Decimal(5, 2) // 本省占比
  cityTeamPercentage      Decimal @default(0) @map("city_team_percentage") @db.Decimal(5, 2)     // 本市占比

  // === 省市分布 (JSON存储详细分布) ===
  // 格式: { "provinceCode": { "cityCode": count, ... }, ... }
  provinceCityDistribution Json @default("{}") @map("province_city_distribution")

  // 时间戳
  lastCalcAt DateTime? @map("last_calc_at")  // 最后计算时间
  createdAt  DateTime  @default(now()) @map("created_at")
  updatedAt  DateTime  @updatedAt @map("updated_at")

  // 关联
  referralRelationship ReferralRelationship @relation(fields: [userId], references: [userId])

  @@map("team_statistics")
  @@index([effectivePlantingCountForRanking(sort: Desc)], name: "idx_leaderboard_score")
  @@index([totalTeamPlantingCount(sort: Desc)], name: "idx_team_planting")
  @@index([selfPlantingCount], name: "idx_self_planting")
}

// ============================================
// 直推用户列表 (冗余表,便于分页查询)
// ============================================
model DirectReferral {
  id          BigInt @id @default(autoincrement()) @map("direct_referral_id")
  referrerId  BigInt @map("referrer_id")      // 推荐人ID
  referralId  BigInt @map("referral_id")      // 被推荐人ID
  referralSequence BigInt @map("referral_sequence")  // 被推荐人序列号

  // 被推荐人信息快照 (冗余存储,避免跨服务查询)
  referralNickname    String? @map("referral_nickname") @db.VarChar(100)
  referralAvatar      String? @map("referral_avatar") @db.VarChar(255)

  // 该直推的认种统计
  personalPlantingCount   Int @default(0) @map("personal_planting_count")   // 个人认种数
  teamPlantingCount       Int @default(0) @map("team_planting_count")       // 团队认种数(含个人)

  // 是否已认种 (用于区分活跃/非活跃)
  hasPlanted          Boolean   @default(false) @map("has_planted")
  firstPlantedAt      DateTime? @map("first_planted_at")

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  @@unique([referrerId, referralId], name: "uk_referrer_referral")
  @@map("direct_referrals")
  @@index([referrerId], name: "idx_direct_referrer")
  @@index([referralId], name: "idx_direct_referral")
  @@index([hasPlanted], name: "idx_has_planted")
  @@index([teamPlantingCount(sort: Desc)], name: "idx_direct_team_planting")
}

// ============================================
// 团队省市分布表 (用于省市权益分配)
// ============================================
model TeamProvinceCityDetail {
  id            BigInt  @id @default(autoincrement()) @map("detail_id")
  userId        BigInt  @map("user_id")

  provinceCode  String  @map("province_code") @db.VarChar(10)
  cityCode      String  @map("city_code") @db.VarChar(10)

  teamPlantingCount Int @default(0) @map("team_planting_count")  // 该省/市团队认种数

  updatedAt DateTime @updatedAt @map("updated_at")

  @@unique([userId, provinceCode, cityCode], name: "uk_user_province_city")
  @@map("team_province_city_details")
  @@index([userId], name: "idx_detail_user")
  @@index([provinceCode], name: "idx_detail_province")
  @@index([cityCode], name: "idx_detail_city")
}

// ============================================
// 推荐事件表 (行为表append-only用于审计和事件溯源)
// ============================================
model ReferralEvent {
  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("referral_events")
  @@index([aggregateType, aggregateId], name: "idx_event_aggregate")
  @@index([eventType], name: "idx_event_type")
  @@index([userId], name: "idx_event_user")
  @@index([occurredAt], name: "idx_event_occurred")
}

2.2 初始化数据库

# 生成 Prisma Client
npx prisma generate

# 创建并运行迁移
npx prisma migrate dev --name init

# 查看数据库 (可选)
npx prisma studio

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

3.1 值对象 (Value Objects)

3.1.1 src/domain/value-objects/user-id.vo.ts

export class UserId {
  private constructor(public readonly value: bigint) {}

  static create(value: bigint | string | number): UserId {
    const bigIntValue = typeof value === 'bigint' ? value : BigInt(value);
    if (bigIntValue <= 0n) {
      throw new Error('用户ID必须大于0');
    }
    return new UserId(bigIntValue);
  }

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

  toString(): string {
    return this.value.toString();
  }
}

3.1.2 src/domain/value-objects/referral-code.vo.ts

export class ReferralCode {
  private constructor(public readonly value: string) {
    if (!value || value.length < 6 || value.length > 20) {
      throw new Error('推荐码长度必须在6-20个字符之间');
    }
    if (!/^[A-Z0-9]+$/.test(value)) {
      throw new Error('推荐码只能包含大写字母和数字');
    }
  }

  static create(value: string): ReferralCode {
    return new ReferralCode(value.toUpperCase());
  }

  static generate(userId: bigint): ReferralCode {
    // 生成规则: 前缀 + 用户ID哈希 + 随机字符
    const prefix = 'RWA';
    const userIdHash = userId.toString(36).toUpperCase().slice(-3).padStart(3, '0');
    const random = Math.random().toString(36).toUpperCase().slice(2, 6);
    return new ReferralCode(`${prefix}${userIdHash}${random}`);
  }

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

3.1.3 src/domain/value-objects/referral-chain.vo.ts

import { UserId } from './user-id.vo';

/**
 * 推荐链值对象
 * 存储从直接推荐人到根节点的用户ID列表
 */
export class ReferralChain {
  private static readonly MAX_DEPTH = 10;

  private constructor(
    public readonly chain: bigint[],  // [直接上级, 上上级, ...]
  ) {
    if (chain.length > ReferralChain.MAX_DEPTH) {
      throw new Error(`推荐链深度不能超过 ${ReferralChain.MAX_DEPTH}`);
    }
  }

  static create(referrerId: bigint | null, parentChain: bigint[] = []): ReferralChain {
    if (!referrerId) {
      return new ReferralChain([]);
    }

    // 新链 = [直接推荐人] + 父链的前 MAX_DEPTH - 1 个元素
    const newChain = [referrerId, ...parentChain.slice(0, this.MAX_DEPTH - 1)];
    return new ReferralChain(newChain);
  }

  static fromArray(chain: bigint[]): ReferralChain {
    return new ReferralChain([...chain]);
  }

  static empty(): ReferralChain {
    return new ReferralChain([]);
  }

  get depth(): number {
    return this.chain.length;
  }

  get directReferrer(): bigint | null {
    return this.chain[0] ?? null;
  }

  /**
   * 获取指定层级的推荐人
   * @param level 层级 (0 = 直接推荐人, 1 = 推荐人的推荐人, ...)
   */
  getReferrerAtLevel(level: number): bigint | null {
    return this.chain[level] ?? null;
  }

  /**
   * 获取所有上级 (用于团队统计更新)
   */
  getAllAncestors(): bigint[] {
    return [...this.chain];
  }

  toArray(): bigint[] {
    return [...this.chain];
  }
}

3.1.4 src/domain/value-objects/leaderboard-score.vo.ts

/**
 * 龙虎榜分值值对象
 *
 * 计算公式: 团队总认种量 - 最大单个直推团队认种量
 *
 * 这个公式的设计目的:
 * - 鼓励均衡发展团队,而不是只依赖单个大团队
 * - 防止"烧伤"现象(单腿发展)
 */
export class LeaderboardScore {
  private constructor(
    public readonly totalTeamCount: number,      // 团队总认种量
    public readonly maxDirectTeamCount: number,  // 最大直推团队认种量
    public readonly score: number,               // 龙虎榜分值
  ) {}

  static calculate(
    totalTeamCount: number,
    directTeamCounts: number[],  // 每个直推的团队认种量
  ): LeaderboardScore {
    const maxDirectTeamCount = directTeamCounts.length > 0
      ? Math.max(...directTeamCounts)
      : 0;
    const score = Math.max(0, totalTeamCount - maxDirectTeamCount);

    return new LeaderboardScore(totalTeamCount, maxDirectTeamCount, score);
  }

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

  /**
   * 当团队认种发生变化时重新计算
   */
  recalculate(
    newTotalTeamCount: number,
    newDirectTeamCounts: number[],
  ): LeaderboardScore {
    return LeaderboardScore.calculate(newTotalTeamCount, newDirectTeamCounts);
  }

  /**
   * 比较排名 (降序排列)
   */
  compareTo(other: LeaderboardScore): number {
    return other.score - this.score;
  }
}

3.1.5 src/domain/value-objects/province-city-distribution.vo.ts

export interface ProvinceCityCount {
  provinceCode: string;
  cityCode: string;
  count: number;
}

/**
 * 省市分布统计值对象
 * 用于计算用户团队在各省/市的认种分布
 */
export class ProvinceCityDistribution {
  private constructor(
    private readonly distribution: Map<string, Map<string, number>>,
  ) {}

  static empty(): ProvinceCityDistribution {
    return new ProvinceCityDistribution(new Map());
  }

  static fromJson(json: Record<string, Record<string, number>> | null): ProvinceCityDistribution {
    if (!json) {
      return ProvinceCityDistribution.empty();
    }

    const map = new Map<string, Map<string, number>>();
    for (const [province, cities] of Object.entries(json)) {
      const cityMap = new Map<string, number>();
      for (const [city, count] of Object.entries(cities)) {
        cityMap.set(city, count);
      }
      map.set(province, cityMap);
    }
    return new ProvinceCityDistribution(map);
  }

  /**
   * 添加认种记录 (返回新实例,不可变)
   */
  add(provinceCode: string, cityCode: string, count: number): ProvinceCityDistribution {
    const newDist = new Map(this.distribution);

    if (!newDist.has(provinceCode)) {
      newDist.set(provinceCode, new Map());
    }

    const cityMap = new Map(newDist.get(provinceCode)!);
    cityMap.set(cityCode, (cityMap.get(cityCode) ?? 0) + count);
    newDist.set(provinceCode, cityMap);

    return new ProvinceCityDistribution(newDist);
  }

  /**
   * 获取某省的总认种量
   */
  getProvinceTotal(provinceCode: string): number {
    const cities = this.distribution.get(provinceCode);
    if (!cities) return 0;

    let total = 0;
    for (const count of cities.values()) {
      total += count;
    }
    return total;
  }

  /**
   * 获取某市的总认种量
   */
  getCityTotal(provinceCode: string, cityCode: string): number {
    return this.distribution.get(provinceCode)?.get(cityCode) ?? 0;
  }

  /**
   * 获取所有省市的统计
   */
  getAll(): ProvinceCityCount[] {
    const result: ProvinceCityCount[] = [];
    for (const [provinceCode, cities] of this.distribution) {
      for (const [cityCode, count] of cities) {
        result.push({ provinceCode, cityCode, count });
      }
    }
    return result;
  }

  /**
   * 获取总数
   */
  getTotal(): number {
    let total = 0;
    for (const cities of this.distribution.values()) {
      for (const count of cities.values()) {
        total += count;
      }
    }
    return total;
  }

  toJson(): Record<string, Record<string, number>> {
    const result: Record<string, Record<string, number>> = {};
    for (const [province, cities] of this.distribution) {
      result[province] = {};
      for (const [city, count] of cities) {
        result[province][city] = count;
      }
    }
    return result;
  }
}

3.1.6 src/domain/value-objects/index.ts

export * from './user-id.vo';
export * from './referral-code.vo';
export * from './referral-chain.vo';
export * from './leaderboard-score.vo';
export * from './province-city-distribution.vo';

3.2 领域事件 (Domain Events)

3.2.1 src/domain/events/domain-event.base.ts

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/referral-relationship-created.event.ts

import { DomainEvent } from './domain-event.base';

export interface ReferralRelationshipCreatedPayload {
  userId: string;
  referrerId: string | null;
  myReferralCode: string;
  usedReferralCode: string | null;
  depth: number;
  ancestorPath: string[];
}

export class ReferralRelationshipCreatedEvent extends DomainEvent {
  constructor(private readonly payload: ReferralRelationshipCreatedPayload) {
    super();
  }

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

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

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

  toPayload(): ReferralRelationshipCreatedPayload {
    return { ...this.payload };
  }
}

3.2.3 src/domain/events/team-statistics-updated.event.ts

import { DomainEvent } from './domain-event.base';

export interface TeamStatisticsUpdatedPayload {
  userId: string;
  selfPlantingCount: number;
  teamPlantingCount: number;
  leaderboardScore: number;
  triggeredByUserId?: string;  // 触发更新的用户ID
}

export class TeamStatisticsUpdatedEvent extends DomainEvent {
  constructor(private readonly payload: TeamStatisticsUpdatedPayload) {
    super();
  }

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

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

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

  toPayload(): TeamStatisticsUpdatedPayload {
    return { ...this.payload };
  }
}

3.2.4 src/domain/events/index.ts

export * from './domain-event.base';
export * from './referral-relationship-created.event';
export * from './team-statistics-updated.event';

3.3 聚合根 (Aggregates)

3.3.1 src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts

import { ReferralCode } from '../../value-objects/referral-code.vo';
import { ReferralChain } from '../../value-objects/referral-chain.vo';
import { UserId } from '../../value-objects/user-id.vo';
import { DomainEvent } from '../../events/domain-event.base';
import { ReferralRelationshipCreatedEvent } from '../../events/referral-relationship-created.event';

/**
 * 推荐关系聚合根
 *
 * 不变式:
 * 1. 推荐关系一旦建立,终生不可修改
 * 2. 不能推荐自己
 * 3. 祖先路径必须完整 (从根节点到当前节点的所有上级)
 */
export class ReferralRelationship {
  private _id: bigint | null = null;
  private readonly _userId: UserId;
  private readonly _myReferralCode: ReferralCode;
  private readonly _usedReferralCode: ReferralCode | null;
  private readonly _referrerId: UserId | null;
  private readonly _rootUserId: UserId | null;
  private readonly _referralChain: ReferralChain;
  private _directReferralCount: number;
  private _activeDirectCount: number;
  private readonly _createdAt: Date;

  private _domainEvents: DomainEvent[] = [];

  private constructor(
    userId: UserId,
    myReferralCode: ReferralCode,
    usedReferralCode: ReferralCode | null,
    referrerId: UserId | null,
    rootUserId: UserId | null,
    referralChain: ReferralChain,
  ) {
    this._userId = userId;
    this._myReferralCode = myReferralCode;
    this._usedReferralCode = usedReferralCode;
    this._referrerId = referrerId;
    this._rootUserId = rootUserId;
    this._referralChain = referralChain;
    this._directReferralCount = 0;
    this._activeDirectCount = 0;
    this._createdAt = new Date();
  }

  // ============ Getters ============
  get id(): bigint | null { return this._id; }
  get userId(): UserId { return this._userId; }
  get myReferralCode(): ReferralCode { return this._myReferralCode; }
  get usedReferralCode(): ReferralCode | null { return this._usedReferralCode; }
  get referrerId(): UserId | null { return this._referrerId; }
  get rootUserId(): UserId | null { return this._rootUserId; }
  get referralChain(): ReferralChain { return this._referralChain; }
  get depth(): number { return this._referralChain.depth; }
  get directReferralCount(): number { return this._directReferralCount; }
  get activeDirectCount(): number { return this._activeDirectCount; }
  get createdAt(): Date { return this._createdAt; }
  get domainEvents(): DomainEvent[] { return [...this._domainEvents]; }

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

  /**
   * 创建推荐关系 (有推荐人)
   */
  static createWithReferrer(
    userId: bigint,
    referrer: ReferralRelationship,
    usedReferralCode: string,
  ): ReferralRelationship {
    const userIdVo = UserId.create(userId);

    // 验证不能推荐自己
    if (userIdVo.equals(referrer.userId)) {
      throw new Error('不能推荐自己');
    }

    const myCode = ReferralCode.generate(userId);
    const usedCode = ReferralCode.create(usedReferralCode);

    // 构建推荐链
    const referralChain = ReferralChain.create(
      referrer.userId.value,
      referrer.referralChain.toArray(),
    );

    // 确定根节点
    const rootUserId = referrer.rootUserId || referrer.userId;

    const relationship = new ReferralRelationship(
      userIdVo,
      myCode,
      usedCode,
      referrer.userId,
      rootUserId,
      referralChain,
    );

    // 发布领域事件
    relationship._domainEvents.push(new ReferralRelationshipCreatedEvent({
      userId: userId.toString(),
      referrerId: referrer.userId.toString(),
      myReferralCode: myCode.value,
      usedReferralCode: usedCode.value,
      depth: referralChain.depth,
      ancestorPath: referralChain.toArray().map(id => id.toString()),
    }));

    return relationship;
  }

  /**
   * 创建推荐关系 (无推荐人,根节点)
   */
  static createRoot(userId: bigint): ReferralRelationship {
    const userIdVo = UserId.create(userId);
    const myCode = ReferralCode.generate(userId);

    const relationship = new ReferralRelationship(
      userIdVo,
      myCode,
      null,
      null,
      null,
      ReferralChain.empty(),
    );

    // 发布领域事件
    relationship._domainEvents.push(new ReferralRelationshipCreatedEvent({
      userId: userId.toString(),
      referrerId: null,
      myReferralCode: myCode.value,
      usedReferralCode: null,
      depth: 0,
      ancestorPath: [],
    }));

    return relationship;
  }

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

  /**
   * 增加直推人数
   */
  incrementDirectReferralCount(): void {
    this._directReferralCount++;
  }

  /**
   * 直推用户认种后,更新活跃直推数
   */
  markDirectAsActive(): void {
    this._activeDirectCount++;
  }

  /**
   * 获取所有上级用户ID (用于更新团队统计)
   */
  getAllAncestorIds(): bigint[] {
    return this._referralChain.getAllAncestors();
  }

  /**
   * 获取直接推荐人ID
   */
  getDirectReferrerId(): bigint | null {
    return this._referrerId?.value ?? null;
  }

  /**
   * 设置ID (用于从数据库重建)
   */
  setId(id: bigint): void {
    this._id = id;
  }

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

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

  /**
   * 从数据库重建聚合
   */
  static reconstitute(data: {
    id: bigint;
    userId: bigint;
    myReferralCode: string;
    usedReferralCode: string | null;
    referrerId: bigint | null;
    rootUserId: bigint | null;
    ancestorPath: bigint[];
    directReferralCount: number;
    activeDirectCount: number;
    createdAt: Date;
  }): ReferralRelationship {
    const relationship = new ReferralRelationship(
      UserId.create(data.userId),
      ReferralCode.create(data.myReferralCode),
      data.usedReferralCode ? ReferralCode.create(data.usedReferralCode) : null,
      data.referrerId ? UserId.create(data.referrerId) : null,
      data.rootUserId ? UserId.create(data.rootUserId) : null,
      ReferralChain.fromArray(data.ancestorPath),
    );
    relationship._id = data.id;
    relationship._directReferralCount = data.directReferralCount;
    relationship._activeDirectCount = data.activeDirectCount;
    return relationship;
  }
}

3.3.2 src/domain/aggregates/team-statistics/team-statistics.aggregate.ts

import { UserId } from '../../value-objects/user-id.vo';
import { LeaderboardScore } from '../../value-objects/leaderboard-score.vo';
import { ProvinceCityDistribution } from '../../value-objects/province-city-distribution.vo';
import { DomainEvent } from '../../events/domain-event.base';
import { TeamStatisticsUpdatedEvent } from '../../events/team-statistics-updated.event';

interface DirectTeamData {
  userId: bigint;
  personalCount: number;
  teamCount: number;
  amount: number;
}

/**
 * 团队统计聚合根
 *
 * 不变式:
 * 1. 团队统计必须在认种后实时更新
 * 2. 龙虎榜分值 = 团队总认种量 - 最大单个直推团队认种量
 */
export class TeamStatistics {
  private _id: bigint | null = null;
  private readonly _userId: UserId;

  // 注册统计
  private _directReferralCount: number;
  private _totalTeamCount: number;

  // 个人认种
  private _selfPlantingCount: number;
  private _selfPlantingAmount: number;

  // 团队认种
  private _directPlantingCount: number;
  private _totalTeamPlantingCount: number;
  private _totalTeamPlantingAmount: number;

  // 直推团队数据
  private _directTeamPlantingData: DirectTeamData[];

  // 龙虎榜
  private _leaderboardScore: LeaderboardScore;

  // 本省本市统计
  private _ownProvinceTeamCount: number;
  private _ownCityTeamCount: number;
  private _provinceTeamPercentage: number;
  private _cityTeamPercentage: number;

  // 省市分布
  private _provinceCityDistribution: ProvinceCityDistribution;

  private _lastCalcAt: Date | null;
  private readonly _createdAt: Date;

  private _domainEvents: DomainEvent[] = [];

  private constructor(userId: UserId) {
    this._userId = userId;
    this._directReferralCount = 0;
    this._totalTeamCount = 0;
    this._selfPlantingCount = 0;
    this._selfPlantingAmount = 0;
    this._directPlantingCount = 0;
    this._totalTeamPlantingCount = 0;
    this._totalTeamPlantingAmount = 0;
    this._directTeamPlantingData = [];
    this._leaderboardScore = LeaderboardScore.zero();
    this._ownProvinceTeamCount = 0;
    this._ownCityTeamCount = 0;
    this._provinceTeamPercentage = 0;
    this._cityTeamPercentage = 0;
    this._provinceCityDistribution = ProvinceCityDistribution.empty();
    this._lastCalcAt = null;
    this._createdAt = new Date();
  }

  // ============ Getters ============
  get id(): bigint | null { return this._id; }
  get userId(): UserId { return this._userId; }
  get directReferralCount(): number { return this._directReferralCount; }
  get totalTeamCount(): number { return this._totalTeamCount; }
  get selfPlantingCount(): number { return this._selfPlantingCount; }
  get selfPlantingAmount(): number { return this._selfPlantingAmount; }
  get directPlantingCount(): number { return this._directPlantingCount; }
  get totalTeamPlantingCount(): number { return this._totalTeamPlantingCount; }
  get totalTeamPlantingAmount(): number { return this._totalTeamPlantingAmount; }
  get directTeamPlantingData(): ReadonlyArray<DirectTeamData> { return this._directTeamPlantingData; }
  get leaderboardScore(): LeaderboardScore { return this._leaderboardScore; }
  get maxSingleTeamPlantingCount(): number { return this._leaderboardScore.maxDirectTeamCount; }
  get effectivePlantingCountForRanking(): number { return this._leaderboardScore.score; }
  get ownProvinceTeamCount(): number { return this._ownProvinceTeamCount; }
  get ownCityTeamCount(): number { return this._ownCityTeamCount; }
  get provinceTeamPercentage(): number { return this._provinceTeamPercentage; }
  get cityTeamPercentage(): number { return this._cityTeamPercentage; }
  get provinceCityDistribution(): ProvinceCityDistribution { return this._provinceCityDistribution; }
  get lastCalcAt(): Date | null { return this._lastCalcAt; }
  get createdAt(): Date { return this._createdAt; }
  get domainEvents(): DomainEvent[] { return [...this._domainEvents]; }

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

  static create(userId: bigint): TeamStatistics {
    return new TeamStatistics(UserId.create(userId));
  }

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

  /**
   * 增加直推注册
   */
  addDirectReferral(referralUserId: bigint): void {
    this._directReferralCount++;
    this._totalTeamCount++;

    // 初始化直推团队数据
    if (!this._directTeamPlantingData.find(d => d.userId === referralUserId)) {
      this._directTeamPlantingData.push({
        userId: referralUserId,
        personalCount: 0,
        teamCount: 0,
        amount: 0,
      });
    }

    this._lastCalcAt = new Date();
  }

  /**
   * 增加团队成员 (非直推)
   */
  addTeamMember(): void {
    this._totalTeamCount++;
    this._lastCalcAt = new Date();
  }

  /**
   * 记录自己的认种
   */
  addSelfPlanting(
    treeCount: number,
    amount: number,
    provinceCode: string,
    cityCode: string,
    userProvince?: string,
    userCity?: string,
  ): void {
    this._selfPlantingCount += treeCount;
    this._selfPlantingAmount += amount;

    // 自己的认种也计入团队
    this._totalTeamPlantingCount += treeCount;
    this._totalTeamPlantingAmount += amount;

    // 更新省市分布
    this._provinceCityDistribution = this._provinceCityDistribution.add(
      provinceCode,
      cityCode,
      treeCount,
    );

    // 更新本省本市统计
    if (userProvince && provinceCode === userProvince) {
      this._ownProvinceTeamCount += treeCount;
    }
    if (userCity && cityCode === userCity) {
      this._ownCityTeamCount += treeCount;
    }

    // 重新计算龙虎榜分值和占比
    this._recalculateLeaderboardScore();
    this._recalculatePercentages();
    this._lastCalcAt = new Date();

    this._domainEvents.push(new TeamStatisticsUpdatedEvent({
      userId: this._userId.toString(),
      selfPlantingCount: this._selfPlantingCount,
      teamPlantingCount: this._totalTeamPlantingCount,
      leaderboardScore: this._leaderboardScore.score,
    }));
  }

  /**
   * 更新团队认种数据 (下级用户认种时调用)
   */
  updatePlanting(params: {
    isDirectReferral: boolean;
    directReferralId?: bigint;
    treeCount: number;
    amount: number;
    province: string;
    city: string;
    userProvince?: string;
    userCity?: string;
  }): void {
    // 1. 更新团队总认种
    this._totalTeamPlantingCount += params.treeCount;
    this._totalTeamPlantingAmount += params.amount;

    // 2. 如果是直推
    if (params.isDirectReferral && params.directReferralId) {
      this._directPlantingCount += params.treeCount;

      // 更新直推明细
      const directTeam = this._directTeamPlantingData.find(
        d => d.userId === params.directReferralId
      );
      if (directTeam) {
        directTeam.personalCount += params.treeCount;
        directTeam.teamCount += params.treeCount;
        directTeam.amount += params.amount;
      }
    } else if (params.directReferralId) {
      // 非直推但需要更新直推的团队数据
      const directTeam = this._directTeamPlantingData.find(
        d => d.userId === params.directReferralId
      );
      if (directTeam) {
        directTeam.teamCount += params.treeCount;
        directTeam.amount += params.amount;
      }
    }

    // 3. 更新省市分布
    this._provinceCityDistribution = this._provinceCityDistribution.add(
      params.province,
      params.city,
      params.treeCount,
    );

    // 4. 更新本省本市统计
    if (params.userProvince && params.province === params.userProvince) {
      this._ownProvinceTeamCount += params.treeCount;
    }
    if (params.userCity && params.city === params.userCity) {
      this._ownCityTeamCount += params.treeCount;
    }

    // 5. 重新计算龙虎榜分值和占比
    this._recalculateLeaderboardScore();
    this._recalculatePercentages();
    this._lastCalcAt = new Date();

    this._domainEvents.push(new TeamStatisticsUpdatedEvent({
      userId: this._userId.toString(),
      selfPlantingCount: this._selfPlantingCount,
      teamPlantingCount: this._totalTeamPlantingCount,
      leaderboardScore: this._leaderboardScore.score,
      triggeredByUserId: params.directReferralId?.toString(),
    }));
  }

  /**
   * 更新直推团队认种数
   */
  updateDirectReferralTeamPlanting(
    directReferralId: bigint,
    teamPlantingCount: number,
  ): void {
    const directTeam = this._directTeamPlantingData.find(
      d => d.userId === directReferralId
    );
    if (directTeam) {
      directTeam.teamCount = teamPlantingCount;
    }

    this._recalculateLeaderboardScore();
    this._lastCalcAt = new Date();
  }

  // ============ 私有方法 ============

  private _recalculateLeaderboardScore(): void {
    const directCounts = this._directTeamPlantingData.map(d => d.teamCount);
    this._leaderboardScore = LeaderboardScore.calculate(
      this._totalTeamPlantingCount,
      directCounts,
    );
  }

  private _recalculatePercentages(): void {
    if (this._totalTeamPlantingCount > 0) {
      this._provinceTeamPercentage =
        (this._ownProvinceTeamCount / this._totalTeamPlantingCount) * 100;
      this._cityTeamPercentage =
        (this._ownCityTeamCount / this._totalTeamPlantingCount) * 100;
    } else {
      this._provinceTeamPercentage = 0;
      this._cityTeamPercentage = 0;
    }
  }

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

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

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

  static reconstitute(data: {
    id: bigint;
    userId: bigint;
    directReferralCount: number;
    totalTeamCount: number;
    selfPlantingCount: number;
    selfPlantingAmount: number;
    directPlantingCount: number;
    totalTeamPlantingCount: number;
    totalTeamPlantingAmount: number;
    directTeamPlantingData: DirectTeamData[];
    ownProvinceTeamCount: number;
    ownCityTeamCount: number;
    provinceCityDistribution: Record<string, Record<string, number>> | null;
    lastCalcAt: Date | null;
    createdAt: Date;
  }): TeamStatistics {
    const stats = new TeamStatistics(UserId.create(data.userId));
    stats._id = data.id;
    stats._directReferralCount = data.directReferralCount;
    stats._totalTeamCount = data.totalTeamCount;
    stats._selfPlantingCount = data.selfPlantingCount;
    stats._selfPlantingAmount = data.selfPlantingAmount;
    stats._directPlantingCount = data.directPlantingCount;
    stats._totalTeamPlantingCount = data.totalTeamPlantingCount;
    stats._totalTeamPlantingAmount = data.totalTeamPlantingAmount;
    stats._directTeamPlantingData = data.directTeamPlantingData || [];
    stats._ownProvinceTeamCount = data.ownProvinceTeamCount;
    stats._ownCityTeamCount = data.ownCityTeamCount;
    stats._provinceCityDistribution = ProvinceCityDistribution.fromJson(
      data.provinceCityDistribution
    );
    stats._lastCalcAt = data.lastCalcAt;

    // 重新计算龙虎榜分值和占比
    stats._recalculateLeaderboardScore();
    stats._recalculatePercentages();

    return stats;
  }
}

3.4 仓储接口 (Repository Interfaces)

3.4.1 src/domain/repositories/referral-relationship.repository.interface.ts

import { ReferralRelationship } from '../aggregates/referral-relationship/referral-relationship.aggregate';

export interface IReferralRelationshipRepository {
  save(relationship: ReferralRelationship): Promise<void>;
  findById(id: bigint): Promise<ReferralRelationship | null>;
  findByUserId(userId: bigint): Promise<ReferralRelationship | null>;
  findByReferralCode(code: string): Promise<ReferralRelationship | null>;
  findDirectReferrals(userId: bigint, page?: number, pageSize?: number): Promise<ReferralRelationship[]>;
  countDirectReferrals(userId: bigint): Promise<number>;
  findByReferrerId(referrerId: bigint): Promise<ReferralRelationship[]>;
  findByAncestor(ancestorId: bigint): Promise<ReferralRelationship[]>;
}

export const REFERRAL_RELATIONSHIP_REPOSITORY = Symbol('IReferralRelationshipRepository');

3.4.2 src/domain/repositories/team-statistics.repository.interface.ts

import { TeamStatistics } from '../aggregates/team-statistics/team-statistics.aggregate';

export interface ITeamStatisticsRepository {
  save(stats: TeamStatistics): Promise<void>;
  findByUserId(userId: bigint): Promise<TeamStatistics | null>;
  getOrCreate(userId: bigint): Promise<TeamStatistics>;
  findTopByLeaderboardScore(limit: number): Promise<TeamStatistics[]>;
  findByProvinceCityRanking(
    provinceCode: string,
    cityCode: string | null,
    limit: number,
  ): Promise<TeamStatistics[]>;
  findByUserIds(userIds: bigint[]): Promise<Map<string, TeamStatistics>>;
}

export const TEAM_STATISTICS_REPOSITORY = Symbol('ITeamStatisticsRepository');

3.4.3 src/domain/repositories/index.ts

export * from './referral-relationship.repository.interface';
export * from './team-statistics.repository.interface';

3.5 领域服务 (Domain Services)

3.5.1 src/domain/services/referral-chain.service.ts

import { Injectable } from '@nestjs/common';
import { ReferralRelationship } from '../aggregates/referral-relationship/referral-relationship.aggregate';

@Injectable()
export class ReferralChainService {
  /**
   * 获取用户的推荐链 (用于分享权益分配)
   * 返回从直接推荐人到最远祖先的用户ID列表
   */
  getReferralChain(relationship: ReferralRelationship): bigint[] {
    return relationship.getAllAncestorIds();
  }

  /**
   * 查找两个用户的最近公共祖先
   */
  findCommonAncestor(
    chain1: bigint[],
    chain2: bigint[],
  ): bigint | null {
    const set1 = new Set(chain1.map(id => id.toString()));
    for (const id of chain2) {
      if (set1.has(id.toString())) {
        return id;
      }
    }
    return null;
  }

  /**
   * 计算两个用户之间的推荐距离
   */
  calculateDistance(
    relationship1: ReferralRelationship,
    relationship2: ReferralRelationship,
  ): number | null {
    const chain1 = [relationship1.userId.value, ...relationship1.getAllAncestorIds()];
    const chain2 = [relationship2.userId.value, ...relationship2.getAllAncestorIds()];

    // 检查 user2 是否在 user1 的链上
    const idx1 = chain1.findIndex(id => id === relationship2.userId.value);
    if (idx1 >= 0) return idx1;

    // 检查 user1 是否在 user2 的链上
    const idx2 = chain2.findIndex(id => id === relationship1.userId.value);
    if (idx2 >= 0) return idx2;

    // 寻找公共祖先
    for (let i = 0; i < chain1.length; i++) {
      const idx = chain2.findIndex(id => id === chain1[i]);
      if (idx >= 0) {
        return i + idx;
      }
    }

    return null;  // 不在同一棵树上
  }
}

3.5.2 src/domain/services/leaderboard-calculator.service.ts

import { Injectable } from '@nestjs/common';
import { TeamStatistics } from '../aggregates/team-statistics/team-statistics.aggregate';

export interface LeaderboardEntry {
  userId: bigint;
  score: number;
  teamPlantingCount: number;
  maxDirectTeamCount: number;
  rank: number;
}

@Injectable()
export class LeaderboardCalculatorService {
  /**
   * 计算龙虎榜排名
   *
   * 龙虎榜分值 = 团队总认种量 - 最大单个直推团队认种量
   */
  calculateLeaderboard(
    allStats: TeamStatistics[],
    limit = 100,
  ): LeaderboardEntry[] {
    // 按龙虎榜分值排序
    const sorted = allStats
      .filter(s => s.effectivePlantingCountForRanking > 0)
      .sort((a, b) => b.effectivePlantingCountForRanking - a.effectivePlantingCountForRanking);

    // 取前 limit 名并添加排名
    return sorted.slice(0, limit).map((stats, index) => ({
      userId: stats.userId.value,
      score: stats.effectivePlantingCountForRanking,
      teamPlantingCount: stats.totalTeamPlantingCount,
      maxDirectTeamCount: stats.maxSingleTeamPlantingCount,
      rank: index + 1,
    }));
  }

  /**
   * 获取用户在龙虎榜中的排名
   */
  getUserRank(
    allStats: TeamStatistics[],
    userId: bigint,
  ): number | null {
    const sorted = allStats
      .filter(s => s.effectivePlantingCountForRanking > 0)
      .sort((a, b) => b.effectivePlantingCountForRanking - a.effectivePlantingCountForRanking);

    const index = sorted.findIndex(s => s.userId.value === userId);
    return index >= 0 ? index + 1 : null;
  }
}

3.5.3 src/domain/services/team-aggregation.service.ts

import { Injectable, Inject } from '@nestjs/common';
import {
  ITeamStatisticsRepository,
  TEAM_STATISTICS_REPOSITORY
} from '../repositories/team-statistics.repository.interface';
import {
  IReferralRelationshipRepository,
  REFERRAL_RELATIONSHIP_REPOSITORY
} from '../repositories/referral-relationship.repository.interface';
import { TeamStatistics } from '../aggregates/team-statistics/team-statistics.aggregate';

@Injectable()
export class TeamAggregationService {
  constructor(
    @Inject(TEAM_STATISTICS_REPOSITORY)
    private readonly statsRepository: ITeamStatisticsRepository,
    @Inject(REFERRAL_RELATIONSHIP_REPOSITORY)
    private readonly relationshipRepository: IReferralRelationshipRepository,
  ) {}

  /**
   * 当用户认种时,更新所有上级的团队统计
   */
  async updateAncestorTeamStats(
    userId: bigint,
    treeCount: number,
    amount: number,
    provinceCode: string,
    cityCode: string,
    userProvince?: string,
    userCity?: string,
  ): Promise<void> {
    // 1. 获取用户的推荐关系
    const relationship = await this.relationshipRepository.findByUserId(userId);
    if (!relationship) {
      throw new Error(`用户 ${userId} 的推荐关系不存在`);
    }

    // 2. 更新自己的统计
    const selfStats = await this.statsRepository.getOrCreate(userId);
    selfStats.addSelfPlanting(treeCount, amount, provinceCode, cityCode, userProvince, userCity);
    await this.statsRepository.save(selfStats);

    // 3. 获取推荐链 (所有上级)
    const ancestors = relationship.getAllAncestorIds();
    if (ancestors.length === 0) return;

    // 4. 找到直接推荐人 (第一层上级)
    const directReferrerId = ancestors[0];

    // 5. 更新所有上级的团队统计
    for (let i = 0; i < ancestors.length; i++) {
      const ancestorId = ancestors[i];
      const ancestorStats = await this.statsRepository.getOrCreate(ancestorId);

      // 对于每个祖先,确定这个认种是通过哪个直推传递来的
      const directUserIdForAncestor = i === 0 ? userId : ancestors[i - 1];

      ancestorStats.updatePlanting({
        isDirectReferral: i === 0,  // 只有第一层是直推
        directReferralId: directUserIdForAncestor,
        treeCount,
        amount,
        province: provinceCode,
        city: cityCode,
        userProvince,
        userCity,
      });

      await this.statsRepository.save(ancestorStats);
    }
  }

  /**
   * 当新用户注册时,更新所有上级的团队人数
   */
  async updateAncestorTeamMemberCount(
    userId: bigint,
    directReferrerId: bigint,
  ): Promise<void> {
    // 1. 更新直接推荐人
    const directReferrerStats = await this.statsRepository.getOrCreate(directReferrerId);
    directReferrerStats.addDirectReferral(userId);
    await this.statsRepository.save(directReferrerStats);

    // 2. 获取直接推荐人的推荐关系
    const directReferrerRelationship = await this.relationshipRepository.findByUserId(directReferrerId);
    if (!directReferrerRelationship) return;

    // 3. 更新所有上级的团队人数
    const ancestors = directReferrerRelationship.getAllAncestorIds();
    for (const ancestorId of ancestors) {
      const ancestorStats = await this.statsRepository.getOrCreate(ancestorId);
      ancestorStats.addTeamMember();
      await this.statsRepository.save(ancestorStats);
    }
  }
}

3.5.4 src/domain/services/index.ts

export * from './referral-chain.service';
export * from './leaderboard-calculator.service';
export * from './team-aggregation.service';

领域不变式 (Domain Invariants)

// 文档参考,不需要创建此文件
class ReferralContextInvariants {
  // 1. 推荐关系不可修改
  static REFERRAL_RELATIONSHIP_IMMUTABLE =
    "推荐关系一旦建立,终生不可修改"

  // 2. 祖先路径必须完整
  static ANCESTOR_PATH_COMPLETE =
    "祖先路径必须包含从根节点到当前节点的所有上级"

  // 3. 不能推荐自己
  static CANNOT_REFER_SELF =
    "用户不能推荐自己"

  // 4. 团队统计必须实时更新
  static TEAM_STATS_REALTIME =
    "团队统计数据必须在认种后实时更新"

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

  // 6. 推荐链最多10层
  static MAX_REFERRAL_DEPTH =
    "推荐链深度限制为10层超过的不计入"
}

API 端点设计

方法 路径 描述 认证
GET /health 健康检查
GET /referrals/me 获取我的推荐信息 (分享页) JWT
GET /referrals/direct 获取我的直推列表 JWT
GET /referrals/chain 获取推荐链 (内部API用于资金分配) JWT
GET /team/statistics 获取我的团队统计 JWT
GET /team/province-city-ranking 获取省/市团队排名 JWT
GET /leaderboard 获取龙虎榜 JWT
POST /internal/referral 创建推荐关系 (内部调用) API-KEY

与前端页面对应关系

向导页5 (分享引导页)

  • 显示用户的推荐码
  • 显示分享链接/二维码
  • 调用: GET /referrals/me

分享页

  • 显示我的推荐码和分享链接
  • 显示直推人数、团队人数
  • 显示团队认种统计
  • 调用: GET /referrals/me
  • 调用: GET /team/statistics

直推列表

  • 显示我的直推用户列表
  • 显示每个直推的认种情况
  • 调用: GET /referrals/direct?page=1&pageSize=20

龙虎榜

  • 显示全平台龙虎榜排名
  • 调用: GET /leaderboard?limit=100

事件订阅 (Kafka Events)

订阅的事件

Topic 事件类型 触发条件 处理逻辑
identity.user.created UserAccountCreatedEvent 用户注册成功 创建推荐关系,更新上级团队人数
planting.order.paid PlantingOrderPaidEvent 认种订单支付成功 更新认种用户及所有上级的团队统计

发布的事件

Topic 事件类型 触发条件
referral.relationship.created ReferralRelationshipCreatedEvent 推荐关系创建成功
referral.statistics.updated TeamStatisticsUpdatedEvent 团队统计更新

开发顺序建议

  1. Phase 1: 项目初始化

    • 创建NestJS项目
    • 安装依赖
    • 配置环境变量和TypeScript
  2. Phase 2: 数据库层

    • 创建Prisma Schema
    • 运行迁移
    • 创建PrismaService
  3. Phase 3: 领域层 (最重要)

    • 实现所有值对象
    • 实现聚合根 (ReferralRelationship, TeamStatistics)
    • 实现领域事件
    • 实现领域服务
    • 编写单元测试
  4. Phase 4: 基础设施层

    • 实现仓储 (Repository Implementations)
    • 实现Kafka消费者和发布者
    • 实现Redis缓存服务
    • 实现外部服务客户端
  5. Phase 5: 应用层

    • 实现应用服务 (ReferralApplicationService)
    • 实现Command/Query handlers
  6. Phase 6: API层

    • 实现DTO
    • 实现Controllers
    • 配置Swagger文档
    • 配置JWT认证
  7. Phase 7: 测试和部署

    • 集成测试
    • E2E测试
    • Docker配置
    • CI/CD配置

注意事项

  1. 推荐关系在用户注册时创建:由 identity-service 发布 UserAccountCreatedEvent,本服务订阅并创建推荐关系

  2. 团队统计在认种时更新:由 planting-service 发布 PlantingOrderPaidEvent,本服务订阅并更新所有上级的统计

  3. 龙虎榜分值设计目的:鼓励均衡发展团队,防止"单腿"发展

  4. 省市权益分配:根据省市团队占比计算,用于省/市代理权益分配

  5. 使用 PostgreSQL 数组类型:存储推荐链,方便查询

  6. 直推团队数据使用 JSON 类型:便于灵活扩展

  7. 保持与 identity-service 架构一致:确保代码风格、目录结构、命名规范统一