refactor: 拆分考勤服务文件并通过 aislop 全项目扫描
Some checks failed
CI / check (pull_request) Failing after 5m24s

This commit is contained in:
2026-08-05 18:55:36 +08:00
parent 6ba7f4e3d0
commit b882411f42
10 changed files with 424 additions and 331 deletions

View File

@@ -22,9 +22,9 @@ import {
getLessonAttendanceImportDateRange,
getLessonAttendanceWindow,
selectDingTalkRecordsForLesson,
mapDingTalkStatus,
getLessonPunchMetadata,
} from './attendance-dingtalk';
import { buildLessonRecord, resolveLessonStatus } from './attendance-lesson-status';
@Injectable()
export class AttendanceLessonService {
@@ -163,7 +163,8 @@ export class AttendanceLessonService {
schedule,
lessonDate,
);
const resolved = await this.resolveLessonStatus(
const resolved = await resolveLessonStatus(
this.dingLeaveRawRepo,
record.studentId,
raw,
schedule,
@@ -183,8 +184,9 @@ export class AttendanceLessonService {
for (const classStudent of classStudents) {
if (existingStudentIds.has(classStudent.studentId)) continue;
updatedRecords.push(
await this.buildLessonRecord(
await buildLessonRecord(
recordRepo,
this.dingLeaveRawRepo,
rawByStudent,
classStudent,
schedule,
@@ -257,8 +259,9 @@ export class AttendanceLessonService {
const lessonSessionKey = mapLessonScheduleTimeToSession(schedule.startTime);
const records = await Promise.all(
classStudents.map((classStudent) =>
this.buildLessonRecord(
buildLessonRecord(
recordRepo,
this.dingLeaveRawRepo,
rawByStudent,
classStudent,
schedule,
@@ -281,122 +284,6 @@ 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 createLessonRecord(
recordRepo: Repository<AttendanceRecord>,
classStudent: ClassStudent,
raw: DingAttendanceRaw[],
options: {
schedule: Pick<ClassSchedule, 'classId' | 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>;
scheduleId: number;
lessonDate: string;
lessonSessionKey: string;
attendanceSessionId: number;
status: string;
remark?: string;
},
): AttendanceRecord {
return recordRepo.create({
studentId: classStudent.studentId,
student: classStudent.student,
classId: options.schedule.classId!,
scheduleId: options.scheduleId,
attendanceSessionId: options.attendanceSessionId,
attendanceDate: options.lessonDate,
session: options.lessonSessionKey,
status: options.status,
source: 'dingtalk',
...getLessonPunchMetadata(raw, options.lessonDate, options.schedule.startTime),
remark: options.remark,
});
}
private async buildLessonRecord(
recordRepo: Repository<AttendanceRecord>,
rawByStudent: Map<number, DingAttendanceRaw[]>,
classStudent: ClassStudent,
schedule: Pick<ClassSchedule, 'classId' | 'startTime' | 'endTime' | 'attendanceAdvanceMinutes'>,
lessonDate: string,
lessonSessionKey: string,
attendanceSessionId: number,
scheduleId: number,
finalize: boolean,
): Promise<AttendanceRecord> {
const raw = selectDingTalkRecordsForLesson(
rawByStudent.get(classStudent.studentId) ?? [],
schedule,
lessonDate,
);
const resolved = await this.resolveLessonStatus(
classStudent.studentId,
raw,
schedule,
lessonDate,
finalize,
);
return this.createLessonRecord(recordRepo, classStudent, raw, {
schedule,
scheduleId,
lessonDate,
lessonSessionKey,
attendanceSessionId,
status: resolved.status,
remark: resolved.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'>,