feat: add ClassesService with CRUD + student/teacher management
This commit is contained in:
195
apps/server/src/classes/classes.service.ts
Normal file
195
apps/server/src/classes/classes.service.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, In, Like } from 'typeorm';
|
||||
import { Class, ClassStudent, ClassTeacher } from '../entities';
|
||||
import { CreateClassDto, UpdateClassDto, QueryClassDto, AddTeacherDto } from './dto/class.dto';
|
||||
|
||||
interface RawStudentCount {
|
||||
classId: string;
|
||||
count: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ClassesService {
|
||||
constructor(
|
||||
@InjectRepository(Class)
|
||||
private classRepo: Repository<Class>,
|
||||
@InjectRepository(ClassStudent)
|
||||
private classStudentRepo: Repository<ClassStudent>,
|
||||
@InjectRepository(ClassTeacher)
|
||||
private classTeacherRepo: Repository<ClassTeacher>,
|
||||
) {}
|
||||
|
||||
async findAll(query: QueryClassDto) {
|
||||
const where: Record<string, unknown> = {};
|
||||
if (query.departmentId) where.departmentId = query.departmentId;
|
||||
if (query.status) where.status = query.status;
|
||||
if (query.classType) where.classType = query.classType;
|
||||
if (query.keyword) where.name = Like(`%${query.keyword}%`);
|
||||
|
||||
const classes = await this.classRepo.find({
|
||||
where,
|
||||
order: { createdAt: 'DESC' as const },
|
||||
});
|
||||
|
||||
// count students per class
|
||||
const studentCounts: RawStudentCount[] = await this.classStudentRepo
|
||||
.createQueryBuilder('cs')
|
||||
.select('cs.class_id', 'classId')
|
||||
.addSelect('COUNT(cs.id)', 'count')
|
||||
.where('cs.status = :status', { status: 'active' })
|
||||
.groupBy('cs.class_id')
|
||||
.getRawMany();
|
||||
|
||||
const countMap = new Map(studentCounts.map((r) => [Number(r.classId), Number(r.count)]));
|
||||
|
||||
return classes.map((c) => ({
|
||||
...c,
|
||||
studentCount: countMap.get(c.id) || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const cls = await this.classRepo.findOne({ where: { id } });
|
||||
if (!cls) throw new NotFoundException('班级不存在');
|
||||
|
||||
const students = await this.classStudentRepo.find({
|
||||
where: { classId: id },
|
||||
relations: ['student'],
|
||||
});
|
||||
const teachers = await this.classTeacherRepo.find({
|
||||
where: { classId: id },
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
return {
|
||||
...cls,
|
||||
students: students.map((s) => ({
|
||||
id: s.id,
|
||||
studentId: s.studentId,
|
||||
studentName: s.student?.name,
|
||||
joinDate: s.joinDate,
|
||||
leaveDate: s.leaveDate,
|
||||
status: s.status,
|
||||
})),
|
||||
teachers: teachers.map((t) => ({
|
||||
id: t.id,
|
||||
userId: t.userId,
|
||||
username: t.user?.username,
|
||||
roleType: t.roleType,
|
||||
subject: t.subject,
|
||||
})),
|
||||
studentCount: students.filter((s) => s.status === 'active').length,
|
||||
};
|
||||
}
|
||||
|
||||
async create(dto: CreateClassDto) {
|
||||
const { studentIds, teachers, ...classData } = dto;
|
||||
|
||||
const cls = this.classRepo.create(classData);
|
||||
const saved = await this.classRepo.save(cls);
|
||||
|
||||
// add students
|
||||
if (studentIds?.length) {
|
||||
const entries = studentIds.map((sid: number) =>
|
||||
this.classStudentRepo.create({ classId: saved.id, studentId: sid, joinDate: new Date().toISOString().split('T')[0] }),
|
||||
);
|
||||
await this.classStudentRepo.save(entries);
|
||||
}
|
||||
|
||||
// add teachers
|
||||
if (teachers?.length) {
|
||||
const entries = teachers.map((t) =>
|
||||
this.classTeacherRepo.create({ classId: saved.id, userId: t.userId, roleType: t.roleType, subject: t.subject }),
|
||||
);
|
||||
await this.classTeacherRepo.save(entries);
|
||||
|
||||
// sync head/life/academic teacher IDs
|
||||
await this.syncClassTeacherIds(saved.id);
|
||||
}
|
||||
|
||||
return this.findOne(saved.id);
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateClassDto) {
|
||||
const cls = await this.classRepo.findOne({ where: { id } });
|
||||
if (!cls) throw new NotFoundException('班级不存在');
|
||||
await this.classRepo.update(id, dto as Record<string, unknown>);
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const cls = await this.classRepo.findOne({ where: { id } });
|
||||
if (!cls) throw new NotFoundException('班级不存在');
|
||||
await this.classRepo.remove(cls);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getStudents(classId: number) {
|
||||
return this.classStudentRepo.find({
|
||||
where: { classId },
|
||||
relations: ['student'],
|
||||
order: { createdAt: 'ASC' as const },
|
||||
});
|
||||
}
|
||||
|
||||
async addStudents(classId: number, studentIds: number[]) {
|
||||
const existing = await this.classStudentRepo.find({
|
||||
where: { classId, studentId: In(studentIds) },
|
||||
});
|
||||
const existingIds = new Set(existing.map((e) => e.studentId));
|
||||
const newIds = studentIds.filter((id) => !existingIds.has(id));
|
||||
|
||||
const entries = newIds.map((sid) =>
|
||||
this.classStudentRepo.create({ classId, studentId: sid, joinDate: new Date().toISOString().split('T')[0] }),
|
||||
);
|
||||
if (entries.length) await this.classStudentRepo.save(entries);
|
||||
|
||||
return { added: entries.length, skipped: studentIds.length - entries.length };
|
||||
}
|
||||
|
||||
async removeStudent(classId: number, studentId: number) {
|
||||
await this.classStudentRepo.delete({ classId, studentId });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getTeachers(classId: number) {
|
||||
return this.classTeacherRepo.find({
|
||||
where: { classId },
|
||||
relations: ['user'],
|
||||
});
|
||||
}
|
||||
|
||||
async addTeacher(classId: number, dto: AddTeacherDto) {
|
||||
const existing = await this.classTeacherRepo.findOne({
|
||||
where: { classId, userId: dto.userId, roleType: dto.roleType },
|
||||
});
|
||||
if (existing) throw new BadRequestException('该教师已分配此角色');
|
||||
|
||||
const entry = this.classTeacherRepo.create({ classId, userId: dto.userId, roleType: dto.roleType, subject: dto.subject });
|
||||
await this.classTeacherRepo.save(entry);
|
||||
|
||||
await this.syncClassTeacherIds(classId);
|
||||
return entry;
|
||||
}
|
||||
|
||||
async removeTeacher(classId: number, userId: number) {
|
||||
await this.classTeacherRepo.delete({ classId, userId });
|
||||
await this.syncClassTeacherIds(classId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
private async syncClassTeacherIds(classId: number) {
|
||||
const teachers = await this.classTeacherRepo.find({ where: { classId } });
|
||||
const updates: Record<string, number> = {};
|
||||
const head = teachers.find((t) => t.roleType === 'head_teacher');
|
||||
const life = teachers.find((t) => t.roleType === 'life_teacher');
|
||||
const academic = teachers.find((t) => t.roleType === 'academic_teacher');
|
||||
if (head) updates.headTeacherId = head.userId;
|
||||
if (life) updates.lifeTeacherId = life.userId;
|
||||
if (academic) updates.academicTeacherId = academic.userId;
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await this.classRepo.update(classId, updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user