/** * 排课冲突检测(纯函数) * * 冲突维度(P3): * - teacher: 同一教师在同一时间段有排课冲突 * - class: 同一班级在同一时间段有排课冲突 * * room_id 仅预留字段(ISSUE-006 决策 #2),P3 不校验教室冲突。 */ export interface ScheduleSlot { id: string; teacherId: string; classId: string; startTime: Date; endTime: Date; roomId?: string | null; } export interface ConflictResult { hasConflict: boolean; conflictingSlot?: ScheduleSlot; conflictType: "teacher" | "class" | null; } /** * 检查时间区间重叠。 * [startA, endA) 与 [startB, endB) 重叠的条件:startA < endB && startB < endA */ export function isTimeOverlap( startA: Date, endA: Date, startB: Date, endB: Date, ): boolean { return startA < endB && startB < endA; } /** * 检查新排课与现有排课列表的冲突。 * 排除自身(如果是更新场景,传入 excludeId)。 */ export function detectConflict( newSlot: Omit & { id?: string }, existingSlots: ScheduleSlot[], excludeId?: string, ): ConflictResult { for (const slot of existingSlots) { if (excludeId && slot.id === excludeId) continue; if ( !isTimeOverlap( newSlot.startTime, newSlot.endTime, slot.startTime, slot.endTime, ) ) { continue; } // 教师冲突 if (newSlot.teacherId === slot.teacherId) { return { hasConflict: true, conflictingSlot: slot, conflictType: "teacher", }; } // 班级冲突 if (newSlot.classId === slot.classId) { return { hasConflict: true, conflictingSlot: slot, conflictType: "class", }; } } return { hasConflict: false, conflictType: null }; }