ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从数据库课程设计到可落地的中学排课系统:建模、约束与贪心调度实现

2026/10/3 6:46:34 拓冰建站 浏览量
从数据库课程设计到可落地的中学排课系统:建模、约束与贪心调度实现 简介面向数据库课程设计的中学排课管理系统源码包适合计算机专业学生参考排课业务建模与数据库编程。项目采用Java后端与Vue前端分离结构覆盖班级、课程、学生、教师等基础信息维护并实现任课教师分配、班级排课、冲突检测及课程表生成等核心功能。包内共79个文件包括39个Java源码、11个Vue组件、7个JS脚本、6个XML配置及Gradle构建文件等压缩包仅202KB结构清晰便于导入开发环境。系统通过存储过程实现指定教师节次冲突检测和班级/教师课表生成并建立表间参照完整性约束可帮助读者完整理解从ER设计到编码实现的课程设计流程。已有509人学习下载适合作为数据库课程设计或毕业设计的参考模板。1. 从“中学排课管理系统”这个课设标题里先读出真正的难点只要在搜索框里敲过“JAVA实现的中学排课管理系统源码 数据库课程设计”你大概率和我当年一样先被一串源码清单吸引再被“排课算法”四个字劝退。这个课题真正的难点从来不是增删改查而是“课表怎么排才不冲突”一个老师不能同时出现在两个班一个班不能同时上两门课同一间教室不能同时被两个班占用。把这三个约束翻译成数据库里的表结构和Java代码逻辑才是课程设计的核心价值。这个项目适合两类人一是正在做数据库课程设计、想拿一个“有业务深度”的题目而不是图书管理系统的在校生二是想补一补“关系建模 约束设计 JDBC 基础调度算法”这条完整链路的新手工程师。今天这篇文章我把这套系统的表结构、SQL脚本、Java连接池配置和贪心排课的核心代码一次讲透附上我踩过的坑。2. 用ER建模把“谁的课、谁在教、在哪儿上、能不能上”固化成 表2.1 排课系统的四类实体与关系建模本质是约束设计中学排课的业务面比大学简单不涉及选课和学分核心是“行政班固定、课程固定、教师固定”的三固定模式。一个班从周一到周五每天上午四节、下午三节这是排课系统要填的棋盘教师、班级、教室、课程就是棋盘上要摆放的棋子。排课系统的ER建模第一步先列实体班级、教师、课程、教室。这四个实体之间不是简单的一对多。一个教师教多门课、一门课也可能由多个教师分别带不同班这是典型的多对多关系一个班一天上多门课每门课由某位教师在某间教室上这又是一组多对多关系。所以不能在基础表上直接加外键了事必须引入“开课计划”这类关联实体。我在给课设做表结构时反复提醒自己一句话排课系统的数据库设计不是为了存数据而是为了“约束数据”。一张合理的表结构应该让“非法课表”在插入数据的瞬间就被数据库拒绝而不是等Java代码去if判断。把这个思路贯彻到ER图里班级、教师、课程、教室是四张基础表开课计划和排课结果表是两张中间表一共六张表这个规模对课程设计来说既完整又不过度设计。2.2 从“班级课表”反推数据字典确定每个字段的边界做数据字典不要凭空想字段我一般是从最终产物“一张班级课表”反向推导。课表的横轴是星期几的第几节纵轴是班级单元格里填的是“课程名 教师 教室”。这一张图推导出以下信息教师要有姓名和教工号班级要有年级和班号课程要有名称和每周课时数教室要有容量和楼层。每个字段的边界必须卡死。班级年级用int还是varchar我建议用varchar(10)因为年级会出现“初一”“高一”这类中文用int存反而要在Java里再做一层映射。教师的工号用varchar(20)主键不要用自增id因为排课结果里大量按工号关联字符串主键能让SQL的join语句一眼可读。课程周课时数是int还是tinyint用tinyint就够因为中学没有任何一门课每周超过20节。另外中学课表有个容易被忽略的字段课程性质必修还是选修。虽然大部分课设不需要选课模块但保留这个字段能让你的排课算法将来扩展“走班制”时有地方落脚。同样教室表里加一个“是否多媒体教室”的布尔字段美术或者信息课可以据此约束教室类型这也是评分时“业务完整性”的加分点。2.3 实体关系落到二维表六张表的结构与字段取舍我最终落地的表结构是这样设计的。六张表里四张基础表班级、教师、课程、教室负责实体属性两张中间表开课计划、排课结果负责关系。之所以把“开课计划”单独拆出来是因为一个班学期内要上哪些课是一份独立于“排课结果”的数据它回答“要上什么”排课表回答“在什么时间上”。表名字段类型约束与说明t_class 班级表class_idvarchar(10)主键例如 C202301grade_namevarchar(10)年级名例如 初一class_namevarchar(10)班级名例如 3班student_countint班级人数排教室时用于容量匹配t_teacher 教师表teacher_idvarchar(20)主键工号teacher_namevarchar(20)教师姓名subjectvarchar(20)主教科目用于排课优先级max_lessons_per_dayint每天最大课时数默认4t_course 课程表course_idvarchar(10)主键course_namevarchar(20)课程名称weekly_periodstinyint每周课时course_typevarchar(10)必修/选修t_classroom 教室表room_idvarchar(10)主键room_namevarchar(10)例如 物理实验室capacityint可容纳人数需 班级人数is_multimediatinyint1是0否用于课程属性约束t_plan 开课计划表plan_idint主键自增class_idvarchar(10)外键关联班级表course_idvarchar(10)外键关联课程表teacher_idvarchar(20)外键关联教师表periods_per_weektinyint该班这门课每周节数t_schedule 排课结果表schedule_idint主键自增plan_idint外键关联开课计划week_daytinyint1~5 对应周一到周五period_numbertinyint1~7 对应第几节课room_idvarchar(10)外键关联教室表注意class_id用varchar做主键但t_plan和t_schedule用自增int做主键。这样设计是有意为之业务表用自然键关系表用代理键。如果你把class_id直接当排课结果表的主键将来一个班一周要排25节课主键就只能包含week_day和period_number组合Java代码里对“修改某一节课”的处理会特别痛苦。3. 用SQL把“不冲突”写进数据库六张表的建表脚本与关键约束3.1 建表顺序与索引设计从父表到子表逐层建立建表要按依赖顺序执行先建四张基础表再建开课计划和排课结果表。外键约束必须此刻就建好不要等Java代码再补。很多课设代码会在Java里手动判断班级是否存在再插入排课数据这等于把数据库该干的活搬到业务层答辩时被问“为什么不用外键”会很被动。-- 1. 班级表 CREATE TABLE t_class ( class_id VARCHAR(10) PRIMARY KEY, grade_name VARCHAR(10) NOT NULL, class_name VARCHAR(10) NOT NULL, student_count INT NOT NULL DEFAULT 45 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 2. 教师表 CREATE TABLE t_teacher ( teacher_id VARCHAR(20) PRIMARY KEY, teacher_name VARCHAR(20) NOT NULL, subject VARCHAR(20) NOT NULL, max_lessons_per_day INT NOT NULL DEFAULT 4 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 3. 课程表 CREATE TABLE t_course ( course_id VARCHAR(10) PRIMARY KEY, course_name VARCHAR(20) NOT NULL, weekly_periods TINYINT NOT NULL, course_type VARCHAR(10) NOT NULL DEFAULT 必修 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 4. 教室表 CREATE TABLE t_classroom ( room_id VARCHAR(10) PRIMARY KEY, room_name VARCHAR(10) NOT NULL, capacity INT NOT NULL, is_multimedia TINYINT NOT NULL DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 5. 开课计划表 CREATE TABLE t_plan ( plan_id INT AUTO_INCREMENT PRIMARY KEY, class_id VARCHAR(10) NOT NULL, course_id VARCHAR(10) NOT NULL, teacher_id VARCHAR(20) NOT NULL, periods_per_week TINYINT NOT NULL, CONSTRAINT fk_plan_class FOREIGN KEY (class_id) REFERENCES t_class(class_id), CONSTRAINT fk_plan_course FOREIGN KEY (course_id) REFERENCES t_course(course_id), CONSTRAINT fk_plan_teacher FOREIGN KEY (teacher_id) REFERENCES t_teacher(teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 6. 排课结果表 CREATE TABLE t_schedule ( schedule_id INT AUTO_INCREMENT PRIMARY KEY, plan_id INT NOT NULL, week_day TINYINT NOT NULL, period_number TINYINT NOT NULL, room_id VARCHAR(10) NOT NULL, CONSTRAINT fk_sched_plan FOREIGN KEY (plan_id) REFERENCES t_plan(plan_id), CONSTRAINT fk_sched_room FOREIGN KEY (room_id) REFERENCES t_classroom(room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段脚本里所有字符型字段统一用utf8mb4因为MySQL 5.5.3之前utf8是utf8mb3存“中文课时”没问题但一旦出现扩展字符就会报“Incorrect string value”错误。另外每张表显式指定InnoDB是为了让外键约束真正生效MyISAM引擎虽然也支持建外键语法但实际不会执行约束检查这种坑在课设里出现过太多次。3.2 用联合唯一约束堵住“同一教师同一时间上两门课”外键解决了“引用的数据必须存在”但排课系统最关键的是一组“业务不冲突”约束同一个教师同一节课只能在一个班同一个班级同一节课只能上一门课同一间教室同一节课只能被一个班占用。这三个约束用联合唯一索引一条SQL就能堵死。ALTER TABLE t_schedule ADD CONSTRAINT uniq_teacher_time UNIQUE (teacher_id, week_day, period_number); ALTER TABLE t_schedule ADD CONSTRAINT uniq_class_time UNIQUE (plan_id, week_day, period_number); ALTER TABLE t_schedule ADD CONSTRAINT uniq_room_time UNIQUE (room_id, week_day, period_number);要说明的是t_schedule表里并没有teacher_id字段而是通过plan_id关联到教师。没关系MySQL支持用“函数索引”或者“联合字段”处理但课设级别最简单的方式是在排课结果表里冗余一个teacher_id字段再建这个唯一约束。冗余字段在数据库理论里要谨慎但这种“查询频繁、写入可控、业务强一致”的场景冗余能让查询少一次join课设答辩时也能讲出“以空间换时间”的道理。这三个唯一约束一旦建立Java代码里就不需要再写“查询是否已存在再插入”的逻辑了。直接insert数据库会抛出DuplicateEntryException你在业务层捕获这个异常并回滚事务、提示“该时间段已占用”即可。这比“查一次再插一次”的写法更可靠因为查询与插入之间的时间窗口仍然可能产生并发冲突。3.3 为什么CHECK约束在排课场景形同虚设以及怎么补救很多同学喜欢在表里写CHECK约束来限制week_day只能填1到5、period_number只能填1到7看起来天经地义但MySQL在5.7及之前的版本里CHECK约束是被解析但被忽略的。也就是说你写了CHECK(week_day BETWEEN 1 AND 5)插入week_day9一样能成功。这个“数据库知识点”如果不在答辩前搞清楚很可能当场被老师问住。解决办法有两个。一是在Java层加参数校验insert之前先用if判断week_day是否在1到5之间二是用触发器作为双保险。我更推荐第一种因为触发器在排课系统里排查成本高而且触发器无法在MyBatis等框架的批量插入中方便地预热。最终我采用的方案是把week_day和period_number统一用TINYINT声明Java代码用常量校验同时在t_schedule表的class_time联合唯一索引上“间接”限制。比如把class_id、week_day、period_number做成唯一时非法数据虽然能插入但永远不会被正常排课逻辑生成出来。校验代码我放在第四章的统一入口里写。4. JAVA代码怎么把课表排出来JDBC连接池、实体类与贪心调度4.1 数据库连接的工具类直接上连接池别用DriverManager课设里最常见的写法是DriverManager.getConnection每次操作数据库都新建连接交作业是没问题的但排课算法要循环上千次插入频繁创建连接会让整个调度过程慢到怀疑人生。我在这个项目里用的是HikariCP连接池它在课设规模下配置不超过二十行却能让你在答辩“性能优化”环节有话可讲。import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DbPool { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/school_schedule ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn) { if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }连接池参数有三个值得注意。maximumPoolSize设置成10就够了因为排课算法是单线程调度并发上限其实是1留10个连接是给查询班级列表、教师列表等操作用的connectionTimeout设30秒是为了防止数据库假死时线程无限等待jdbcUrl里的serverTimezone必须显式声明否则MySQL 8.0以上会在首次连接时报时区错误。这段配置还硬编码了密码课设项目无所谓但写到生产环境一定要改成读取配置文件。4.2 三个基础数据访问方法排课算法的“数据弹药库”排课算法要从数据库读出三个清单所有班级的开课计划、所有教师的可用课时、所有教室的容量信息。下面这段代码是读取开课计划的DAO方法使用PreparedStatement防注入这是课设里必须写对的基本功。public ListPlan findPlansByClass(String classId) throws SQLException { String sql SELECT p.plan_id, p.course_id, c.course_name, p.teacher_id, t.teacher_name, p.periods_per_week FROM t_plan p JOIN t_course c ON p.course_id c.course_id JOIN t_teacher t ON p.teacher_id t.teacher_id WHERE p.class_id ? ORDER BY c.weekly_periods DESC; try (Connection conn DbPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, classId); ResultSet rs ps.executeQuery(); ListPlan list new ArrayList(); while (rs.next()) { Plan p new Plan(); p.setPlanId(rs.getInt(plan_id)); p.setCourseId(rs.getString(course_id)); p.setCourseName(rs.getString(course_name)); p.setTeacherId(rs.getString(teacher_id)); p.setTeacherName(rs.getString(teacher_name)); p.setPeriodsPerWeek(rs.getInt(periods_per_week)); list.add(p); } return list; } }为什么ORDER BY c.weekly_periods DESC要对课时多的课程做降序排序我在第四章第三节详述这里先记住结论排课算法里约束最多的课程应当优先分配时间片这是贪心策略的灵魂。另外这段代码用了try-with-resourcesConnection、PreparedStatement、ResultSet都会自动关闭这是Java 7以后的规范写法在课设答辩里属于“代码习惯良好”的加分项。4.3 核心贪心调度算法用优先级把约束转换成可执行方案真正决定课表质量的是调度算法本身。课程设计里我不推荐实现回溯或禁忌搜索这类复杂算法一个是工作量太大另一个是排课规模只有几十个班时贪心算法加随机扰动已经能得到合格课表。下面这段代码是核心调度的骨架每次插入前检查三个冲突条件。public boolean scheduleClass(String classId, String courseId, String teacherId, int weekDay, int period) { String checkTeacherSql SELECT COUNT(*) FROM t_schedule WHERE teacher_id ? AND week_day ? AND period_number ?; String checkClassSql SELECT COUNT(*) FROM t_schedule WHERE plan_id ? AND week_day ? AND period_number ?; String checkRoomSql SELECT COUNT(*) FROM t_schedule WHERE room_id ? AND week_day ? AND period_number ?; String insertSql INSERT INTO t_schedule (plan_id, week_day, period_number, teacher_id, room_id) VALUES (?, ?, ?, ?, ?); Connection conn null; try { conn DbPool.getConnection(); conn.setAutoCommit(false); // 选教室遍历教室表取容量 班级人数且该时段空闲的一间 String findRoomSql SELECT room_id FROM t_classroom WHERE capacity ? AND room_id NOT IN ( SELECT room_id FROM t_schedule WHERE week_day ? AND period_number ? ) LIMIT 1; PreparedStatement psRoom conn.prepareStatement(findRoomSql); psRoom.setInt(1, 45); psRoom.setInt(2, weekDay); psRoom.setInt(3, period); ResultSet rsRoom psRoom.executeQuery(); if (!rsRoom.next()) { conn.rollback(); return false; // 没有空闲教室当前时间片不可用 } String roomId rsRoom.getString(room_id); // 检查教师冲突 PreparedStatement psTeacher conn.prepareStatement(checkTeacherSql); psTeacher.setString(1, teacherId); psTeacher.setInt(2, weekDay); psTeacher.setInt(3, period); ResultSet rsTeacher psTeacher.executeQuery(); rsTeacher.next(); if (rsTeacher.getInt(1) 0) { conn.rollback(); return false; // 教师已被占用 } // 检查班级冲突 int planId findPlanId(classId, courseId); PreparedStatement psClass conn.prepareStatement(checkClassSql); psClass.setInt(1, planId); psClass.setInt(2, weekDay); psClass.setInt(3, period); ResultSet rsClass psClass.executeQuery(); rsClass.next(); if (rsClass.getInt(1) 0) { conn.rollback(); return false; // 班级已有课 } // 都通过则插入 PreparedStatement psInsert conn.prepareStatement(insertSql); psInsert.setInt(1, planId); psInsert.setInt(2, weekDay); psInsert.setInt(3, period); psInsert.setString(4, teacherId); psInsert.setString(5, roomId); psInsert.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { DbPool.close(conn); } }这段代码的逻辑顺序是先找空闲教室再查教师冲突再查班级冲突最后插入。三个查询的顺序不是随机的应该先把最稀缺的资源排在前面。中学排课里教室的稀缺程度通常高于教师所以先查教室可以直接跳过后续无意义的检查。整个事务用setAutoCommit(false)包裹任何一个检查失败都rollback防止半截数据残留在表里。调用这段代码的上层调度器采用“按周课时数降序逐课程尝试”的策略。外层循环跑课时数多的课程内层循环遍历一周35个时间片(5天×7节)把每周n节课均匀分布到不同天避免语文数学全部挤在星期一。每次scheduleClass返回false时记录失败次数如果连续20次全部失败说明当前课程已无可排的时间片那就先跳过它最后做一轮“补漏”再尝试。4.4 参数调优时钟段数、最大日课时、固定占位的边界与调节排课系统的参数设置直接决定课表能不能落地的“可用性”。首先是“每天课时数”我设定为7节上午四节、下午三节这符合大多数中学作息。如果你的目标学校下午有五节课直接把t_schedule表里period_number的取值范围改成8Java常量一并调整即可。其次是“教师每日上限”max_lessons_per_day。这个字段在调度代码里没有被直接用上因为它属于“软约束”在贪心算法的每一轮里单独统计复杂度太高。我的做法是每排完一天统计该教师当天课时数超过4节则当天剩余时间片全部标记为不可用。这就是前面教师表里max_lessons_per_day字段存在的意义。有些课程需要固定占位比如周一的升旗仪式班会课固定在第1节。这种情况我在调度器里增加一个fixedSchedule集合先把固定占位写入t_schedule并把对应时间片的week_day和period_number加入一个“已占用集合”贪心循环时跳过这些时间片。固定占位看起来简单但总有人直接用UPDATE语句去改已排好的数据导致唯一索引冲突后怀疑算法先想清楚占位是预占还是改排会少很多麻烦。5. 排课系统课设翻车集中营五个最常见的踩坑记录与排查方案5.1 现象ClassNotFoundException: com.mysql.cj.jdbc.Driver这是课设启动的第一道坎通常是mysql-connector-java的jar包没有打进运行环境或者IDE里添加的是“Modulepath”而不是“Classpath”。现象就是运行到Class.forName(com.mysql.cj.jdbc.Driver)时直接抛出类找不到但看着项目里明明有jar包。原因分两种如果你用的是Java 9以上的模块化项目jar包应该放在Classpath而不是Modulepath如果是Maven项目则要看pom.xml里的scope是不是写成了provided导致运行时没带进依赖。解决方法是检查IDE的Project Structure配置或者直接把pom里mysql依赖的scope改成默认的compile。另外MySQL 8.0驱动类的全名是com.mysql.cj.jdbc.Driver老版本则是com.mysql.jdbc.Driver。如果jar包是5.1.x驱动类名必须用旧名称。不少人的连接串是从网上旧教程抄来的jar包却是新版本全名对不上这就是问题的根源。5.2 现象连接建立时报“The server time zone value is unrecognized”MySQL 8.x的时区问题几乎是每个连过数据库的人都遇到过的。现象是连接串没问题、驱动也加载成功但getConnection那一行抛异常提示时区无法识别。原因在于新版MySQL的时区信息存储方式变了连接参数里没有告诉驱动用哪个时区。解决方法是给jdbcUrl加serverTimezoneAsia/Shanghai或者用UTC。网上还有一种做法是在MySQL服务端执行set global time_zone 8:00但课设场景下改连接串是最不侵入的。我一般顺手把useSSLfalse也带上——开发环境不用SSL能少一个证书告警。这种“数据库知识点”虽然没有多少技术深度但每次答辩前翻车的概率都不低。5.3 现象生成的课表里同一个教师在同一节同时出现在两个班出现这个现象的第一反应是算法有问题但排查三步之后往往发现是表结构缺了约束。原因正是第三章里强调的联合唯一索引没有建或者建错了字段排列顺序。举个具体例子如果unique索引是(plan_id, week_day, period_number)它只约束了“同一个班同一节不能重复”压根不约束教师冲突。解决方法是确认索引字段同时覆盖teacher_id、week_day、period_number三列。注意索引里的字段顺序也影响生效方式查询条件必须从索引最左列开始匹配所以把查询频率最高的teacher_id放在联合索引最左边。如果建错了用DROP INDEX删除后重建这算是排课系统为数不多的“后悔药”之一。5.4 现象排课结果查询页面加载要十几秒页面慢的原因几乎都出在t_schedule表全表扫描上。排课结果表数据量不大但查询语句习惯性写成SELECT * FROM t_schedule WHERE class_id ?而class_id在表里根本不存在——这字段属于t_plan必须join关联表而联合了几个表之后没有索引可用速度自然慢。解决方法是给t_plan表的class_id字段加普通索引并让查询SQL只select需要的列。在课设规模里不需要考虑覆盖索引的极致优化但在“查询语句不要写成SELECT *”“join字段要有索引”这两点上做到规范已经是答辩时能拿出来讲的性能优化实践了。如果遇到排课结果页面需要按班级展示可以考虑在t_schedule里冗余class_id用空间换查询时间索引设计一节里说过这个思路。5.5 现象答辩被问“为什么贪心算法能保证不冲突能不能证明”这个问题看着像算法题实际上考察的是工程理解。贪心算法的正确性证明在排课场景里确实不成立因为它不保证全局最优解也就是不保证所有课程都被排进去。答辩时要主动交代这个边界条件“我的算法保证了已排入的课不违反所有硬约束但不保证全部课程都能被排入当无法排入时会回滚并记录失败课程由用户手动调整。”这比硬着头皮说“算法正确”可信得多。数据库课程设计评分的一个关键点就是“能否清晰描述系统边界”主动承认贪心的局限并给出失败处理方案通常能比那些背出“贪心算法是每一步取局部最优”的同学拿到更高分。另外也可以补充一句“如果要保证一定有解需要换成回溯算法代价是时间复杂度指数级”表明你清楚升级路径这个我在第六章展开。6. 进阶验证用一份固定学期的课表数据反推调度正确性做完整个排课系统后别急着交作业。你先构造一份“已知正确答案”的小数据用固定值填充开课计划手工排出一周课表再写一段校验SQL来验证系统生成的课表是否与正确答案冲突。具体做法是随机抽取三个班、五个教师、十门课的规模人工排出一个可行课表并录入t_schedule然后执行冲突检测SQL。-- 教师冲突检测同一位教师同一时间被安排在两处 SELECT t1.teacher_id, t1.week_day, t1.period_number, COUNT(*) AS cnt FROM t_schedule t1 GROUP BY t1.teacher_id, t1.week_day, t1.period_number HAVING cnt 1; -- 班级冲突检测同一个班同一时间被安排两门课 SELECT t2.class_id, t2.week_day, t2.period_number, COUNT(*) AS cnt FROM t_schedule t2 GROUP BY t2.class_id, t2.week_day, t2.period_number HAVING cnt 1; -- 教室冲突检测同一间教室同一时间被安排两门课 SELECT t3.room_id, t3.week_day, t3.period_number, COUNT(*) AS cnt FROM t_schedule t3 GROUP BY t3.room_id, t3.week_day, t3.period_number HAVING cnt 1;这三条SQL是排课系统的灵魂验证工具比任何单元测试都直接。如果返回结果全部为空说明联合唯一约束和Java代码共同工作正常如果出现记录则说明代码里绕过了约束需要回去查事务边界是否写对。再进阶一步如果时间充裕可以把贪心算法升级成“带回溯的贪心”。思路是每门课排完后检查剩余课时数是否仍小于剩余可用时间片如果发现某个班级剩余课程的周课时数之和大于剩余天数乘以每天可排课数量就回退上一步换一个时间片重试。这个逻辑用递归实现代码量不大但会让你的课设在“算法深度”上明显超出平均值。最后说说我的个人习惯每次拿到这类课设项目我都是先把数据库脚本写好、用三个冲突查询SQL验证数据正确性再动手写Java代码。这个顺序帮我避免了大半的返工。数据库课程设计和生产系统一样表结构是整个系统的地基地基歪了后面排课算法再漂亮也会翻车。希望这篇围绕排课系统的建模、约束、算法和排错心得能帮到你。本文还有配套的精品资源点击获取