import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { Student } from '../entities/student.entity'; import { ClassStudent } from '../entities/class-student.entity'; import { AttendanceRecord } from '../entities/attendance-record.entity'; import { Occupancy } from '../entities/occupancy.entity'; import { PersonalExpense } from '../entities/personal-expense.entity'; import { Bill } from '../entities/bill.entity'; import { Deposit } from '../entities/deposit.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 { StudentDingMapping } from '../entities/student-ding-mapping.entity'; import { StudentWallet } from '../entities/student-wallet.entity'; import { RoomInspectionDetail } from '../entities/room-inspection-detail.entity'; @Injectable() export class StudentsLifecycleService { constructor( @InjectRepository(Student) private readonly repo: Repository, @InjectRepository(ClassStudent) private readonly classStudentRepo: Repository, @InjectRepository(AttendanceRecord) private readonly attendanceRepo: Repository, @InjectRepository(StudentProfile) private readonly profileRepo: Repository, @InjectRepository(StudentEnrollment) private readonly enrollmentRepo: Repository, @InjectRepository(ExamScore) private readonly examScoreRepo: Repository, @InjectRepository(LearningRecord) private readonly learningRecordRepo: Repository, @InjectRepository(ResultArchive) private readonly resultRepo: Repository, @InjectRepository(Occupancy) private readonly occupancyRepo: Repository, @InjectRepository(PersonalExpense) private readonly personalExpenseRepo: Repository, @InjectRepository(Bill) private readonly billRepo: Repository, @InjectRepository(Deposit) private readonly depositRepo: Repository, @InjectRepository(ArchiveAttachment) private readonly attachmentRepo: Repository, @InjectRepository(StudentDingMapping) private readonly dingMappingRepo: Repository, @InjectRepository(StudentWallet) private readonly walletRepo: Repository, @InjectRepository(RoomInspectionDetail) private readonly inspectionDetailRepo: Repository, ) {} private async findOne(id: number) { const student = await this.repo.findOne({ where: { id }, relations: ['occupancies', 'occupancies.room'], }); if (!student) throw new NotFoundException('学生不存在'); return student; } async getArchiveExportMaps(studentIds: number[]) { if (studentIds.length === 0) { return { profiles: new Map(), results: new Map(), }; } const [profiles, results] = await Promise.all([ this.profileRepo.find({ where: { studentId: In(studentIds) } }), this.resultRepo.find({ where: { studentId: In(studentIds) } }), ]); return { profiles: new Map(profiles.map((profile) => [profile.studentId, profile])), results: new Map(results.map((result) => [result.studentId, result])), }; } async batchRemove(ids: number[]) { if (!ids || ids.length === 0) throw new BadRequestException('请选择要归档的学生'); const students = await this.repo.find({ where: { id: In(ids) } }); const skipped: string[] = []; const targetIds: number[] = []; for (const s of students) { if (s.status === 'archived') skipped.push(s.name); else targetIds.push(s.id); } let affected = 0; if (targetIds.length > 0) { const result = await this.repo .createQueryBuilder() .update() .set({ status: 'archived' }) .where('id IN (:...ids)', { ids: targetIds }) .execute(); affected = result.affected || 0; } const message = skipped.length > 0 ? `成功归档 ${affected} 人;${skipped.length} 人已是归档状态被跳过(${skipped.slice(0, 5).join('、')}${skipped.length > 5 ? '…' : ''})` : `已批量归档 ${affected} 人(数据已保留,可随时恢复)`; return { message, archived: affected, skipped: skipped.length }; } async restore(id: number) { const student = await this.findOne(id); if (student.status !== 'archived') { throw new BadRequestException('该学生未被归档'); } await this.repo.update(id, { status: 'active' }); return { message: '已恢复' }; } private async assertNoStudentReferences(studentId: number) { const [ occupancyCount, personalExpenseCount, billCount, depositCount, classMemberCount, profileCount, enrollmentCount, examScoreCount, learningRecordCount, attachmentCount, resultCount, attendanceCount, dingMappingCount, walletCount, inspectionDetailCount, ] = await Promise.all([ this.occupancyRepo.count({ where: { studentId } }), this.personalExpenseRepo.count({ where: { studentId } }), this.billRepo.count({ where: { studentId } }), this.depositRepo.count({ where: { studentId } }), this.classStudentRepo.count({ where: { studentId } }), this.profileRepo.count({ where: { studentId } }), this.enrollmentRepo.count({ where: { studentId } }), this.examScoreRepo.count({ where: { studentId } }), this.learningRecordRepo.count({ where: { studentId } }), this.attachmentRepo.count({ where: { studentId } }), this.resultRepo.count({ where: { studentId } }), this.attendanceRepo.count({ where: { studentId } }), this.dingMappingRepo.count({ where: { studentId } }), this.walletRepo.count({ where: { studentId } }), this.inspectionDetailRepo.count({ where: { studentId } }), ]); const refs: Array<[string, number]> = [ ['入住记录', occupancyCount], ['个人费用', personalExpenseCount], ['账单', billCount], ['押金', depositCount], ['班级成员', classMemberCount], ['档案信息', profileCount], ['报名记录', enrollmentCount], ['考试成绩', examScoreCount], ['学习记录', learningRecordCount], ['档案附件', attachmentCount], ['录取结果', resultCount], ['考勤记录', attendanceCount], ['钉钉映射', dingMappingCount], ['学生钱包', walletCount], ['查寝明细', inspectionDetailCount], ]; const references = refs.filter(([, count]) => count > 0); if (references.length > 0) { const names = references.map(([name]) => name).join('、'); throw new BadRequestException(`该学生存在关联数据(${names}),无法永久删除`); } } async purge(id: number) { const student = await this.findOne(id); if (student.status !== 'archived') { throw new BadRequestException('仅已归档学生可以永久删除,请先归档'); } await this.assertNoStudentReferences(id); await this.repo.delete(id); return { message: '已永久删除学生(不可恢复)' }; } async batchPurge(ids: number[]) { const uniqueIds = [...new Set(ids || [])]; if (uniqueIds.length === 0) throw new BadRequestException('请选择要永久删除的学生'); if (uniqueIds.some((id) => !Number.isInteger(id) || id <= 0)) { throw new BadRequestException('学生 ID 无效'); } const students = await this.repo.find({ where: { id: In(uniqueIds) } }); if (students.length !== uniqueIds.length) throw new NotFoundException('部分学生不存在'); const deleted: number[] = []; const skipped: string[] = []; for (const student of students) { if (student.status !== 'archived') { skipped.push(`${student.name}(未归档)`); continue; } try { await this.assertNoStudentReferences(student.id); } catch { skipped.push(`${student.name}(存在关联数据)`); continue; } await this.repo.delete(student.id); deleted.push(student.id); } const message = skipped.length > 0 ? `已永久删除 ${deleted.length} 人;${skipped.length} 人被跳过(${skipped.slice(0, 5).join('、')}${skipped.length > 5 ? '…' : ''})` : `已永久删除 ${deleted.length} 名学生(不可恢复)`; return { message, deleted: deleted.length, skipped: skipped.length }; } async batchRestore(ids: number[]) { const uniqueIds = [...new Set(ids || [])]; if (uniqueIds.length === 0) throw new BadRequestException('请选择要恢复的学生'); if (uniqueIds.some((id) => !Number.isInteger(id) || id <= 0)) { throw new BadRequestException('学生 ID 无效'); } const students = await this.repo.find({ where: { id: In(uniqueIds) } }); if (students.length !== uniqueIds.length) throw new NotFoundException('部分学生不存在'); const targetIds = students .filter((student) => student.status === 'archived') .map((student) => student.id); const skipped = students.length - targetIds.length; let restored = 0; if (targetIds.length > 0) { const result = await this.repo .createQueryBuilder() .update() .set({ status: 'active' }) .where('id IN (:...ids)', { ids: targetIds }) .execute(); restored = result.affected || 0; } return { message: `已批量恢复 ${restored} 名学生`, restored, skipped }; } }