# Referral Service 开发指导 ## 项目概述 Referral Service 是 RWA 榴莲皇后平台的推荐团队微服务,负责管理推荐关系树、团队统计维护、龙虎榜分值计算、省市团队占比统计等功能。 ### 核心职责 ✅ - 推荐关系树构建(基于序列号/推荐码) - 团队统计数据维护 - 上级查找(最近的授权省/市/社区) - 直推用户管理 - 团队层级关系管理 - 本省/本市认种占比计算 - 龙虎榜分值计算 ### 不负责 ❌ - 用户账户管理(Identity Context) - 奖励计算(Reward Context) - 授权考核判定(Authorization Context) --- ## 技术栈 | 组件 | 技术选型 | |------|----------| | **框架** | NestJS 10.x | | **数据库** | PostgreSQL + Prisma ORM | | **架构** | DDD + Hexagonal Architecture (六边形架构) | | **语言** | TypeScript 5.x | | **消息队列** | Kafka (kafkajs) | | **缓存** | Redis (ioredis) | | **API文档** | Swagger (@nestjs/swagger) | --- ## 架构设计 参考 `identity-service` 的架构模式,严格保持一致性: ``` referral-service/ ├── prisma/ │ ├── schema.prisma # 数据库模型定义 │ └── migrations/ # 数据库迁移文件 │ ├── src/ │ ├── api/ # 🔵 Presentation Layer (表现层/API层) │ │ ├── controllers/ │ │ │ ├── health.controller.ts │ │ │ ├── referral.controller.ts │ │ │ ├── team.controller.ts │ │ │ └── leaderboard.controller.ts │ │ ├── dto/ │ │ │ ├── request/ │ │ │ │ └── create-referral.dto.ts │ │ │ └── response/ │ │ │ ├── referral-info.dto.ts │ │ │ ├── team-statistics.dto.ts │ │ │ ├── direct-referral.dto.ts │ │ │ └── leaderboard.dto.ts │ │ └── api.module.ts │ │ │ ├── application/ # 🟢 Application Layer (应用层) │ │ ├── commands/ │ │ │ ├── create-referral-relationship/ │ │ │ │ ├── create-referral-relationship.command.ts │ │ │ │ └── create-referral-relationship.handler.ts │ │ │ ├── update-team-statistics/ │ │ │ │ ├── update-team-statistics.command.ts │ │ │ │ └── update-team-statistics.handler.ts │ │ │ └── index.ts │ │ ├── queries/ │ │ │ ├── get-my-referral-info/ │ │ │ │ ├── get-my-referral-info.query.ts │ │ │ │ └── get-my-referral-info.handler.ts │ │ │ ├── get-direct-referrals/ │ │ │ │ ├── get-direct-referrals.query.ts │ │ │ │ └── get-direct-referrals.handler.ts │ │ │ ├── get-team-statistics/ │ │ │ │ ├── get-team-statistics.query.ts │ │ │ │ └── get-team-statistics.handler.ts │ │ │ ├── get-referral-chain/ │ │ │ │ ├── get-referral-chain.query.ts │ │ │ │ └── get-referral-chain.handler.ts │ │ │ ├── get-leaderboard/ │ │ │ │ ├── get-leaderboard.query.ts │ │ │ │ └── get-leaderboard.handler.ts │ │ │ └── index.ts │ │ ├── services/ │ │ │ └── referral-application.service.ts │ │ └── application.module.ts │ │ │ ├── domain/ # 🟡 Domain Layer (领域层) - 核心业务逻辑 │ │ ├── aggregates/ │ │ │ ├── referral-relationship/ │ │ │ │ ├── referral-relationship.aggregate.ts │ │ │ │ ├── referral-relationship.spec.ts │ │ │ │ └── index.ts │ │ │ └── team-statistics/ │ │ │ ├── team-statistics.aggregate.ts │ │ │ ├── team-statistics.spec.ts │ │ │ └── index.ts │ │ ├── value-objects/ │ │ │ ├── referral-code.vo.ts │ │ │ ├── referral-chain.vo.ts │ │ │ ├── leaderboard-score.vo.ts │ │ │ ├── province-city-distribution.vo.ts │ │ │ ├── user-id.vo.ts │ │ │ └── index.ts │ │ ├── events/ │ │ │ ├── domain-event.base.ts │ │ │ ├── referral-relationship-created.event.ts │ │ │ ├── team-statistics-updated.event.ts │ │ │ ├── leaderboard-score-changed.event.ts │ │ │ └── index.ts │ │ ├── repositories/ │ │ │ ├── referral-relationship.repository.interface.ts │ │ │ ├── team-statistics.repository.interface.ts │ │ │ └── index.ts │ │ ├── services/ │ │ │ ├── referral-chain.service.ts │ │ │ ├── leaderboard-calculator.service.ts │ │ │ ├── team-aggregation.service.ts │ │ │ └── index.ts │ │ └── domain.module.ts │ │ │ ├── infrastructure/ # 🔴 Infrastructure Layer (基础设施层) │ │ ├── persistence/ │ │ │ ├── prisma/ │ │ │ │ └── prisma.service.ts │ │ │ ├── mappers/ │ │ │ │ ├── referral-relationship.mapper.ts │ │ │ │ └── team-statistics.mapper.ts │ │ │ └── repositories/ │ │ │ ├── referral-relationship.repository.impl.ts │ │ │ └── team-statistics.repository.impl.ts │ │ ├── external/ │ │ │ ├── identity-service/ │ │ │ │ └── identity-service.client.ts │ │ │ └── planting-service/ │ │ │ └── planting-service.client.ts │ │ ├── kafka/ │ │ │ ├── event-consumer.controller.ts │ │ │ ├── event-publisher.service.ts │ │ │ └── kafka.module.ts │ │ ├── redis/ │ │ │ ├── redis.service.ts │ │ │ └── redis.module.ts │ │ └── infrastructure.module.ts │ │ │ ├── shared/ # 共享模块 │ │ ├── decorators/ │ │ │ ├── current-user.decorator.ts │ │ │ ├── public.decorator.ts │ │ │ └── index.ts │ │ ├── exceptions/ │ │ │ ├── domain.exception.ts │ │ │ ├── application.exception.ts │ │ │ └── index.ts │ │ ├── filters/ │ │ │ ├── global-exception.filter.ts │ │ │ └── domain-exception.filter.ts │ │ ├── guards/ │ │ │ └── jwt-auth.guard.ts │ │ ├── interceptors/ │ │ │ └── transform.interceptor.ts │ │ └── strategies/ │ │ └── jwt.strategy.ts │ │ │ ├── config/ # 配置 │ │ ├── app.config.ts │ │ ├── database.config.ts │ │ ├── jwt.config.ts │ │ ├── redis.config.ts │ │ ├── kafka.config.ts │ │ └── index.ts │ │ │ ├── app.module.ts # 主模块 │ └── main.ts # 入口文件 │ ├── test/ # 测试 │ ├── unit/ │ ├── integration/ │ └── e2e/ │ ├── .env.example ├── .env.development ├── .env.production ├── .eslintrc.js ├── .prettierrc ├── nest-cli.json ├── package.json ├── tsconfig.json ├── tsconfig.build.json └── Dockerfile ``` --- ## 第一阶段:项目初始化 ### 1.1 创建 NestJS 项目 ```bash cd backend/services/referral-service npx @nestjs/cli new . --skip-git --package-manager npm ``` ### 1.2 安装依赖 ```bash # 核心依赖 npm install @nestjs/config @nestjs/swagger @nestjs/jwt @nestjs/passport @nestjs/microservices npm install @prisma/client class-validator class-transformer uuid ioredis kafkajs npm install passport passport-jwt # 开发依赖 npm install -D prisma @types/uuid @types/passport-jwt npm install -D @nestjs/testing jest ts-jest @types/jest supertest @types/supertest ``` ### 1.3 package.json 配置 ```json { "name": "referral-service", "version": "1.0.0", "description": "RWA Referral & Team Context Service", "author": "RWA Team", "private": true, "license": "UNLICENSED", "prisma": { "schema": "prisma/schema.prisma", "seed": "ts-node prisma/seed.ts" }, "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:migrate:prod": "prisma migrate deploy", "prisma:studio": "prisma studio" }, "dependencies": { "@nestjs/axios": "^3.0.0", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/microservices": "^10.0.0", "@nestjs/passport": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.1.17", "@prisma/client": "^5.7.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "ioredis": "^5.3.2", "kafkajs": "^2.2.4", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "uuid": "^9.0.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/passport-jwt": "^4.0.0", "@types/supertest": "^6.0.0", "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", "prettier": "^3.0.0", "prisma": "^5.7.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" }, "jest": { "moduleFileExtensions": ["js", "json", "ts"], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "collectCoverageFrom": ["**/*.(t|j)s"], "coverageDirectory": "../coverage", "testEnvironment": "node", "moduleNameMapper": { "^@/(.*)$": "/$1" } } } ``` ### 1.4 tsconfig.json 配置 ```json { "compilerOptions": { "module": "commonjs", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ES2021", "sourceMap": true, "outDir": "./dist", "baseUrl": "./src", "paths": { "@/*": ["./*"] }, "incremental": true, "skipLibCheck": true, "strictNullChecks": true, "noImplicitAny": true, "strictBindCallApply": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true } } ``` ### 1.5 环境变量配置 创建 `.env.development`: ```env # 应用配置 NODE_ENV=development PORT=3004 APP_NAME=referral-service # 数据库 DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_referral?schema=public" # JWT (与 identity-service 共享密钥) JWT_SECRET=your-super-secret-jwt-key-change-in-production JWT_ACCESS_EXPIRES_IN=2h JWT_REFRESH_EXPIRES_IN=7d # Redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= # Kafka KAFKA_BROKERS=localhost:9092 KAFKA_GROUP_ID=referral-service-group KAFKA_CLIENT_ID=referral-service # 外部服务 IDENTITY_SERVICE_URL=http://localhost:3001 PLANTING_SERVICE_URL=http://localhost:3003 ``` --- ## 第二阶段:数据库设计 (Prisma Schema) ### 2.1 创建 prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================ // 推荐关系表 (聚合根1) // 记录用户与推荐人的关系,推荐关系一旦建立终生不可修改 // ============================================ model ReferralRelationship { id BigInt @id @default(autoincrement()) @map("relationship_id") userId BigInt @unique @map("user_id") // 推荐人信息 referrerId BigInt? @map("referrer_id") // 直接推荐人 (null = 无推荐人/根节点) rootUserId BigInt? @map("root_user_id") // 顶级上级用户ID // 推荐码 myReferralCode String @unique @map("my_referral_code") @db.VarChar(20) usedReferralCode String? @map("used_referral_code") @db.VarChar(20) // 推荐链 (使用PostgreSQL数组类型,最多存储10层上级) ancestorPath BigInt[] @map("ancestor_path") // [父节点, 祖父节点, ...] 从根到父的路径 depth Int @default(0) @map("depth") // 层级深度 (0=根节点) // 直推统计 (快速查询用,冗余存储) directReferralCount Int @default(0) @map("direct_referral_count") activeDirectCount Int @default(0) @map("active_direct_count") // 已认种的直推人数 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 自引用关系 (方便查询推荐人) referrer ReferralRelationship? @relation("ReferrerToReferral", fields: [referrerId], references: [userId]) directReferrals ReferralRelationship[] @relation("ReferrerToReferral") // 关联团队统计 teamStatistics TeamStatistics? @@map("referral_relationships") @@index([referrerId], name: "idx_referrer") @@index([myReferralCode], name: "idx_my_referral_code") @@index([usedReferralCode], name: "idx_used_referral_code") @@index([rootUserId], name: "idx_root_user") @@index([depth], name: "idx_depth") @@index([createdAt], name: "idx_referral_created") } // ============================================ // 团队统计表 (聚合根2) // 每个用户的团队认种统计数据,需要实时更新 // ============================================ model TeamStatistics { id BigInt @id @default(autoincrement()) @map("statistics_id") userId BigInt @unique @map("user_id") // === 注册统计 === directReferralCount Int @default(0) @map("direct_referral_count") // 直推注册数 totalTeamCount Int @default(0) @map("total_team_count") // 团队总注册数 // === 个人认种 === selfPlantingCount Int @default(0) @map("self_planting_count") // 自己认种数量 selfPlantingAmount Decimal @default(0) @map("self_planting_amount") @db.Decimal(20, 8) // === 团队认种 (包含自己和所有下级) === directPlantingCount Int @default(0) @map("direct_planting_count") // 直推认种数 totalTeamPlantingCount Int @default(0) @map("total_team_planting_count") // 团队总认种数 totalTeamPlantingAmount Decimal @default(0) @map("total_team_planting_amount") @db.Decimal(20, 8) // === 直推团队数据 (JSON存储每个直推的团队认种量) === // 格式: [{ userId: bigint, personalCount: int, teamCount: int, amount: decimal }, ...] directTeamPlantingData Json @default("[]") @map("direct_team_planting_data") // === 龙虎榜相关 === // 龙虎榜分值 = 团队总认种量 - 最大单个直推团队认种量 maxSingleTeamPlantingCount Int @default(0) @map("max_single_team_planting_count") effectivePlantingCountForRanking Int @default(0) @map("effective_planting_count_for_ranking") // === 本省本市统计 (用于省市授权考核) === ownProvinceTeamCount Int @default(0) @map("own_province_team_count") // 自有团队本省认种 ownCityTeamCount Int @default(0) @map("own_city_team_count") // 自有团队本市认种 provinceTeamPercentage Decimal @default(0) @map("province_team_percentage") @db.Decimal(5, 2) // 本省占比 cityTeamPercentage Decimal @default(0) @map("city_team_percentage") @db.Decimal(5, 2) // 本市占比 // === 省市分布 (JSON存储详细分布) === // 格式: { "provinceCode": { "cityCode": count, ... }, ... } provinceCityDistribution Json @default("{}") @map("province_city_distribution") // 时间戳 lastCalcAt DateTime? @map("last_calc_at") // 最后计算时间 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 referralRelationship ReferralRelationship @relation(fields: [userId], references: [userId]) @@map("team_statistics") @@index([effectivePlantingCountForRanking(sort: Desc)], name: "idx_leaderboard_score") @@index([totalTeamPlantingCount(sort: Desc)], name: "idx_team_planting") @@index([selfPlantingCount], name: "idx_self_planting") } // ============================================ // 直推用户列表 (冗余表,便于分页查询) // ============================================ model DirectReferral { id BigInt @id @default(autoincrement()) @map("direct_referral_id") referrerId BigInt @map("referrer_id") // 推荐人ID referralId BigInt @map("referral_id") // 被推荐人ID referralSequence BigInt @map("referral_sequence") // 被推荐人序列号 // 被推荐人信息快照 (冗余存储,避免跨服务查询) referralNickname String? @map("referral_nickname") @db.VarChar(100) referralAvatar String? @map("referral_avatar") @db.VarChar(255) // 该直推的认种统计 personalPlantingCount Int @default(0) @map("personal_planting_count") // 个人认种数 teamPlantingCount Int @default(0) @map("team_planting_count") // 团队认种数(含个人) // 是否已认种 (用于区分活跃/非活跃) hasPlanted Boolean @default(false) @map("has_planted") firstPlantedAt DateTime? @map("first_planted_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@unique([referrerId, referralId], name: "uk_referrer_referral") @@map("direct_referrals") @@index([referrerId], name: "idx_direct_referrer") @@index([referralId], name: "idx_direct_referral") @@index([hasPlanted], name: "idx_has_planted") @@index([teamPlantingCount(sort: Desc)], name: "idx_direct_team_planting") } // ============================================ // 团队省市分布表 (用于省市权益分配) // ============================================ model TeamProvinceCityDetail { id BigInt @id @default(autoincrement()) @map("detail_id") userId BigInt @map("user_id") provinceCode String @map("province_code") @db.VarChar(10) cityCode String @map("city_code") @db.VarChar(10) teamPlantingCount Int @default(0) @map("team_planting_count") // 该省/市团队认种数 updatedAt DateTime @updatedAt @map("updated_at") @@unique([userId, provinceCode, cityCode], name: "uk_user_province_city") @@map("team_province_city_details") @@index([userId], name: "idx_detail_user") @@index([provinceCode], name: "idx_detail_province") @@index([cityCode], name: "idx_detail_city") } // ============================================ // 推荐事件表 (行为表,append-only,用于审计和事件溯源) // ============================================ model ReferralEvent { id BigInt @id @default(autoincrement()) @map("event_id") eventType String @map("event_type") @db.VarChar(50) // 聚合根信息 aggregateId String @map("aggregate_id") @db.VarChar(100) aggregateType String @map("aggregate_type") @db.VarChar(50) // 事件数据 eventData Json @map("event_data") // 元数据 userId BigInt? @map("user_id") occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamp(6) version Int @default(1) @map("version") @@map("referral_events") @@index([aggregateType, aggregateId], name: "idx_event_aggregate") @@index([eventType], name: "idx_event_type") @@index([userId], name: "idx_event_user") @@index([occurredAt], name: "idx_event_occurred") } ``` ### 2.2 初始化数据库 ```bash # 生成 Prisma Client npx prisma generate # 创建并运行迁移 npx prisma migrate dev --name init # 查看数据库 (可选) npx prisma studio ``` --- ## 第三阶段:领域层实现 (Domain Layer) ### 3.1 值对象 (Value Objects) #### 3.1.1 src/domain/value-objects/user-id.vo.ts ```typescript export class UserId { private constructor(public readonly value: bigint) {} static create(value: bigint | string | number): UserId { const bigIntValue = typeof value === 'bigint' ? value : BigInt(value); if (bigIntValue <= 0n) { throw new Error('用户ID必须大于0'); } return new UserId(bigIntValue); } equals(other: UserId): boolean { return this.value === other.value; } toString(): string { return this.value.toString(); } } ``` #### 3.1.2 src/domain/value-objects/referral-code.vo.ts ```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).padStart(3, '0'); const random = Math.random().toString(36).toUpperCase().slice(2, 6); return new ReferralCode(`${prefix}${userIdHash}${random}`); } equals(other: ReferralCode): boolean { return this.value === other.value; } } ``` #### 3.1.3 src/domain/value-objects/referral-chain.vo.ts ```typescript import { UserId } from './user-id.vo'; /** * 推荐链值对象 * 存储从直接推荐人到根节点的用户ID列表 */ export class ReferralChain { private static readonly MAX_DEPTH = 10; private constructor( public readonly chain: bigint[], // [直接上级, 上上级, ...] ) { if (chain.length > ReferralChain.MAX_DEPTH) { throw new Error(`推荐链深度不能超过 ${ReferralChain.MAX_DEPTH}`); } } static create(referrerId: bigint | null, parentChain: bigint[] = []): ReferralChain { if (!referrerId) { return new ReferralChain([]); } // 新链 = [直接推荐人] + 父链的前 MAX_DEPTH - 1 个元素 const newChain = [referrerId, ...parentChain.slice(0, this.MAX_DEPTH - 1)]; return new ReferralChain(newChain); } static fromArray(chain: bigint[]): ReferralChain { return new ReferralChain([...chain]); } static empty(): ReferralChain { return new ReferralChain([]); } get depth(): number { return this.chain.length; } get directReferrer(): bigint | null { return this.chain[0] ?? null; } /** * 获取指定层级的推荐人 * @param level 层级 (0 = 直接推荐人, 1 = 推荐人的推荐人, ...) */ getReferrerAtLevel(level: number): bigint | null { return this.chain[level] ?? null; } /** * 获取所有上级 (用于团队统计更新) */ getAllAncestors(): bigint[] { return [...this.chain]; } toArray(): bigint[] { return [...this.chain]; } } ``` #### 3.1.4 src/domain/value-objects/leaderboard-score.vo.ts ```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 = directTeamCounts.length > 0 ? Math.max(...directTeamCounts) : 0; const score = Math.max(0, totalTeamCount - maxDirectTeamCount); return new LeaderboardScore(totalTeamCount, maxDirectTeamCount, score); } static zero(): LeaderboardScore { return new LeaderboardScore(0, 0, 0); } /** * 当团队认种发生变化时重新计算 */ recalculate( newTotalTeamCount: number, newDirectTeamCounts: number[], ): LeaderboardScore { return LeaderboardScore.calculate(newTotalTeamCount, newDirectTeamCounts); } /** * 比较排名 (降序排列) */ compareTo(other: LeaderboardScore): number { return other.score - this.score; } } ``` #### 3.1.5 src/domain/value-objects/province-city-distribution.vo.ts ```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> | null): ProvinceCityDistribution { if (!json) { return ProvinceCityDistribution.empty(); } 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; } /** * 获取总数 */ getTotal(): number { let total = 0; for (const cities of this.distribution.values()) { for (const count of cities.values()) { total += count; } } return total; } 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.1.6 src/domain/value-objects/index.ts ```typescript export * from './user-id.vo'; export * from './referral-code.vo'; export * from './referral-chain.vo'; export * from './leaderboard-score.vo'; export * from './province-city-distribution.vo'; ``` ### 3.2 领域事件 (Domain Events) #### 3.2.1 src/domain/events/domain-event.base.ts ```typescript import { v4 as uuidv4 } from 'uuid'; export abstract class DomainEvent { public readonly eventId: string; public readonly occurredAt: Date; public readonly version: number; protected constructor(version: number = 1) { this.eventId = uuidv4(); this.occurredAt = new Date(); this.version = version; } abstract get eventType(): string; abstract get aggregateId(): string; abstract get aggregateType(): string; abstract toPayload(): Record; } ``` #### 3.2.2 src/domain/events/referral-relationship-created.event.ts ```typescript import { DomainEvent } from './domain-event.base'; export interface ReferralRelationshipCreatedPayload { userId: string; referrerId: string | null; myReferralCode: string; usedReferralCode: string | null; depth: number; ancestorPath: string[]; } export class ReferralRelationshipCreatedEvent extends DomainEvent { constructor(private readonly payload: ReferralRelationshipCreatedPayload) { super(); } get eventType(): string { return 'ReferralRelationshipCreated'; } get aggregateId(): string { return this.payload.userId; } get aggregateType(): string { return 'ReferralRelationship'; } toPayload(): ReferralRelationshipCreatedPayload { return { ...this.payload }; } } ``` #### 3.2.3 src/domain/events/team-statistics-updated.event.ts ```typescript import { DomainEvent } from './domain-event.base'; export interface TeamStatisticsUpdatedPayload { userId: string; selfPlantingCount: number; teamPlantingCount: number; leaderboardScore: number; triggeredByUserId?: string; // 触发更新的用户ID } export class TeamStatisticsUpdatedEvent extends DomainEvent { constructor(private readonly payload: TeamStatisticsUpdatedPayload) { super(); } get eventType(): string { return 'TeamStatisticsUpdated'; } get aggregateId(): string { return this.payload.userId; } get aggregateType(): string { return 'TeamStatistics'; } toPayload(): TeamStatisticsUpdatedPayload { return { ...this.payload }; } } ``` #### 3.2.4 src/domain/events/index.ts ```typescript export * from './domain-event.base'; export * from './referral-relationship-created.event'; export * from './team-statistics-updated.event'; ``` ### 3.3 聚合根 (Aggregates) #### 3.3.1 src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts ```typescript import { ReferralCode } from '../../value-objects/referral-code.vo'; import { ReferralChain } from '../../value-objects/referral-chain.vo'; import { UserId } from '../../value-objects/user-id.vo'; import { DomainEvent } from '../../events/domain-event.base'; import { ReferralRelationshipCreatedEvent } from '../../events/referral-relationship-created.event'; /** * 推荐关系聚合根 * * 不变式: * 1. 推荐关系一旦建立,终生不可修改 * 2. 不能推荐自己 * 3. 祖先路径必须完整 (从根节点到当前节点的所有上级) */ export class ReferralRelationship { private _id: bigint | null = null; private readonly _userId: UserId; private readonly _myReferralCode: ReferralCode; private readonly _usedReferralCode: ReferralCode | null; private readonly _referrerId: UserId | null; private readonly _rootUserId: UserId | null; private readonly _referralChain: ReferralChain; private _directReferralCount: number; private _activeDirectCount: number; private readonly _createdAt: Date; private _domainEvents: DomainEvent[] = []; private constructor( userId: UserId, myReferralCode: ReferralCode, usedReferralCode: ReferralCode | null, referrerId: UserId | null, rootUserId: UserId | null, referralChain: ReferralChain, ) { this._userId = userId; this._myReferralCode = myReferralCode; this._usedReferralCode = usedReferralCode; this._referrerId = referrerId; this._rootUserId = rootUserId; this._referralChain = referralChain; this._directReferralCount = 0; this._activeDirectCount = 0; this._createdAt = new Date(); } // ============ Getters ============ get id(): bigint | null { return this._id; } get userId(): UserId { return this._userId; } get myReferralCode(): ReferralCode { return this._myReferralCode; } get usedReferralCode(): ReferralCode | null { return this._usedReferralCode; } get referrerId(): UserId | null { return this._referrerId; } get rootUserId(): UserId | null { return this._rootUserId; } get referralChain(): ReferralChain { return this._referralChain; } get depth(): number { return this._referralChain.depth; } get directReferralCount(): number { return this._directReferralCount; } get activeDirectCount(): number { return this._activeDirectCount; } get createdAt(): Date { return this._createdAt; } get domainEvents(): DomainEvent[] { return [...this._domainEvents]; } // ============ 工厂方法 ============ /** * 创建推荐关系 (有推荐人) */ static createWithReferrer( userId: bigint, referrer: ReferralRelationship, usedReferralCode: string, ): ReferralRelationship { const userIdVo = UserId.create(userId); // 验证不能推荐自己 if (userIdVo.equals(referrer.userId)) { throw new Error('不能推荐自己'); } const myCode = ReferralCode.generate(userId); const usedCode = ReferralCode.create(usedReferralCode); // 构建推荐链 const referralChain = ReferralChain.create( referrer.userId.value, referrer.referralChain.toArray(), ); // 确定根节点 const rootUserId = referrer.rootUserId || referrer.userId; const relationship = new ReferralRelationship( userIdVo, myCode, usedCode, referrer.userId, rootUserId, referralChain, ); // 发布领域事件 relationship._domainEvents.push(new ReferralRelationshipCreatedEvent({ userId: userId.toString(), referrerId: referrer.userId.toString(), myReferralCode: myCode.value, usedReferralCode: usedCode.value, depth: referralChain.depth, ancestorPath: referralChain.toArray().map(id => id.toString()), })); return relationship; } /** * 创建推荐关系 (无推荐人,根节点) */ static createRoot(userId: bigint): ReferralRelationship { const userIdVo = UserId.create(userId); const myCode = ReferralCode.generate(userId); const relationship = new ReferralRelationship( userIdVo, myCode, null, null, null, ReferralChain.empty(), ); // 发布领域事件 relationship._domainEvents.push(new ReferralRelationshipCreatedEvent({ userId: userId.toString(), referrerId: null, myReferralCode: myCode.value, usedReferralCode: null, depth: 0, ancestorPath: [], })); return relationship; } // ============ 领域行为 ============ /** * 增加直推人数 */ incrementDirectReferralCount(): void { this._directReferralCount++; } /** * 直推用户认种后,更新活跃直推数 */ markDirectAsActive(): void { this._activeDirectCount++; } /** * 获取所有上级用户ID (用于更新团队统计) */ getAllAncestorIds(): bigint[] { return this._referralChain.getAllAncestors(); } /** * 获取直接推荐人ID */ getDirectReferrerId(): bigint | null { return this._referrerId?.value ?? null; } /** * 设置ID (用于从数据库重建) */ setId(id: bigint): void { this._id = id; } clearDomainEvents(): void { this._domainEvents = []; } // ============ 重建 ============ /** * 从数据库重建聚合 */ static reconstitute(data: { id: bigint; userId: bigint; myReferralCode: string; usedReferralCode: string | null; referrerId: bigint | null; rootUserId: bigint | null; ancestorPath: bigint[]; directReferralCount: number; activeDirectCount: number; createdAt: Date; }): ReferralRelationship { const relationship = new ReferralRelationship( UserId.create(data.userId), ReferralCode.create(data.myReferralCode), data.usedReferralCode ? ReferralCode.create(data.usedReferralCode) : null, data.referrerId ? UserId.create(data.referrerId) : null, data.rootUserId ? UserId.create(data.rootUserId) : null, ReferralChain.fromArray(data.ancestorPath), ); relationship._id = data.id; relationship._directReferralCount = data.directReferralCount; relationship._activeDirectCount = data.activeDirectCount; return relationship; } } ``` #### 3.3.2 src/domain/aggregates/team-statistics/team-statistics.aggregate.ts ```typescript import { UserId } from '../../value-objects/user-id.vo'; import { LeaderboardScore } from '../../value-objects/leaderboard-score.vo'; import { ProvinceCityDistribution } from '../../value-objects/province-city-distribution.vo'; import { DomainEvent } from '../../events/domain-event.base'; import { TeamStatisticsUpdatedEvent } from '../../events/team-statistics-updated.event'; interface DirectTeamData { userId: bigint; personalCount: number; teamCount: number; amount: number; } /** * 团队统计聚合根 * * 不变式: * 1. 团队统计必须在认种后实时更新 * 2. 龙虎榜分值 = 团队总认种量 - 最大单个直推团队认种量 */ export class TeamStatistics { private _id: bigint | null = null; private readonly _userId: UserId; // 注册统计 private _directReferralCount: number; private _totalTeamCount: number; // 个人认种 private _selfPlantingCount: number; private _selfPlantingAmount: number; // 团队认种 private _directPlantingCount: number; private _totalTeamPlantingCount: number; private _totalTeamPlantingAmount: number; // 直推团队数据 private _directTeamPlantingData: DirectTeamData[]; // 龙虎榜 private _leaderboardScore: LeaderboardScore; // 本省本市统计 private _ownProvinceTeamCount: number; private _ownCityTeamCount: number; private _provinceTeamPercentage: number; private _cityTeamPercentage: number; // 省市分布 private _provinceCityDistribution: ProvinceCityDistribution; private _lastCalcAt: Date | null; private readonly _createdAt: Date; private _domainEvents: DomainEvent[] = []; private constructor(userId: UserId) { this._userId = userId; this._directReferralCount = 0; this._totalTeamCount = 0; this._selfPlantingCount = 0; this._selfPlantingAmount = 0; this._directPlantingCount = 0; this._totalTeamPlantingCount = 0; this._totalTeamPlantingAmount = 0; this._directTeamPlantingData = []; this._leaderboardScore = LeaderboardScore.zero(); this._ownProvinceTeamCount = 0; this._ownCityTeamCount = 0; this._provinceTeamPercentage = 0; this._cityTeamPercentage = 0; this._provinceCityDistribution = ProvinceCityDistribution.empty(); this._lastCalcAt = null; this._createdAt = new Date(); } // ============ Getters ============ get id(): bigint | null { return this._id; } get userId(): UserId { return this._userId; } get directReferralCount(): number { return this._directReferralCount; } get totalTeamCount(): number { return this._totalTeamCount; } get selfPlantingCount(): number { return this._selfPlantingCount; } get selfPlantingAmount(): number { return this._selfPlantingAmount; } get directPlantingCount(): number { return this._directPlantingCount; } get totalTeamPlantingCount(): number { return this._totalTeamPlantingCount; } get totalTeamPlantingAmount(): number { return this._totalTeamPlantingAmount; } get directTeamPlantingData(): ReadonlyArray { return this._directTeamPlantingData; } get leaderboardScore(): LeaderboardScore { return this._leaderboardScore; } get maxSingleTeamPlantingCount(): number { return this._leaderboardScore.maxDirectTeamCount; } get effectivePlantingCountForRanking(): number { return this._leaderboardScore.score; } get ownProvinceTeamCount(): number { return this._ownProvinceTeamCount; } get ownCityTeamCount(): number { return this._ownCityTeamCount; } get provinceTeamPercentage(): number { return this._provinceTeamPercentage; } get cityTeamPercentage(): number { return this._cityTeamPercentage; } get provinceCityDistribution(): ProvinceCityDistribution { return this._provinceCityDistribution; } get lastCalcAt(): Date | null { return this._lastCalcAt; } get createdAt(): Date { return this._createdAt; } get domainEvents(): DomainEvent[] { return [...this._domainEvents]; } // ============ 工厂方法 ============ static create(userId: bigint): TeamStatistics { return new TeamStatistics(UserId.create(userId)); } // ============ 领域行为 ============ /** * 增加直推注册 */ addDirectReferral(referralUserId: bigint): void { this._directReferralCount++; this._totalTeamCount++; // 初始化直推团队数据 if (!this._directTeamPlantingData.find(d => d.userId === referralUserId)) { this._directTeamPlantingData.push({ userId: referralUserId, personalCount: 0, teamCount: 0, amount: 0, }); } this._lastCalcAt = new Date(); } /** * 增加团队成员 (非直推) */ addTeamMember(): void { this._totalTeamCount++; this._lastCalcAt = new Date(); } /** * 记录自己的认种 */ addSelfPlanting( treeCount: number, amount: number, provinceCode: string, cityCode: string, userProvince?: string, userCity?: string, ): void { this._selfPlantingCount += treeCount; this._selfPlantingAmount += amount; // 自己的认种也计入团队 this._totalTeamPlantingCount += treeCount; this._totalTeamPlantingAmount += amount; // 更新省市分布 this._provinceCityDistribution = this._provinceCityDistribution.add( provinceCode, cityCode, treeCount, ); // 更新本省本市统计 if (userProvince && provinceCode === userProvince) { this._ownProvinceTeamCount += treeCount; } if (userCity && cityCode === userCity) { this._ownCityTeamCount += treeCount; } // 重新计算龙虎榜分值和占比 this._recalculateLeaderboardScore(); this._recalculatePercentages(); this._lastCalcAt = new Date(); this._domainEvents.push(new TeamStatisticsUpdatedEvent({ userId: this._userId.toString(), selfPlantingCount: this._selfPlantingCount, teamPlantingCount: this._totalTeamPlantingCount, leaderboardScore: this._leaderboardScore.score, })); } /** * 更新团队认种数据 (下级用户认种时调用) */ updatePlanting(params: { isDirectReferral: boolean; directReferralId?: bigint; treeCount: number; amount: number; province: string; city: string; userProvince?: string; userCity?: string; }): void { // 1. 更新团队总认种 this._totalTeamPlantingCount += params.treeCount; this._totalTeamPlantingAmount += params.amount; // 2. 如果是直推 if (params.isDirectReferral && params.directReferralId) { this._directPlantingCount += params.treeCount; // 更新直推明细 const directTeam = this._directTeamPlantingData.find( d => d.userId === params.directReferralId ); if (directTeam) { directTeam.personalCount += params.treeCount; directTeam.teamCount += params.treeCount; directTeam.amount += params.amount; } } else if (params.directReferralId) { // 非直推但需要更新直推的团队数据 const directTeam = this._directTeamPlantingData.find( d => d.userId === params.directReferralId ); if (directTeam) { directTeam.teamCount += params.treeCount; directTeam.amount += params.amount; } } // 3. 更新省市分布 this._provinceCityDistribution = this._provinceCityDistribution.add( params.province, params.city, params.treeCount, ); // 4. 更新本省本市统计 if (params.userProvince && params.province === params.userProvince) { this._ownProvinceTeamCount += params.treeCount; } if (params.userCity && params.city === params.userCity) { this._ownCityTeamCount += params.treeCount; } // 5. 重新计算龙虎榜分值和占比 this._recalculateLeaderboardScore(); this._recalculatePercentages(); this._lastCalcAt = new Date(); this._domainEvents.push(new TeamStatisticsUpdatedEvent({ userId: this._userId.toString(), selfPlantingCount: this._selfPlantingCount, teamPlantingCount: this._totalTeamPlantingCount, leaderboardScore: this._leaderboardScore.score, triggeredByUserId: params.directReferralId?.toString(), })); } /** * 更新直推团队认种数 */ updateDirectReferralTeamPlanting( directReferralId: bigint, teamPlantingCount: number, ): void { const directTeam = this._directTeamPlantingData.find( d => d.userId === directReferralId ); if (directTeam) { directTeam.teamCount = teamPlantingCount; } this._recalculateLeaderboardScore(); this._lastCalcAt = new Date(); } // ============ 私有方法 ============ private _recalculateLeaderboardScore(): void { const directCounts = this._directTeamPlantingData.map(d => d.teamCount); this._leaderboardScore = LeaderboardScore.calculate( this._totalTeamPlantingCount, directCounts, ); } private _recalculatePercentages(): void { if (this._totalTeamPlantingCount > 0) { this._provinceTeamPercentage = (this._ownProvinceTeamCount / this._totalTeamPlantingCount) * 100; this._cityTeamPercentage = (this._ownCityTeamCount / this._totalTeamPlantingCount) * 100; } else { this._provinceTeamPercentage = 0; this._cityTeamPercentage = 0; } } setId(id: bigint): void { this._id = id; } clearDomainEvents(): void { this._domainEvents = []; } // ============ 重建 ============ static reconstitute(data: { id: bigint; userId: bigint; directReferralCount: number; totalTeamCount: number; selfPlantingCount: number; selfPlantingAmount: number; directPlantingCount: number; totalTeamPlantingCount: number; totalTeamPlantingAmount: number; directTeamPlantingData: DirectTeamData[]; ownProvinceTeamCount: number; ownCityTeamCount: number; provinceCityDistribution: Record> | null; lastCalcAt: Date | null; createdAt: Date; }): TeamStatistics { const stats = new TeamStatistics(UserId.create(data.userId)); stats._id = data.id; stats._directReferralCount = data.directReferralCount; stats._totalTeamCount = data.totalTeamCount; stats._selfPlantingCount = data.selfPlantingCount; stats._selfPlantingAmount = data.selfPlantingAmount; stats._directPlantingCount = data.directPlantingCount; stats._totalTeamPlantingCount = data.totalTeamPlantingCount; stats._totalTeamPlantingAmount = data.totalTeamPlantingAmount; stats._directTeamPlantingData = data.directTeamPlantingData || []; stats._ownProvinceTeamCount = data.ownProvinceTeamCount; stats._ownCityTeamCount = data.ownCityTeamCount; stats._provinceCityDistribution = ProvinceCityDistribution.fromJson( data.provinceCityDistribution ); stats._lastCalcAt = data.lastCalcAt; // 重新计算龙虎榜分值和占比 stats._recalculateLeaderboardScore(); stats._recalculatePercentages(); return stats; } } ``` ### 3.4 仓储接口 (Repository Interfaces) #### 3.4.1 src/domain/repositories/referral-relationship.repository.interface.ts ```typescript import { ReferralRelationship } from '../aggregates/referral-relationship/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; findByAncestor(ancestorId: bigint): Promise; } export const REFERRAL_RELATIONSHIP_REPOSITORY = Symbol('IReferralRelationshipRepository'); ``` #### 3.4.2 src/domain/repositories/team-statistics.repository.interface.ts ```typescript import { TeamStatistics } from '../aggregates/team-statistics/team-statistics.aggregate'; export interface ITeamStatisticsRepository { save(stats: TeamStatistics): Promise; findByUserId(userId: bigint): Promise; getOrCreate(userId: bigint): Promise; findTopByLeaderboardScore(limit: number): Promise; findByProvinceCityRanking( provinceCode: string, cityCode: string | null, limit: number, ): Promise; findByUserIds(userIds: bigint[]): Promise>; } export const TEAM_STATISTICS_REPOSITORY = Symbol('ITeamStatisticsRepository'); ``` #### 3.4.3 src/domain/repositories/index.ts ```typescript export * from './referral-relationship.repository.interface'; export * from './team-statistics.repository.interface'; ``` ### 3.5 领域服务 (Domain Services) #### 3.5.1 src/domain/services/referral-chain.service.ts ```typescript import { Injectable } from '@nestjs/common'; import { ReferralRelationship } from '../aggregates/referral-relationship/referral-relationship.aggregate'; @Injectable() export class ReferralChainService { /** * 获取用户的推荐链 (用于分享权益分配) * 返回从直接推荐人到最远祖先的用户ID列表 */ getReferralChain(relationship: ReferralRelationship): bigint[] { return relationship.getAllAncestorIds(); } /** * 查找两个用户的最近公共祖先 */ findCommonAncestor( chain1: bigint[], chain2: bigint[], ): bigint | null { const set1 = new Set(chain1.map(id => id.toString())); for (const id of chain2) { if (set1.has(id.toString())) { return id; } } return null; } /** * 计算两个用户之间的推荐距离 */ calculateDistance( relationship1: ReferralRelationship, relationship2: ReferralRelationship, ): number | null { const chain1 = [relationship1.userId.value, ...relationship1.getAllAncestorIds()]; const chain2 = [relationship2.userId.value, ...relationship2.getAllAncestorIds()]; // 检查 user2 是否在 user1 的链上 const idx1 = chain1.findIndex(id => id === relationship2.userId.value); if (idx1 >= 0) return idx1; // 检查 user1 是否在 user2 的链上 const idx2 = chain2.findIndex(id => id === relationship1.userId.value); if (idx2 >= 0) return idx2; // 寻找公共祖先 for (let i = 0; i < chain1.length; i++) { const idx = chain2.findIndex(id => id === chain1[i]); if (idx >= 0) { return i + idx; } } return null; // 不在同一棵树上 } } ``` #### 3.5.2 src/domain/services/leaderboard-calculator.service.ts ```typescript import { Injectable } from '@nestjs/common'; import { TeamStatistics } from '../aggregates/team-statistics/team-statistics.aggregate'; export interface LeaderboardEntry { userId: bigint; score: number; teamPlantingCount: number; maxDirectTeamCount: number; rank: number; } @Injectable() export class LeaderboardCalculatorService { /** * 计算龙虎榜排名 * * 龙虎榜分值 = 团队总认种量 - 最大单个直推团队认种量 */ calculateLeaderboard( allStats: TeamStatistics[], limit = 100, ): LeaderboardEntry[] { // 按龙虎榜分值排序 const sorted = allStats .filter(s => s.effectivePlantingCountForRanking > 0) .sort((a, b) => b.effectivePlantingCountForRanking - a.effectivePlantingCountForRanking); // 取前 limit 名并添加排名 return sorted.slice(0, limit).map((stats, index) => ({ userId: stats.userId.value, score: stats.effectivePlantingCountForRanking, teamPlantingCount: stats.totalTeamPlantingCount, maxDirectTeamCount: stats.maxSingleTeamPlantingCount, rank: index + 1, })); } /** * 获取用户在龙虎榜中的排名 */ getUserRank( allStats: TeamStatistics[], userId: bigint, ): number | null { const sorted = allStats .filter(s => s.effectivePlantingCountForRanking > 0) .sort((a, b) => b.effectivePlantingCountForRanking - a.effectivePlantingCountForRanking); const index = sorted.findIndex(s => s.userId.value === userId); return index >= 0 ? index + 1 : null; } } ``` #### 3.5.3 src/domain/services/team-aggregation.service.ts ```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/team-statistics.aggregate'; @Injectable() export class TeamAggregationService { constructor( @Inject(TEAM_STATISTICS_REPOSITORY) private readonly statsRepository: ITeamStatisticsRepository, @Inject(REFERRAL_RELATIONSHIP_REPOSITORY) private readonly relationshipRepository: IReferralRelationshipRepository, ) {} /** * 当用户认种时,更新所有上级的团队统计 */ async updateAncestorTeamStats( userId: bigint, treeCount: number, amount: number, provinceCode: string, cityCode: string, userProvince?: string, userCity?: string, ): Promise { // 1. 获取用户的推荐关系 const relationship = await this.relationshipRepository.findByUserId(userId); if (!relationship) { throw new Error(`用户 ${userId} 的推荐关系不存在`); } // 2. 更新自己的统计 const selfStats = await this.statsRepository.getOrCreate(userId); selfStats.addSelfPlanting(treeCount, amount, provinceCode, cityCode, userProvince, userCity); await this.statsRepository.save(selfStats); // 3. 获取推荐链 (所有上级) const ancestors = relationship.getAllAncestorIds(); if (ancestors.length === 0) return; // 4. 找到直接推荐人 (第一层上级) const directReferrerId = ancestors[0]; // 5. 更新所有上级的团队统计 for (let i = 0; i < ancestors.length; i++) { const ancestorId = ancestors[i]; const ancestorStats = await this.statsRepository.getOrCreate(ancestorId); // 对于每个祖先,确定这个认种是通过哪个直推传递来的 const directUserIdForAncestor = i === 0 ? userId : ancestors[i - 1]; ancestorStats.updatePlanting({ isDirectReferral: i === 0, // 只有第一层是直推 directReferralId: directUserIdForAncestor, treeCount, amount, province: provinceCode, city: cityCode, userProvince, userCity, }); await this.statsRepository.save(ancestorStats); } } /** * 当新用户注册时,更新所有上级的团队人数 */ async updateAncestorTeamMemberCount( userId: bigint, directReferrerId: bigint, ): Promise { // 1. 更新直接推荐人 const directReferrerStats = await this.statsRepository.getOrCreate(directReferrerId); directReferrerStats.addDirectReferral(userId); await this.statsRepository.save(directReferrerStats); // 2. 获取直接推荐人的推荐关系 const directReferrerRelationship = await this.relationshipRepository.findByUserId(directReferrerId); if (!directReferrerRelationship) return; // 3. 更新所有上级的团队人数 const ancestors = directReferrerRelationship.getAllAncestorIds(); for (const ancestorId of ancestors) { const ancestorStats = await this.statsRepository.getOrCreate(ancestorId); ancestorStats.addTeamMember(); await this.statsRepository.save(ancestorStats); } } } ``` #### 3.5.4 src/domain/services/index.ts ```typescript export * from './referral-chain.service'; export * from './leaderboard-calculator.service'; export * from './team-aggregation.service'; ``` --- ## 领域不变式 (Domain Invariants) ```typescript // 文档参考,不需要创建此文件 class ReferralContextInvariants { // 1. 推荐关系不可修改 static REFERRAL_RELATIONSHIP_IMMUTABLE = "推荐关系一旦建立,终生不可修改" // 2. 祖先路径必须完整 static ANCESTOR_PATH_COMPLETE = "祖先路径必须包含从根节点到当前节点的所有上级" // 3. 不能推荐自己 static CANNOT_REFER_SELF = "用户不能推荐自己" // 4. 团队统计必须实时更新 static TEAM_STATS_REALTIME = "团队统计数据必须在认种后实时更新" // 5. 龙虎榜分值计算规则 static LEADERBOARD_SCORE_RULE = "龙虎榜分值 = 团队总认种量 - 最大单个直推团队认种量" // 6. 推荐链最多10层 static MAX_REFERRAL_DEPTH = "推荐链深度限制为10层,超过的不计入" } ``` --- ## API 端点设计 | 方法 | 路径 | 描述 | 认证 | |------|------|------|------| | GET | `/health` | 健康检查 | 否 | | GET | `/referrals/me` | 获取我的推荐信息 (分享页) | JWT | | GET | `/referrals/direct` | 获取我的直推列表 | JWT | | GET | `/referrals/chain` | 获取推荐链 (内部API,用于资金分配) | JWT | | GET | `/team/statistics` | 获取我的团队统计 | JWT | | GET | `/team/province-city-ranking` | 获取省/市团队排名 | JWT | | GET | `/leaderboard` | 获取龙虎榜 | JWT | | POST | `/internal/referral` | 创建推荐关系 (内部调用) | API-KEY | --- ## 与前端页面对应关系 ### 向导页5 (分享引导页) - 显示用户的推荐码 - 显示分享链接/二维码 - 调用: `GET /referrals/me` ### 分享页 - 显示我的推荐码和分享链接 - 显示直推人数、团队人数 - 显示团队认种统计 - 调用: `GET /referrals/me` - 调用: `GET /team/statistics` ### 直推列表 - 显示我的直推用户列表 - 显示每个直推的认种情况 - 调用: `GET /referrals/direct?page=1&pageSize=20` ### 龙虎榜 - 显示全平台龙虎榜排名 - 调用: `GET /leaderboard?limit=100` --- ## 事件订阅 (Kafka Events) ### 订阅的事件 | Topic | 事件类型 | 触发条件 | 处理逻辑 | |-------|---------|---------|---------| | `identity.user.created` | UserAccountCreatedEvent | 用户注册成功 | 创建推荐关系,更新上级团队人数 | | `planting.order.paid` | PlantingOrderPaidEvent | 认种订单支付成功 | 更新认种用户及所有上级的团队统计 | ### 发布的事件 | Topic | 事件类型 | 触发条件 | |-------|---------|---------| | `referral.relationship.created` | ReferralRelationshipCreatedEvent | 推荐关系创建成功 | | `referral.statistics.updated` | TeamStatisticsUpdatedEvent | 团队统计更新 | --- ## 开发顺序建议 1. **Phase 1: 项目初始化** - 创建NestJS项目 - 安装依赖 - 配置环境变量和TypeScript 2. **Phase 2: 数据库层** - 创建Prisma Schema - 运行迁移 - 创建PrismaService 3. **Phase 3: 领域层 (最重要)** - 实现所有值对象 - 实现聚合根 (ReferralRelationship, TeamStatistics) - 实现领域事件 - 实现领域服务 - 编写单元测试 4. **Phase 4: 基础设施层** - 实现仓储 (Repository Implementations) - 实现Kafka消费者和发布者 - 实现Redis缓存服务 - 实现外部服务客户端 5. **Phase 5: 应用层** - 实现应用服务 (ReferralApplicationService) - 实现Command/Query handlers 6. **Phase 6: API层** - 实现DTO - 实现Controllers - 配置Swagger文档 - 配置JWT认证 7. **Phase 7: 测试和部署** - 集成测试 - E2E测试 - Docker配置 - CI/CD配置 --- ## 注意事项 1. **推荐关系在用户注册时创建**:由 identity-service 发布 `UserAccountCreatedEvent`,本服务订阅并创建推荐关系 2. **团队统计在认种时更新**:由 planting-service 发布 `PlantingOrderPaidEvent`,本服务订阅并更新所有上级的统计 3. **龙虎榜分值设计目的**:鼓励均衡发展团队,防止"单腿"发展 4. **省市权益分配**:根据省市团队占比计算,用于省/市代理权益分配 5. **使用 PostgreSQL 数组类型**:存储推荐链,方便查询 6. **直推团队数据使用 JSON 类型**:便于灵活扩展 7. **保持与 identity-service 架构一致**:确保代码风格、目录结构、命名规范统一