
简介面向数据库课程设计的中学排课管理系统源码采用Java后端与Vue前端分离实现覆盖班级课程、学生教师信息管理以及排课分配和任课教师设置等核心业务。压缩包共收录79个文件以39个Java源码为主另有11个Vue页面、JS脚本、XML及properties配置等整体约202KB项目采用Gradle构建目录结构清晰适合直接导入开发环境阅读改造压缩包内的说明文档也能帮助快速了解启动方式。系统在数据库层面设计了多项存储过程包括指定教师指定节次冲突检测、按班级生成课程表、按教师生成课程表并通过参照完整性约束保证各表数据一致清晰展示了排课场景下数据库设计、存储过程与Java业务调用的配合思路。目前已有509人学习适合数据库课程设计、Java Web初学者参考也可作为中学排课管理系统设计与实现的完整示例。1. JAVA中学排课管理系统源码在做什么难点不在Java在冲突约束“JAVA实现的中学排课管理系统源码”这个标题每年都会出现在数据库课程设计的选题清单里。它看起来是一个Java Web项目可真正卡住人的地方不在Java语法而在数据库设计班级、教师、课程、教室之间的关系怎么组织课表时隙怎么存冲突怎么在入库之前就被挡住。把这三件事想清楚剩下的就是围绕课表结果表做增删改查想不清楚排课演示到一半就会翻车——同一个老师同时出现在两个班的第三节或者改一次课时整张课表乱掉。这篇内容面向正在做数据库课程设计、需要交付可运行源码和设计说明的同学也适合想接手教务类管理系统的后端开发者。我会把建表SQL、排课算法核心类和连接池配置直接铺开写最后给出我实际踩过的五个坑。2. 把排课需求翻译成数据表六张表的最小ER设计与建表SQL2.1 排课系统真正的主角是“教学任务表”刚开始做这类系统最常见的设计是班级、教师、课程、教室四张表。这个方案最快但它回答不了一个核心问题高一的数学课为什么一班是李老师、二班是王老师如果把teacher_id直接挂在课程表上同一门课只能对应一位老师现实中“两位老师分担同一个年级的数学课”就写不进去了。中学排课真正要调度的对象不是“班级”也不是“课程”而是某个老师在某个班教某门课、每周上几节课。这个组合在教务系统里叫教学任务需要单独的一张任务表。有了它教师、班级、课程之间就是多对多关系而任务表就是这群关系的交集。后面所有排课逻辑本质都是把教学任务逐个塞进一周的时隙里。我一般会把表拆成三层基础数据层放班级、教师、课程、教室业务层放教学任务表记录“谁教谁、每周几节”结果层放排课结果表记录“某班周几第几节、在哪上课、谁上课”。这样拆的好处是排课失败时能精确定位到某一条教学任务而不是在一堆课程记录里翻。2.2 六张表的建表SQL与字段选择下面是这套结构在MySQL 8.x下的最小建表脚本可以直接放进课程设计报告里当初始化脚本用。CREATE DATABASE IF NOT EXISTS course_schedule DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_schedule; -- 教室表普通教室、实验室、计算机房都算一条记录 CREATE TABLE t_classroom ( classroom_id INT AUTO_INCREMENT PRIMARY KEY, classroom_name VARCHAR(50) NOT NULL COMMENT 教室名如 高二(3)班教室 / 物理实验室, capacity INT NOT NULL DEFAULT 45 COMMENT 容量 ) ENGINEInnoDB; -- 课程表只存课程基础信息不挂教师 CREATE TABLE t_course ( course_id INT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(50) NOT NULL COMMENT 课程名如 数学、英语 ) ENGINEInnoDB; -- 教师表 CREATE TABLE t_teacher ( teacher_id INT AUTO_INCREMENT PRIMARY KEY, teacher_name VARCHAR(50) NOT NULL, daily_limit INT NOT NULL DEFAULT 6 COMMENT 每日最大课时数默认6节 ) ENGINEInnoDB; -- 班级表每个班绑定一个默认教室 CREATE TABLE t_class ( class_id INT AUTO_INCREMENT PRIMARY KEY, class_name VARCHAR(50) NOT NULL COMMENT 班级名如 高一(1)班, grade_name VARCHAR(20) NOT NULL DEFAULT 高一 COMMENT 年级, default_room_id INT NOT NULL, CONSTRAINT fk_class_room FOREIGN KEY (default_room_id) REFERENCES t_classroom(classroom_id) ) ENGINEInnoDB; -- 教学任务表哪个老师教哪个班哪门课每周几节 CREATE TABLE t_task ( task_id INT AUTO_INCREMENT PRIMARY KEY, class_id INT NOT NULL, course_id INT NOT NULL, teacher_id INT NOT NULL, hours_per_week INT NOT NULL DEFAULT 0 COMMENT 每周课时数, CONSTRAINT fk_task_class FOREIGN KEY (class_id) REFERENCES t_class(class_id), CONSTRAINT fk_task_course FOREIGN KEY (course_id) REFERENCES t_course(course_id), CONSTRAINT fk_task_teacher FOREIGN KEY (teacher_id) REFERENCES t_teacher(teacher_id) ) ENGINEInnoDB; -- 排课结果表一条记录 一个班级在某天某节在某教室上某课 CREATE TABLE t_schedule ( schedule_id INT AUTO_INCREMENT PRIMARY KEY, task_id INT NOT NULL, class_id INT NOT NULL, course_id INT NOT NULL, teacher_id INT NOT NULL, classroom_id INT NOT NULL, week_day TINYINT NOT NULL COMMENT 1-5 对应周一至周五, period_no TINYINT NOT NULL COMMENT 1-8 对应第几节课, CONSTRAINT fk_sched_task FOREIGN KEY (task_id) REFERENCES t_task(task_id), UNIQUE KEY uk_class_slot (class_id, week_day, period_no), UNIQUE KEY uk_teacher_slot (teacher_id, week_day, period_no), UNIQUE KEY uk_room_slot (classroom_id, week_day, period_no) ) ENGINEInnoDB;这里有两个字段选择值得在答辩时讲清楚。week_day和period_no用TINYINT而不是datetime是因为课表的时隙是周期性时间“周一第一节”每周都会出现和具体日期没有关系。用两个小整数表达查询直接写WHERE week_day ? AND period_no ?非常直观。如果存成datetime“每周重复”这套逻辑得写一堆日期条件排课时反而把自己绕晕。t_schedule表里冗余了course_id和teacher_id严格按第三范式看是多余的因为通过task_id都能join出来。我保留冗余是因为查课表时每行已经带着课程名、教师名、教室名展示一周课表只差一次join省去三次关联。这种“查询冗余”在几百条数据的课程设计里没有任何副作用答辩时能说清为什么冗余比盲目追求范式更实用。2.3 三个唯一索引把冲突“先堵在数据库里”如果表结构到此为止排课演示还是可能翻车。业务上有三个硬约束一个班同一时隙不能上两门课一个教师同一时隙不能出现在两个教室一个教室同一时隙不能借给两个班。这些约束在应用层写判断当然可以但代码是人写的总有漏写分支的一天。常见做法是在t_schedule上加三个复合唯一索引uk_class_slot(class_id, week_day, period_no)、uk_teacher_slot(teacher_id, week_day, period_no)、uk_room_slot(classroom_id, week_day, period_no)。每次插入新课时只要班级、教师、教室三个维度里有任何一个冲突MySQL直接抛DuplicateKeyException应用层捕获后把这条任务标记为失败而不是让脏数据落库。这套设计的代价是每次插入都要维护三个索引写性能会下降一点。但在中学几百条课表的规模下这点开销可以忽略换来的是无论如何都不会出现“一名教师同一时间在两间教室上课”这种事故。课程设计报告里写一句“用数据库约束兜底业务规则”比写十行if判断更有说服力。3. 排课算法用Java实现时隙数组与三重占用检查3.1 为什么不能“边遍历边分配”第一次写排课逻辑的人最容易写成“按班级循环每个任务直接分下一个空闲位”。这个办法在数据量小时能跑通但课表稍微密一点就会翻车前面的任务把某些时段占了后面课时数最多的主课排不进去失败任务集中在最后几门课上页面一展示全是空缺。原因在于排课是个多约束分配问题班级、教师、教室三个维度互相牵制占位顺序直接影响结果。课程设计不要求求最优解但需要一个稳定不假死、失败原因可见的贪心策略。我一般用“按课时数降序处理”的简单规则课时多的任务先挑时隙零散的课后填空。这样主科永远优先拿到好时段副科排不进去时也更容易人工处理。3.2 核心实现SchedulerService与三重占用检查排课服务的核心类如下逻辑集中在loadUsedSlots、findFreeSlot、scheduleAll三个方法里。public class SchedulerService { private static final int DAYS 5; private static final int PERIODS 8; private static final int TOTAL_SLOTS DAYS * PERIODS; private final DataSource dataSource; public SchedulerService(DataSource dataSource) { this.dataSource dataSource; } /** * 查出某一维度已经占用的时隙返回长度为40的boolean数组。 * columnName 只允许传入 class_id / teacher_id / classroom_id * 这是代码内部约定不是让用户拼接SQL的入口。 */ private boolean[] loadUsedSlots(String columnName, int id) throws SQLException { boolean[] used new boolean[TOTAL_SLOTS]; String sql SELECT week_day, period_no FROM t_schedule WHERE columnName ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { int day rs.getInt(week_day); int period rs.getInt(period_no); used[(day - 1) * PERIODS (period - 1)] true; } } } return used; } /** * 在三张占用表都为false的位置中找到第一个空闲时隙。 * 返回数组 [0] 为星期1-5[1] 为节次1-8找不到返回null。 */ public int[] findFreeSlot(boolean[] classUsed, boolean[] teacherUsed, boolean[] roomUsed) { for (int day 1; day DAYS; day) { for (int period 1; period PERIODS; period) { int idx (day - 1) * PERIODS (period - 1); if (!classUsed[idx] !teacherUsed[idx] !roomUsed[idx]) { return new int[]{day, period}; } } } return null; } /** * 排课主流程按课时数降序处理任务。 * 返回值为排课失败的任务列表调用方拿到后展示给教务人员人工处理。 */ public ListTask scheduleAll() throws SQLException { ListTask tasks loadTasks(); ListTask failed new ArrayList(); for (Task task : tasks) { boolean[] classUsed loadUsedSlots(class_id, task.getClassId()); boolean[] teacherUsed loadUsedSlots(teacher_id, task.getTeacherId()); boolean[] roomUsed loadUsedSlots(classroom_id, task.getRoomId()); int[] slot findFreeSlot(classUsed, teacherUsed, roomUsed); if (slot null) { failed.add(task); continue; } try { insertSchedule(task, slot[0], slot[1]); } catch (DuplicateKeyException e) { // 数据库唯一索引兜住了并发或逻辑遗漏 failed.add(task); } } return failed; } private ListTask loadTasks() throws SQLException { String sql SELECT t.task_id, t.class_id, t.course_id, t.teacher_id, t.hours_per_week, c.default_room_id AS room_id FROM t_task t JOIN t_class c ON t.class_id c.class_id ORDER BY t.hours_per_week DESC; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { ListTask tasks new ArrayList(); while (rs.next()) { tasks.add(new Task( rs.getInt(task_id), rs.getInt(class_id), rs.getInt(course_id), rs.getInt(teacher_id), rs.getInt(room_id), rs.getInt(hours_per_week))); } return tasks; } } private void insertSchedule(Task task, int day, int period) throws SQLException { String sql INSERT INTO t_schedule (task_id, class_id, course_id, teacher_id, classroom_id, week_day, period_no) VALUES (?, ?, ?, ?, ?, ?, ?); try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, task.getTaskId()); ps.setInt(2, task.getClassId()); ps.setInt(3, task.getCourseId()); ps.setInt(4, task.getTeacherId()); ps.setInt(5, task.getRoomId()); ps.setInt(6, day); ps.setInt(7, period); ps.executeUpdate(); } conn.commit(); } } }用boolean[40]而不是Set 是为了让占用检查变成O(1)的索引判断。把(week_day, period_no)映射成(day - 1) * 8 (period - 1)后一个二维时隙就成了长度40的一维数组。班级、教师、教室三份数组做“三处都为false”的判断就是完整的三重占用检查。如果学校周六也要上课把DAYS改成6数组长度改成48其余代码不用动。教室的选择目前直接取班级的default_room_id这是中学最典型的场景每个班有固定教室课表跟着班走。如果要做实验室、计算机房这类跨班共用教室可以在t_classroom加classroom_type字段在t_task里加need_room_type字段排课时优先匹配同类型的空闲教室。这是扩展方向不影响当前主流程。3.3 用事务和唯一索引兜住“检查与插入之间的缝隙”上面scheduleAll里loadUsedSlots和insertSchedule用的是两个独立的数据库连接中间存在一个时间缝隙检查完有空位之后、插入之前另一个线程可能抢先插入了同一个时隙。课程设计的单用户演示很少触发这个问题但一旦演示时点了两次“排课”按钮脏数据就会出现。解决思路是用事务把“检查”和“插入”合并成原子操作更简单可靠的做法是直接依赖数据库的三个唯一索引。insertSchedule里我用setAutoCommit(false)手动控制事务插入遇到DuplicateKeyException说明时隙已经被别人占用回滚后把任务加进失败清单。这样就算代码逻辑漏查了某一维数据库也会拒绝数据落库。排课失败清单是很多课程设计忽略的部分。scheduleAll返回的failed列表前端一定要展示出来提示“以下任务未能排入课表请手工调整”。有失败清单系统才不算黑匣子答辩时老师问“排不进怎么办”你把失败列表拿出来比任何解释都管用。4. 从DAO到课表页面连接池、课表查询与调课操作4.1 用Druid连接池替换DriverManager裸连排课循环里要频繁访问t_schedule如果每次new一个Connection数据库端会产生大量TIME_WAIT连接演示到一半就报“Too many connections”。我一般直接用Druid连接池初始化代码放到一个单独的DataSourceFactory里。Properties props new Properties(); props.setProperty(driverClassName, com.mysql.cj.jdbc.Driver); props.setProperty(url, jdbc:mysql://localhost:3306/course_schedule?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai); props.setProperty(username, root); props.setProperty(password, 123456); props.setProperty(initialSize, 2); props.setProperty(maxActive, 10); props.setProperty(maxWait, 3000); DataSource dataSource DruidDataSourceFactory.createDataSource(props);这里最重要的坑是从连接池拿出来的Connection用完必须closeclose不是断开数据库而是把连接归还给池子。用try-with-resources包裹所有连接操作连接就不会泄漏。连接池参数默认值课程设计建议值作用initialSize02启动时预建的物理连接数maxActive810同一时刻最大连接数minIdle02最少保留的空闲连接数maxWait-13000拿不到连接的最大等待毫秒数避免页面无限挂起maxWait一定要设不设的话连接池耗尽的瞬间页面会卡在那里没有任何提示看起来像死机。设成3000毫秒后拿不到连接会快速抛异常并提示。4.2 课表查询把t_schedule行记录转成二维展示矩阵t_schedule里的数据是一行一个时隙真实的课表是一张二维矩阵行为节次列为周一至周五。所以DAO层要做一次“行转矩阵”的转换。public class ScheduleDao { private final DataSource dataSource; public ScheduleDao(DataSource dataSource) { this.dataSource dataSource; } public ListScheduleRow findByClass(int classId) throws SQLException { String sql SELECT s.week_day, s.period_no, c.course_name, t.teacher_name, r.classroom_name FROM t_schedule s JOIN t_course c ON s.course_id c.course_id JOIN t_teacher t ON s.teacher_id t.teacher_id JOIN t_classroom r ON s.classroom_id r.classroom_id WHERE s.class_id ? ORDER BY s.week_day, s.period_no; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, classId); try (ResultSet rs ps.executeQuery()) { ListScheduleRow rows new ArrayList(); while (rs.next()) { rows.add(new ScheduleRow( rs.getInt(week_day), rs.getInt(period_no), rs.getString(course_name), rs.getString(teacher_name), rs.getString(classroom_name))); } return rows; } } } public String[][] toClassGrid(ListScheduleRow rows) { // 8行 x 6列行表示第几节列表示周一至周五第0列放节次名 String[][] grid new String[8][6]; for (int i 0; i 8; i) { grid[i][0] 第 (i 1) 节; } for (ScheduleRow row : rows) { int period row.getPeriodNo() - 1; int day row.getWeekDay(); grid[period][day] row.getCourseName() row.getClassroomName(); } return grid; } }转换时注意两件事空单元格保持null而不是空字符串因为页面上要区分“没排课”和“排了课但文本为空”节次行索引从0开始数据的period_no从1开始赋值前必须减一这是数组越界的高发点。JSP页面用JSTL循环渲染这个二维数组就行外层遍历节次内层遍历周一至周五遇到null输出补位符。4.3 调课操作检查目标时隙后做UPDATE别忘排除自身课表不可能永远一次排对调课是系统里必须有的功能。调课的难点不是UPDATE语句而是检查目标时隙时要把自己排除掉否则会出现“要移动的这条记录占用了目标位置导致自己挡住自己”的假冲突。public void reschedule(int scheduleId, int newDay, int newPeriod) throws Exception { // 1. 查出当前记录的 class_id、teacher_id、classroom_id ScheduleRow own findById(scheduleId); // 2. 分别载入三个维度的占用排除自身后判断目标时隙是否空闲 boolean[] classUsed loadUsedSlotsExcept(class_id, own.getClassId(), scheduleId); boolean[] teacherUsed loadUsedSlotsExcept(teacher_id, own.getTeacherId(), scheduleId); boolean[] roomUsed loadUsedSlotsExcept(classroom_id, own.getClassroomId(), scheduleId); int idx (newDay - 1) * 8 (newPeriod - 1); if (classUsed[idx] || teacherUsed[idx] || roomUsed[idx]) { throw new BusinessException(目标时隙已被占用请重新选择); } // 3. 同一事务内执行更新 try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement( UPDATE t_schedule SET week_day ?, period_no ? WHERE schedule_id ?)) { ps.setInt(1, newDay); ps.setInt(2, newPeriod); ps.setInt(3, scheduleId); ps.executeUpdate(); } conn.commit(); } }loadUsedSlotsExcept里的SQL就是在原来loadUsedSlots的WHERE条件后面加一个“AND schedule_id ?”把当前这条记录排除在占用集合外。这个细节很多初版实现会漏漏掉的结果就是“调课永远提示冲突”非常劝退。5. 让排课源码能跑过答辩五个最容易翻车的现场与解决5.1 SQL脚本导入后中文全是问号现象把初始化脚本导入MySQL后打开课表页面班级名、课程名显示成一串“???”或者乱码“æ··ä¹±”。原因SQL文件本身是GBK编码保存导入时MySQL连接也没指定字符集中文字节被按默认编码解析后写入utf8mb4字段信息已经丢了一部分。乱码一旦写入重新修改显示编码也救不回来。解决用编辑器把SQL脚本另存为UTF-8编码导入时显式指定字符集mysql --default-character-setutf8mb4 -uroot -p course_schedule init.sqlJDBC连接串里的characterEncodingutf8也要保留。养成“文件编码、连接编码、页面编码”三处都统一用UTF-8的习惯这类问题能彻底绕开。5.2 同一教师“分身”出现在两个班的课表现象排完课后打开李老师的教师课表周三第3节同时在两个班讲课。原因排课循环里只检查了班级占位没有检查教师占位。这是排课算法最常见的逻辑遗漏尤其当任务按班级分组处理时教师的占用情况很容易被跳过。解决每一轮分配必须同时载入class_id、teacher_id、classroom_id三个维度的占用数组做三重检查就是第三章SchedulerService里的做法。另外t_schedule表上保底设置uk_teacher_slot唯一索引即使代码漏查数据库也会拒绝插入并抛DuplicateKeyException。5.3 重排课表不先清旧数据DuplicateKeyException连环报现象第一次排课成功修改教学任务后点“重新排课”控制台刷出一堆DuplicateKeyException新课表却还是旧内容。原因t_schedule里上一轮的旧数据还在时隙仍然被旧记录占用。新记录插入相同(week_day, period_no)时触发唯一索引冲突插入失败旧课表自然原封不动。解决重新排课的逻辑应该是“清空旧结果 重新生成”两步放在同一个事务里执行。清空范围可以是整张t_schedule也可以用batch_id之类的字段只清某一次排课产生的数据。删除和插入在一个事务里能避免删了之后插入失败导致一张空表的尴尬情况。5.4 用datetime存“上课时间”两节连排永远做不出来现象教学任务里要求语文连排两节数据库课表里却始终只有孤零零的一节课连排的效果怎么都出不来。原因把“上课时间”设计成了datetime字段。datetime存的是绝对时间点而排课系统需要表达的是周期性的“每周一第3节、第4节”这种模式。连排意味着两个相邻周期时隙被同一条课程规则占用datetime模型表达不了这种需求。解决用week_day(TINYINT 1-5)和period_no(TINYINT 1-8)两个字段组合表达时隙展示层再拼成“周三第3节”这样的字符串。数据库里不要出现任何和具体日期相关的时间戳字段这是课表类系统的通用建模规则。5.5 连接用完不还演示到一半报Too many connections现象排课演示刚开始还能正常访问跑了三四分钟后页面报错重试也没用只能重启MySQL。原因代码里使用DriverManager.getConnection()裸连连接用完没有close或者是用了连接池但连接对象没有被try-with-resources包裹连接从池子里拿走却不归还池子很快被掏空数据库端的max_connections被打满。解决统一使用Druid连接池所有获取连接的代码都放进try-with-resources。连接池里close不是关闭连接而是把连接归还给池子所以“用完就close”这句习惯在这个场景下尤为重要。同时把maxActive设成5到10这种小值配合maxWait3000连接异常时页面能快速报错而不是无限挂起。6. 给排课系统再加一个教师视角软冲突提示与周课表矩阵6.1 教师课表复用同一个矩阵转换方法班级课表能展示教师课表其实只要改一个查询条件。ScheduleDao里加一个findByTeacher(int teacherId)SQL的WHERE条件从class_id换成teacher_id返回的ScheduleRow结构完全复用toClassGrid直接接着用。这样一套矩阵转换逻辑班级课表和教师课表共用页面路径区分参数即可不需要各写一套。6.2 软冲突提示把连续排课这类教务规则变成警告硬约束是“同一时隙不能冲突”软约束是“一天不要连排太多节”。软冲突不违反数据库约束但教务人员看了不舒服也确实是真实业务里会被问到的问题。private ListString softWarnings(String[][] teacherGrid) { ListString warnings new ArrayList(); for (int day 1; day 5; day) { int continuous 0; for (int period 1; period 8; period) { if (teacherGrid[period - 1][day] ! null) { continuous; if (continuous 3) { warnings.add(周 day 出现连续 continuous 节课); break; } } else { continuous 0; } } } return warnings; }这段代码扫描教师课表的每一列统计连续非空单元格的数量超过三节就提示。它不阻止排课只在教师课表页面上以警告列表形式展示。硬约束靠数据库和事务保证软约束靠这个扫描方法提示两类规则分开实现代码边界清晰答辩时也能讲出层次。我当初接手别人写的排课源码时第一件事就是去看t_schedule有没有三个唯一索引没有就先补上再聊算法。后来答辩老师问“排课失败怎么办”我直接把失败清单和软冲突提示调出来讲这一项比界面多几个按钮都加分。希望帮到你。本文还有配套的精品资源点击获取