refactor(referral): remove legacy leaderboard code
Remove duplicate leaderboard functionality from referral-service. All leaderboard features should now go through leaderboard-service. Removed files: - api/controllers/leaderboard.controller.ts - api/dto/leaderboard.dto.ts - application/queries/get-leaderboard.query.ts - infrastructure/cache/leaderboard-cache.service.ts - Related test files Modified: - TeamStatisticsService: removed getLeaderboard() and getUserRank() - Module registrations updated 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
594f7880c3
commit
2bf5ca933e
|
|
@ -1,4 +1,3 @@
|
|||
export * from './referral.controller';
|
||||
export * from './leaderboard.controller';
|
||||
export * from './team-statistics.controller';
|
||||
export * from './health.controller';
|
||||
|
|
|
|||
|
|
@ -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<LeaderboardResponseDto> {
|
||||
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<UserRankResponseDto> {
|
||||
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<UserRankResponseDto> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
export * from './referral.dto';
|
||||
export * from './leaderboard.dto';
|
||||
export * from './team-statistics.dto';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<LeaderboardResult> {
|
||||
// 尝试从缓存获取
|
||||
const cachedEntries = await this.leaderboardCache.getTopN(query.limit);
|
||||
if (cachedEntries.length > 0) {
|
||||
// 补充完整数据
|
||||
const userIds = cachedEntries.map((e) => e.userId);
|
||||
const statsMap = new Map<string, { totalTeamCount: number; directReferralCount: number }>();
|
||||
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<number | null> {
|
||||
// 先尝试缓存
|
||||
const cachedRank = await this.leaderboardCache.getUserRank(userId);
|
||||
if (cachedRank !== null) {
|
||||
return cachedRank;
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
return this.teamStatsRepo.getUserRank(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取省市分布统计
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export * from './redis.service';
|
||||
export * from './leaderboard-cache.service';
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<number | null> {
|
||||
const rank = await this.redisService.zrevrank(LEADERBOARD_KEY, userId.toString());
|
||||
return rank !== null ? rank + 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排行榜前N名
|
||||
*/
|
||||
async getTopN(n: number): Promise<LeaderboardEntry[]> {
|
||||
// 尝试从缓存获取
|
||||
const cached = await this.redisService.get<LeaderboardEntry[]>(`${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<number> {
|
||||
return this.redisService.zincrby(LEADERBOARD_KEY, delta, userId.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新分值
|
||||
*/
|
||||
async batchUpdateScores(updates: Array<{ userId: bigint; score: number }>): Promise<void> {
|
||||
for (const update of updates) {
|
||||
await this.updateScore(update.userId, update.score);
|
||||
}
|
||||
// 清除缓存
|
||||
await this.invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除排行榜缓存
|
||||
*/
|
||||
async invalidateCache(): Promise<void> {
|
||||
// 清除所有排行榜缓存
|
||||
for (const n of [10, 50, 100]) {
|
||||
await this.redisService.del(`${LEADERBOARD_CACHE_KEY}:top${n}`);
|
||||
}
|
||||
this.logger.debug('Leaderboard cache invalidated');
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue