51 KiB
51 KiB
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('团队统计更新完成');
}
}
关键业务规则 (不变式)
- 推荐关系不可修改: 一旦建立推荐关系,终生不可修改
- 推荐链最多10层: 推荐链深度限制为10层,超过的不计入
- 龙虎榜分值计算:
团队总认种量 - 最大单个直推团队认种量 - 省市权益分配: 根据用户团队在该省/市的认种占比分配
- 团队统计实时更新: 任何下级认种都会实时更新所有上级的统计
API 端点汇总
| 方法 | 路径 | 描述 | 认证 |
|---|---|---|---|
| GET | /referrals/me | 获取我的推荐信息 (分享页) | 需要 |
| GET | /referrals/direct | 获取直推列表 | 需要 |
| GET | /referrals/chain | 获取推荐链 (内部API) | 需要 |
| GET | /team/statistics | 获取我的团队统计 | 需要 |
| GET | /team/province-city-ranking | 获取省/市团队排名 | 需要 |
| GET | /leaderboard | 获取龙虎榜 | 需要 |
开发顺序建议
- 项目初始化和 Prisma Schema
- 值对象和枚举实现
- 聚合根实现 (ReferralRelationship, TeamStatistics)
- 领域服务实现 (ReferralChainService, LeaderboardCalculatorService, TeamAggregationService)
- 仓储接口和实现
- 应用服务实现
- Kafka 事件消费者
- DTO 和控制器实现
- 模块配置和测试
与前端页面对应关系
向导页5 (分享引导页)
- 显示用户的推荐码
- 显示分享链接/二维码
- 调用:
GET /referrals/me
分享页
- 显示我的推荐码和分享链接
- 显示直推人数、团队人数
- 显示团队认种统计
- 调用:
GET /referrals/me - 调用:
GET /team/statistics
直推列表
- 显示我的直推用户列表
- 显示每个直推的认种情况
- 调用:
GET /referrals/direct?page=1&pageSize=20
龙虎榜
- 显示全平台龙虎榜排名
- 调用:
GET /leaderboard?limit=100
注意事项
- 推荐关系在用户注册时由 identity-service 触发创建
- 团队统计在认种支付完成时由 planting-service 发布事件触发更新
- 龙虎榜分值设计目的是鼓励均衡发展团队
- 省市权益分配需要根据省市团队占比计算
- 使用 PostgreSQL 数组类型存储推荐链,方便查询
- 直推团队数据使用 JSON 类型存储,便于灵活扩展