# 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 类型存储,便于灵活扩展