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

51 KiB
Raw Blame History

Referral Service 开发指导

项目概述

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

技术栈

  • 框架: NestJS
  • 数据库: PostgreSQL + Prisma ORM
  • 架构: DDD + Hexagonal Architecture (六边形架构)
  • 语言: TypeScript

架构参考

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

referral-service/
├── prisma/
│   └── schema.prisma              # 数据库模型
├── src/
│   ├── api/                       # Presentation Layer (API层)
│   │   ├── controllers/
│   │   │   ├── referral.controller.ts
│   │   │   ├── team.controller.ts
│   │   │   └── leaderboard.controller.ts
│   │   ├── dto/
│   │   │   ├── 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.command.ts
│   │   │   ├── update-team-statistics.command.ts
│   │   │   └── recalculate-leaderboard-score.command.ts
│   │   ├── queries/
│   │   │   ├── get-my-referral-info.query.ts
│   │   │   ├── get-direct-referrals.query.ts
│   │   │   ├── get-team-statistics.query.ts
│   │   │   ├── get-referral-chain.query.ts
│   │   │   ├── get-province-team-ranking.query.ts
│   │   │   └── get-leaderboard.query.ts
│   │   ├── handlers/
│   │   │   ├── create-referral-relationship.handler.ts
│   │   │   ├── update-team-statistics.handler.ts
│   │   │   └── on-planting-order-paid.handler.ts
│   │   └── services/
│   │       ├── referral-application.service.ts
│   │       └── team-statistics.service.ts
│   │
│   ├── domain/                    # Domain Layer (领域层)
│   │   ├── aggregates/
│   │   │   ├── referral-relationship.aggregate.ts
│   │   │   └── team-statistics.aggregate.ts
│   │   ├── value-objects/
│   │   │   ├── referral-code.vo.ts
│   │   │   ├── referral-chain.vo.ts
│   │   │   ├── team-planting-count.vo.ts
│   │   │   ├── leaderboard-score.vo.ts
│   │   │   └── province-city-distribution.vo.ts
│   │   ├── events/
│   │   │   ├── referral-relationship-created.event.ts
│   │   │   ├── team-statistics-updated.event.ts
│   │   │   └── leaderboard-score-changed.event.ts
│   │   ├── repositories/
│   │   │   ├── referral-relationship.repository.interface.ts
│   │   │   └── team-statistics.repository.interface.ts
│   │   └── services/
│   │       ├── referral-chain.service.ts
│   │       ├── leaderboard-calculator.service.ts
│   │       └── team-aggregation.service.ts
│   │
│   ├── infrastructure/            # Infrastructure Layer (基础设施层)
│   │   ├── persistence/
│   │   │   ├── mappers/
│   │   │   │   ├── referral-relationship.mapper.ts
│   │   │   │   └── team-statistics.mapper.ts
│   │   │   └── repositories/
│   │   │       ├── referral-relationship.repository.impl.ts
│   │   │       └── team-statistics.repository.impl.ts
│   │   ├── external/
│   │   │   ├── identity-service.client.ts
│   │   │   └── planting-service.client.ts
│   │   ├── kafka/
│   │   │   ├── event-consumer.controller.ts
│   │   │   └── event-publisher.service.ts
│   │   └── infrastructure.module.ts
│   │
│   ├── app.module.ts
│   └── main.ts
├── .env.development
├── .env.example
├── package.json
└── tsconfig.json

第一阶段:项目初始化

1.1 创建 NestJS 项目

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

1.2 安装依赖

npm install @nestjs/config @prisma/client class-validator class-transformer uuid
npm install -D prisma @types/uuid

1.3 配置环境变量

创建 .env.development:

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_referral?schema=public"
NODE_ENV=development
PORT=3004

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

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

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

2.1 创建 prisma/schema.prisma

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

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

// ============================================
// 推荐关系表 (状态表)
// 记录用户与推荐人的关系
// ============================================
model ReferralRelationship {
  id          BigInt   @id @default(autoincrement()) @map("relationship_id")
  userId      BigInt   @unique @map("user_id")
  referrerId  BigInt?  @map("referrer_id")  // 直接推荐人 (null = 系统推荐/无推荐人)

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

  // 推荐链 (上级链最多10层)
  referralChain     BigInt[] @map("referral_chain")  // [直接上级, 上上级, ...]
  depth             Int      @default(0) @map("depth")  // 在推荐树中的深度

  // 直推统计 (快速查询用)
  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])
  @@index([myReferralCode])
  @@index([usedReferralCode])
  @@index([depth])
  @@index([createdAt])
}

// ============================================
// 团队统计表 (状态表)
// 每个用户的团队认种统计数据
// ============================================
model TeamStatistics {
  id      BigInt @id @default(autoincrement()) @map("statistics_id")
  userId  BigInt @unique @map("user_id")

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

  // 团队认种 (包含自己和所有下级)
  teamPlantingCount      Int @default(0) @map("team_planting_count")      // 团队总认种数量
  teamPlantingAmount     Decimal @default(0) @map("team_planting_amount") @db.Decimal(20, 8)

  // 直推团队认种 (按直推分组)
  directTeamPlantingData Json @default("[]") @map("direct_team_planting_data")
  // 格式: [{ userId: bigint, count: int, amount: decimal }, ...]

  // 龙虎榜分值
  // 计算公式: 团队总认种量 - 最大单个直推团队认种量
  leaderboardScore       Int @default(0) @map("leaderboard_score")
  maxDirectTeamCount     Int @default(0) @map("max_direct_team_count")  // 最大直推团队认种量

  // 省市分布 (用于计算省/市团队占比)
  provinceCityDistribution Json @default("{}") @map("province_city_distribution")
  // 格式: { "province_code": { "city_code": count, ... }, ... }

  // 团队层级统计
  teamMemberCount        Int @default(0) @map("team_member_count")       // 团队总人数
  directMemberCount      Int @default(0) @map("direct_member_count")     // 直推人数

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

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

  @@map("team_statistics")
  @@index([leaderboardScore(sort: Desc)])
  @@index([teamPlantingCount(sort: Desc)])
  @@index([selfPlantingCount])
}

// ============================================
// 直推列表表 (便于分页查询)
// ============================================
model DirectReferral {
  id          BigInt @id @default(autoincrement()) @map("direct_referral_id")
  referrerId  BigInt @map("referrer_id")
  referralId  BigInt @map("referral_id")

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

  // 该直推的认种统计
  plantingCount       Int @default(0) @map("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])
  @@map("direct_referrals")
  @@index([referrerId])
  @@index([referralId])
  @@index([hasPlanted])
  @@index([teamPlantingCount(sort: Desc)])
}

// ============================================
// 省市团队排名表 (用于省/市权益分配)
// ============================================
model ProvinceCityTeamRanking {
  id            BigInt @id @default(autoincrement()) @map("ranking_id")
  provinceCode  String @map("province_code") @db.VarChar(10)
  cityCode      String? @map("city_code") @db.VarChar(10)  // null = 省级排名

  userId        BigInt @map("user_id")

  // 该用户在此省/市的团队认种量
  plantingCount Int @default(0) @map("planting_count")

  // 占比 (百分比,如 25.50 表示 25.50%)
  percentage    Decimal @default(0) @map("percentage") @db.Decimal(10, 4)

  // 排名
  rank          Int @default(0) @map("rank")

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

  @@unique([provinceCode, cityCode, userId])
  @@map("province_city_team_rankings")
  @@index([provinceCode, cityCode])
  @@index([provinceCode, plantingCount(sort: Desc)])
  @@index([userId])
}

// ============================================
// 推荐事件表 (行为表, 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")
  version    Int      @default(1) @map("version")

  @@map("referral_events")
  @@index([aggregateType, aggregateId])
  @@index([eventType])
  @@index([userId])
  @@index([occurredAt])
}

2.2 初始化数据库

npx prisma migrate dev --name init
npx prisma generate

第三阶段:领域层实现

3.1 值对象 (Value Objects)

3.1.1 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);
    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.2 referral-chain.vo.ts

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);
  }

  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.3 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 = Math.max(0, ...directTeamCounts);
    const score = totalTeamCount - maxDirectTeamCount;

    return new LeaderboardScore(
      totalTeamCount,
      maxDirectTeamCount,
      Math.max(0, score),  // 分值不能为负
    );
  }

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

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

3.1.4 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>>): ProvinceCityDistribution {
    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;
  }

  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.2 聚合根 (Aggregates)

3.2.1 referral-relationship.aggregate.ts

