215 lines
6.9 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|