import type { AttendanceWarning, AttendanceWarningSeverity, AttendanceWarningSummary, } from "./types" /** * L-3:预警阈值默认值(当班级未配置规则时使用)。 */ export const DEFAULT_ATTENDANCE_RATE_THRESHOLD = 90 export const DEFAULT_CONSECUTIVE_ABSENCE_THRESHOLD = 3 /** * 出勤率预警的严重等级划分: * - high:低于阈值 10 个百分点以上 * - medium:低于阈值 5-10 个百分点 * - low:低于阈值 5 个百分点以内 */ const rateSeverity = (current: number, threshold: number): AttendanceWarningSeverity => { const diff = threshold - current if (diff >= 10) return "high" if (diff >= 5) return "medium" return "low" } /** * 连续缺勤预警的严重等级划分: * - high:连续缺勤 ≥ 阈值 + 2 * - medium:连续缺勤 ≥ 阈值 + 1 * - low:连续缺勤 = 阈值 */ const absenceSeverity = (current: number, threshold: number): AttendanceWarningSeverity => { if (current >= threshold + 2) return "high" if (current >= threshold + 1) return "medium" return "low" } /** * 从按日期升序的状态列表中计算最长连续缺勤段。 * 缺勤定义为 status === "absent"(不含 late/early_leave 等其他异常状态)。 * 返回 { maxStreak, lastStreakDates }:最长连续段及其日期列表。 */ export const computeConsecutiveAbsence = ( records: { date: string; status: string }[] ): { maxStreak: number; lastStreakDates: string[] } => { if (records.length === 0) return { maxStreak: 0, lastStreakDates: [] } // 按日期升序排序 const sorted = [...records].sort((a, b) => a.date.localeCompare(b.date)) let maxStreak = 0 let currentStreak = 0 let currentStreakStart = 0 let lastMaxStart = 0 for (let i = 0; i < sorted.length; i++) { if (sorted[i].status === "absent") { if (currentStreak === 0) currentStreakStart = i currentStreak += 1 if (currentStreak > maxStreak) { maxStreak = currentStreak lastMaxStart = currentStreakStart } } else { currentStreak = 0 } } const lastStreakDates = maxStreak > 0 ? sorted.slice(lastMaxStart, lastMaxStart + maxStreak).map((r) => r.date) : [] return { maxStreak, lastStreakDates } } /** * L-3:根据学生出勤率统计和阈值生成预警列表(纯函数,便于测试)。 * * @param studentStats 学生列表,每项含 studentId/studentName/total/present 和按日期升序的记录 * @param attendanceRateThreshold 出勤率阈值(百分比) * @param consecutiveAbsenceThreshold 连续缺勤阈值(次) */ export const computeAttendanceWarnings = ( studentStats: { studentId: string studentName: string total: number present: number records: { date: string; status: string }[] }[], attendanceRateThreshold: number, consecutiveAbsenceThreshold: number ): AttendanceWarning[] => { const warnings: AttendanceWarning[] = [] for (const s of studentStats) { // 跳过无记录的学生(避免除零,且无记录不触发预警) if (s.total === 0) continue const presentRate = Math.round((s.present / s.total) * 10000) / 100 // 出勤率低于阈值 if (presentRate < attendanceRateThreshold) { warnings.push({ studentId: s.studentId, studentName: s.studentName, type: "low_attendance_rate", severity: rateSeverity(presentRate, attendanceRateThreshold), currentValue: presentRate, threshold: attendanceRateThreshold, relatedDates: [], }) } // 连续缺勤达到阈值 const { maxStreak, lastStreakDates } = computeConsecutiveAbsence(s.records) if (maxStreak >= consecutiveAbsenceThreshold) { warnings.push({ studentId: s.studentId, studentName: s.studentName, type: "consecutive_absence", severity: absenceSeverity(maxStreak, consecutiveAbsenceThreshold), currentValue: maxStreak, threshold: consecutiveAbsenceThreshold, relatedDates: lastStreakDates, }) } } // 严重等级排序:high → medium → low const severityOrder: Record = { high: 0, medium: 1, low: 2, } warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]) return warnings } /** * 构造空的预警汇总(当班级无数据或无规则时使用)。 */ export const createEmptyWarningSummary = ( classId: string, className: string ): AttendanceWarningSummary => ({ classId, className, attendanceRateThreshold: DEFAULT_ATTENDANCE_RATE_THRESHOLD, consecutiveAbsenceThreshold: DEFAULT_CONSECUTIVE_ABSENCE_THRESHOLD, warnings: [], })