import { ReferralCode } from '../value-objects/referral-code.vo';
import { ReferralChain } from '../value-objects/referral-chain.vo';

export class ReferralRelationship {
  private _id: bigint | null;
  private readonly _userId: bigint;
  private readonly _myReferralCode: ReferralCode;
  private readonly _usedReferralCode: ReferralCode | null;
  private readonly _referrerId: bigint | null;
  private readonly _referralChain: ReferralChain;
  private _directReferralCount: number;
  private _activeDirectCount: number;
  private readonly _createdAt: Date;

  private _domainEvents: any[] = [];

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

  // Getters
  get id(): bigint | null { return this._id; }
  get userId(): bigint { return this._userId; }
  get myReferralCode(): ReferralCode { return this._myReferralCode; }
  get usedReferralCode(): ReferralCode | null { return this._usedReferralCode; }
  get referrerId(): bigint | null { return this._referrerId; }
  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 domainEvents(): any[] { return this._domainEvents; }

  /**
   * 工厂方法:创建推荐关系
   * @param userId 用户ID
   * @param referrer 推荐人 (可选)
   * @param usedReferralCode 使用的推荐码 (可选)
   */
  static create(
    userId: bigint,
    referrer: ReferralRelationship | null,
    usedReferralCode: string | null,
  ): ReferralRelationship {
    const myCode = ReferralCode.generate(userId);
    const usedCode = usedReferralCode ? ReferralCode.create(usedReferralCode) : null;

    // 构建推荐链
    const referralChain = ReferralChain.create(
      referrer?.userId ?? null,
      referrer?.referralChain.toArray() ?? [],
    );

    const relationship = new ReferralRelationship(
      userId,
      myCode,
      usedCode,
      referrer?.userId ?? null,
      referralChain,
    );

    // 发布领域事件
    relationship._domainEvents.push({
      type: 'ReferralRelationshipCreated',
      data: {
        userId: userId.toString(),
        referrerId: referrer?.userId?.toString() ?? null,
        myReferralCode: myCode.value,
        usedReferralCode: usedCode?.value ?? null,
        depth: referralChain.depth,
      },
    });

    return relationship;
  }

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

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

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

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

  // 从数据库重建
  static reconstitute(data: any): ReferralRelationship {
    const relationship = new ReferralRelationship(
      data.userId,
      ReferralCode.create(data.myReferralCode),
      data.usedReferralCode ? ReferralCode.create(data.usedReferralCode) : null,
      data.referrerId,
      ReferralChain.fromArray(data.referralChain || []),
    );
    relationship._id = data.id;
    relationship._directReferralCount = data.directReferralCount ?? 0;
    relationship._activeDirectCount = data.activeDirectCount ?? 0;
    return relationship;
  }
}

3.2.2 team-statistics.aggregate.ts

import { LeaderboardScore } from '../value-objects/leaderboard-score.vo';
import { ProvinceCityDistribution } from '../value-objects/province-city-distribution.vo';

interface DirectTeamData {
  userId: bigint;
  count: number;
  amount: number;
}

export class TeamStatistics {
  private _id: bigint | null;
  private readonly _userId: bigint;

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

  // 团队认种
  private _teamPlantingCount: number;
  private _teamPlantingAmount: number;

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

  // 龙虎榜
  private _leaderboardScore: LeaderboardScore;

  // 省市分布
  private _provinceCityDistribution: ProvinceCityDistribution;

  // 团队人数
  private _teamMemberCount: number;
  private _directMemberCount: number;

  private _domainEvents: any[] = [];

  private constructor(userId: bigint) {
    this._userId = userId;
    this._selfPlantingCount = 0;
    this._selfPlantingAmount = 0;
    this._teamPlantingCount = 0;
    this._teamPlantingAmount = 0;
    this._directTeamPlantingData = [];
    this._leaderboardScore = LeaderboardScore.calculate(0, []);
    this._provinceCityDistribution = ProvinceCityDistribution.empty();
    this._teamMemberCount = 0;
    this._directMemberCount = 0;
  }

