diff --git a/backend/services/referral-service/src/api/controllers/index.ts b/backend/services/referral-service/src/api/controllers/index.ts index 2c5426c4..5be60f3a 100644 --- a/backend/services/referral-service/src/api/controllers/index.ts +++ b/backend/services/referral-service/src/api/controllers/index.ts @@ -1,4 +1,3 @@ export * from './referral.controller'; -export * from './leaderboard.controller'; export * from './team-statistics.controller'; export * from './health.controller'; diff --git a/backend/services/referral-service/src/api/controllers/leaderboard.controller.ts b/backend/services/referral-service/src/api/controllers/leaderboard.controller.ts deleted file mode 100644 index 90d1c87b..00000000 --- a/backend/services/referral-service/src/api/controllers/leaderboard.controller.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Controller, Get, Query, Param, UseGuards } from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBearerAuth, - ApiParam, -} from '@nestjs/swagger'; -import { JwtAuthGuard } from '../guards'; -import { CurrentUser } from '../decorators'; -import { TeamStatisticsService } from '../../application/services'; -import { - GetLeaderboardDto, - LeaderboardResponseDto, - UserRankResponseDto, -} from '../dto'; -import { GetLeaderboardQuery } from '../../application/queries'; - -@ApiTags('Leaderboard') -@Controller('leaderboard') -export class LeaderboardController { - constructor(private readonly teamStatisticsService: TeamStatisticsService) {} - - @Get() - @ApiOperation({ summary: '获取龙虎榜排名' }) - @ApiResponse({ status: 200, type: LeaderboardResponseDto }) - async getLeaderboard(@Query() dto: GetLeaderboardDto): Promise { - const query = new GetLeaderboardQuery(dto.limit, dto.offset); - return this.teamStatisticsService.getLeaderboard(query); - } - - @Get('me') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth() - @ApiOperation({ summary: '获取当前用户龙虎榜排名' }) - @ApiResponse({ status: 200, type: UserRankResponseDto }) - async getMyRank(@CurrentUser('userId') userId: bigint): Promise { - const rank = await this.teamStatisticsService.getUserRank(userId); - const leaderboard = await this.teamStatisticsService.getLeaderboard( - new GetLeaderboardQuery(1000, 0), - ); - const userEntry = leaderboard.entries.find((e) => e.userId === userId.toString()); - - return { - userId: userId.toString(), - rank, - score: userEntry?.score ?? 0, - }; - } - - @Get('user/:userId') - @ApiOperation({ summary: '获取指定用户龙虎榜排名' }) - @ApiParam({ name: 'userId', description: '用户ID' }) - @ApiResponse({ status: 200, type: UserRankResponseDto }) - async getUserRank(@Param('userId') userId: string): Promise { - const userIdBigInt = BigInt(userId); - const rank = await this.teamStatisticsService.getUserRank(userIdBigInt); - const leaderboard = await this.teamStatisticsService.getLeaderboard( - new GetLeaderboardQuery(1000, 0), - ); - const userEntry = leaderboard.entries.find((e) => e.userId === userId); - - return { - userId, - rank, - score: userEntry?.score ?? 0, - }; - } -} diff --git a/backend/services/referral-service/src/api/dto/index.ts b/backend/services/referral-service/src/api/dto/index.ts index d188cc08..26332ea7 100644 --- a/backend/services/referral-service/src/api/dto/index.ts +++ b/backend/services/referral-service/src/api/dto/index.ts @@ -1,3 +1,2 @@ export * from './referral.dto'; -export * from './leaderboard.dto'; export * from './team-statistics.dto'; diff --git a/backend/services/referral-service/src/api/dto/leaderboard.dto.ts b/backend/services/referral-service/src/api/dto/leaderboard.dto.ts deleted file mode 100644 index 8e7443db..00000000 --- a/backend/services/referral-service/src/api/dto/leaderboard.dto.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { IsInt, Min, Max, IsOptional } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -export class GetLeaderboardDto { - @ApiPropertyOptional({ description: '每页数量', default: 100 }) - @IsOptional() - @IsInt() - @Min(1) - @Max(100) - limit?: number = 100; - - @ApiPropertyOptional({ description: '偏移量', default: 0 }) - @IsOptional() - @IsInt() - @Min(0) - offset?: number = 0; -} - -export class LeaderboardEntryResponseDto { - @ApiProperty({ description: '排名' }) - rank: number; - - @ApiProperty({ description: '用户ID' }) - userId: string; - - @ApiProperty({ description: '龙虎榜分值' }) - score: number; - - @ApiProperty({ description: '团队总认种量' }) - totalTeamCount: number; - - @ApiProperty({ description: '直推人数' }) - directReferralCount: number; -} - -export class LeaderboardResponseDto { - @ApiProperty({ type: [LeaderboardEntryResponseDto] }) - entries: LeaderboardEntryResponseDto[]; - - @ApiProperty({ description: '总数' }) - total: number; - - @ApiProperty({ description: '是否有更多' }) - hasMore: boolean; -} - -export class UserRankResponseDto { - @ApiProperty({ description: '用户ID' }) - userId: string; - - @ApiProperty({ description: '排名', nullable: true }) - rank: number | null; - - @ApiProperty({ description: '龙虎榜分值' }) - score: number; -} diff --git a/backend/services/referral-service/src/application/queries/get-leaderboard.query.ts b/backend/services/referral-service/src/application/queries/get-leaderboard.query.ts deleted file mode 100644 index 99ad241e..00000000 --- a/backend/services/referral-service/src/application/queries/get-leaderboard.query.ts +++ /dev/null @@ -1,20 +0,0 @@ -export class GetLeaderboardQuery { - constructor( - public readonly limit: number = 100, - public readonly offset: number = 0, - ) {} -} - -export interface LeaderboardEntryResult { - rank: number; - userId: string; - score: number; - totalTeamCount: number; - directReferralCount: number; -} - -export interface LeaderboardResult { - entries: LeaderboardEntryResult[]; - total: number; - hasMore: boolean; -} diff --git a/backend/services/referral-service/src/application/queries/index.ts b/backend/services/referral-service/src/application/queries/index.ts index f2892848..92806165 100644 --- a/backend/services/referral-service/src/application/queries/index.ts +++ b/backend/services/referral-service/src/application/queries/index.ts @@ -1,4 +1,3 @@ export * from './get-user-referral-info.query'; -export * from './get-leaderboard.query'; export * from './get-direct-referrals.query'; export * from './get-province-city-distribution.query'; diff --git a/backend/services/referral-service/src/application/services/team-statistics.service.ts b/backend/services/referral-service/src/application/services/team-statistics.service.ts index 34bc51c7..0193437f 100644 --- a/backend/services/referral-service/src/application/services/team-statistics.service.ts +++ b/backend/services/referral-service/src/application/services/team-statistics.service.ts @@ -6,11 +6,9 @@ import { ITeamStatisticsRepository, ReferralChainService, } from '../../domain'; -import { EventPublisherService, LeaderboardCacheService } from '../../infrastructure'; +import { EventPublisherService } from '../../infrastructure'; import { UpdateTeamStatisticsCommand } from '../commands'; import { - GetLeaderboardQuery, - LeaderboardResult, GetProvinceCityDistributionQuery, ProvinceCityDistributionResult, } from '../queries'; @@ -26,7 +24,6 @@ export class TeamStatisticsService { private readonly teamStatsRepo: ITeamStatisticsRepository, private readonly referralChainService: ReferralChainService, private readonly eventPublisher: EventPublisherService, - private readonly leaderboardCache: LeaderboardCacheService, ) {} /** @@ -45,7 +42,6 @@ export class TeamStatisticsService { if (userStats) { userStats.addPersonalPlanting(command.plantingCount, command.provinceCode, command.cityCode); await this.teamStatsRepo.save(userStats); - await this.leaderboardCache.updateScore(command.userId, userStats.leaderboardScore); await this.eventPublisher.publishDomainEvents(userStats.domainEvents); userStats.clearDomainEvents(); } @@ -65,7 +61,6 @@ export class TeamStatisticsService { // 第一级上级 (直接推荐人) 的 fromDirectReferralId 是当前用户 // 第二级及以上的 fromDirectReferralId 是第一级上级 - let currentSource = command.userId; for (let i = 0; i < ancestors.length; i++) { const ancestorId = ancestors[i]; @@ -81,90 +76,11 @@ export class TeamStatisticsService { // 批量更新 await this.teamStatsRepo.batchUpdateTeamCounts(updates); - // 更新排行榜缓存 - for (const ancestorId of ancestors) { - const stats = await this.teamStatsRepo.findByUserId(ancestorId); - if (stats) { - await this.leaderboardCache.updateScore(ancestorId, stats.leaderboardScore); - } - } - this.logger.log( `Updated team statistics for ${ancestors.length} ancestors of user ${command.userId}`, ); } - /** - * 获取龙虎榜 - */ - async getLeaderboard(query: GetLeaderboardQuery): Promise { - // 尝试从缓存获取 - const cachedEntries = await this.leaderboardCache.getTopN(query.limit); - if (cachedEntries.length > 0) { - // 补充完整数据 - const userIds = cachedEntries.map((e) => e.userId); - const statsMap = new Map(); - const stats = await this.teamStatsRepo.findByUserIds(userIds); - stats.forEach((s) => { - statsMap.set(s.userId.toString(), { - totalTeamCount: s.totalTeamCount, - directReferralCount: s.directReferralCount, - }); - }); - - const entries = cachedEntries.map((e) => { - const extra = statsMap.get(e.userId.toString()); - return { - rank: e.rank, - userId: e.userId.toString(), - score: e.score, - totalTeamCount: extra?.totalTeamCount ?? 0, - directReferralCount: extra?.directReferralCount ?? 0, - }; - }); - - return { - entries, - total: entries.length, - hasMore: false, // 缓存暂不支持分页 - }; - } - - // 从数据库获取 - const dbEntries = await this.teamStatsRepo.getLeaderboard({ - limit: query.limit, - offset: query.offset, - }); - - const entries = dbEntries.map((e) => ({ - rank: e.rank, - userId: e.userId.toString(), - score: e.score, - totalTeamCount: e.totalTeamCount, - directReferralCount: e.directReferralCount, - })); - - return { - entries, - total: entries.length, - hasMore: entries.length === query.limit, - }; - } - - /** - * 获取用户排名 - */ - async getUserRank(userId: bigint): Promise { - // 先尝试缓存 - const cachedRank = await this.leaderboardCache.getUserRank(userId); - if (cachedRank !== null) { - return cachedRank; - } - - // 从数据库获取 - return this.teamStatsRepo.getUserRank(userId); - } - /** * 获取省市分布统计 */ diff --git a/backend/services/referral-service/src/infrastructure/cache/index.ts b/backend/services/referral-service/src/infrastructure/cache/index.ts index c5276a20..321457bf 100644 --- a/backend/services/referral-service/src/infrastructure/cache/index.ts +++ b/backend/services/referral-service/src/infrastructure/cache/index.ts @@ -1,2 +1 @@ export * from './redis.service'; -export * from './leaderboard-cache.service'; diff --git a/backend/services/referral-service/src/infrastructure/cache/leaderboard-cache.service.ts b/backend/services/referral-service/src/infrastructure/cache/leaderboard-cache.service.ts deleted file mode 100644 index 52eb391e..00000000 --- a/backend/services/referral-service/src/infrastructure/cache/leaderboard-cache.service.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { RedisService } from './redis.service'; -import { LeaderboardEntry } from '../../domain'; - -const LEADERBOARD_KEY = 'leaderboard:scores'; -const LEADERBOARD_CACHE_KEY = 'leaderboard:cache'; -const CACHE_TTL_SECONDS = 300; // 5分钟 - -@Injectable() -export class LeaderboardCacheService { - private readonly logger = new Logger(LeaderboardCacheService.name); - - constructor(private readonly redisService: RedisService) {} - - /** - * 更新用户的龙虎榜分值 - */ - async updateScore(userId: bigint, score: number): Promise { - await this.redisService.zadd(LEADERBOARD_KEY, score, userId.toString()); - this.logger.debug(`Updated leaderboard score for user ${userId}: ${score}`); - } - - /** - * 获取用户排名 (0-based) - */ - async getUserRank(userId: bigint): Promise { - const rank = await this.redisService.zrevrank(LEADERBOARD_KEY, userId.toString()); - return rank !== null ? rank + 1 : null; - } - - /** - * 获取排行榜前N名 - */ - async getTopN(n: number): Promise { - // 尝试从缓存获取 - const cached = await this.redisService.get(`${LEADERBOARD_CACHE_KEY}:top${n}`); - if (cached) { - return cached; - } - - // 从有序集合获取 - const items = await this.redisService.zrevrangeWithScores(LEADERBOARD_KEY, 0, n - 1); - const entries: LeaderboardEntry[] = items.map((item, index) => ({ - userId: BigInt(item.member), - score: item.score, - rank: index + 1, - totalTeamCount: 0, // 需要从数据库补充 - directReferralCount: 0, - })); - - // 缓存结果 - await this.redisService.set(`${LEADERBOARD_CACHE_KEY}:top${n}`, entries, CACHE_TTL_SECONDS); - - return entries; - } - - /** - * 增量更新分值 - */ - async incrementScore(userId: bigint, delta: number): Promise { - return this.redisService.zincrby(LEADERBOARD_KEY, delta, userId.toString()); - } - - /** - * 批量更新分值 - */ - async batchUpdateScores(updates: Array<{ userId: bigint; score: number }>): Promise { - for (const update of updates) { - await this.updateScore(update.userId, update.score); - } - // 清除缓存 - await this.invalidateCache(); - } - - /** - * 清除排行榜缓存 - */ - async invalidateCache(): Promise { - // 清除所有排行榜缓存 - for (const n of [10, 50, 100]) { - await this.redisService.del(`${LEADERBOARD_CACHE_KEY}:top${n}`); - } - this.logger.debug('Leaderboard cache invalidated'); - } -} diff --git a/backend/services/referral-service/src/modules/api.module.ts b/backend/services/referral-service/src/modules/api.module.ts index c06bb111..a8fa828d 100644 --- a/backend/services/referral-service/src/modules/api.module.ts +++ b/backend/services/referral-service/src/modules/api.module.ts @@ -3,7 +3,6 @@ import { ConfigModule } from '@nestjs/config'; import { ApplicationModule } from './application.module'; import { ReferralController, - LeaderboardController, TeamStatisticsController, HealthController, } from '../api'; @@ -13,7 +12,6 @@ import { InternalReferralController } from '../api/controllers/referral.controll imports: [ConfigModule, ApplicationModule], controllers: [ ReferralController, - LeaderboardController, TeamStatisticsController, HealthController, InternalReferralController, diff --git a/backend/services/referral-service/src/modules/infrastructure.module.ts b/backend/services/referral-service/src/modules/infrastructure.module.ts index 4a709c35..44f796f5 100644 --- a/backend/services/referral-service/src/modules/infrastructure.module.ts +++ b/backend/services/referral-service/src/modules/infrastructure.module.ts @@ -7,7 +7,6 @@ import { KafkaService, EventPublisherService, RedisService, - LeaderboardCacheService, } from '../infrastructure'; import { REFERRAL_RELATIONSHIP_REPOSITORY, @@ -22,7 +21,6 @@ import { KafkaService, RedisService, EventPublisherService, - LeaderboardCacheService, { provide: REFERRAL_RELATIONSHIP_REPOSITORY, useClass: ReferralRelationshipRepository, @@ -37,7 +35,6 @@ import { KafkaService, RedisService, EventPublisherService, - LeaderboardCacheService, REFERRAL_RELATIONSHIP_REPOSITORY, TEAM_STATISTICS_REPOSITORY, ], diff --git a/backend/services/referral-service/test/domain/services/leaderboard-calculation.service.spec.ts b/backend/services/referral-service/test/domain/services/leaderboard-calculation.service.spec.ts deleted file mode 100644 index 5ac41c51..00000000 --- a/backend/services/referral-service/test/domain/services/leaderboard-calculation.service.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { LeaderboardCalculationService } from '../../../src/domain/services/leaderboard-calculation.service'; -import { LeaderboardScore } from '../../../src/domain/value-objects'; - -describe('LeaderboardCalculationService', () => { - let service: LeaderboardCalculationService; - - beforeEach(() => { - service = new LeaderboardCalculationService(); - }); - - describe('calculateScore', () => { - it('should calculate score correctly', () => { - const stats = [ - { referralId: 1n, teamCount: 30 }, - { referralId: 2n, teamCount: 40 }, - { referralId: 3n, teamCount: 30 }, - ]; - const score = service.calculateScore(100, stats); - - expect(score.totalTeamCount).toBe(100); - expect(score.maxDirectTeamCount).toBe(40); - expect(score.score).toBe(60); - }); - - it('should return zero score for empty teams', () => { - const score = service.calculateScore(0, []); - - expect(score.score).toBe(0); - }); - }); - - describe('updateScoreOnPlanting', () => { - it('should update score when planting added to direct referral team', () => { - const currentScore = LeaderboardScore.calculate(100, [30, 40, 30]); - const stats = [ - { referralId: 1n, teamCount: 30 }, - { referralId: 2n, teamCount: 40 }, - { referralId: 3n, teamCount: 30 }, - ]; - - const newScore = service.updateScoreOnPlanting(currentScore, 10, 1n, stats); - - // New total = 110, new max = 40 (unchanged), score = 70 - expect(newScore.totalTeamCount).toBe(110); - expect(newScore.score).toBe(70); - }); - - it('should update score when max team increases', () => { - const currentScore = LeaderboardScore.calculate(100, [30, 40, 30]); - const stats = [ - { referralId: 1n, teamCount: 30 }, - { referralId: 2n, teamCount: 40 }, - { referralId: 3n, teamCount: 30 }, - ]; - - const newScore = service.updateScoreOnPlanting(currentScore, 20, 2n, stats); - - // New total = 120, new max = 60, score = 60 - expect(newScore.totalTeamCount).toBe(120); - expect(newScore.maxDirectTeamCount).toBe(60); - expect(newScore.score).toBe(60); - }); - }); - - describe('compareRank', () => { - it('should correctly compare for ranking', () => { - const scoreA = LeaderboardScore.calculate(100, [40, 30, 30]); // score = 60 - const scoreB = LeaderboardScore.calculate(80, [30, 30, 20]); // score = 50 - - expect(service.compareRank(scoreA, scoreB)).toBeLessThan(0); // A ranks higher - }); - }); - - describe('validateScore', () => { - it('should return true for valid score', () => { - const score = LeaderboardScore.calculate(100, [40, 30, 30]); - expect(service.validateScore(score, 100, 40)).toBe(true); - }); - - it('should return false for invalid score', () => { - const score = LeaderboardScore.calculate(100, [40, 30, 30]); - expect(service.validateScore(score, 100, 50)).toBe(false); - }); - }); -}); diff --git a/backend/services/referral-service/test/domain/value-objects/leaderboard-score.vo.spec.ts b/backend/services/referral-service/test/domain/value-objects/leaderboard-score.vo.spec.ts deleted file mode 100644 index 87360227..00000000 --- a/backend/services/referral-service/test/domain/value-objects/leaderboard-score.vo.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { LeaderboardScore } from '../../../src/domain/value-objects/leaderboard-score.vo'; - -describe('LeaderboardScore Value Object', () => { - describe('calculate', () => { - it('should calculate score correctly with single team', () => { - const score = LeaderboardScore.calculate(100, [100]); - expect(score.totalTeamCount).toBe(100); - expect(score.maxDirectTeamCount).toBe(100); - expect(score.score).toBe(0); // 100 - 100 = 0 - }); - - it('should calculate score correctly with multiple teams', () => { - const score = LeaderboardScore.calculate(100, [30, 40, 30]); - expect(score.totalTeamCount).toBe(100); - expect(score.maxDirectTeamCount).toBe(40); - expect(score.score).toBe(60); // 100 - 40 = 60 - }); - - it('should calculate score correctly with no teams', () => { - const score = LeaderboardScore.calculate(0, []); - expect(score.totalTeamCount).toBe(0); - expect(score.maxDirectTeamCount).toBe(0); - expect(score.score).toBe(0); - }); - - it('should not return negative score', () => { - const score = LeaderboardScore.calculate(50, [80]); - expect(score.score).toBe(0); - }); - - it('should encourage balanced teams', () => { - // 不均衡: 100 total, max 80 -> score = 20 - const unbalanced = LeaderboardScore.calculate(100, [80, 10, 10]); - // 均衡: 100 total, max 34 -> score = 66 - const balanced = LeaderboardScore.calculate(100, [34, 33, 33]); - - expect(balanced.score).toBeGreaterThan(unbalanced.score); - }); - }); - - describe('zero', () => { - it('should create zero score', () => { - const score = LeaderboardScore.zero(); - expect(score.totalTeamCount).toBe(0); - expect(score.maxDirectTeamCount).toBe(0); - expect(score.score).toBe(0); - }); - }); - - describe('recalculate', () => { - it('should recalculate with new values', () => { - const initial = LeaderboardScore.calculate(50, [30, 20]); - const updated = initial.recalculate(100, [50, 50]); - - expect(updated.totalTeamCount).toBe(100); - expect(updated.maxDirectTeamCount).toBe(50); - expect(updated.score).toBe(50); - }); - }); - - describe('compareTo', () => { - it('should compare scores for ranking (descending)', () => { - const scoreA = LeaderboardScore.calculate(100, [30, 30, 40]); - const scoreB = LeaderboardScore.calculate(80, [20, 20, 40]); - - // scoreA.score = 60, scoreB.score = 40 - // compareTo returns other.score - this.score for descending order - expect(scoreA.compareTo(scoreB)).toBeLessThan(0); // A ranks higher - expect(scoreB.compareTo(scoreA)).toBeGreaterThan(0); // B ranks lower - }); - - it('should return 0 for equal scores', () => { - const scoreA = LeaderboardScore.calculate(100, [50, 50]); - const scoreB = LeaderboardScore.calculate(100, [50, 50]); - - expect(scoreA.compareTo(scoreB)).toBe(0); - }); - }); -});