Files
gongxue-base/apps/server/src/students/students.lifecycle.service.ts

236 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 };
}
}