  // Getters
  get id(): bigint | null { return this._id; }
  get userId(): bigint { return this._userId; }
  get selfPlantingCount(): number { return this._selfPlantingCount; }
  get selfPlantingAmount(): number { return this._selfPlantingAmount; }
  get teamPlantingCount(): number { return this._teamPlantingCount; }
  get teamPlantingAmount(): number { return this._teamPlantingAmount; }
  get directTeamPlantingData(): ReadonlyArray<DirectTeamData> { return this._directTeamPlantingData; }
  get leaderboardScore(): LeaderboardScore { return this._leaderboardScore; }
  get provinceCityDistribution(): ProvinceCityDistribution { return this._provinceCityDistribution; }
  get teamMemberCount(): number { return this._teamMemberCount; }
  get directMemberCount(): number { return this._directMemberCount; }
  get domainEvents(): any[] { return this._domainEvents; }

  /**
   * 工厂方法:创建团队统计
   */
  static create(userId: bigint): TeamStatistics {
    return new TeamStatistics(userId);
  }

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

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

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

    // 重新计算龙虎榜分值
    this._recalculateLeaderboardScore();

    this._domainEvents.push({
      type: 'TeamStatisticsUpdated',
      data: {
        userId: this._userId.toString(),
        selfPlantingCount: this._selfPlantingCount,
        teamPlantingCount: this._teamPlantingCount,
        leaderboardScore: this._leaderboardScore.score,
      },
    });
  }

  /**
   * 下级用户认种,更新团队统计
   * @param directUserId 直推用户ID (第一层下级)
   * @param treeCount 认种数量
   * @param amount 认种金额
   * @param provinceCode 省份代码
   * @param cityCode 城市代码
   */
  addTeamPlanting(
    directUserId: bigint,
    treeCount: number,
    amount: number,
    provinceCode: string,
    cityCode: string,
  ): void {
    // 更新团队总量
    this._teamPlantingCount += treeCount;
    this._teamPlantingAmount += amount;

    // 更新直推团队数据
    const directTeamIndex = this._directTeamPlantingData.findIndex(
      d => d.userId === directUserId,
    );

    if (directTeamIndex >= 0) {
      this._directTeamPlantingData[directTeamIndex].count += treeCount;
      this._directTeamPlantingData[directTeamIndex].amount += amount;
    } else {
      this._directTeamPlantingData.push({
        userId: directUserId,
        count: treeCount,
        amount,
      });
    }

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

    // 重新计算龙虎榜分值
    this._recalculateLeaderboardScore();

    this._domainEvents.push({
      type: 'TeamStatisticsUpdated',
      data: {
        userId: this._userId.toString(),
        teamPlantingCount: this._teamPlantingCount,
        leaderboardScore: this._leaderboardScore.score,
        updatedByDirectUserId: directUserId.toString(),
      },
    });
  }

  /**
   * 添加团队成员
   * @param isDirect 是否是直推
   */
  addTeamMember(isDirect: boolean): void {
    this._teamMemberCount++;
    if (isDirect) {
      this._directMemberCount++;
    }
  }

  /**
   * 重新计算龙虎榜分值
   */
  private _recalculateLeaderboardScore(): void {
    const directCounts = this._directTeamPlantingData.map(d => d.count);
    this._leaderboardScore = LeaderboardScore.calculate(
      this._teamPlantingCount,
      directCounts,
    );
  }

  /**
   * 获取最大直推团队认种量
   */
  get maxDirectTeamCount(): number {
    return this._leaderboardScore.maxDirectTeamCount;
  }

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

  // 从数据库重建
  static reconstitute(data: any): TeamStatistics {
    const stats = new TeamStatistics(data.userId);
    stats._id = data.id;
    stats._selfPlantingCount = data.selfPlantingCount ?? 0;
    stats._selfPlantingAmount = Number(data.selfPlantingAmount) ?? 0;
    stats._teamPlantingCount = data.teamPlantingCount ?? 0;
    stats._teamPlantingAmount = Number(data.teamPlantingAmount) ?? 0;
    stats._directTeamPlantingData = data.directTeamPlantingData ?? [];
    stats._teamMemberCount = data.teamMemberCount ?? 0;
    stats._directMemberCount = data.directMemberCount ?? 0;

    // 重建省市分布
    stats._provinceCityDistribution = ProvinceCityDistribution.fromJson(
      data.provinceCityDistribution ?? {},
    );

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

    return stats;
  }
}

3.3 领域服务

3.3.1 referral-chain.service.ts

