import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Like, Not, In } from 'typeorm'; import { Student } from '../entities/student.entity'; import { CreateStudentDto, UpdateStudentDto } from './dto/student.dto'; @Injectable() export class StudentsService { constructor(@InjectRepository(Student) private repo: Repository) {} async findAll(query?: { name?: string; status?: string; includeArchived?: boolean }) { const where: any = {}; if (query?.name) where.name = Like(`%${query.name}%`); if (query?.status) { where.status = query.status; } else if (!query?.includeArchived) { where.status = Not('archived'); } return this.repo.find({ where, order: { createdAt: 'DESC' } }); } async findOne(id: number) { const student = await this.repo.findOne({ where: { id }, relations: ['occupancies', 'occupancies.room'] }); if (!student) throw new NotFoundException('学生不存在'); return student; } async create(dto: CreateStudentDto) { return this.repo.save(this.repo.create(dto)); } async update(id: number, dto: UpdateStudentDto) { await this.findOne(id); await this.repo.update(id, dto); return this.repo.findOne({ where: { id } }); } async remove(id: number) { const student = await this.findOne(id); if (student.status === 'archived') { throw new BadRequestException('该学生已归档'); } // 软删除:归档而非物理删除,保留历史数据 await this.repo.update(id, { status: 'archived' }); return { message: '已归档(数据已保留,可随时恢复)' }; } 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: '已恢复' }; } async batchImport(rows: { name: string; phone?: string; idNumber?: string; gender?: string; ethnicity?: string; emergencyContact?: string; emergencyPhone?: string; organization?: string; supervisor?: string }[]) { let imported = 0; let skipped = 0; for (const row of rows) { if (!row.name || !row.name.trim()) { skipped++; continue; } const exists = await this.repo.findOne({ where: { name: row.name.trim() } }); if (exists) { skipped++; continue; } await this.repo.save(this.repo.create({ name: row.name.trim(), phone: row.phone?.trim() || undefined, idNumber: row.idNumber?.trim() || undefined, gender: row.gender || undefined, ethnicity: row.ethnicity || undefined, emergencyContact: row.emergencyContact || undefined, emergencyPhone: row.emergencyPhone || undefined, organization: row.organization || undefined, supervisor: row.supervisor || undefined, })); imported++; } return { message: `成功导入 ${imported} 名学生,跳过 ${skipped} 条(重复或空行)`, imported, skipped }; } }