首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >排课冲突别再靠层层 if:一套时间段重叠检测与多资源约束校验方案

排课冲突别再靠层层 if:一套时间段重叠检测与多资源约束校验方案

原创
作者头像
用户5598620
发布2026-09-14 11:02:46
发布2026-09-14 11:02:46
1000
举报

排课、约课、排班类系统最隐蔽的 bug,几乎都出在“时间冲突”上:课表上看着没问题,直到一位老师被排进同时开的两门课、一间教室在同一时段被两个班占用。这篇把冲突判断从直觉写法讲到正确的区间条件,再到多资源、数据库兜底和整表批量校验,文末附一张可直接照抄的排查清单。

一、先纠正一个最常见的反向判断错误

很多人第一反应是枚举“不冲突”的情况:A 在 B 前面、或 A 在 B 后面,于是写出一堆大于小于。这种写法不仅绕,还很容易在“首尾相接算不算冲突”上出错。

更稳的思路是反过来:先求“什么情况下一定冲突”,剩下的都不冲突。 两个半开区间 [s1,e1)、[s2,e2) 发生重叠,当且仅当:

区间重叠条件(半开区间):s1 小于 e2,且 s2 小于 e1。

注意是“开始早于对方结束”,用半开区间(结束时刻不含)后,09:00-10:00 和 10:00-11:00 首尾相接不算冲突,正好符合排课习惯。

代码语言:javascript
复制
function isOverlap(s1, e1, s2, e2) {
  // 入参统一为分钟数或时间戳,避免字符串比较
  return s1 < e2 && s2 < e1;
}

只记这一条就够了:“两个开始都早于对方结束”即重叠,其余情况天然不冲突,不需要再枚举。

二、单资源:一条 SQL 把冲突查出来

课程安排落库后,新增或拖拽一节课时,最可靠的是直接让数据库判断“这个资源在这个时段是否已有课”。以“同一间教室”为例:

代码语言:sql
复制
SELECT lesson_id, start_min, end_min
FROM schedule
WHERE room_id = :room_id
  AND start_min < :new_end
  AND end_min   > :new_start
LIMIT 1;

查到任意一行就是冲突,返回给前端标红即可。把时间统一换算成“从 0 点起的分钟数”(或时间戳)再比较,能彻底避开 '09:00' 字符串比较、跨天、12 小时制这些坑。

应用层封装时把资源做成参数,老师、教室、班级各查一遍:

代码语言:javascript
复制
async function findConflicts(db, {teacherId, roomId, classId, start, end, excludeId}) {
  const sql = `
    SELECT lesson_id, teacher_id, room_id, class_id FROM schedule
    WHERE lesson_id <> :excludeId
      AND start_min < :end AND end_min > :start
      AND (teacher_id = :teacherId OR room_id = :roomId OR class_id = :classId)
    LIMIT 20`;
  return db.query(sql, {teacherId, roomId, classId, start, end, excludeId: excludeId || 0});
}

excludeId 用来排除“正在编辑的那节课自己”,否则改时间时会和自己判重,这是很容易漏掉的一点。

三、多资源约束:老师、教室、班级是三条独立的线

一节排课记录同时占用三类资源,任何一类撞了都算冲突,且冲突原因要分别告诉用户:

资源

冲突含义

提示文案应区分

教师

同一老师同时段已有课

“张老师该时段还有另一节课”

教室

同一教室同时段被占

“301 教室该时段已被占用”

班级

同一班级同时段另有安排

“该班该时段已有课程”

把三类资源拆开判定、分别返回冲突类型,前端才能在对应行精确提示,而不是笼统报一句“时间冲突”。有些场景还有第四类约束,比如“特定课程只能用特定类型教室(机房/琴房)”,这类是资源匹配约束,和时间重叠是正交的两层,建议分开校验、分开报错。

四、数据库层兜底:别只信应用层判断

应用层“先查再插”在并发下并不可靠:两个排课请求同时查到“无冲突”,随后都插入,冲突课就这么落库了。和库存超卖同理,需要数据库这一层独立兜底。

做法一:事务加锁。 在同一事务里对相关资源的排课记录加锁再判断:

