feat(core-edu): 完整实现 core-edu 教学核心服务

包含 classes/exams/homework/grades/attendance/scheduling 域、outbox、iam-consumer、redis 配置等完整实现
This commit is contained in:
SpecialX
2026-07-10 19:08:56 +08:00
parent 06a646ea4e
commit 58c0ba1bd9
55 changed files with 4204 additions and 305 deletions

View File

@@ -0,0 +1,82 @@
/**
* 排课冲突检测(纯函数)
*
* 冲突维度P3
* - teacher: 同一教师在同一时间段有排课冲突
* - class: 同一班级在同一时间段有排课冲突
*
* room_id 仅预留字段ISSUE-006 决策 #2P3 不校验教室冲突。
*/
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<ScheduleSlot, "id"> & { 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 };
}