feat: settle course attendance automatically

This commit is contained in:
2026-07-13 10:26:38 +08:00
parent b9b295c997
commit 1c8bd08be4
11 changed files with 730 additions and 87 deletions

View File

@@ -195,24 +195,16 @@ export class AttendanceService {
return timed.length > 0 ? timed : records.filter((record) => !record.checkInTime && !record.checkOutTime);
}
private mapDingTalkStatus(records: DingAttendanceRaw[]): string {
const results = new Set(records.map((record) => record.timeResult?.toLowerCase()));
if (results.has('late') || results.has('seriouslate')) return 'late';
if (
results.has('notsigned') ||
results.has('absenteeism') ||
results.has('absent')
) {
return 'absent';
}
if (results.has('leave') || results.has('vacation')) return 'leave';
if (results.has('normal')) return 'present';
return 'pending';
private mapDingTalkStatus(records: DingAttendanceRaw[], finalize = false): string {
const hasPunch = records.some((record) => record.checkInTime || record.checkOutTime);
if (hasPunch) return 'present';
return finalize ? 'absent' : 'pending';
}
async createLessonAttendanceFromDingTalk(
scheduleId: number,
lessonDate: string,
userId: number,
finalize = false,
) {
const schedule = await this.getScheduleOccurrence(scheduleId, lessonDate);
const now = new Date();
@@ -244,9 +236,13 @@ export class AttendanceService {
});
return { schedule, session: existing, records };
}
if (existing.status !== 'in_progress' && !(finalize && existing.status === 'settling')) {
throw new BadRequestException('课程考勤正在结算');
}
// Refresh in_progress session from latest DingTalk data
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 existingRecords = await recordRepo.find({
@@ -264,8 +260,8 @@ export class AttendanceService {
const updatedRecords = existingRecords.map((record) => {
record.student = studentsById.get(record.studentId)!;
// Preserve manually corrected records.
if (record.source !== 'dingtalk') return record;
// Preserve manual corrections only while the lesson is still in progress.
if (!finalize && record.source !== 'dingtalk') return record;
const raw = this.selectDingTalkRecordsForLesson(
rawByStudent.get(record.studentId) ?? [],
@@ -273,8 +269,12 @@ export class AttendanceService {
schedule.startTime,
schedule.endTime,
);
record.status = this.mapDingTalkStatus(raw);
record.remark = raw.length === 0 ? '未获取到钉钉打卡结果,请老师确认' : null;
record.status = this.mapDingTalkStatus(raw, finalize);
record.remark = raw.some((item) => item.checkInTime || item.checkOutTime)
? null
: finalize
? '课程截止仍未打卡'
: '未获取到钉钉打卡结果';
return record;
});
for (const classStudent of classStudents) {
@@ -294,14 +294,24 @@ export class AttendanceService {
attendanceSessionId: existing.id,
attendanceDate: lessonDate,
session: this.mapScheduleTimeToSession(schedule.startTime),
status: this.mapDingTalkStatus(raw),
status: this.mapDingTalkStatus(raw, finalize),
source: 'dingtalk',
remark: raw.length === 0 ? '未获取到钉钉打卡结果,请老师确认' : undefined,
remark: raw.some((item) => item.checkInTime || item.checkOutTime)
? undefined
: finalize
? '课程截止仍未打卡'
: '未获取到钉钉打卡结果',
}),
);
}
const saved = await recordRepo.save(updatedRecords);
if (finalize) {
existing.status = 'completed';
existing.completedBy = userId;
existing.completedAt = new Date();
await sessionRepo.save(existing);
}
return { schedule, session: existing, records: saved };
});
}
@@ -366,12 +376,22 @@ export class AttendanceService {
attendanceSessionId: session.id,
attendanceDate: lessonDate,
session: this.mapScheduleTimeToSession(schedule.startTime),
status: this.mapDingTalkStatus(raw),
status: this.mapDingTalkStatus(raw, finalize),
source: 'dingtalk',
remark: raw.length === 0 ? '未获取到钉钉打卡结果,请老师确认' : undefined,
remark: raw.some((item) => item.checkInTime || item.checkOutTime)
? undefined
: finalize
? '课程截止仍未打卡'
: '未获取到钉钉打卡结果',
});
});
const saved = await recordRepo.save(records);
if (finalize) {
session.status = 'completed';
session.completedBy = userId;
session.completedAt = new Date();
session = await sessionRepo.save(session);
}
return { schedule, session, records: saved };
});
}