代码语言:sql
复制
START TRANSACTION;
SELECT lesson_id FROM schedule
WHERE room_id = :room_id AND start_min < :new_end AND end_min > :new_start
FOR UPDATE;
-- 应用层确认无冲突后再 INSERT
INSERT INTO schedule(teacher_id, room_id, class_id, start_min, end_min)
VALUES (:teacherId, :roomId, :classId, :start, :end);
COMMIT;

做法二:用排他约束表。 如果数据库支持范围类型(如 PostgreSQL 的 tstzrange),可以对“资源 + 时间段”建排他约束,从根上拒绝重叠插入:

代码语言:sql
复制
ALTER TABLE schedule ADD CONSTRAINT no_room_overlap
EXCLUDE USING gist (
  room_id WITH =,
  tstzrange(start_at, end_at, '[)') WITH &&
);

应用层校验负责“体验好、提示准”,数据库约束负责“绝不出错”,两层都要有,不能互相替代。

五、整张课表批量校验:拖拽改时间时一次扫完

课表视图里常常是连续拖拽多节课,逐节查库既慢又容易闪烁。可以一次性把相关资源一周的课拉到内存,用排序扫描在线性时间内判完全部冲突:

代码语言:javascript
复制
function findAllOverlaps(lessons, resourceKey) {
  const sorted = [...lessons].sort((a, b) => a.start - b.start);
  const conflicts = [];
  for (let i = 1; i < sorted.length; i++) {
    const prev = sorted[i - 1], cur = sorted[i];
    if (prev[resourceKey] === cur[resourceKey] && prev.end > cur.start) {
      conflicts.push([prev.lesson_id, cur.lesson_id]);
    }
  }
  return conflicts; // 同资源按开始排序后,只需比较相邻两条
}

同一资源按开始时间排序后,重叠只可能发生在相邻记录之间,复杂度从两两比较的 O(n²) 降到排序的 O(n log n)。老师、教室、班级各跑一遍即可。

六、踩坑清单

  • 用闭区间判断,导致 10:00 结束和 10:00 开始被误判冲突——统一用半开区间 [s,e)
  • 直接比较 '9:00' 和 '10:00' 字符串,跨上午下午出错——先换算成分钟数或时间戳。
  • 编辑课程时没排除自身,结果和自己判重——查询带 lesson_id <> 当前id
  • 只判了教室,漏了老师和班级,或三类混在一起无法给出准确提示——分资源、分类型返回。
  • 只做应用层“先查后插”,并发下插入双份冲突课——加事务锁或排他约束兜底。
  • 跨天课程(晚课跨过 0 点)按同一天比较漏掉冲突——按时间戳比较并在入库时规范起止。

七、工程落地建议

冲突校验建议沉淀成一个独立的领域服务,对外只暴露“校验排课意图、返回结构化冲突列表”一个接口,前端拖拽、批量导入、复制课表三种入口都复用它,避免每个入口各写一套判断。时间表示在系统内统一为 UTC 时间戳或分钟整数、只在展示层转本地时区;资源维度用枚举固定下来,后续新增“设备”“监考老师”等资源时只需扩展维度、不改判定算法。批量导入场景先在内存里做一次全量预检、把所有冲突一次性返回,比逐条报错、用户改一条再导一次体验好得多。

八、复盘清单

  • 是否用一条 s1<e2 AND s2<e1 替代了枚举式判断?
  • 老师、教室、班级三类资源是否分别校验、分别提示?
  • 编辑场景是否排除了记录自身?
  • 数据库层是否有事务锁或排他约束兜底并发?
  • 批量校验是否用排序扫描把复杂度控制在 O(n log n)?

排课冲突的难点从来不是“判断两个时间段”,而是把多资源、并发、批量、跨天这些边界一次性兜住。把区间条件写对、把约束分层、把提示做细,这类问题就能从“线上偶发”变成“提交前拦截”。本文为工程实践经验分享,具体实现以所用数据库与业务规则为准。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先纠正一个最常见的反向判断错误
  • 二、单资源:一条 SQL 把冲突查出来
  • 三、多资源约束:老师、教室、班级是三条独立的线
  • 四、数据库层兜底:别只信应用层判断
  • 五、整张课表批量校验:拖拽改时间时一次扫完
  • 六、踩坑清单
  • 七、工程落地建议
  • 八、复盘清单
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档