ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

高校排课系统源码与SQL脚本实战:数据库设计与冲突检测算法解析

高校排课系统源码与SQL脚本实战:数据库设计与冲突检测算法解析 简介面向高校教务与毕业设计场景这套排课系统源码包提供了一套可运行、可二次开发的完整实现。围绕班级、教室、教师、课程等核心实体覆盖学期排课流程、多条件下课表编排与教学资源调度等关键模块帮助学习者理解SpringBoot项目从需求分析到编码落地的全过程。压缩包共235个文件含56个Java源文件、55个class文件、31个xml配置、82个html页面以及yml、sql等资源后端逻辑、前端页面与数据库脚本齐全整体约617KB适合快速导入IDE查看和启动。目前已有1133人学习下载适合高校学生用于课程设计、毕业设计参考或排课业务的开发入门。通过源码与SQL脚本可直观掌握班级、教师、教室等实体的数据模型设计及排课冲突处理思路是一份实用性较强的练手与借鉴资源。1. 高校排课系统源码与SQL脚本到底解决了什么排课这件事越到学期末越像一门玄学。同一个老师不能同时出现在两间教室同一个教室不能在同一个时间片塞两个班A课程要求的多媒体教室偏偏和B课程的大班课撞在一起。这些规则单条看都很简单叠在一起就变成了典型的 NP 完全问题。也正因为如此高校排课系统成了数据库课程设计里出现频率最高的题目之一——它既有足够清晰的业务边界又能把 ER 建模、约束设计、存储过程、算法回溯全部串起来。而“源码数据库SQL脚本”这种交付形式意味着你不只是交一篇论文还要交一套能初始化、能跑通、能演示的完整工程。我一般会这么理解这套东西源码解决的是“课表怎么从无到有算出来”SQL脚本解决的是“算出来的课表怎么落地、怎么查、怎么改”。这两部分缺一个演示时都会卡壳。2. 排课系统源码的模块划分与技术选型拿到一套排课系统源码先不要急着打开 IDE 跑起来先理清楚它把业务拆成了哪几个模块。常见做法是三层结构表现层负责课表录入、展示和调课操作业务层负责排课算法、冲突检测和课程分配数据层通过 SQL 脚本建表再用 ORM 或 MyBatis 做持久化。每一层之间用明确的接口隔开这样你替换算法或调整数据库字段时不需要把整个工程推倒重来。2.1 课程、班级、教师、教室四元模型及依赖关系排课系统的数据模型绕不开四个核心实体课程、班级、教师、教室。它们之间不是互相独立而是有强依赖的。课程决定周学时和授课方式比如理论课每周 4 学时、实验课每周 2 学时教师负责多门课程并在同一时间片只能出现在一个地点班级是排课的基本对象一个班有固定人数和必选课程列表教室则提供容量、类型普通/多媒体/机房两个关键属性。建表时课程表要外键关联教师表排课结果表要把班级、课程、教师、教室、时间片五个维度一起关联起来这样查询课表时才能一 JOIN 出完整信息。2.1.1 排课结果表的最小字段集排课结果表是整套系统的核心字段不能乱加但也不能少。下面是一段 Java 实体类的最小设计同时也对应数据库里的schedule表public class Schedule { private Integer id; private Integer courseId; // 课程ID关联 course 表 private Integer classId; // 班级ID关联 class 表 private Integer teacherId; // 教师ID关联 teacher 表 private Integer roomId; // 教室ID关联 room 表 private Integer dayOfWeek; // 星期几1-7 private Integer startSlot; // 开始节次1-5 private Integer endSlot; // 结束节次1-5 // getter/setter 省略 }这里的startSlot和endSlot用整数表示节次而不是用具体时间字符串是因为排课算法在做冲突检测时只需要比较数字区间是否重叠不需要解析时间字符串。如果你在源码里看到这两个字段被设计成varchar那多半是赶进度的产物建议你在改源码时换成int。另外dayOfWeek startSlot endSlot这三个字段共同决定了“时间片”加上classId和teacherId后基本上所有冲突检测都可以用 SQL 或内存循环完成。2.2 后端业务层的排课引擎接口排课算法的入口应该是一个独立接口而不是散落在 Controller 里。源码里常见的写法是public interface SchedulerService { /** * 对指定学期执行自动排课 * param termId 学期ID * return 成功生成的课表记录数 */ int scheduleTerm(Integer termId); }这个接口的实现在内部会依次完成读取该学期所有开课计划读取所有可用的教室和时间片然后按班级或课程优先级逐个尝试分配。把排课逻辑收敛到一个方法里是为了方便在毕业设计答辩时演示“一键排课”的操作也方便你后续替换不同的算法实现而不影响 Controller 层代码。2.2.1 时间片资源池的初始化排课之前要先建一个时间片资源池。一周 5 天每天 5 大节上午 2 小节算 1 个节次下午 2 小节晚上 1 个节次总共 25 个标准时间片。但加上教室维度后资源就不是 25 个而是“25 × 可用教室数”。这一步通常用内存循环生成列表ListInteger roomIds roomMapper.findAvailableRooms(termId); ListTimeSlot pool new ArrayList(); for (int day 1; day 5; day) { for (int slot 1; slot 5; slot) { for (Integer roomId : roomIds) { pool.add(new TimeSlot(day, slot, slot, roomId)); } } }这段代码看起来简单但它决定了算法的时间复杂度上限。如果教室是 50 间资源池就是 25×501250 个位置每门课去遍历一遍冲突检测是 O(n²)对毕业设计的规模来说完全够用。startSlot和endSlot在这里相等表示只分配单个节次如果课程要求连续两节则需要额外生成startSloti, endSloti1的扩展时间片。2.3 前端展示与课表数据的 JSON 格式前端课表大多是类似 Excel 的网格视图行是节次列是星期几。后端返回给前端的数据最好按“班级课表”或“教师课表”组织。一个比较稳妥的 JSON 结构是{ classId: 2023001, className: 软件工程2301班, courses: [ { courseName: 操作系统, teacher: 张老师, room: A301, dayOfWeek: 1, startSlot: 3, endSlot: 4 } ] }前端拿到这个结构后直接用dayOfWeek定位列用startSlot和endSlot定位行并设置rowspan就能渲染出标准课表。这个格式同时也方便导出成图片或 PDF是源码里最值得复用的一段设计。3. 数据库SQL脚本表结构设计、初始化数据与约束陷阱很多毕业设计项目的源码本身不难难的是 SQL 脚本能不能在一台干净的机器上直接跑通。所谓“数据库SQL脚本”不是单纯把建表语句堆在一起而是要包含建库、建表、插入初始化数据、创建索引、设置外键约束这一整套流程。如果评审老师直接用 Navicat 运行你的脚本跑一半报错那后面所有演示都会失去意义。3.1 核心表结构设计含 DDL 示例下面给出一段适合排课系统的最小 DDL数据库以 MySQL 为例。注意字符集统一用 utf8mb4否则中文课程名容易乱码。CREATE DATABASE IF NOT EXISTS course_schedule DEFAULT CHARACTER SET utf8mb4; USE course_schedule; CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, title VARCHAR(20) DEFAULT 讲师, load_limit INT DEFAULT 12 COMMENT 每周最多课时数 ) ENGINEInnoDB; CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL UNIQUE, capacity INT DEFAULT 60, room_type TINYINT DEFAULT 0 COMMENT 0普通, 1多媒体, 2机房 ) ENGINEInnoDB; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, week_hours INT DEFAULT 3, require_type TINYINT DEFAULT 0 COMMENT 0多媒体, 1机房, 2无要求, FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINEInnoDB; CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, class_id INT NOT NULL, teacher_id INT NOT NULL, room_id INT NOT NULL, day_of_week TINYINT NOT NULL CHECK (day_of_week BETWEEN 1 AND 7), start_slot TINYINT NOT NULL, end_slot TINYINT NOT NULL, term_id VARCHAR(20) NOT NULL, FOREIGN KEY (course_id) REFERENCES course(id), FOREIGN KEY (teacher_id) REFERENCES teacher(id), FOREIGN KEY (room_id) REFERENCES room(id) ) ENGINEInnoDB;这段脚本的设计要点在于teacher和course分开建避免教师信息冗余schedule保留teacher_id的冗余是为了查询教师课表时减少一次 JOIN这个字段在排课算法里也会频繁用到。CHECK约束在 MySQL 8.0.16 之后才生效如果你用的是 5.7建议还是靠应用层校验。3.1.1 班级表与开课计划表班级表不能只有班级名称还要保存入学年份和人数。开课计划表则把“某学期某专业开哪些课”单独存一张表否则排课算法要同时过滤课程和班级两个维度逻辑会乱。开课计划表的常见设计是plan_id、course_id、class_id、week_hours四列其中week_hours可以覆盖课程默认学时比如同一门课软件工程专业上 4 学时网络工程专业只上 2 学时。3.2 排课结果表的唯一性约束设计排课结果表最容易忽视的是联合唯一约束。如果不在数据库层面加约束源码里算法一旦出现小 bug就会生成重复课表比如同一个教室同一时间安排了俩班。建议在schedule表上追加三个唯一索引ALTER TABLE schedule ADD UNIQUE INDEX uniq_class_time (class_id, day_of_week, start_slot, end_slot), ADD UNIQUE INDEX uniq_teacher_time (teacher_id, day_of_week, start_slot, end_slot), ADD UNIQUE INDEX uniq_room_time (room_id, day_of_week, start_slot, end_slot);这三个约束分别对应班级冲突、教师冲突、教室冲突。有人会问为什么不创建一个四字段的联合唯一索引因为排课算法在冲突检测时是分维度的分开建索引可以让数据库在插入时直接拦截某一类冲突排错时也更清楚到底违反了哪条规则。需要注意的是如果课程允许“合班上课”即一个老师对多个班那uniq_class_time就需要调整把上课班级拆成子表单独记录这里就不展开了。3.3 SQL脚本中初始化数据的正确姿势初始化数据时要避免一个大坑直接用固定自增 ID 写插入语句。例如INSERT INTO teacher (id, name) VALUES (1, 张三)虽然能跑通但如果你后续调整了表的前置数据或使用不同的数据库版本自增序列会对不上。更稳妥的做法是只插入非自增字段INSERT INTO teacher (name, title, load_limit) VALUES (张老师, 副教授, 10), (李老师, 讲师, 12), (王老师, 教授, 8);课程表中引用教师时可以用子查询避免硬编码 IDINSERT INTO course (course_name, teacher_id, week_hours, require_type) SELECT 数据结构, id, 4, 1 FROM teacher WHERE name 张老师;这种写法的好处是脚本可重复执行且不用关心 ID 分配顺序评审老师拿到你的脚本时不会因为库里已有数据导致 ID 错乱。另外terminology的初始化数据最好单独放在data.sql文件里和schema.sql分开方便答辩时只重置数据不动表结构。4. 从SQL到代码排课算法中的冲突检测与回溯有了表结构和 SQL 脚本真正的难点是把排课规则变成代码逻辑。这一章不讨论遗传算法这类复杂方案先讲最实用、也最容易在答辩时讲清楚的贪心 冲突检测算法。4.1 冲突检测的四个维度和优先级排课前要先定义什么是“冲突”。我用一个表格来归纳冲突维度判定条件检测优先级班级冲突同一班级在同一时间已被分配课程高教师冲突同一教师在同一时间已被分配课程高教室冲突同一教室在同一时间已被分配课程高容量冲突班级人数 教室容量低优先级不代表重要性而是检测顺序。班级和教师冲突一旦发生就是硬性错误必须在算法内跳过该时间片容量冲突可以通过调换教室解决所以放到最后判断。实际编码时我一般会写成独立的三个HashSet比反复查数据库要快得多。4.1.1 内存中的占用标记结构// 用字符串拼接作为占用标记 SetString classUsed new HashSet(); SetString teacherUsed new HashSet(); SetString roomUsed new HashSet(); // 标记某个时间片被占用 String key dayOfWeek - startSlot - endSlot; classUsed.add(classId | key); teacherUsed.add(teacherId | key); roomUsed.add(roomId | key);这样做的目的是把三维冲突检测降维成字符串查找HashSet 的查找复杂度是 O(1)比每次写 SQL 查数据库快几个数量级。注意这里的时间片 key 包含了startSlot和endSlot是为了支持连续两节上课的情况。如果课程是 2 个课时连着上那就不能用单个节次去判断否则会把教室中间空出一节造成碎片。4.2 带优先级的贪心排课算法贪心算法的核心思路是先排“难排”的课。难点如何定义这里我按三个因素排序合班课程优先、教师工作量大的优先、有多媒体需求的优先。代码如下public int scheduleTerm(Integer termId) { ListPlan plans planMapper.findByTerm(termId); // 按难度排序 plans.sort((a, b) - { int pa a.weekHours * 2 (a.requireType 0 ? 1 : 0); int pb b.weekHours * 2 (b.requireType 0 ? 1 : 0); return pb - pa; }); int successCount 0; for (Plan plan : plans) { boolean scheduled false; // 遍历所有时间片和教室 for (int day 1; day 5 !scheduled; day) { for (int start 1; start plan.continuousSlot - 1 5 !scheduled; start) { int end start plan.continuousSlot - 1; for (Room room : availableRooms) { if (isAvailable(plan, room, day, start, end)) { insertSchedule(plan, room, day, start, end); successCount; scheduled true; break; } } } } if (!scheduled) { log.warn(课程 {} 无法排入课表, plan.getCourseName()); } } return successCount; }这里的关键是三重循环的顺序先固定星期几再固定起始节次最后遍历教室。这种顺序会让每个班级一周内的课程尽量分布在不同的天避免出现周一一整天满课、周二一节没有的情况。教室遍历放在最内层意味着当前时间片所有教室都冲突时才会跳到下一个节次。continuousSlot参数表示连续上课的小节数通常为 1 或 2。4.2.2 冲突检测函数的实现private boolean isAvailable(Plan plan, Room room, int day, int start, int end) { if (room.capacity plan.classSize) return false; for (int s start; s end; s) { String key day - s - s; if (teacherUsed.contains(plan.teacherId | key)) return false; if (classUsed.contains(plan.classId | key)) return false; if (roomUsed.contains(room.id | key)) return false; } return true; }把连续节次拆成单个 slot 去检测是因为即使上课时间是 2 小节中间也不允许插入其他碎片课程。拆开后如果这个教室第 3 节被占用而第 4 节空闲那么第 3-4 节的连续请求也会被拒绝防止排出的课表出现教室空档。这种实现方式简单直接也方便在答辩时解释“为什么这里不能只查 start 和 end 两个点”。4.3 贪心失败时的数据库层面的兜底当某门课遍历完所有时间片都无法排入时说明单靠贪心已经没解了。这时有两种处理路径一是记录失败课程输出人工调整清单二是启用简单的回溯即“回退最近 10 门课尝试换一个时间片再继续”。回溯深度不建议设置太大否则排课时间会指数增长。回退后需要在schedule表先执行DELETE删除原有记录再重新插入新记录这一步要注意在事务里完成避免半途中异常导致数据不一致。5. 排课结果的SQL审计与人工调课技巧最后这一章从“怎么验证排课结果没问题”切入顺便说说答辩前你应该准备哪些数据层面的支撑。5.1 用一条SQL查出所有冲突不管算法跑得多漂亮最终都要落到数据库里用 SQL 自证清白。下面这条 SQL 可以用来检测教室冲突SELECT s1.term_id, s1.day_of_week, s1.start_slot, s1.end_slot, s1.room_id, s1.course_id AS c1, s2.course_id AS c2 FROM schedule s1 JOIN schedule s2 ON s1.term_id s2.term_id AND s1.room_id s2.room_id AND s1.id s2.id AND s1.day_of_week s2.day_of_week AND s1.start_slot s2.end_slot AND s2.start_slot s1.end_slot;这里利用了区间重叠判断条件s1.start_slot s2.end_slot AND s2.start_slot s1.end_slot可以一次性把所有重叠时间片找出来。把room_id换成teacher_id或class_id就是对应的教师冲突和班级冲突审计。答辩时跑出零结果比口头说“算法不会冲突”要有说服力得多。5.2 手动调课的最小方案自动排课总会有几门课不完美比如某个老师的课被拆到三天或者某班连上四节。调整的时候直接在数据库里 UPDATE 并不安全容易破坏唯一约束。推荐的方式是在源码里加一个MoveSchedule的接口PutMapping(/schedule/move) public Result moveSchedule(RequestParam Integer scheduleId, RequestParam Integer newDay, RequestParam Integer newStartSlot) { Schedule s scheduleMapper.selectById(scheduleId); s.setDayOfWeek(newDay); s.setStartSlot(newStartSlot); s.setEndSlot(newStartSlot s.getCourse().getContinuousSlots() - 1); // 再次做冲突检测 if (!checkAvailable(s)) { return Result.fail(目标时间片冲突); } scheduleMapper.updateById(s); return Result.success(); }这段代码最关键的是在 UPDATE 前再执行一次checkAvailable复用第 4 章的冲突检测逻辑。很多系统的调课功能只做了“更新数据库”和“前端刷新”漏了这一步改动后马上就有一条冲突数据而且不会立刻暴露直到查课表时才发现两门课叠一起。5.3 课程时间分布的可视化校验调课完成后还可以用 SQL 统计每个班级的每天课程数分布判断是否均匀SELECT class_id, day_of_week, COUNT(*) AS course_count FROM schedule WHERE term_id 2024-2025-1 GROUP BY class_id, day_of_week ORDER BY class_id, course_count DESC;如果某个班级某天有 6 节课而周三是 0 节那大概率需要人工调整。这里不必开发复杂图表直接把查询结果导出到 Excel 做条件格式标红即可。对于源码 数据库 SQL 脚本的交付物来说能够用 SQL 完成结果审计和均衡性分析才算把课表从“生成出来”做到了“能落地用”。本文还有配套的精品资源点击获取
返回列表