library-picturebook-activity/backend/src/contests/results/results.service.ts
zhangxiaohua 2981449353 修复评审相关数据显示问题
- 修复作品详情弹框评审记录字段名(score->totalScore, comment->comments)
- 修复评审详情弹框评分字段名(score->totalScore)
- 修复赛果发布详情评委评分不显示问题(添加judgeScore计算)
- 修复赛事列表已递交数统计(添加status in条件和覆盖_count.works)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 14:44:27 +08:00

711 lines
17 KiB
TypeScript

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';
import { QueryResultsDto } from './dto/query-results.dto';
@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,
},
});
const judgeWeights = new Map<number, number>(
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,
};
}
/**
* 获取比赛结果列表(作品列表)
*/
async getResults(contestId: number, queryDto: QueryResultsDto) {
const { page = 1, pageSize = 10, workNo, accountNo } = queryDto;
const contest = await this.prisma.contest.findUnique({
where: { id: contestId },
include: {
reviewRule: true,
},
});
if (!contest) {
throw new NotFoundException('比赛不存在');
}
const skip = (page - 1) * pageSize;
// 构建查询条件
const where: any = {
contestId,
validState: 1,
isLatest: true,
};
// 作品编号搜索
if (workNo) {
where.workNo = { contains: workNo };
}
// 报名账号搜索(需要关联查询)
if (accountNo) {
where.registration = {
user: {
username: { contains: accountNo },
},
};
}
const [works, total] = await Promise.all([
this.prisma.contestWork.findMany({
where,
include: {
registration: {
include: {
user: {
include: {
tenant: {
select: {
id: true,
name: true,
},
},
student: {
include: {
class: {
include: {
grade: {
select: {
id: true,
name: true,
},
},
},
},
},
},
},
},
team: {
select: {
id: true,
teamName: true,
},
},
teachers: {
include: {
user: {
select: {
id: true,
username: true,
nickname: true,
},
},
},
},
},
},
scores: {
where: { validState: 1 },
select: {
id: true,
totalScore: true,
judgeName: true,
scoreTime: true,
},
},
},
orderBy: [
{ finalScore: 'desc' },
],
skip,
take: pageSize,
}),
this.prisma.contestWork.count({ where }),
]);
// 计算每个作品的评委平均分(用于显示)
const enrichedWorks = works.map((work) => {
let judgeScore: number | null = work.finalScore
? Number(work.finalScore)
: null;
// 如果没有最终得分但有评分记录,则计算平均分作为评委评分
if (judgeScore === null && work.scores && work.scores.length > 0) {
const totalScores = work.scores.reduce(
(sum, s) => sum + Number(s.totalScore),
0,
);
judgeScore = Number((totalScores / work.scores.length).toFixed(2));
}
return {
...work,
judgeScore, // 评委评分(平均分)
};
});
return {
contest: {
id: contest.id,
contestName: contest.contestName,
resultState: contest.resultState,
resultPublishTime: contest.resultPublishTime,
},
list: enrichedWorks,
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,
},
};
}
}