import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, EntityManager, In, Like, Repository } from 'typeorm'; import { Class, ClassStudent, ClassTeacher, Exam, ExamScore } from '../entities'; import { CreateExamDto, QueryExamDto } from './dto/exam.dto'; @Injectable() export class ExamsService { constructor( @InjectRepository(Exam) private examRepo: Repository, @InjectRepository(ExamScore) private scoreRepo: Repository, @InjectRepository(Class) private classRepo: Repository, @InjectRepository(ClassStudent) private classStudentRepo: Repository, @InjectRepository(ClassTeacher) private classTeacherRepo: Repository, private dataSource: DataSource, ) {} async getAccessibleClassIds(userId: number, canManageAll: boolean) { if (canManageAll) return undefined; const rows = await this.classTeacherRepo.find({ where: { userId } }); return [...new Set(rows.map((row) => row.classId))]; } async assertClassAccess(userId: number, classId: number, canManageAll: boolean) { if (canManageAll) return; const assignment = await this.classTeacherRepo.findOne({ where: { userId, classId } }); if (!assignment) throw new ForbiddenException('只能访问自己被分配的班级'); } async findAll(query: QueryExamDto, accessibleClassIds?: number[]) { const where: Record = { status: 'active', }; if (query.keyword) where.examName = Like(`%${query.keyword}%`); if (query.examType) where.examType = query.examType; if (query.classId) where.classId = query.classId; if (accessibleClassIds) { if (accessibleClassIds.length === 0) return []; where.classId = query.classId ? accessibleClassIds.includes(query.classId) ? query.classId : -1 : In(accessibleClassIds); } const exams = await this.examRepo.find({ where, relations: ['class'], order: { examDate: 'DESC', createdAt: 'DESC' }, }); if (exams.length === 0) return []; const scoreRows = await this.scoreRepo.find({ where: { examId: In(exams.map((exam) => exam.id)), status: 'active' }, }); const progress = new Map(); for (const row of scoreRows) { const item = progress.get(row.examId!) ?? { total: 0, entered: 0 }; item.total++; if (row.score !== null && row.score !== undefined) item.entered++; progress.set(row.examId!, item); } return exams.map((exam) => ({ ...exam, className: exam.class?.name, totalStudents: progress.get(exam.id)?.total ?? 0, enteredScores: progress.get(exam.id)?.entered ?? 0, })); } async findOne(id: number, userId: number, canManageAll: boolean) { const exam = await this.examRepo.findOne({ where: { id }, relations: ['class'] }); if (!exam) throw new NotFoundException('考试不存在'); await this.assertClassAccess(userId, exam.classId, canManageAll); const scores = await this.scoreRepo.find({ where: { examId: id, status: 'active' }, relations: ['student'], order: { id: 'ASC' }, }); return { ...exam, className: exam.class?.name, totalStudents: scores.length, enteredScores: scores.filter((row) => row.score !== null && row.score !== undefined).length, scores: scores.map((row) => ({ id: row.id, studentId: row.studentId, phone: row.student?.phone, name: row.student?.name, score: row.score === null ? null : Number(row.score), classAvg: row.classAvg === null ? null : Number(row.classAvg), rank: row.rank, })), }; } async create(dto: CreateExamDto, userId: number, canManageAll: boolean) { await this.assertClassAccess(userId, dto.classId, canManageAll); return this.dataSource.transaction(async (manager) => { const cls = await manager.findOne(Class, { where: { id: dto.classId } }); if (!cls) throw new NotFoundException('班级不存在'); if (cls.isArchived) throw new BadRequestException('归档班级不能创建考试'); const members = await manager.find(ClassStudent, { where: { classId: dto.classId, status: 'active' }, order: { createdAt: 'ASC' }, }); if (members.length === 0) throw new BadRequestException('班级暂无在读学员,不能创建考试'); const exam = await manager.save(Exam, manager.create(Exam, { ...dto, status: 'active' })); await this.createScoreRows(manager, exam, members); return exam; }); } async updateScore( examId: number, scoreId: number, score: number | null | undefined, userId: number, canManageAll: boolean, ) { return this.dataSource.transaction(async (manager) => { const exam = await manager.findOne(Exam, { where: { id: examId } }); if (!exam) throw new NotFoundException('考试不存在'); await this.assertClassAccess(userId, exam.classId, canManageAll); if (exam.status === 'archived') throw new BadRequestException('已归档考试不能录入成绩'); const row = await manager.findOne(ExamScore, { where: { id: scoreId, examId } }); if (!row) throw new NotFoundException('成绩记录不存在'); row.score = score === undefined ? null : score; await manager.save(ExamScore, row); await this.recalculate(manager, examId); return manager.findOne(ExamScore, { where: { id: scoreId } }); }); } private async createScoreRows(manager: EntityManager, exam: Exam, members: ClassStudent[]) { const rows = members.map((member) => manager.create(ExamScore, { examId: exam.id, studentId: member.studentId, examType: exam.examType, examName: exam.examName, subject: exam.subject, score: null, classAvg: null, rank: null, examDate: exam.examDate, status: 'active', }), ); await manager.save(ExamScore, rows); } private async recalculate(manager: EntityManager, examId: number) { const rows = await manager.find(ExamScore, { where: { examId, status: 'active' } }); const entered = rows .filter((row) => row.score !== null && row.score !== undefined) .sort((a, b) => Number(b.score) - Number(a.score)); const average = entered.length === 0 ? null : Math.round((entered.reduce((sum, row) => sum + Number(row.score), 0) / entered.length) * 100) / 100; let previousScore: number | null = null; let previousRank = 0; for (let index = 0; index < entered.length; index++) { const row = entered[index]; const value = Number(row.score); const rank = previousScore !== null && value === previousScore ? previousRank : index + 1; row.classAvg = average; row.rank = rank; previousScore = value; previousRank = rank; } const enteredIds = new Set(entered.map((row) => row.id)); for (const row of rows) { if (!enteredIds.has(row.id)) { row.classAvg = average; row.rank = null; } } await manager.save(ExamScore, rows); } }