forked from wangziqi/gongxue-base
feat: improve attendance scheduling and API validation
This commit is contained in:
@@ -175,24 +175,45 @@ export class AttendanceService {
|
||||
return { schedule, session, records };
|
||||
}
|
||||
|
||||
private getLessonAttendanceWindow(
|
||||
schedule: Pick<ClassSchedule, 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
|
||||
lessonDate: string,
|
||||
): { start: number; end: number; dateFrom: string; dateTo: string } {
|
||||
const startMinuteOfDay = this.toMinutes(schedule.startTime);
|
||||
const endMinuteOfDay = this.toMinutes(schedule.endTime);
|
||||
const advanceMinutes = Math.max(0, schedule.attendanceAdvanceMinutes ?? 30);
|
||||
const lessonStart = new Date(`${lessonDate}T${schedule.startTime}:00+08:00`).getTime();
|
||||
let lessonEnd = new Date(`${lessonDate}T${schedule.endTime}:00+08:00`).getTime();
|
||||
const overnight = endMinuteOfDay <= startMinuteOfDay;
|
||||
if (overnight) lessonEnd += 24 * 60 * 60 * 1000;
|
||||
|
||||
return {
|
||||
start: lessonStart - advanceMinutes * 60 * 1000,
|
||||
end: lessonEnd,
|
||||
dateFrom: advanceMinutes > startMinuteOfDay ? this.shiftDate(lessonDate, -1) : lessonDate,
|
||||
dateTo: overnight ? this.shiftDate(lessonDate, 1) : lessonDate,
|
||||
};
|
||||
}
|
||||
|
||||
getLessonAttendanceImportDateRange(
|
||||
schedule: Pick<ClassSchedule, 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
|
||||
lessonDate: string,
|
||||
): { startDate: string; endDate: string } {
|
||||
const window = this.getLessonAttendanceWindow(schedule, lessonDate);
|
||||
return { startDate: window.dateFrom, endDate: window.dateTo };
|
||||
}
|
||||
|
||||
private selectDingTalkRecordsForLesson(
|
||||
records: DingAttendanceRaw[],
|
||||
schedule: Pick<ClassSchedule, 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
|
||||
lessonDate: string,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
): DingAttendanceRaw[] {
|
||||
const [startHour, startMinute] = startTime.split(':').map(Number);
|
||||
const [endHour, endMinute] = endTime.split(':').map(Number);
|
||||
const start = new Date(`${lessonDate}T${startTime}:00+08:00`).getTime();
|
||||
let end = new Date(`${lessonDate}T${endTime}:00+08:00`).getTime();
|
||||
if (endHour * 60 + endMinute <= startHour * 60 + startMinute) end += 24 * 60 * 60 * 1000;
|
||||
const windowStart = start - 3 * 60 * 60 * 1000;
|
||||
const windowEnd = end + 3 * 60 * 60 * 1000;
|
||||
const timed = records.filter((record) => {
|
||||
const window = this.getLessonAttendanceWindow(schedule, lessonDate);
|
||||
return records.filter((record) => {
|
||||
// 上班、下班打卡都有效,按原始记录中实际存在的时间判断。
|
||||
const time = record.checkInTime ?? record.checkOutTime;
|
||||
return time && time.getTime() >= windowStart && time.getTime() <= windowEnd;
|
||||
return time && time.getTime() >= window.start && time.getTime() <= window.end;
|
||||
});
|
||||
return timed.length > 0 ? timed : records.filter((record) => !record.checkInTime && !record.checkOutTime);
|
||||
}
|
||||
|
||||
private mapDingTalkStatus(records: DingAttendanceRaw[], finalize = false): string {
|
||||
@@ -291,7 +312,7 @@ export class AttendanceService {
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const sessionRepo = manager.getRepository(AttendanceSession);
|
||||
const recordRepo = manager.getRepository(AttendanceRecord);
|
||||
const rawByStudent = await this.fetchDingTalkRawByStudent(schedule.classId!, lessonDate);
|
||||
const rawByStudent = await this.fetchDingTalkRawByStudent(schedule.classId!, schedule, lessonDate);
|
||||
const existingRecords = await recordRepo.find({
|
||||
where: { attendanceSessionId: existing.id },
|
||||
order: { studentId: 'ASC' },
|
||||
@@ -312,9 +333,8 @@ export class AttendanceService {
|
||||
|
||||
const raw = this.selectDingTalkRecordsForLesson(
|
||||
rawByStudent.get(record.studentId) ?? [],
|
||||
schedule,
|
||||
lessonDate,
|
||||
schedule.startTime,
|
||||
schedule.endTime,
|
||||
);
|
||||
record.status = this.mapDingTalkStatus(raw, finalize);
|
||||
Object.assign(record, this.getLessonPunchMetadata(
|
||||
@@ -333,9 +353,8 @@ export class AttendanceService {
|
||||
if (existingStudentIds.has(classStudent.studentId)) continue;
|
||||
const raw = this.selectDingTalkRecordsForLesson(
|
||||
rawByStudent.get(classStudent.studentId) ?? [],
|
||||
schedule,
|
||||
lessonDate,
|
||||
schedule.startTime,
|
||||
schedule.endTime,
|
||||
);
|
||||
updatedRecords.push(
|
||||
recordRepo.create({
|
||||
@@ -377,7 +396,7 @@ export class AttendanceService {
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const sessionRepo = manager.getRepository(AttendanceSession);
|
||||
const recordRepo = manager.getRepository(AttendanceRecord);
|
||||
const rawByStudent = await this.fetchDingTalkRawByStudent(schedule.classId!, lessonDate);
|
||||
const rawByStudent = await this.fetchDingTalkRawByStudent(schedule.classId!, schedule, lessonDate);
|
||||
|
||||
const classStudents = await this.classStudentRepo.find({
|
||||
where: { classId: schedule.classId!, status: 'active' },
|
||||
@@ -421,9 +440,8 @@ export class AttendanceService {
|
||||
const records = classStudents.map((classStudent) => {
|
||||
const raw = this.selectDingTalkRecordsForLesson(
|
||||
rawByStudent.get(classStudent.studentId) ?? [],
|
||||
schedule,
|
||||
lessonDate,
|
||||
schedule.startTime,
|
||||
schedule.endTime,
|
||||
);
|
||||
return recordRepo.create({
|
||||
studentId: classStudent.studentId,
|
||||
@@ -460,6 +478,7 @@ export class AttendanceService {
|
||||
|
||||
private async fetchDingTalkRawByStudent(
|
||||
classId: number,
|
||||
schedule: Pick<ClassSchedule, 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
|
||||
lessonDate: string,
|
||||
): Promise<Map<number, DingAttendanceRaw[]>> {
|
||||
const classStudents = await this.classStudentRepo.find({
|
||||
@@ -467,9 +486,10 @@ export class AttendanceService {
|
||||
});
|
||||
if (classStudents.length === 0) return new Map();
|
||||
const studentIds = classStudents.map((cs) => cs.studentId);
|
||||
const window = this.getLessonAttendanceWindow(schedule, lessonDate);
|
||||
const rawRecords = await this.dingRawRepo.find({
|
||||
where: {
|
||||
attendanceDate: lessonDate,
|
||||
attendanceDate: Between(window.dateFrom, window.dateTo),
|
||||
matchedStudentId: In(studentIds),
|
||||
},
|
||||
});
|
||||
@@ -651,6 +671,17 @@ export class AttendanceService {
|
||||
});
|
||||
}
|
||||
|
||||
private toMinutes(time: string): number {
|
||||
const [hour, minute] = time.split(':').map(Number);
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
private shiftDate(date: string, days: number): string {
|
||||
const shifted = new Date(`${date}T00:00:00.000Z`);
|
||||
shifted.setUTCDate(shifted.getUTCDate() + days);
|
||||
return shifted.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
private mapScheduleTimeToSession(startTime: string): string {
|
||||
const hour = parseInt(startTime.slice(0, 2), 10);
|
||||
if (hour < 8) return 'morning_reading';
|
||||
|
||||
Reference in New Issue
Block a user