2026-01-08 09:17:46 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Injectable,
|
|
|
|
|
|
NotFoundException,
|
|
|
|
|
|
BadRequestException,
|
|
|
|
|
|
} from '@nestjs/common';
|
|
|
|
|
|
import { PrismaService } from '../../prisma/prisma.service';
|
|
|
|
|
|
import { SetAwardDto } from './dto/set-award.dto';
|
|
|
|
|
|
import { BatchSetAwardsDto } from './dto/batch-set-awards.dto';
|
2026-01-15 16:35:00 +08:00
|
|
|
|
import { QueryResultsDto } from './dto/query-results.dto';
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
export class ResultsService {
|
|
|
|
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 计算比赛所有作品的最终得分
|
|
|
|
|
|
*/
|
|
|
|
|
|
async calculateAllFinalScores(contestId: number) {
|
|
|
|
|
|
// 验证比赛是否存在
|
|
|
|
|
|
const contest = await this.prisma.contest.findUnique({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
reviewRule: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!contest) {
|
|
|
|
|
|
throw new NotFoundException('比赛不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有有效作品
|
|
|
|
|
|
const works = await this.prisma.contestWork.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
include: {
|
|
|
|
|
|
scores: {
|
|
|
|
|
|
where: { validState: 1 },
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (works.length === 0) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
message: '没有需要计算的作品',
|
|
|
|
|
|
calculatedCount: 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const calculationRule = contest.reviewRule?.calculationRule || 'average';
|
|
|
|
|
|
let calculatedCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取评委权重(用于加权平均)
|
|
|
|
|
|
const judges = await this.prisma.contestJudge.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-18 17:58:38 +08:00
|
|
|
|
const judgeWeights = new Map<number, number>(
|
2026-01-08 09:17:46 +08:00
|
|
|
|
judges.map((j) => [j.judgeId, Number(j.weight || 1)]),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
for (const work of works) {
|
|
|
|
|
|
if (work.scores.length === 0) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const scores = work.scores.map((s) => Number(s.totalScore));
|
|
|
|
|
|
let finalScore = 0;
|
|
|
|
|
|
|
|
|
|
|
|
switch (calculationRule) {
|
|
|
|
|
|
case 'average':
|
|
|
|
|
|
finalScore = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'max':
|
|
|
|
|
|
finalScore = Math.max(...scores);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'min':
|
|
|
|
|
|
finalScore = Math.min(...scores);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'weighted':
|
|
|
|
|
|
let totalWeight = 0;
|
|
|
|
|
|
let weightedSum = 0;
|
|
|
|
|
|
|
|
|
|
|
|
work.scores.forEach((score) => {
|
|
|
|
|
|
const weight = judgeWeights.get(score.judgeId) || 1;
|
|
|
|
|
|
weightedSum += Number(score.totalScore) * weight;
|
|
|
|
|
|
totalWeight += weight;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
finalScore = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
finalScore = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.prisma.contestWork.update({
|
|
|
|
|
|
where: { id: work.id },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
finalScore: Number(finalScore.toFixed(2)),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
calculatedCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
message: `成功计算 ${calculatedCount} 件作品的最终得分`,
|
|
|
|
|
|
calculatedCount,
|
|
|
|
|
|
calculationRule,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 计算排名
|
|
|
|
|
|
*/
|
|
|
|
|
|
async calculateRankings(contestId: number) {
|
|
|
|
|
|
// 验证比赛是否存在
|
|
|
|
|
|
const contest = await this.prisma.contest.findUnique({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!contest) {
|
|
|
|
|
|
throw new NotFoundException('比赛不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有有最终得分的作品,按得分降序排列
|
|
|
|
|
|
const works = await this.prisma.contestWork.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
finalScore: { not: null },
|
|
|
|
|
|
},
|
|
|
|
|
|
orderBy: {
|
|
|
|
|
|
finalScore: 'desc',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (works.length === 0) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
message: '没有需要排名的作品,请先计算最终得分',
|
|
|
|
|
|
rankedCount: 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算排名(同分同名次)
|
|
|
|
|
|
let currentRank = 1;
|
|
|
|
|
|
let previousScore: number | null = null;
|
|
|
|
|
|
let sameScoreCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < works.length; i++) {
|
|
|
|
|
|
const work = works[i];
|
|
|
|
|
|
const currentScore = Number(work.finalScore);
|
|
|
|
|
|
|
|
|
|
|
|
if (previousScore !== null && currentScore === previousScore) {
|
|
|
|
|
|
// 同分,保持相同排名
|
|
|
|
|
|
sameScoreCount++;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 不同分,更新排名(跳过同分数量)
|
|
|
|
|
|
currentRank = i + 1;
|
|
|
|
|
|
sameScoreCount = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.prisma.contestWork.update({
|
|
|
|
|
|
where: { id: work.id },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
rank: currentRank,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
previousScore = currentScore;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
message: `成功计算 ${works.length} 件作品的排名`,
|
|
|
|
|
|
rankedCount: works.length,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置单个作品奖项
|
|
|
|
|
|
*/
|
|
|
|
|
|
async setAward(workId: number, setAwardDto: SetAwardDto) {
|
|
|
|
|
|
const work = await this.prisma.contestWork.findUnique({
|
|
|
|
|
|
where: { id: workId },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!work) {
|
|
|
|
|
|
throw new NotFoundException('作品不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return this.prisma.contestWork.update({
|
|
|
|
|
|
where: { id: workId },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
awardLevel: setAwardDto.awardLevel,
|
|
|
|
|
|
awardName: setAwardDto.awardName,
|
|
|
|
|
|
certificateUrl: setAwardDto.certificateUrl,
|
|
|
|
|
|
status: 'awarded',
|
|
|
|
|
|
},
|
|
|
|
|
|
include: {
|
|
|
|
|
|
registration: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
user: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
username: true,
|
|
|
|
|
|
nickname: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
team: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
teamName: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量设置奖项
|
|
|
|
|
|
*/
|
|
|
|
|
|
async batchSetAwards(contestId: number, batchSetAwardsDto: BatchSetAwardsDto) {
|
|
|
|
|
|
// 验证比赛是否存在
|
|
|
|
|
|
const contest = await this.prisma.contest.findUnique({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!contest) {
|
|
|
|
|
|
throw new NotFoundException('比赛不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const results = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const award of batchSetAwardsDto.awards) {
|
|
|
|
|
|
const work = await this.prisma.contestWork.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
id: award.workId,
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!work) {
|
|
|
|
|
|
results.push({
|
|
|
|
|
|
workId: award.workId,
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '作品不存在',
|
|
|
|
|
|
});
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await this.prisma.contestWork.update({
|
|
|
|
|
|
where: { id: award.workId },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
awardLevel: award.awardLevel,
|
|
|
|
|
|
awardName: award.awardName,
|
|
|
|
|
|
status: 'awarded',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
results.push({
|
|
|
|
|
|
workId: award.workId,
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
message: `批量设置完成`,
|
|
|
|
|
|
total: batchSetAwardsDto.awards.length,
|
|
|
|
|
|
successCount: results.filter((r) => r.success).length,
|
|
|
|
|
|
failedCount: results.filter((r) => !r.success).length,
|
|
|
|
|
|
results,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据排名自动设置奖项
|
|
|
|
|
|
*/
|
|
|
|
|
|
async autoSetAwards(
|
|
|
|
|
|
contestId: number,
|
|
|
|
|
|
awardConfig: {
|
|
|
|
|
|
first?: number;
|
|
|
|
|
|
second?: number;
|
|
|
|
|
|
third?: number;
|
|
|
|
|
|
excellent?: number;
|
|
|
|
|
|
},
|
|
|
|
|
|
) {
|
|
|
|
|
|
const contest = await this.prisma.contest.findUnique({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!contest) {
|
|
|
|
|
|
throw new NotFoundException('比赛不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取已排名的作品
|
|
|
|
|
|
const works = await this.prisma.contestWork.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
rank: { not: null },
|
|
|
|
|
|
},
|
|
|
|
|
|
orderBy: {
|
|
|
|
|
|
rank: 'asc',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (works.length === 0) {
|
|
|
|
|
|
throw new BadRequestException('没有已排名的作品,请先计算排名');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const firstCount = awardConfig.first || 0;
|
|
|
|
|
|
const secondCount = awardConfig.second || 0;
|
|
|
|
|
|
const thirdCount = awardConfig.third || 0;
|
|
|
|
|
|
const excellentCount = awardConfig.excellent || 0;
|
|
|
|
|
|
|
|
|
|
|
|
let assignedCount = 0;
|
|
|
|
|
|
const awards: { workId: number; awardLevel: string; awardName: string }[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < works.length; i++) {
|
|
|
|
|
|
const work = works[i];
|
|
|
|
|
|
let awardLevel: string | null = null;
|
|
|
|
|
|
let awardName: string | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (i < firstCount) {
|
|
|
|
|
|
awardLevel = 'first';
|
|
|
|
|
|
awardName = '一等奖';
|
|
|
|
|
|
} else if (i < firstCount + secondCount) {
|
|
|
|
|
|
awardLevel = 'second';
|
|
|
|
|
|
awardName = '二等奖';
|
|
|
|
|
|
} else if (i < firstCount + secondCount + thirdCount) {
|
|
|
|
|
|
awardLevel = 'third';
|
|
|
|
|
|
awardName = '三等奖';
|
|
|
|
|
|
} else if (i < firstCount + secondCount + thirdCount + excellentCount) {
|
|
|
|
|
|
awardLevel = 'excellent';
|
|
|
|
|
|
awardName = '优秀奖';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (awardLevel) {
|
|
|
|
|
|
await this.prisma.contestWork.update({
|
|
|
|
|
|
where: { id: work.id },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
awardLevel,
|
|
|
|
|
|
awardName,
|
|
|
|
|
|
status: 'awarded',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
awards.push({
|
|
|
|
|
|
workId: work.id,
|
|
|
|
|
|
awardLevel,
|
|
|
|
|
|
awardName,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
assignedCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
message: `自动设置奖项完成`,
|
|
|
|
|
|
assignedCount,
|
|
|
|
|
|
awards,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发布赛果
|
|
|
|
|
|
*/
|
|
|
|
|
|
async publishResults(contestId: number) {
|
|
|
|
|
|
const contest = await this.prisma.contest.findUnique({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!contest) {
|
|
|
|
|
|
throw new NotFoundException('比赛不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (contest.resultState === 'published') {
|
|
|
|
|
|
throw new BadRequestException('赛果已发布');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有已排名的作品
|
|
|
|
|
|
const rankedWorks = await this.prisma.contestWork.count({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
rank: { not: null },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (rankedWorks === 0) {
|
|
|
|
|
|
throw new BadRequestException('没有已排名的作品,请先计算排名');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新比赛状态
|
|
|
|
|
|
const updatedContest = await this.prisma.contest.update({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
resultState: 'published',
|
|
|
|
|
|
resultPublishTime: new Date(),
|
|
|
|
|
|
status: 'finished',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
message: '赛果发布成功',
|
|
|
|
|
|
contest: updatedContest,
|
|
|
|
|
|
publishTime: updatedContest.resultPublishTime,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 撤回发布
|
|
|
|
|
|
*/
|
|
|
|
|
|
async unpublishResults(contestId: number) {
|
|
|
|
|
|
const contest = await this.prisma.contest.findUnique({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!contest) {
|
|
|
|
|
|
throw new NotFoundException('比赛不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (contest.resultState !== 'published') {
|
|
|
|
|
|
throw new BadRequestException('赛果未发布');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const updatedContest = await this.prisma.contest.update({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
resultState: 'unpublished',
|
|
|
|
|
|
resultPublishTime: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
message: '已撤回赛果发布',
|
|
|
|
|
|
contest: updatedContest,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-15 16:35:00 +08:00
|
|
|
|
* 获取比赛结果列表(作品列表)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
*/
|
2026-01-15 16:35:00 +08:00
|
|
|
|
async getResults(contestId: number, queryDto: QueryResultsDto) {
|
|
|
|
|
|
const { page = 1, pageSize = 10, workNo, accountNo } = queryDto;
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const contest = await this.prisma.contest.findUnique({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
reviewRule: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!contest) {
|
|
|
|
|
|
throw new NotFoundException('比赛不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const skip = (page - 1) * pageSize;
|
|
|
|
|
|
|
2026-01-15 16:35:00 +08:00
|
|
|
|
// 构建查询条件
|
|
|
|
|
|
const where: any = {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 作品编号搜索
|
|
|
|
|
|
if (workNo) {
|
|
|
|
|
|
where.workNo = { contains: workNo };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 报名账号搜索(需要关联查询)
|
|
|
|
|
|
if (accountNo) {
|
|
|
|
|
|
where.registration = {
|
|
|
|
|
|
user: {
|
|
|
|
|
|
username: { contains: accountNo },
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const [works, total] = await Promise.all([
|
|
|
|
|
|
this.prisma.contestWork.findMany({
|
2026-01-15 16:35:00 +08:00
|
|
|
|
where,
|
2026-01-08 09:17:46 +08:00
|
|
|
|
include: {
|
|
|
|
|
|
registration: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
user: {
|
2026-01-15 16:35:00 +08:00
|
|
|
|
include: {
|
|
|
|
|
|
tenant: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
name: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
student: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
class: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
grade: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
name: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-08 09:17:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
team: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
teamName: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-15 16:35:00 +08:00
|
|
|
|
teachers: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
user: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
username: true,
|
|
|
|
|
|
nickname: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-01-08 09:17:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
orderBy: [
|
|
|
|
|
|
{ finalScore: 'desc' },
|
|
|
|
|
|
],
|
|
|
|
|
|
skip,
|
|
|
|
|
|
take: pageSize,
|
|
|
|
|
|
}),
|
2026-01-15 16:35:00 +08:00
|
|
|
|
this.prisma.contestWork.count({ where }),
|
2026-01-08 09:17:46 +08:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
contest: {
|
|
|
|
|
|
id: contest.id,
|
|
|
|
|
|
contestName: contest.contestName,
|
|
|
|
|
|
resultState: contest.resultState,
|
|
|
|
|
|
resultPublishTime: contest.resultPublishTime,
|
|
|
|
|
|
},
|
|
|
|
|
|
list: works,
|
|
|
|
|
|
total,
|
|
|
|
|
|
page,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取比赛结果统计摘要
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getResultsSummary(contestId: number) {
|
|
|
|
|
|
const contest = await this.prisma.contest.findUnique({
|
|
|
|
|
|
where: { id: contestId },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!contest) {
|
|
|
|
|
|
throw new NotFoundException('比赛不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取总作品数
|
|
|
|
|
|
const totalWorks = await this.prisma.contestWork.count({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取已计分作品数
|
|
|
|
|
|
const scoredWorks = await this.prisma.contestWork.count({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
finalScore: { not: null },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取已排名作品数
|
|
|
|
|
|
const rankedWorks = await this.prisma.contestWork.count({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
rank: { not: null },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取已设奖作品数
|
|
|
|
|
|
const awardedWorks = await this.prisma.contestWork.count({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
awardLevel: { not: null },
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 奖项分布
|
|
|
|
|
|
const awardStats = await this.prisma.contestWork.groupBy({
|
|
|
|
|
|
by: ['awardLevel'],
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
awardLevel: { not: null },
|
|
|
|
|
|
},
|
|
|
|
|
|
_count: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const awardDistribution: Record<string, number> = {
|
|
|
|
|
|
first: 0,
|
|
|
|
|
|
second: 0,
|
|
|
|
|
|
third: 0,
|
|
|
|
|
|
excellent: 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
awardStats.forEach((stat) => {
|
|
|
|
|
|
if (stat.awardLevel) {
|
|
|
|
|
|
awardDistribution[stat.awardLevel] = stat._count.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取分数统计
|
|
|
|
|
|
const scoreStats = await this.prisma.contestWork.aggregate({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId,
|
|
|
|
|
|
validState: 1,
|
|
|
|
|
|
isLatest: true,
|
|
|
|
|
|
finalScore: { not: null },
|
|
|
|
|
|
},
|
|
|
|
|
|
_avg: { finalScore: true },
|
|
|
|
|
|
_max: { finalScore: true },
|
|
|
|
|
|
_min: { finalScore: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
contest: {
|
|
|
|
|
|
id: contest.id,
|
|
|
|
|
|
contestName: contest.contestName,
|
|
|
|
|
|
resultState: contest.resultState,
|
|
|
|
|
|
resultPublishTime: contest.resultPublishTime,
|
|
|
|
|
|
},
|
|
|
|
|
|
summary: {
|
|
|
|
|
|
totalWorks,
|
|
|
|
|
|
scoredWorks,
|
|
|
|
|
|
rankedWorks,
|
|
|
|
|
|
awardedWorks,
|
|
|
|
|
|
unscoredWorks: totalWorks - scoredWorks,
|
|
|
|
|
|
},
|
|
|
|
|
|
awardDistribution,
|
|
|
|
|
|
scoreStats: {
|
|
|
|
|
|
avgScore: scoreStats._avg.finalScore
|
|
|
|
|
|
? Number(scoreStats._avg.finalScore).toFixed(2)
|
|
|
|
|
|
: null,
|
|
|
|
|
|
maxScore: scoreStats._max.finalScore
|
|
|
|
|
|
? Number(scoreStats._max.finalScore).toFixed(2)
|
|
|
|
|
|
: null,
|
|
|
|
|
|
minScore: scoreStats._min.finalScore
|
|
|
|
|
|
? Number(scoreStats._min.finalScore).toFixed(2)
|
|
|
|
|
|
: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|