forked from wangziqi/gongxue-base
- Delete department.entity.ts, user-department.entity.ts - Remove Department/UserDepartment from entities/index.ts - Remove departmentId column from 18 entities (AttendanceRecord, ArchiveAttachment, Bill, ClassSchedule, Classroom, ClassroomRental, Deposit, DepositInstallment, ExamScore, LearningRecord, Occupancy, PersonalExpense, ResultArchive, Room, RoomExpense, Student, StudentEnrollment, StudentProfile, StudentReport) - Remove departments/ module entirely - Delete campus-scope.ts, campus-scope.middleware.ts (request-utils.ts kept — it's just IP extraction) - Simplify common.module.ts to empty module - Remove CampusScopeMiddleware from app.module.ts - Remove all CampusScope injections and filter calls across all services - Remove departmentId from all DTOs and controllers - Simplify dingtalk/wecom sync to only sync users (no dept table) - Update seed module to remove department seeding - Clean frontend compilation
200 lines
6.8 KiB
TypeScript
200 lines
6.8 KiB
TypeScript
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { Repository, Like, Not, In, FindOptionsWhere } from 'typeorm';
|
||
import { Student } from '../entities/student.entity';
|
||
import { Class } from '../entities/class.entity';
|
||
import { ClassStudent } from '../entities/class-student.entity';
|
||
import { AttendanceRecord } from '../entities/attendance-record.entity';
|
||
import { CreateStudentDto, UpdateStudentDto } from './dto/student.dto';
|
||
|
||
@Injectable()
|
||
export class StudentsService {
|
||
|
||
constructor(
|
||
@InjectRepository(Student) private repo: Repository<Student>,
|
||
@InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
|
||
@InjectRepository(Class) private classRepo: Repository<Class>,
|
||
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
|
||
) {}
|
||
|
||
async findAll(query?: { name?: string; status?: string; includeArchived?: boolean; tenantId?: number | string }) {
|
||
const where: FindOptionsWhere<Student> = {};
|
||
if (query?.name) where.name = Like(`%${query.name}%`);
|
||
if (query?.tenantId) where.tenantId = Number(query.tenantId);
|
||
if (query?.status) {
|
||
where.status = query.status;
|
||
} else if (!query?.includeArchived) {
|
||
where.status = Not(In(['archived', 'staff']));
|
||
}
|
||
return this.repo.find({ where, order: { createdAt: 'DESC' }, relations: ['tenant'] });
|
||
}
|
||
|
||
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;
|
||
tenantId?: number;
|
||
}[],
|
||
) {
|
||
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,
|
||
tenantId: row.tenantId || undefined,
|
||
}),
|
||
);
|
||
imported++;
|
||
}
|
||
return {
|
||
message: `成功导入 ${imported} 名学生,跳过 ${skipped} 条(重复或空行)`,
|
||
imported,
|
||
skipped,
|
||
};
|
||
}
|
||
|
||
async compareClasses(studentId: number) {
|
||
const student = await this.repo.findOne({ where: { id: studentId } });
|
||
if (!student) throw new NotFoundException('学生不存在');
|
||
|
||
const enrollments = await this.classStudentRepo.find({
|
||
where: { studentId },
|
||
relations: ['class'],
|
||
});
|
||
|
||
if (enrollments.length === 0) {
|
||
return { student, enrollments: [] };
|
||
}
|
||
|
||
const classIds = enrollments.map((e) => e.classId);
|
||
const attendanceRecords = await this.attendanceRepo.find({
|
||
where: { studentId, classId: In(classIds) },
|
||
});
|
||
|
||
const attendanceByClass = new Map<number, AttendanceRecord[]>();
|
||
for (const r of attendanceRecords) {
|
||
const list = attendanceByClass.get(r.classId) || [];
|
||
list.push(r);
|
||
attendanceByClass.set(r.classId, list);
|
||
}
|
||
|
||
const comparison = enrollments.map((e) => {
|
||
const records = attendanceByClass.get(e.classId) || [];
|
||
const total = records.length;
|
||
const present = records.filter((r) => r.status === 'present' || r.status === '正常').length;
|
||
const absent = records.filter((r) => r.status === 'absent' || r.status === '缺勤').length;
|
||
const late = records.filter((r) => r.status === 'late' || r.status === '迟到').length;
|
||
const leave = records.filter((r) => r.status === 'leave' || r.status === '请假').length;
|
||
|
||
return {
|
||
classId: e.classId,
|
||
className: e.class?.name || '',
|
||
classType: e.class?.classType || '',
|
||
startDate: e.class?.startDate || '',
|
||
endDate: e.class?.endDate || '',
|
||
joinDate: e.joinDate,
|
||
leaveDate: e.leaveDate,
|
||
status: e.status,
|
||
attendanceStats: {
|
||
total,
|
||
present,
|
||
absent,
|
||
late,
|
||
leave,
|
||
rate: total > 0 ? Math.round((present / total) * 100) : 0,
|
||
},
|
||
};
|
||
});
|
||
|
||
return { student, enrollments: comparison };
|
||
}
|
||
}
|