feat: 重构各业务模块管理页面与服务

This commit is contained in:
2026-08-05 17:12:00 +08:00
parent 80e6fccf05
commit fd39e1686a
163 changed files with 18409 additions and 13449 deletions

View File

@@ -0,0 +1,235 @@
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 };
}
}