import { Injectable } from '@nestjs/common';
import { ReferralRelationship } from '../aggregates/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, ...relationship1.getAllAncestorIds()];
    const chain2 = [relationship2.userId, ...relationship2.getAllAncestorIds()];

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

    // 检查 user1 是否在 user2 的链上
    const idx2 = chain2.findIndex(id => id === relationship1.userId);
    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.3.2 leaderboard-calculator.service.ts

import { Injectable } from '@nestjs/common';
import { TeamStatistics } from '../aggregates/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.leaderboardScore.score > 0)
      .sort((a, b) => b.leaderboardScore.score - a.leaderboardScore.score);

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

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

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

3.3.3 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.aggregate';

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

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

    // 2. 更新自己的统计
    let selfStats = await this.statsRepository.findByUserId(userId);
    if (!selfStats) {
      selfStats = TeamStatistics.create(userId);
    }
    selfStats.addSelfPlanting(treeCount, amount, provinceCode, cityCode);
    await this.statsRepository.save(selfStats);

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

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

    // 5. 更新所有上级的团队统计
    for (const ancestorId of ancestors) {
      let ancestorStats = await this.statsRepository.findByUserId(ancestorId);
      if (!ancestorStats) {
        ancestorStats = TeamStatistics.create(ancestorId);
      }

      // 对于所有上级,都传入"通过哪个直推"来的数据
      // 需要追溯:这个认种是哪个直推带来的
      const directUserIdForAncestor = this.findDirectReferralForAncestor(
        userId,
        ancestorId,
        ancestors,
      );

      ancestorStats.addTeamPlanting(
        directUserIdForAncestor,
        treeCount,
        amount,
        provinceCode,
        cityCode,
      );
      await this.statsRepository.save(ancestorStats);
    }
  }

  /**
   * 找出对于某个祖先来说,这个认种是通过哪个直推传递上来的
   */
  private findDirectReferralForAncestor(
    plantingUserId: bigint,
    ancestorId: bigint,
    fullChain: bigint[],
  ): bigint {
    // fullChain: [直接推荐人, 上上级, 上上上级, ...]
    // 如果 ancestorId 是直接推荐人,那么直推就是 plantingUserId
    // 如果 ancestorId 是上上级,那么直推就是直接推荐人 (fullChain[0])
    // 以此类推

    const ancestorIndex = fullChain.findIndex(id => id === ancestorId);
    if (ancestorIndex === 0) {
      // ancestorId 是直接推荐人,所以直推就是认种的用户本身
      return plantingUserId;
    } else if (ancestorIndex > 0) {
      // ancestorId 在链的更高位置,直推是它的下一级
      return fullChain[ancestorIndex - 1];
    }

    // 不应该到这里
    return plantingUserId;
  }
}

3.4 仓储接口

3.4.1 referral-relationship.repository.interface.ts

import { ReferralRelationship } from '../aggregates/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[]>;
}

export const REFERRAL_RELATIONSHIP_REPOSITORY = Symbol('IReferralRelationshipRepository');

3.4.2 team-statistics.repository.interface.ts

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

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

export const TEAM_STATISTICS_REPOSITORY = Symbol('ITeamStatisticsRepository');

第四阶段:应用层实现

4.1 应用服务

// application/services/referral-application.service.ts

import { Injectable, Inject } from '@nestjs/common';
import { ReferralRelationship } from '../../domain/aggregates/referral-relationship.aggregate';
import { TeamStatistics } from '../../domain/aggregates/team-statistics.aggregate';
import {
  IReferralRelationshipRepository,
  REFERRAL_RELATIONSHIP_REPOSITORY
} from '../../domain/repositories/referral-relationship.repository.interface';
import {
  ITeamStatisticsRepository,
  TEAM_STATISTICS_REPOSITORY
} from '../../domain/repositories/team-statistics.repository.interface';
import { ReferralChainService } from '../../domain/services/referral-chain.service';
import { LeaderboardCalculatorService } from '../../domain/services/leaderboard-calculator.service';

@Injectable()
export class ReferralApplicationService {
  constructor(
    @Inject(REFERRAL_RELATIONSHIP_REPOSITORY)
    private readonly relationshipRepository: IReferralRelationshipRepository,
    @Inject(TEAM_STATISTICS_REPOSITORY)
    private readonly statsRepository: ITeamStatisticsRepository,
    private readonly referralChainService: ReferralChainService,
    private readonly leaderboardService: LeaderboardCalculatorService,
  ) {}

  /**
   * 创建推荐关系 (用户注册时调用)
   */
  async createRelationship(
    userId: bigint,
    referralCode: string | null,
  ) {
    // 检查是否已存在
    const existing = await this.relationshipRepository.findByUserId(userId);
    if (existing) {
      throw new Error('用户推荐关系已存在');
    }

    // 查找推荐人
    let referrer: ReferralRelationship | null = null;
    if (referralCode) {
      referrer = await this.relationshipRepository.findByReferralCode(referralCode);
      if (!referrer) {
        throw new Error('无效的推荐码');
      }
    }

    // 创建推荐关系
    const relationship = ReferralRelationship.create(userId, referrer, referralCode);
    await this.relationshipRepository.save(relationship);

    // 更新推荐人的直推计数
    if (referrer) {
      referrer.incrementDirectReferralCount();
      await this.relationshipRepository.save(referrer);

      // 更新推荐人的团队人数
      let referrerStats = await this.statsRepository.findByUserId(referrer.userId);
      if (!referrerStats) {
        referrerStats = TeamStatistics.create(referrer.userId);
      }
      referrerStats.addTeamMember(true);
      await this.statsRepository.save(referrerStats);

      // 更新所有上级的团队人数
      for (const ancestorId of referrer.getAllAncestorIds()) {
        let ancestorStats = await this.statsRepository.findByUserId(ancestorId);
        if (!ancestorStats) {
          ancestorStats = TeamStatistics.create(ancestorId);
        }
        ancestorStats.addTeamMember(false);
        await this.statsRepository.save(ancestorStats);
      }
    }

    // 创建用户自己的团队统计
    const userStats = TeamStatistics.create(userId);
    await this.statsRepository.save(userStats);

    return {
      myReferralCode: relationship.myReferralCode.value,
      referrerId: referrer?.userId.toString() ?? null,
      depth: relationship.depth,
    };
  }

  /**
   * 获取我的推荐信息 (分享页)
   */
  async getMyReferralInfo(userId: bigint) {
    const relationship = await this.relationshipRepository.findByUserId(userId);
    if (!relationship) {
      throw new Error('用户推荐关系不存在');
    }

    const stats = await this.statsRepository.findByUserId(userId);

    return {
      myReferralCode: relationship.myReferralCode.value,
      directReferralCount: relationship.directReferralCount,
      activeDirectCount: relationship.activeDirectCount,
      teamMemberCount: stats?.teamMemberCount ?? 0,
      teamPlantingCount: stats?.teamPlantingCount ?? 0,
      leaderboardScore: stats?.leaderboardScore.score ?? 0,
      leaderboardRank: await this.getUserLeaderboardRank(userId),
    };
  }

  /**
   * 获取直推列表
   */
  async getDirectReferrals(userId: bigint, page = 1, pageSize = 20) {
    const directReferrals = await this.relationshipRepository.findDirectReferrals(
      userId,
      page,
      pageSize,
    );

    const result = [];
    for (const referral of directReferrals) {
      const stats = await this.statsRepository.findByUserId(referral.userId);
      result.push({
        userId: referral.userId.toString(),
        referralCode: referral.myReferralCode.value,
        plantingCount: stats?.selfPlantingCount ?? 0,
        teamPlantingCount: stats?.teamPlantingCount ?? 0,
        hasPlanted: (stats?.selfPlantingCount ?? 0) > 0,
      });
    }

    const total = await this.relationshipRepository.countDirectReferrals(userId);

    return {
      list: result,
      total,
      page,
      pageSize,
    };
  }

  /**
   * 获取推荐链 (用于分享权益分配)
   */
  async getReferralChain(userId: bigint): Promise<string[]> {
    const relationship = await this.relationshipRepository.findByUserId(userId);
    if (!relationship) {
      return [];
    }
    return relationship.getAllAncestorIds().map(id => id.toString());
  }

  /**
   * 获取团队统计
   */
  async getTeamStatistics(userId: bigint) {
    const stats = await this.statsRepository.findByUserId(userId);
    if (!stats) {
      return {
        selfPlantingCount: 0,
        teamPlantingCount: 0,
        teamMemberCount: 0,
        directMemberCount: 0,
        leaderboardScore: 0,
        provinceCityDistribution: {},
      };
    }

    return {
      selfPlantingCount: stats.selfPlantingCount,
      teamPlantingCount: stats.teamPlantingCount,
      teamMemberCount: stats.teamMemberCount,
      directMemberCount: stats.directMemberCount,
      leaderboardScore: stats.leaderboardScore.score,
      provinceCityDistribution: stats.provinceCityDistribution.toJson(),
    };
  }

