feat: add attendance device SN classroom bindings

This commit is contained in:
2026-07-16 10:22:23 +08:00
parent d037787346
commit 559b8a56e9
16 changed files with 1323 additions and 23 deletions

View File

@@ -4,6 +4,7 @@ import { Repository, In, Between, LessThanOrEqual, MoreThanOrEqual, DataSource }
import {
AttendanceRecord,
AttendanceSession,
AttendanceDevice,
DingAttendanceRaw,
Class,
Student,
@@ -66,11 +67,71 @@ export class AttendanceService {
private classTeacherRepo: Repository<ClassTeacher>,
@InjectRepository(AttendanceSession)
private attendanceSessionRepo: Repository<AttendanceSession>,
@InjectRepository(AttendanceDevice)
private attendanceDeviceRepo: Repository<AttendanceDevice>,
private dataSource: DataSource,
) {}
private sessionMutex = new SessionMutex();
private formatDeviceDetail(device: AttendanceDevice): string {
const classroomName = device.classroom?.name;
return classroomName ? `${device.deviceName} · ${classroomName}` : device.deviceName;
}
private async attachAttendanceDeviceMappings<T extends AttendanceRecord>(
records: T[],
classroomId?: number | null,
): Promise<T[]> {
if (records.length === 0) return records;
const sns = [...new Set(records.map((record) => record.punchDeviceId?.trim()).filter(Boolean) as string[])];
const devicesBySn = new Map<string, AttendanceDevice>();
if (sns.length > 0) {
const devices = await this.attendanceDeviceRepo.find({
where: { deviceSn: In(sns), status: 'active' },
relations: ['classroom'],
});
for (const device of devices) devicesBySn.set(device.deviceSn, device);
}
const classroomIds = [...new Set([
...records.map((record) => record.classId).filter((id): id is number => id != null),
...(classroomId != null ? [classroomId] : []),
])];
const devicesByClassroom = new Map<number, AttendanceDevice>();
if (classroomIds.length > 0) {
const devices = await this.attendanceDeviceRepo.find({
where: { classroomId: In(classroomIds), status: 'active' },
relations: ['classroom'],
order: { id: 'ASC' },
});
for (const device of devices) {
if (!devicesByClassroom.has(device.classroomId)) devicesByClassroom.set(device.classroomId, device);
}
}
for (const record of records) {
const sn = record.punchDeviceId?.trim();
const mappedBySn = sn ? devicesBySn.get(sn) : undefined;
if (mappedBySn) {
record.punchDeviceName = this.formatDeviceDetail(mappedBySn);
record.punchDeviceId = mappedBySn.deviceSn;
continue;
}
const source = (record.punchSource || '').trim().toUpperCase();
const isMachine = ['ATM', 'ATTENDANCE_MACHINE', 'MACHINE', 'DEVICE'].some(
(value) => source === value || source.includes(value),
);
const fallbackClassroomId = record.classId ?? classroomId ?? undefined;
const mappedByClassroom = fallbackClassroomId ? devicesByClassroom.get(fallbackClassroomId) : undefined;
if (isMachine && mappedByClassroom && !record.punchDeviceName) {
record.punchDeviceName = this.formatDeviceDetail(mappedByClassroom);
record.punchDeviceId = record.punchDeviceId || mappedByClassroom.deviceSn;
}
}
return records;
}
async getAccessibleClassIds(userId: number, canManageAll = false): Promise<number[] | undefined> {
if (canManageAll) return undefined;
const assignments = await this.classTeacherRepo.find({ where: { userId } });
@@ -172,7 +233,7 @@ export class AttendanceService {
order: { studentId: 'ASC' },
})
: [];
return { schedule, session, records };
return { schedule, session, records: await this.attachAttendanceDeviceMappings(records, schedule.classId) };
}
private getLessonAttendanceWindow(
@@ -299,7 +360,7 @@ export class AttendanceService {
relations: ['student'],
order: { studentId: 'ASC' },
});
return { schedule, session: existing, records };
return { schedule, session: existing, records: await this.attachAttendanceDeviceMappings(records, schedule.classId) };
}
if (existing.status !== 'in_progress' && !(finalize && existing.status === 'settling')) {
throw new BadRequestException('课程考勤正在结算');
@@ -385,7 +446,7 @@ export class AttendanceService {
existing.completedAt = new Date();
await sessionRepo.save(existing);
}
return { schedule, session: existing, records: saved };
return { schedule, session: existing, records: await this.attachAttendanceDeviceMappings(saved, schedule.classId) };
});
}
@@ -428,7 +489,7 @@ export class AttendanceService {
relations: ['student'],
order: { studentId: 'ASC' },
});
return { schedule, session, records: existingRecords };
return { schedule, session, records: await this.attachAttendanceDeviceMappings(existingRecords, schedule.classId) };
}
}
throw err;
@@ -469,7 +530,7 @@ export class AttendanceService {
session.completedAt = new Date();
session = await sessionRepo.save(session);
}
return { schedule, session, records: saved };
return { schedule, session, records: await this.attachAttendanceDeviceMappings(saved, schedule.classId) };
});
}
@@ -516,7 +577,7 @@ export class AttendanceService {
relations: ['student'],
order: { studentId: 'ASC' },
});
return { session, records };
return { session, records: await this.attachAttendanceDeviceMappings(records, session.classId) };
}
const pendingRecords = await recordRepo.count({
@@ -535,7 +596,7 @@ export class AttendanceService {
relations: ['student'],
order: { studentId: 'ASC' },
});
return { session: savedSession, records };
return { session: savedSession, records: await this.attachAttendanceDeviceMappings(records, session.classId) };
}),
);
}