diff --git a/backend/services/planting-service/.claude/settings.local.json b/backend/services/planting-service/.claude/settings.local.json index b4139862..1d0b05e7 100644 --- a/backend/services/planting-service/.claude/settings.local.json +++ b/backend/services/planting-service/.claude/settings.local.json @@ -10,7 +10,8 @@ "Bash(npm run test:e2e:*)", "Bash(npm run test:cov:*)", "Bash(cat:*)", - "Bash(git add:*)" + "Bash(git add:*)", + "Bash(git commit:*)" ], "deny": [], "ask": [] diff --git a/backend/services/referral-service/DEVELOPMENT_GUIDE.md b/backend/services/referral-service/DEVELOPMENT_GUIDE.md new file mode 100644 index 00000000..f1120db3 --- /dev/null +++ b/backend/services/referral-service/DEVELOPMENT_GUIDE.md @@ -0,0 +1,1716 @@ +# 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 项目 + +```bash +cd backend/services +npx @nestjs/cli new referral-service --skip-git --package-manager npm +cd referral-service +``` + +### 1.2 安装依赖 + +```bash +npm install @nestjs/config @prisma/client class-validator class-transformer uuid +npm install -D prisma @types/uuid +``` + +### 1.3 配置环境变量 + +创建 `.env.development`: +```env +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 + +```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 初始化数据库 + +```bash +npx prisma migrate dev --name init +npx prisma generate +``` + +--- + +## 第三阶段:领域层实现 + +### 3.1 值对象 (Value Objects) + +#### 3.1.1 referral-code.vo.ts +```typescript +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 +```typescript +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 +```typescript +/** + * 龙虎榜分值 + * 计算公式: 团队总认种量 - 最大单个直推团队认种量 + * + * 这个公式的设计目的: + * - 鼓励均衡发展团队,而不是只依赖单个大团队 + * - 防止"烧伤"现象(单腿发展) + */ +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 +```typescript +export interface ProvinceCityCount { + provinceCode: string; + cityCode: string; + count: number; +} + +/** + * 省市分布统计 + * 用于计算用户团队在各省/市的认种分布 + */ +export class ProvinceCityDistribution { + private constructor( + private readonly distribution: Map>, + ) {} + + static empty(): ProvinceCityDistribution { + return new ProvinceCityDistribution(new Map()); + } + + static fromJson(json: Record>): ProvinceCityDistribution { + const map = new Map>(); + for (const [province, cities] of Object.entries(json)) { + const cityMap = new Map(); + 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> { + const result: Record> = {}; + 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 + +```typescript +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 + +```typescript +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 { 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 + +```typescript +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 + +```typescript +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 + +```typescript +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 { + // 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 + +```typescript +import { ReferralRelationship } from '../aggregates/referral-relationship.aggregate'; + +export interface IReferralRelationshipRepository { + save(relationship: ReferralRelationship): Promise; + findById(id: bigint): Promise; + findByUserId(userId: bigint): Promise; + findByReferralCode(code: string): Promise; + findDirectReferrals(userId: bigint, page?: number, pageSize?: number): Promise; + countDirectReferrals(userId: bigint): Promise; + findByReferrerId(referrerId: bigint): Promise; +} + +export const REFERRAL_RELATIONSHIP_REPOSITORY = Symbol('IReferralRelationshipRepository'); +``` + +#### 3.4.2 team-statistics.repository.interface.ts + +```typescript +import { TeamStatistics } from '../aggregates/team-statistics.aggregate'; + +export interface ITeamStatisticsRepository { + save(stats: TeamStatistics): Promise; + findByUserId(userId: bigint): Promise; + findTopByLeaderboardScore(limit: number): Promise; + findByProvinceCityRanking( + provinceCode: string, + cityCode: string | null, + limit: number, + ): Promise; + getOrCreate(userId: bigint): Promise; +} + +export const TEAM_STATISTICS_REPOSITORY = Symbol('ITeamStatisticsRepository'); +``` + +--- + +## 第四阶段:应用层实现 + +### 4.1 应用服务 + +```typescript +// 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 { + 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 { + 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 定义 + +```typescript +// 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>; +} + +// api/dto/leaderboard.dto.ts +export class LeaderboardEntryDto { + userId: string; + score: number; + teamPlantingCount: number; + maxDirectTeamCount: number; + rank: number; +} +``` + +### 5.2 控制器 + +```typescript +// 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) + +### 当用户认种时更新团队统计 + +```typescript +// 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 类型存储,便于灵活扩展