  /**
   * 获取龙虎榜
   */
  async getLeaderboard(limit = 100) {
    const allStats = await this.statsRepository.findTopByLeaderboardScore(limit);
    return this.leaderboardService.calculateLeaderboard(allStats, limit);
  }

  /**
   * 获取用户龙虎榜排名
   */
  private async getUserLeaderboardRank(userId: bigint): Promise<number | null> {
    const allStats = await this.statsRepository.findTopByLeaderboardScore(1000);
    return this.leaderboardService.getUserRank(allStats, userId);
  }

  /**
   * 获取省/市团队排名
   */
  async getProvinceCityTeamRanking(
    provinceCode: string,
    cityCode: string | null,
    limit = 50,
  ) {
    const stats = await this.statsRepository.findByProvinceCityRanking(
      provinceCode,
      cityCode,
      limit,
    );

    // 计算总量
    let totalPlanting = 0;
    for (const s of stats) {
      totalPlanting += cityCode
        ? s.provinceCityDistribution.getCityTotal(provinceCode, cityCode)
        : s.provinceCityDistribution.getProvinceTotal(provinceCode);
    }

    // 计算每个用户的占比
    return stats.map((s, index) => {
      const userPlanting = cityCode
        ? s.provinceCityDistribution.getCityTotal(provinceCode, cityCode)
        : s.provinceCityDistribution.getProvinceTotal(provinceCode);

      return {
        userId: s.userId.toString(),
        plantingCount: userPlanting,
        percentage: totalPlanting > 0
          ? ((userPlanting / totalPlanting) * 100).toFixed(2)
          : '0.00',
        rank: index + 1,
      };
    });
  }
}

第五阶段API层实现

5.1 DTO 定义

// api/dto/referral-info.dto.ts
export class ReferralInfoDto {
  myReferralCode: string;
  directReferralCount: number;
  activeDirectCount: number;
  teamMemberCount: number;
  teamPlantingCount: number;
  leaderboardScore: number;
  leaderboardRank: number | null;
}

// api/dto/direct-referral.dto.ts
export class DirectReferralDto {
  userId: string;
  referralCode: string;
  plantingCount: number;
  teamPlantingCount: number;
  hasPlanted: boolean;
}

// api/dto/team-statistics.dto.ts
export class TeamStatisticsDto {
  selfPlantingCount: number;
  teamPlantingCount: number;
  teamMemberCount: number;
  directMemberCount: number;
  leaderboardScore: number;
  provinceCityDistribution: Record<string, Record<string, number>>;
}

// api/dto/leaderboard.dto.ts
export class LeaderboardEntryDto {
  userId: string;
  score: number;
  teamPlantingCount: number;
  maxDirectTeamCount: number;
  rank: number;
}

5.2 控制器

// api/controllers/referral.controller.ts

import { Controller, Get, Post, Body, Query, UseGuards, Req } from '@nestjs/common';
import { ReferralApplicationService } from '../../application/services/referral-application.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';

@Controller('referrals')
@UseGuards(JwtAuthGuard)
export class ReferralController {
  constructor(private readonly referralService: ReferralApplicationService) {}

  /**
   * 获取我的推荐信息 (分享页)
   */
  @Get('me')
  async getMyReferralInfo(@Req() req: any) {
    const userId = BigInt(req.user.id);
    return this.referralService.getMyReferralInfo(userId);
  }

  /**
   * 获取直推列表
   */
  @Get('direct')
  async getDirectReferrals(
    @Req() req: any,
    @Query('page') page = 1,
    @Query('pageSize') pageSize = 20,
  ) {
    const userId = BigInt(req.user.id);
    return this.referralService.getDirectReferrals(userId, page, pageSize);
  }

  /**
   * 获取推荐链 (内部API用于资金分配)
   */
  @Get('chain')
  async getReferralChain(@Req() req: any) {
    const userId = BigInt(req.user.id);
    return this.referralService.getReferralChain(userId);
  }
}

// api/controllers/team.controller.ts

