rwadurian/backend/services/leaderboard-service/src/infrastructure/repositories/leaderboard-ranking.reposit...

215 lines
6.9 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../database/prisma.service';
import { ILeaderboardRankingRepository } from '../../domain/repositories/leaderboard-ranking.repository.interface';
import { LeaderboardRanking } from '../../domain/aggregates/leaderboard-ranking/leaderboard-ranking.aggregate';
import { LeaderboardType } from '../../domain/value-objects/leaderboard-type.enum';
import { LeaderboardPeriod } from '../../domain/value-objects/leaderboard-period.vo';
@Injectable()
export class LeaderboardRankingRepositoryImpl implements ILeaderboardRankingRepository {
constructor(private readonly prisma: PrismaService) {}
async save(ranking: LeaderboardRanking): Promise<void> {
const data = {
leaderboardType: ranking.leaderboardType,
periodKey: ranking.periodKey,
userId: ranking.userId,
isVirtual: ranking.isVirtual,
rankPosition: ranking.rankPosition.value,
displayPosition: ranking.displayPosition.value,
previousRank: ranking.previousRank?.value || null,
totalTeamPlanting: ranking.score.totalTeamPlanting,
maxDirectTeamPlanting: ranking.score.maxDirectTeamPlanting,
effectiveScore: ranking.score.effectiveScore,
userSnapshot: ranking.userSnapshot.toJson(),
periodStartAt: ranking.period.startAt,
periodEndAt: ranking.period.endAt,
calculatedAt: ranking.calculatedAt,
};
if (ranking.id) {
await this.prisma.leaderboardRanking.update({
where: { id: ranking.id },
data,
});
} else {
const result = await this.prisma.leaderboardRanking.upsert({
where: {
uk_type_period_user: {
leaderboardType: ranking.leaderboardType,
periodKey: ranking.periodKey,
userId: ranking.userId,
},
},
update: data,
create: data,
});
ranking.setId(result.id);
}
}
async saveAll(rankings: LeaderboardRanking[]): Promise<void> {
// 使用事务批量保存
await this.prisma.$transaction(
rankings.map((ranking) =>
this.prisma.leaderboardRanking.upsert({
where: {
uk_type_period_user: {
leaderboardType: ranking.leaderboardType,
periodKey: ranking.periodKey,
userId: ranking.userId,
},
},
update: {
rankPosition: ranking.rankPosition.value,
displayPosition: ranking.displayPosition.value,
previousRank: ranking.previousRank?.value || null,
totalTeamPlanting: ranking.score.totalTeamPlanting,
maxDirectTeamPlanting: ranking.score.maxDirectTeamPlanting,
effectiveScore: ranking.score.effectiveScore,
userSnapshot: ranking.userSnapshot.toJson(),
calculatedAt: ranking.calculatedAt,
},
create: {
leaderboardType: ranking.leaderboardType,
periodKey: ranking.periodKey,
userId: ranking.userId,
isVirtual: ranking.isVirtual,
rankPosition: ranking.rankPosition.value,
displayPosition: ranking.displayPosition.value,
previousRank: ranking.previousRank?.value || null,
totalTeamPlanting: ranking.score.totalTeamPlanting,
maxDirectTeamPlanting: ranking.score.maxDirectTeamPlanting,
effectiveScore: ranking.score.effectiveScore,
userSnapshot: ranking.userSnapshot.toJson(),
periodStartAt: ranking.period.startAt,
periodEndAt: ranking.period.endAt,
calculatedAt: ranking.calculatedAt,
},
})
)
);
}
async findById(id: bigint): Promise<LeaderboardRanking | null> {
const record = await this.prisma.leaderboardRanking.findUnique({
where: { id },
});
if (!record) return null;
return this.toDomain(record);
}
async findByTypeAndPeriod(
type: LeaderboardType,
periodKey: string,
options?: {
limit?: number;
includeVirtual?: boolean;
},
): Promise<LeaderboardRanking[]> {
const records = await this.prisma.leaderboardRanking.findMany({
where: {
leaderboardType: type,
periodKey,
...(options?.includeVirtual === false ? { isVirtual: false } : {}),
},
orderBy: { displayPosition: 'asc' },
take: options?.limit,
});
return records.map((r) => this.toDomain(r));
}
async findUserRanking(
type: LeaderboardType,
periodKey: string,
userId: bigint,
): Promise<LeaderboardRanking | null> {
const record = await this.prisma.leaderboardRanking.findUnique({
where: {
uk_type_period_user: {
leaderboardType: type,
periodKey,
userId,
},
},
});
if (!record) return null;
return this.toDomain(record);
}
async findUserPreviousRanking(
type: LeaderboardType,
userId: bigint,
): Promise<LeaderboardRanking | null> {
const currentPeriod = LeaderboardPeriod.current(type);
const previousPeriod = currentPeriod.getPreviousPeriod();
return this.findUserRanking(type, previousPeriod.key, userId);
}
async deleteByTypeAndPeriod(
type: LeaderboardType,
periodKey: string,
): Promise<void> {
await this.prisma.leaderboardRanking.deleteMany({
where: {
leaderboardType: type,
periodKey,
},
});
}
async countByTypeAndPeriod(
type: LeaderboardType,
periodKey: string,
): Promise<number> {
return this.prisma.leaderboardRanking.count({
where: {
leaderboardType: type,
periodKey,
isVirtual: false,
},
});
}
async getTopScore(
type: LeaderboardType,
periodKey: string,
): Promise<number> {
const result = await this.prisma.leaderboardRanking.findFirst({
where: {
leaderboardType: type,
periodKey,
isVirtual: false,
},
orderBy: { effectiveScore: 'desc' },
select: { effectiveScore: true },
});
return result?.effectiveScore || 0;
}
private toDomain(record: any): LeaderboardRanking {
return LeaderboardRanking.reconstitute({
id: record.id,
leaderboardType: record.leaderboardType as LeaderboardType,
periodKey: record.periodKey,
periodStartAt: record.periodStartAt,
periodEndAt: record.periodEndAt,
userId: record.userId,
isVirtual: record.isVirtual,
rankPosition: record.rankPosition,
displayPosition: record.displayPosition,
previousRank: record.previousRank,
totalTeamPlanting: record.totalTeamPlanting,
maxDirectTeamPlanting: record.maxDirectTeamPlanting,
effectiveScore: record.effectiveScore,
userSnapshot: record.userSnapshot as Record<string, any>,
calculatedAt: record.calculatedAt,
});
}
}