feat: 结算时同步钉钉已审批请假并标记为请假
This commit is contained in:
@@ -4,6 +4,7 @@ import { DataSource, Repository, In, Between } from 'typeorm';
|
||||
import {
|
||||
AttendanceRecord,
|
||||
DingAttendanceRaw,
|
||||
DingLeaveRaw,
|
||||
Class,
|
||||
Student,
|
||||
ClassSchedule,
|
||||
@@ -32,6 +33,7 @@ export class AttendanceLessonService {
|
||||
constructor(
|
||||
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
|
||||
@InjectRepository(DingAttendanceRaw) private dingRawRepo: Repository<DingAttendanceRaw>,
|
||||
@InjectRepository(DingLeaveRaw) private dingLeaveRawRepo: Repository<DingLeaveRaw>,
|
||||
@InjectRepository(Class) private classRepo: Repository<Class>,
|
||||
@InjectRepository(Student) private studentRepo: Repository<Student>,
|
||||
@InjectRepository(ClassSchedule) private scheduleRepo: Repository<ClassSchedule>,
|
||||
@@ -150,29 +152,34 @@ export class AttendanceLessonService {
|
||||
const existingStudentIds = new Set(existingRecords.map((record) => record.studentId));
|
||||
|
||||
const lessonSessionKey = mapLessonScheduleTimeToSession(schedule.startTime);
|
||||
const updatedRecords = existingRecords.map((record) => {
|
||||
record.student = studentsById.get(record.studentId)!;
|
||||
// Preserve manual corrections only while the lesson is still in progress.
|
||||
if (!finalize && record.source !== 'dingtalk') return record;
|
||||
const updatedRecords = await Promise.all(
|
||||
existingRecords.map(async (record) => {
|
||||
record.student = studentsById.get(record.studentId)!;
|
||||
// Preserve manual corrections only while the lesson is still in progress.
|
||||
if (!finalize && record.source !== 'dingtalk') return record;
|
||||
|
||||
const raw = selectDingTalkRecordsForLesson(
|
||||
rawByStudent.get(record.studentId) ?? [],
|
||||
schedule,
|
||||
lessonDate,
|
||||
);
|
||||
record.status = mapDingTalkStatus(raw, effectiveFinalize);
|
||||
Object.assign(record, getLessonPunchMetadata(
|
||||
raw,
|
||||
const raw = selectDingTalkRecordsForLesson(
|
||||
rawByStudent.get(record.studentId) ?? [],
|
||||
schedule,
|
||||
lessonDate,
|
||||
schedule.startTime,
|
||||
));
|
||||
record.remark = raw.some((item) => item.checkInTime || item.checkOutTime)
|
||||
? null
|
||||
: effectiveFinalize
|
||||
? '课程截止仍未打卡'
|
||||
: '未获取到钉钉打卡结果';
|
||||
return record;
|
||||
});
|
||||
);
|
||||
const resolved = await this.resolveLessonStatus(
|
||||
record.studentId,
|
||||
raw,
|
||||
schedule,
|
||||
lessonDate,
|
||||
effectiveFinalize,
|
||||
);
|
||||
record.status = resolved.status;
|
||||
Object.assign(record, getLessonPunchMetadata(
|
||||
raw,
|
||||
lessonDate,
|
||||
schedule.startTime,
|
||||
));
|
||||
record.remark = resolved.remark ?? null;
|
||||
return record;
|
||||
}),
|
||||
);
|
||||
for (const classStudent of classStudents) {
|
||||
if (existingStudentIds.has(classStudent.studentId)) continue;
|
||||
const raw = selectDingTalkRecordsForLesson(
|
||||
@@ -180,6 +187,13 @@ export class AttendanceLessonService {
|
||||
schedule,
|
||||
lessonDate,
|
||||
);
|
||||
const resolved = await this.resolveLessonStatus(
|
||||
classStudent.studentId,
|
||||
raw,
|
||||
schedule,
|
||||
lessonDate,
|
||||
effectiveFinalize,
|
||||
);
|
||||
updatedRecords.push(
|
||||
recordRepo.create({
|
||||
studentId: classStudent.studentId,
|
||||
@@ -189,18 +203,14 @@ export class AttendanceLessonService {
|
||||
attendanceSessionId: existing.id,
|
||||
attendanceDate: lessonDate,
|
||||
session: lessonSessionKey,
|
||||
status: mapDingTalkStatus(raw, effectiveFinalize),
|
||||
status: resolved.status,
|
||||
source: 'dingtalk',
|
||||
...getLessonPunchMetadata(
|
||||
raw,
|
||||
lessonDate,
|
||||
schedule.startTime,
|
||||
),
|
||||
remark: raw.some((item) => item.checkInTime || item.checkOutTime)
|
||||
? undefined
|
||||
: effectiveFinalize
|
||||
? '课程截止仍未打卡'
|
||||
: '未获取到钉钉打卡结果',
|
||||
remark: resolved.remark,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -263,34 +273,39 @@ export class AttendanceLessonService {
|
||||
}
|
||||
|
||||
const lessonSessionKey = mapLessonScheduleTimeToSession(schedule.startTime);
|
||||
const records = classStudents.map((classStudent) => {
|
||||
const raw = selectDingTalkRecordsForLesson(
|
||||
rawByStudent.get(classStudent.studentId) ?? [],
|
||||
schedule,
|
||||
lessonDate,
|
||||
);
|
||||
return recordRepo.create({
|
||||
studentId: classStudent.studentId,
|
||||
student: classStudent.student,
|
||||
classId: schedule.classId!,
|
||||
scheduleId,
|
||||
attendanceSessionId: session.id,
|
||||
attendanceDate: lessonDate,
|
||||
session: lessonSessionKey,
|
||||
status: mapDingTalkStatus(raw, finalize),
|
||||
source: 'dingtalk',
|
||||
...getLessonPunchMetadata(
|
||||
raw,
|
||||
const records = await Promise.all(
|
||||
classStudents.map(async (classStudent) => {
|
||||
const raw = selectDingTalkRecordsForLesson(
|
||||
rawByStudent.get(classStudent.studentId) ?? [],
|
||||
schedule,
|
||||
lessonDate,
|
||||
schedule.startTime,
|
||||
),
|
||||
remark: raw.some((item) => item.checkInTime || item.checkOutTime)
|
||||
? undefined
|
||||
: finalize
|
||||
? '课程截止仍未打卡'
|
||||
: '未获取到钉钉打卡结果',
|
||||
});
|
||||
});
|
||||
);
|
||||
const resolved = await this.resolveLessonStatus(
|
||||
classStudent.studentId,
|
||||
raw,
|
||||
schedule,
|
||||
lessonDate,
|
||||
finalize,
|
||||
);
|
||||
return recordRepo.create({
|
||||
studentId: classStudent.studentId,
|
||||
student: classStudent.student,
|
||||
classId: schedule.classId!,
|
||||
scheduleId,
|
||||
attendanceSessionId: session.id,
|
||||
attendanceDate: lessonDate,
|
||||
session: lessonSessionKey,
|
||||
status: resolved.status,
|
||||
source: 'dingtalk',
|
||||
...getLessonPunchMetadata(
|
||||
raw,
|
||||
lessonDate,
|
||||
schedule.startTime,
|
||||
),
|
||||
remark: resolved.remark,
|
||||
});
|
||||
}),
|
||||
);
|
||||
const saved = await recordRepo.save(records);
|
||||
if (finalize) {
|
||||
session.status = 'completed';
|
||||
@@ -302,6 +317,59 @@ export class AttendanceLessonService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算(finalize)时无打卡的学生,若当天存在钉钉已审批通过的请假且与
|
||||
* 本节课时间窗口重叠,则记为 leave,而不是缺勤。
|
||||
*/
|
||||
private async resolveLessonStatus(
|
||||
studentId: number,
|
||||
raw: DingAttendanceRaw[],
|
||||
schedule: Pick<ClassSchedule, 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
|
||||
lessonDate: string,
|
||||
finalize: boolean,
|
||||
): Promise<{ status: string; remark?: string }> {
|
||||
const hasPunch = raw.some((item) => item.checkInTime || item.checkOutTime);
|
||||
if (!finalize) {
|
||||
return {
|
||||
status: mapDingTalkStatus(raw, false),
|
||||
remark: hasPunch ? undefined : '未获取到钉钉打卡结果',
|
||||
};
|
||||
}
|
||||
if (hasPunch) return { status: 'present', remark: undefined };
|
||||
|
||||
const leave = await this.findApprovedLeaveForStudent(studentId, schedule, lessonDate);
|
||||
if (leave) {
|
||||
return {
|
||||
status: 'leave',
|
||||
remark: `钉钉请假已通过(${leave.leaveType || leave.tagName || '请假'})`,
|
||||
};
|
||||
}
|
||||
return { status: 'absent', remark: '课程截止仍未打卡' };
|
||||
}
|
||||
|
||||
private async findApprovedLeaveForStudent(
|
||||
studentId: number,
|
||||
schedule: Pick<ClassSchedule, 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
|
||||
lessonDate: string,
|
||||
): Promise<DingLeaveRaw | null> {
|
||||
const leaves = await this.dingLeaveRawRepo.find({
|
||||
where: { matchedStudentId: studentId },
|
||||
});
|
||||
const window = getLessonAttendanceWindow(schedule, lessonDate);
|
||||
const overlapping = leaves.filter(
|
||||
(leave) =>
|
||||
leave.startTime &&
|
||||
leave.endTime &&
|
||||
leave.startTime.getTime() <= window.end &&
|
||||
leave.endTime.getTime() >= window.start,
|
||||
);
|
||||
overlapping.sort(
|
||||
(left, right) =>
|
||||
(right.approvedAt?.getTime() ?? 0) - (left.approvedAt?.getTime() ?? 0),
|
||||
);
|
||||
return overlapping[0] ?? null;
|
||||
}
|
||||
|
||||
private async fetchDingTalkRawByStudent(
|
||||
classId: number,
|
||||
schedule: Pick<ClassSchedule, 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
|
||||
|
||||
Reference in New Issue
Block a user