- 迁移:超长科目原值备份移到 step 2 拆分 UPDATE 之前(原位置截断后按长度查不到原值,备份无效) - keepPlus:'+' 后紧跟公式触发符(- + = @ 等)不保留前缀,写回 Excel 不会被当公式 - workbook rels:逐元素解析任意属性顺序/引号,声明大小超限拒绝,防文件名绕过与内存放大 - create():钉钉导入失败时补偿删除刚建班级(成员外键 CASCADE),避免孤儿班级
583 lines
21 KiB
TypeScript
583 lines
21 KiB
TypeScript
import {
|
||
Injectable,
|
||
NotFoundException,
|
||
BadRequestException,
|
||
ForbiddenException,
|
||
} from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { DataSource, EntityManager, In, Like, Repository } from 'typeorm';
|
||
import { escapeLike } from '../common/like-escape';
|
||
import {
|
||
Class,
|
||
ClassStudent,
|
||
ClassTeacher,
|
||
ClassSchedule,
|
||
AttendanceRecord,
|
||
AttendanceSession,
|
||
Exam,
|
||
Student,
|
||
StudentDingMapping,
|
||
} from '../entities';
|
||
import { ClassesQueriesService, isDuplicateEntryError } from './classes-queries.service';
|
||
import { normalizeDateOnly } from '../database/date-normalization';
|
||
import dayjs from '../common/dayjs';
|
||
import {
|
||
CreateClassDto,
|
||
UpdateClassDto,
|
||
QueryClassDto,
|
||
AddTeacherDto,
|
||
QueryClassScheduleDto,
|
||
QueryClassAttendanceSummaryDto,
|
||
MAX_TEACHER_SUBJECTS,
|
||
MAX_SUBJECT_LENGTH,
|
||
} from './dto/class.dto';
|
||
import type { ClassRosterCreateRow, ClassRosterImportRow } from './class-roster-import';
|
||
|
||
interface RawStudentCount {
|
||
classId: string;
|
||
count: string;
|
||
}
|
||
|
||
/** 归一化多科目:去空白、去重、超限丢弃(长度上限由 dto MAX_SUBJECT_LENGTH 校验前置拦截)。
|
||
* 去重键统一小写:class_teacher 唯一索引在 MySQL 默认 collation 下大小写不敏感,
|
||
* 否则 'Math'+'math' 会通过此处去重却在 save 时撞 1062(保留首个原始大小写展示)。 */
|
||
export function normalizeTeacherSubjects(subjects?: string[]): string[] {
|
||
if (!subjects) return [];
|
||
const seen = new Set<string>();
|
||
const result: string[] = [];
|
||
for (const raw of subjects) {
|
||
const trimmed = raw?.trim() ?? '';
|
||
if (!trimmed || trimmed.length > MAX_SUBJECT_LENGTH) continue;
|
||
const key = trimmed.toLowerCase();
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
result.push(trimmed);
|
||
if (result.length >= MAX_TEACHER_SUBJECTS) break;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/** 解析老师科目:优先 subjects,归一化后为空时回退兼容旧字段 subject(含空白项场景)。 */
|
||
function resolveTeacherSubjects(
|
||
subjects: string[] | undefined,
|
||
subject: string | undefined,
|
||
): string[] {
|
||
const normalized = normalizeTeacherSubjects(subjects);
|
||
if (normalized.length > 0) return normalized;
|
||
if (subject) return normalizeTeacherSubjects([subject]);
|
||
return [];
|
||
}
|
||
|
||
@Injectable()
|
||
export class ClassesService {
|
||
constructor(
|
||
@InjectRepository(Class)
|
||
private classRepo: Repository<Class>,
|
||
@InjectRepository(ClassStudent)
|
||
private classStudentRepo: Repository<ClassStudent>,
|
||
@InjectRepository(ClassTeacher)
|
||
private classTeacherRepo: Repository<ClassTeacher>,
|
||
@InjectRepository(ClassSchedule)
|
||
private scheduleRepo: Repository<ClassSchedule>,
|
||
@InjectRepository(AttendanceRecord)
|
||
private attendanceRepo: Repository<AttendanceRecord>,
|
||
@InjectRepository(AttendanceSession)
|
||
private attendanceSessionRepo: Repository<AttendanceSession>,
|
||
@InjectRepository(Student)
|
||
private studentRepo: Repository<Student>,
|
||
@InjectRepository(StudentDingMapping)
|
||
private studentDingMappingRepo: Repository<StudentDingMapping>,
|
||
private dataSource: DataSource,
|
||
@InjectRepository(Exam)
|
||
private examRepo: Repository<Exam>,
|
||
private queries: ClassesQueriesService,
|
||
) {}
|
||
|
||
async getAccessibleClassIds(userId: number, canManageAll = false): Promise<number[] | undefined> {
|
||
if (canManageAll) return undefined;
|
||
const assignments = await this.classTeacherRepo.find({ where: { userId } });
|
||
return [...new Set(assignments.map((assignment) => assignment.classId))];
|
||
}
|
||
|
||
async assertClassAccess(userId: number, classId: number, canManageAll = false): Promise<void> {
|
||
if (canManageAll) return;
|
||
const assignment = await this.classTeacherRepo.findOne({ where: { userId, classId } });
|
||
if (!assignment) throw new ForbiddenException('只能访问自己被分配的班级');
|
||
}
|
||
|
||
async agentSearchClasses(
|
||
userId: number,
|
||
canManageAll: boolean,
|
||
query: { keyword?: string; status?: string; limit?: number },
|
||
) {
|
||
const accessibleClassIds = await this.getAccessibleClassIds(userId, canManageAll);
|
||
return this.queries.agentSearchClasses(accessibleClassIds, query);
|
||
}
|
||
|
||
async batchImportStudents(
|
||
classId: number,
|
||
users: Array<{ dingUserId: string; name: string; mobile?: string }>,
|
||
): Promise<{ imported: number; skipped: number; conflicts: number }> {
|
||
return this.queries.batchImportStudents(classId, users);
|
||
}
|
||
|
||
async previewRosterImport(classId: number, rows: ClassRosterImportRow[]) {
|
||
return this.queries.previewRosterImport(classId, rows);
|
||
}
|
||
|
||
async commitRosterImport(
|
||
classId: number,
|
||
input: { addStudentIds?: number[]; createRows?: ClassRosterCreateRow[] },
|
||
) {
|
||
return this.queries.commitRosterImport(classId, input);
|
||
}
|
||
|
||
async getSchedule(classId: number, query: QueryClassScheduleDto) {
|
||
return this.queries.getSchedule(classId, query);
|
||
}
|
||
|
||
async getAttendanceSummary(classId: number, query: QueryClassAttendanceSummaryDto) {
|
||
return this.queries.getAttendanceSummary(classId, query);
|
||
}
|
||
|
||
async findAll(query: QueryClassDto, accessibleClassIds?: number[]) {
|
||
const where: Record<string, unknown> = {};
|
||
if (query.status) where.status = query.status;
|
||
if (query.classType) where.classType = query.classType;
|
||
if (query.keyword) where.name = Like(`%${escapeLike(query.keyword)}%`);
|
||
// Default: hide archived, unless explicitly requested
|
||
where.isArchived = query.isArchived ?? false;
|
||
|
||
if (accessibleClassIds) {
|
||
if (accessibleClassIds.length === 0) return [];
|
||
where.id = In(accessibleClassIds);
|
||
}
|
||
|
||
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'],
|
||
order: { id: 'ASC' },
|
||
});
|
||
|
||
return {
|
||
...cls,
|
||
students: students.map((s) => ({
|
||
id: s.id,
|
||
studentId: s.studentId,
|
||
studentName: s.student?.name,
|
||
studentNo: s.student?.studentNo,
|
||
joinDate: s.joinDate,
|
||
leaveDate: s.leaveDate,
|
||
status: s.status,
|
||
})),
|
||
// 一行一科目:直接返回原始行,同一老师多科时按科目拆成多行
|
||
teachers: teachers.map((t) => ({
|
||
id: t.id,
|
||
userId: t.userId,
|
||
name: t.user?.name,
|
||
username: t.user?.username,
|
||
roleType: t.roleType,
|
||
subject: t.subject,
|
||
subjects: t.subject ? [t.subject] : [],
|
||
})),
|
||
studentCount: students.filter((s) => s.status === 'active').length,
|
||
};
|
||
}
|
||
|
||
async create(dto: CreateClassDto) {
|
||
const { studentIds, teachers, users, ...classData } = dto;
|
||
|
||
// 先校验老师科目(任课老师必须至少一个科目),避免 400 时已写入班级留下孤儿数据
|
||
const teacherPlans = (teachers ?? []).map((t) => ({
|
||
userId: t.userId,
|
||
roleType: t.roleType,
|
||
subjects: resolveTeacherSubjects(t.subjects, t.subject),
|
||
}));
|
||
for (const plan of teacherPlans) {
|
||
if (plan.roleType === 'subject_teacher' && plan.subjects.length === 0) {
|
||
throw new BadRequestException('任课老师至少需要一个科目');
|
||
}
|
||
}
|
||
|
||
// 班级/学生/教师写入整体包进事务:任一环节失败(并发唯一索引冲突、断连等)整体回滚,
|
||
// 避免「班级与已加入学生已落库、教师写入失败」留下孤儿数据。
|
||
// batchImportStudents(钉钉导入)自带事务,保持事务外调用,失败行为与原来一致。
|
||
const savedClassId = await this.dataSource.transaction(async (manager) => {
|
||
const cls = manager.create(Class, {
|
||
...classData,
|
||
startDate: normalizeDateOnly(classData.startDate) ?? undefined,
|
||
endDate: normalizeDateOnly(classData.endDate) ?? undefined,
|
||
});
|
||
const saved = await manager.save(cls);
|
||
|
||
// add students
|
||
if (studentIds?.length) {
|
||
const entries = studentIds.map((sid: number) =>
|
||
manager.create(ClassStudent, {
|
||
classId: saved.id,
|
||
studentId: sid,
|
||
joinDate: dayjs().utcOffset(8).format('YYYY-MM-DD'),
|
||
}),
|
||
);
|
||
await manager.save(ClassStudent, entries);
|
||
}
|
||
|
||
// add teachers:任课老师一行一科目;其他角色单行无科目
|
||
if (teacherPlans.length) {
|
||
const entries: ClassTeacher[] = [];
|
||
const seen = new Set<string>();
|
||
for (const plan of teacherPlans) {
|
||
if (plan.roleType === 'subject_teacher') {
|
||
for (const subject of plan.subjects) {
|
||
// 唯一索引 collation 大小写不敏感,去重键统一小写避免 'Math'+'math' 撞 1062
|
||
const key = `${plan.userId}:${plan.roleType}:${subject.toLowerCase()}`;
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
entries.push(
|
||
manager.create(ClassTeacher, {
|
||
classId: saved.id,
|
||
userId: plan.userId,
|
||
roleType: plan.roleType,
|
||
subject,
|
||
}),
|
||
);
|
||
}
|
||
} else {
|
||
const key = `${plan.userId}:${plan.roleType}:`;
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
entries.push(
|
||
manager.create(ClassTeacher, {
|
||
classId: saved.id,
|
||
userId: plan.userId,
|
||
roleType: plan.roleType,
|
||
subject: '',
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
if (entries.length) await manager.save(ClassTeacher, entries);
|
||
|
||
// sync head/life/academic teacher IDs(事务内用 manager 保证同事务可见性)
|
||
await this.syncClassTeacherIds(saved.id, manager);
|
||
}
|
||
|
||
return saved.id;
|
||
});
|
||
|
||
// batch import students by dingUserIds(自带事务,事务外调用)
|
||
if (users?.length) {
|
||
try {
|
||
await this.batchImportStudents(savedClassId, users);
|
||
} catch (error) {
|
||
// 钉钉导入失败会抛错,班级已在事务中提交——补偿删除刚创建的班级(学生/教师成员
|
||
// 外键均 CASCADE),避免留下孤儿班级且前端重试会重复建班
|
||
try {
|
||
await this.classRepo.delete(savedClassId);
|
||
} catch {
|
||
// 补偿删除失败:保留班级,优先抛原始错误
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
return this.findOne(savedClassId);
|
||
}
|
||
|
||
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,
|
||
...(dto.startDate !== undefined
|
||
? { startDate: normalizeDateOnly(dto.startDate) ?? undefined }
|
||
: {}),
|
||
...(dto.endDate !== undefined
|
||
? { endDate: normalizeDateOnly(dto.endDate) ?? undefined }
|
||
: {}),
|
||
});
|
||
return this.findOne(id);
|
||
}
|
||
|
||
/** 归档班级(软删除) */
|
||
async archive(id: number) {
|
||
const cls = await this.classRepo.findOne({ where: { id } });
|
||
if (!cls) throw new NotFoundException('班级不存在');
|
||
if (cls.isArchived) throw new BadRequestException('班级已归档');
|
||
await this.classRepo.update(id, { isArchived: true });
|
||
return { success: true };
|
||
}
|
||
|
||
/** 取消归档 */
|
||
async restore(id: number) {
|
||
const cls = await this.classRepo.findOne({ where: { id } });
|
||
if (!cls) throw new NotFoundException('班级不存在');
|
||
if (!cls.isArchived) throw new BadRequestException('班级未归档');
|
||
await this.classRepo.update(id, { isArchived: false });
|
||
return { success: true };
|
||
}
|
||
|
||
/** 归档班级(兼容旧删除入口,不物理删除) */
|
||
async remove(id: number) {
|
||
return this.archive(id);
|
||
}
|
||
|
||
/** 永久删除班级(仅已归档) */
|
||
async purge(id: number) {
|
||
const cls = await this.classRepo.findOne({ where: { id } });
|
||
if (!cls) throw new NotFoundException('班级不存在');
|
||
if (!cls.isArchived) throw new BadRequestException('仅已归档班级可以永久删除,请先归档');
|
||
const [studentCount, teacherCount, scheduleCount, examCount, sessionCount, attendanceCount] =
|
||
await Promise.all([
|
||
this.classStudentRepo.count({ where: { classId: id } }),
|
||
this.classTeacherRepo.count({ where: { classId: id } }),
|
||
this.scheduleRepo.count({ where: { classId: id } }),
|
||
this.examRepo.count({ where: { classId: id } }),
|
||
this.attendanceSessionRepo.count({ where: { classId: id } }),
|
||
this.attendanceRepo.count({ where: { classId: id } }),
|
||
]);
|
||
const references: string[] = [];
|
||
if (studentCount > 0) references.push('班级学生');
|
||
if (teacherCount > 0) references.push('任课教师');
|
||
if (scheduleCount > 0) references.push('排课');
|
||
if (examCount > 0) references.push('考试');
|
||
if (sessionCount > 0 || attendanceCount > 0) references.push('考勤记录');
|
||
if (references.length > 0) {
|
||
throw new BadRequestException(`该班级存在关联数据(${references.join('、')}),无法永久删除`);
|
||
}
|
||
await this.classRepo.delete(id);
|
||
return { message: '已永久删除班级(不可恢复)' };
|
||
}
|
||
|
||
async getStudents(classId: number) {
|
||
return this.classStudentRepo.find({
|
||
where: { classId },
|
||
relations: ['student'],
|
||
order: { createdAt: 'ASC' as const },
|
||
});
|
||
}
|
||
|
||
async addStudents(classId: number, studentIds: number[]) {
|
||
const uniqueStudentIds = [...new Set(studentIds)];
|
||
if (uniqueStudentIds.length === 0) return { added: 0, skipped: 0 };
|
||
|
||
const cls = await this.classRepo.findOne({ where: { id: classId } });
|
||
if (!cls) throw new NotFoundException('班级不存在');
|
||
|
||
const students = await this.studentRepo.find({ where: { id: In(uniqueStudentIds) } });
|
||
if (students.length !== uniqueStudentIds.length) {
|
||
throw new NotFoundException('部分学生不存在');
|
||
}
|
||
|
||
const existing = await this.classStudentRepo.find({
|
||
where: { classId, studentId: In(uniqueStudentIds) },
|
||
});
|
||
const existingByStudentId = new Map(
|
||
existing.map((classStudent) => [classStudent.studentId, classStudent]),
|
||
);
|
||
const today = dayjs().utcOffset(8).format('YYYY-MM-DD');
|
||
let skipped = 0;
|
||
const memberships = uniqueStudentIds.flatMap((studentId) => {
|
||
const current = existingByStudentId.get(studentId);
|
||
if (current?.status === 'active') {
|
||
skipped++;
|
||
return [];
|
||
}
|
||
if (current) {
|
||
current.status = 'active';
|
||
current.joinDate = today;
|
||
current.leaveDate = null;
|
||
return [current];
|
||
}
|
||
return [
|
||
this.classStudentRepo.create({
|
||
classId,
|
||
studentId,
|
||
status: 'active',
|
||
joinDate: today,
|
||
}),
|
||
];
|
||
});
|
||
if (memberships.length) await this.classStudentRepo.save(memberships);
|
||
|
||
return { added: memberships.length, skipped };
|
||
}
|
||
|
||
async removeStudent(classId: number, studentId: number) {
|
||
const membership = await this.classStudentRepo.findOne({
|
||
where: { classId, studentId },
|
||
});
|
||
if (!membership) throw new NotFoundException('学生不在该班级');
|
||
if (membership.status !== 'active') throw new BadRequestException('学生已离班');
|
||
|
||
membership.status = 'left';
|
||
membership.leaveDate = dayjs().utcOffset(8).format('YYYY-MM-DD');
|
||
await this.classStudentRepo.save(membership);
|
||
return { success: true };
|
||
}
|
||
|
||
async getTeachers(classId: number) {
|
||
const rows = await this.classTeacherRepo.find({
|
||
where: { classId },
|
||
relations: ['user'],
|
||
order: { id: 'ASC' },
|
||
});
|
||
// 一行一科目:直接返回原始行,同一老师多科时按科目拆成多行
|
||
return rows.map((t) => ({
|
||
id: t.id,
|
||
userId: t.userId,
|
||
username: t.user?.username,
|
||
name: t.user?.name,
|
||
roleType: t.roleType,
|
||
subject: t.subject,
|
||
subjects: t.subject ? [t.subject] : [],
|
||
}));
|
||
}
|
||
|
||
async addTeacher(classId: number, dto: AddTeacherDto) {
|
||
const cls = await this.classRepo.findOne({ where: { id: classId } });
|
||
if (!cls) throw new NotFoundException('班级不存在');
|
||
|
||
// 任课老师:一个科目一行;同科目已存在则跳过,允许追加新科目
|
||
if (dto.roleType === 'subject_teacher') {
|
||
const subjects = resolveTeacherSubjects(dto.subjects, dto.subject);
|
||
if (subjects.length === 0) {
|
||
throw new BadRequestException('任课老师至少需要一个科目');
|
||
}
|
||
const existing = await this.classTeacherRepo.find({
|
||
where: { classId, userId: dto.userId, roleType: dto.roleType },
|
||
});
|
||
const existingSubjects = new Set(
|
||
existing
|
||
.map((row) => row.subject?.trim().toLowerCase())
|
||
.filter((subject): subject is string => !!subject),
|
||
);
|
||
const entries: ClassTeacher[] = [];
|
||
for (const subject of subjects) {
|
||
// 大小写不敏感比对:与唯一索引 collation 一致
|
||
if (existingSubjects.has(subject.toLowerCase())) continue;
|
||
entries.push(
|
||
this.classTeacherRepo.create({
|
||
classId,
|
||
userId: dto.userId,
|
||
roleType: dto.roleType,
|
||
subject,
|
||
}),
|
||
);
|
||
}
|
||
// 逐行保存:MySQL 多行 INSERT 是原子语句,任意一行撞唯一索引整批失败;
|
||
// 逐行可让并发下已存在的科目跳过(1062 不中止 InnoDB 事务)、其余科目正常落库;
|
||
// 非重复错误(断连等)整体回滚,避免部分科目已落库而接口报错。
|
||
// 返回真实持久化的行。
|
||
const persisted: ClassTeacher[] = [];
|
||
if (entries.length) {
|
||
await this.dataSource.transaction(async (manager) => {
|
||
for (const entry of entries) {
|
||
try {
|
||
persisted.push(await manager.save(entry));
|
||
} catch (error) {
|
||
// 并发添加同一科目时唯一索引冲突:该行已存在,跳过即可
|
||
if (!isDuplicateEntryError(error)) throw error;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
await this.syncClassTeacherIds(classId);
|
||
return persisted;
|
||
}
|
||
|
||
// 班主任/生活老师/学服老师:单行无科目(subject 占位空串 '')。
|
||
// 先查重快速失败;并发下两个请求都可能通过查重,由 4 列唯一索引
|
||
// (class_id,user_id,role_type,subject) 兜底,save 捕获 1062 转 400
|
||
// (TypeORM 0.3.x 无事务时不可用悲观锁,故不依赖锁串行化)。
|
||
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: '',
|
||
});
|
||
try {
|
||
await this.classTeacherRepo.save(entry);
|
||
} catch (error) {
|
||
if (!isDuplicateEntryError(error)) throw error;
|
||
throw new BadRequestException('该教师已分配此角色');
|
||
}
|
||
|
||
await this.syncClassTeacherIds(classId);
|
||
return [entry];
|
||
}
|
||
|
||
async removeTeacher(classId: number, userId: number) {
|
||
const assignments = await this.classTeacherRepo.find({ where: { classId, userId } });
|
||
if (assignments.length === 0) throw new NotFoundException('教师未分配到该班级');
|
||
await this.classTeacherRepo.delete({ classId, userId });
|
||
await this.syncClassTeacherIds(classId);
|
||
return { success: true };
|
||
}
|
||
|
||
async removeTeacherAssignment(classId: number, assignmentId: number) {
|
||
const assignment = await this.classTeacherRepo.findOne({
|
||
where: { id: assignmentId, classId },
|
||
});
|
||
if (!assignment) throw new NotFoundException('教师角色分配不存在');
|
||
await this.classTeacherRepo.delete({ id: assignmentId, classId });
|
||
await this.syncClassTeacherIds(classId);
|
||
return { success: true };
|
||
}
|
||
|
||
private async syncClassTeacherIds(classId: number, manager?: EntityManager) {
|
||
const teachers = manager
|
||
? await manager.find(ClassTeacher, { where: { classId } })
|
||
: await this.classTeacherRepo.find({ where: { classId } });
|
||
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');
|
||
const patch = {
|
||
headTeacherId: head?.userId ?? null,
|
||
lifeTeacherId: life?.userId ?? null,
|
||
academicTeacherId: academic?.userId ?? null,
|
||
} as Partial<Class>;
|
||
if (manager) {
|
||
await manager.update(Class, classId, patch);
|
||
} else {
|
||
await this.classRepo.update(classId, patch);
|
||
}
|
||
}
|
||
}
|