import { Controller, Get, Query, UseGuards, Req } from '@nestjs/common';
import { ReferralApplicationService } from '../../application/services/referral-application.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';

@Controller('team')
@UseGuards(JwtAuthGuard)
export class TeamController {
  constructor(private readonly referralService: ReferralApplicationService) {}

  /**
   * 获取我的团队统计
   */
  @Get('statistics')
  async getTeamStatistics(@Req() req: any) {
    const userId = BigInt(req.user.id);
    return this.referralService.getTeamStatistics(userId);
  }

  /**
   * 获取省/市团队排名
   */
  @Get('province-city-ranking')
  async getProvinceCityRanking(
    @Query('provinceCode') provinceCode: string,
    @Query('cityCode') cityCode: string | null,
    @Query('limit') limit = 50,
  ) {
    return this.referralService.getProvinceCityTeamRanking(provinceCode, cityCode, limit);
  }
}

// api/controllers/leaderboard.controller.ts

import { Controller, Get, Query, UseGuards, Req } from '@nestjs/common';
import { ReferralApplicationService } from '../../application/services/referral-application.service';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';

@Controller('leaderboard')
@UseGuards(JwtAuthGuard)
export class LeaderboardController {
  constructor(private readonly referralService: ReferralApplicationService) {}

  /**
   * 获取龙虎榜
   */
  @Get()
  async getLeaderboard(@Query('limit') limit = 100) {
    return this.referralService.getLeaderboard(limit);
  }
}

事件处理 (Kafka)

当用户认种时更新团队统计

// infrastructure/kafka/event-consumer.controller.ts

import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { TeamAggregationService } from '../../domain/services/team-aggregation.service';

interface PlantingOrderPaidEvent {
  userId: string;
  orderNo: string;
  treeCount: number;
  totalAmount: number;
  provinceCode: string;
  cityCode: string;
}

@Controller()
export class EventConsumerController {
  constructor(
    private readonly teamAggregationService: TeamAggregationService,
  ) {}

  /**
   * 监听认种订单支付事件
   * 更新认种用户及其所有上级的团队统计
   */
  @MessagePattern('planting.order.paid')
  async handlePlantingOrderPaid(@Payload() event: PlantingOrderPaidEvent) {
    console.log('收到认种订单支付事件:', event);

    await this.teamAggregationService.updateAncestorTeamStats(
      BigInt(event.userId),
      event.treeCount,
      event.totalAmount,
      event.provinceCode,
      event.cityCode,
    );

    console.log('团队统计更新完成');
  }
}

关键业务规则 (不变式)

  1. 推荐关系不可修改: 一旦建立推荐关系,终生不可修改
  2. 推荐链最多10层: 推荐链深度限制为10层超过的不计入
  3. 龙虎榜分值计算: 团队总认种量 - 最大单个直推团队认种量
  4. 省市权益分配: 根据用户团队在该省/市的认种占比分配
  5. 团队统计实时更新: 任何下级认种都会实时更新所有上级的统计

API 端点汇总

方法 路径 描述 认证
GET /referrals/me 获取我的推荐信息 (分享页) 需要
GET /referrals/direct 获取直推列表 需要
GET /referrals/chain 获取推荐链 (内部API) 需要
GET /team/statistics 获取我的团队统计 需要
GET /team/province-city-ranking 获取省/市团队排名 需要
GET /leaderboard 获取龙虎榜 需要

开发顺序建议

  1. 项目初始化和 Prisma Schema
  2. 值对象和枚举实现
  3. 聚合根实现 (ReferralRelationship, TeamStatistics)
  4. 领域服务实现 (ReferralChainService, LeaderboardCalculatorService, TeamAggregationService)
  5. 仓储接口和实现
  6. 应用服务实现
  7. Kafka 事件消费者
  8. DTO 和控制器实现
  9. 模块配置和测试

与前端页面对应关系

向导页5 (分享引导页)

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

分享页

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

直推列表

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

龙虎榜

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

注意事项

  1. 推荐关系在用户注册时由 identity-service 触发创建
  2. 团队统计在认种支付完成时由 planting-service 发布事件触发更新
  3. 龙虎榜分值设计目的是鼓励均衡发展团队
  4. 省市权益分配需要根据省市团队占比计算
  5. 使用 PostgreSQL 数组类型存储推荐链,方便查询
  6. 直推团队数据使用 JSON 类型存储,便于灵活扩展