import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import * as fs from 'fs'; import * as path from 'path'; import { NotificationsService } from '../notifications/notifications.service'; import { Student } from '../entities/student.entity'; import { StudentProfile } from '../entities/student-profile.entity'; import { StudentEnrollment } from '../entities/student-enrollment.entity'; import { ExamScore } from '../entities/exam-score.entity'; import { LearningRecord } from '../entities/learning-record.entity'; import { ResultArchive } from '../entities/result-archive.entity'; import { ArchiveAttachment } from '../entities/archive-attachment.entity'; import { AttendanceRecord } from '../entities/attendance-record.entity'; import { UpsertProfileDto, CreateEnrollmentDto, UpdateEnrollmentDto, CreateExamScoreDto, UpdateExamScoreDto, CreateLearningRecordDto, UpdateLearningRecordDto, UpsertResultDto, } from './dto/archive.dto'; @Injectable() export class ArchiveService { constructor( @InjectRepository(Student) private studentRepo: Repository, @InjectRepository(StudentProfile) private profileRepo: Repository, @InjectRepository(StudentEnrollment) private enrollmentRepo: Repository, @InjectRepository(ExamScore) private examScoreRepo: Repository, @InjectRepository(LearningRecord) private learningRecordRepo: Repository, @InjectRepository(ResultArchive) private resultRepo: Repository, @InjectRepository(ArchiveAttachment) private attachmentRepo: Repository, @InjectRepository(AttendanceRecord) private attendanceRepo: Repository, private readonly notificationsService: NotificationsService, ) {} get uploadDir(): string { const base = process.env.UPLOAD_DIR || './uploads'; return path.resolve(base, 'archive'); } private resolveAttachmentPath(filePath: string): string { const normalizedPath = filePath.replace(/\\/g, '/'); const fullPath = normalizedPath.startsWith('uploads/') ? path.resolve(process.cwd(), normalizedPath) : path.resolve(this.uploadDir, normalizedPath); const allowedRoots = [this.uploadDir, path.resolve(process.cwd(), 'uploads', 'archive')]; if ( !allowedRoots.some((root) => fullPath === root || fullPath.startsWith(`${root}${path.sep}`)) ) { throw new BadRequestException('路径非法'); } return fullPath; } async getProfile(studentId: number) { const student = await this.studentRepo.findOne({ where: { id: studentId } }); if (!student) throw new NotFoundException('学生不存在'); const [profileRaw, enrollments, examScores, learningRecords, resultArchive, attachments, attendances] = await Promise.all([ this.profileRepo.findOne({ where: { studentId } }), this.enrollmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }), this.examScoreRepo.find({ where: { studentId }, order: { examDate: 'DESC' } }), this.learningRecordRepo.find({ where: { studentId }, order: { recordDate: 'DESC' } }), this.resultRepo.findOne({ where: { studentId } }), this.attachmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }), this.attendanceRepo.find({ where: { studentId }, relations: ['schedule', 'class'], order: { attendanceDate: 'DESC', punchTime: 'DESC' }, }), ]); return { student, profile: profileRaw, enrollments, examScores, learningRecords, result: resultArchive, attachments, attendances, }; } async upsertProfile(studentId: number, dto: UpsertProfileDto) { const student = await this.studentRepo.findOne({ where: { id: studentId } }); if (!student) throw new NotFoundException('学生不存在'); let profile = await this.profileRepo.findOne({ where: { studentId } }); if (profile) { Object.assign(profile, dto); } else { profile = this.profileRepo.create({ ...dto, studentId }); } return this.profileRepo.save(profile); } async addEnrollment(studentId: number, dto: CreateEnrollmentDto) { const student = await this.studentRepo.findOne({ where: { id: studentId } }); if (!student) throw new NotFoundException('学生不存在'); const entity = this.enrollmentRepo.create({ ...dto, studentId }); return this.enrollmentRepo.save(entity); } async updateEnrollment(id: number, dto: UpdateEnrollmentDto) { const entity = await this.enrollmentRepo.findOne({ where: { id } }); if (!entity) throw new NotFoundException('报名记录不存在'); Object.assign(entity, dto); return this.enrollmentRepo.save(entity); } async deleteEnrollment(id: number) { const entity = await this.enrollmentRepo.findOne({ where: { id } }); if (!entity) throw new NotFoundException('报名记录不存在'); await this.enrollmentRepo.remove(entity); return { message: '已删除' }; } async addExamScore(studentId: number, dto: CreateExamScoreDto) { const student = await this.studentRepo.findOne({ where: { id: studentId } }); if (!student) throw new NotFoundException('学生不存在'); const entity = this.examScoreRepo.create({ ...dto, studentId }); return this.examScoreRepo.save(entity); } async updateExamScore(id: number, dto: UpdateExamScoreDto) { const entity = await this.examScoreRepo.findOne({ where: { id } }); if (!entity) throw new NotFoundException('考试成绩不存在'); Object.assign(entity, dto); return this.examScoreRepo.save(entity); } async deleteExamScore(id: number) { const entity = await this.examScoreRepo.findOne({ where: { id } }); if (!entity) throw new NotFoundException('考试成绩不存在'); await this.examScoreRepo.remove(entity); return { message: '已删除' }; } async addLearningRecord(studentId: number, dto: CreateLearningRecordDto) { const student = await this.studentRepo.findOne({ where: { id: studentId } }); if (!student) throw new NotFoundException('学生不存在'); const entity = this.learningRecordRepo.create({ ...dto, studentId }); return this.learningRecordRepo.save(entity); } async updateLearningRecord(id: number, dto: UpdateLearningRecordDto) { const entity = await this.learningRecordRepo.findOne({ where: { id } }); if (!entity) throw new NotFoundException('学习记录不存在'); Object.assign(entity, dto); return this.learningRecordRepo.save(entity); } async deleteLearningRecord(id: number) { const entity = await this.learningRecordRepo.findOne({ where: { id } }); if (!entity) throw new NotFoundException('学习记录不存在'); await this.learningRecordRepo.remove(entity); return { message: '已删除' }; } async upsertResult(studentId: number, dto: UpsertResultDto) { const student = await this.studentRepo.findOne({ where: { id: studentId } }); if (!student) throw new NotFoundException('学生不存在'); let result = await this.resultRepo.findOne({ where: { studentId } }); if (result) { Object.assign(result, dto); } else { result = this.resultRepo.create({ ...dto, studentId }); } return this.resultRepo.save(result); } async addAttachment(studentId: number, file: Express.Multer.File, category: string) { const student = await this.studentRepo.findOne({ where: { id: studentId } }); if (!student) throw new NotFoundException('学生不存在'); const uploadDir = this.uploadDir; if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } const ext = path.extname(file.originalname); const filename = `${studentId}_${Date.now()}${ext}`; const filePath = path.join(uploadDir, filename); fs.writeFileSync(filePath, file.buffer); const entity = this.attachmentRepo.create({ studentId, category, fileName: file.originalname, filePath: filename, fileSize: file.size, mimeType: file.mimetype, }); return this.attachmentRepo.save(entity); } async getAttachmentFile(studentId: number, id: number) { const entity = await this.attachmentRepo.findOne({ where: { id, studentId } }); if (!entity) throw new NotFoundException('附件不存在'); const fullPath = this.resolveAttachmentPath(entity.filePath); if (!fs.existsSync(fullPath)) throw new NotFoundException('附件文件丢失'); return { fullPath, fileName: entity.fileName || path.basename(fullPath), mimeType: entity.mimeType || 'application/octet-stream', }; } async deleteAttachment(id: number) { const entity = await this.attachmentRepo.findOne({ where: { id } }); if (!entity) throw new NotFoundException('附件不存在'); const absPath = this.resolveAttachmentPath(entity.filePath); if (fs.existsSync(absPath)) { fs.unlinkSync(absPath); } await this.attachmentRepo.remove(entity); return { message: '已删除' }; } }