236 lines
9.9 KiB
TypeScript
236 lines
9.9 KiB
TypeScript
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<Student>,
|
||
@InjectRepository(ClassStudent) private readonly classStudentRepo: Repository<ClassStudent>,
|
||
@InjectRepository(AttendanceRecord)
|
||
private readonly attendanceRepo: Repository<AttendanceRecord>,
|
||
@InjectRepository(StudentProfile) private readonly profileRepo: Repository<StudentProfile>,
|
||
@InjectRepository(StudentEnrollment)
|
||
private readonly enrollmentRepo: Repository<StudentEnrollment>,
|
||
@InjectRepository(ExamScore) private readonly examScoreRepo: Repository<ExamScore>,
|
||
@InjectRepository(LearningRecord)
|
||
private readonly learningRecordRepo: Repository<LearningRecord>,
|
||
@InjectRepository(ResultArchive) private readonly resultRepo: Repository<ResultArchive>,
|
||
@InjectRepository(Occupancy) private readonly occupancyRepo: Repository<Occupancy>,
|
||
@InjectRepository(PersonalExpense)
|
||
private readonly personalExpenseRepo: Repository<PersonalExpense>,
|
||
@InjectRepository(Bill) private readonly billRepo: Repository<Bill>,
|
||
@InjectRepository(Deposit) private readonly depositRepo: Repository<Deposit>,
|
||
@InjectRepository(ArchiveAttachment)
|
||
private readonly attachmentRepo: Repository<ArchiveAttachment>,
|
||
@InjectRepository(StudentDingMapping)
|
||
private readonly dingMappingRepo: Repository<StudentDingMapping>,
|
||
@InjectRepository(StudentWallet) private readonly walletRepo: Repository<StudentWallet>,
|
||
@InjectRepository(RoomInspectionDetail)
|
||
private readonly inspectionDetailRepo: Repository<RoomInspectionDetail>,
|
||
) {}
|
||
|
||
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<number, StudentProfile>(),
|
||
results: new Map<number, ResultArchive>(),
|
||
};
|
||
}
|
||
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 };
|
||
}
|
||
}
|