ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JAVA排课管理系统数据库课程设计:ER图、JDBC与回溯算法实践

JAVA排课管理系统数据库课程设计:ER图、JDBC与回溯算法实践 简介这套中学排课管理系统源码面向正在完成数据库课程设计的高校学生以及希望学习JAVA Web项目开发的学习者。系统覆盖班级课程管理、学生教师信息维护、排课管理等功能并以存储过程实现指定教师/节次冲突检测、指定班级或教师课程表生成同时建立表间参照完整性约束完整呈现数据库设计从建表到业务调用的核心流程。压缩包共79个文件包含39个Java后端源码、11个Vue前端页面、7个JavaScript脚本、6个XML配置及Gradle构建文件等包体仅202KB结构清晰便于快速导入工程阅读。目前已有508人学习下载适合作为课程设计参考或排课业务开发的起点。通过阅读源码可掌握前端与后端交互方式以及存储过程在教务系统中的应用技巧对提升数据库设计与JAVA实际开发能力有直接帮助。1. JAVA排课管理系统数据库课程设计的关键在冲突建模一个“JAVA实现的中学排课管理系统”能做到什么程度不取决于按钮做得多好看而取决于你如何把排课冲突变成数据库能理解和约束的结构。班级、教师、课程、教室、时间槽五个对象听起来简单真正落表时很多人会为“怎么避免同一节课被拆成两个教师”或“某个教室同一瞬间被排两门课”而反复修改SQL。我见过不少课程设计源码评分差距往往从ER图开始拉大没有直接落表建模的项目后边会为了修冲突不断硬编码反过来把约束写进表设计的人即使Java代码粗糙也能在答辩时把“为什么不会冲突”讲得很扎实。本文按数据库课程设计的思路把ER图、建表SQL、JDBC数据访问层、回溯排课算法和验证手段串成一条线不引入重量级框架只用标准JDBC就能跑通。2. 排课系统的数据库设计从ER图到建表的完整SQL2.1 实体关系梳理班级、教师、课程、教室和时间槽中学排课和大学选课有一个本质区别中学班级是固定教室制班级里的学生不是一个随机选课集合而是跟着行政班走。这种固定关系反而让数据建模更容易因为排课时只需要考虑行政班维度不用拆到学生个体。ER图里的实体可以这样归纳班级、教师、课程、教室、时间槽、排课记录。它们之间的主要联系有三条一个教师能教多门课程这是1:N关系一个班级要学习多门课程一个课程也要发散到多个班级这是M:N关系必须由中间的“排课记录”表承载一个教室在一个时间槽只能承载一次排课记录一个班级在一个时间槽也只能有一个排课记录。因此“排课记录”既是联系表也是整个系统的核心表。在画ER图时需要给每个实体的主键、联系、基数关系标注清楚。班级和课程之间用菱形“学习”相连教师与课程之间用“讲授”相连教师、教室与排课记录之间都有“担任/占用”的联系。不需要引入学生表除非你还要做学生成绩记录或请销假否则学生表在排课场景没有参与度评委反而会问为什么把无关实体塞进来。2.2 建表SQL用约束把“不冲突”写进数据库接下来直接给一套MySQL的建表SQL。数据库名设为school_schedule字符集utf8mb4存储引擎用InnoDB这样能演示外键约束和事务。-- 1. 班级表 CREATE TABLE class ( class_id INT PRIMARY KEY AUTO_INCREMENT, grade_name VARCHAR(20) NOT NULL, -- 年级如“高一” class_name VARCHAR(20) NOT NULL, -- 班级如“3班” fixed_room_id INT, -- 固定教室ID可为空 student_count INT DEFAULT 0, UNIQUE KEY uk_grade_class (grade_name, class_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 2. 教师表 CREATE TABLE teacher ( teacher_id INT PRIMARY KEY AUTO_INCREMENT, teacher_name VARCHAR(20) NOT NULL, major VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 3. 课程表 CREATE TABLE course ( course_id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(50) NOT NULL, weekly_hours INT NOT NULL DEFAULT 2, -- 每周课时数用于排课校验 teacher_id INT NOT NULL, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 4. 教室表 CREATE TABLE classroom ( room_id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL, capacity INT NOT NULL DEFAULT 40, UNIQUE KEY uk_room_no (room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 5. 时间槽表 CREATE TABLE timeslot ( slot_id INT PRIMARY KEY AUTO_INCREMENT, day_of_week TINYINT NOT NULL, -- 1-5 表示周一至周五 period_no TINYINT NOT NULL, -- 第几节1-8 UNIQUE KEY uk_day_period (day_of_week, period_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 6. 排课表核心关系表 CREATE TABLE schedule ( schedule_id INT PRIMARY KEY AUTO_INCREMENT, class_id INT NOT NULL, course_id INT NOT NULL, teacher_id INT NOT NULL, room_id INT NOT NULL, slot_id INT NOT NULL, semester VARCHAR(20) NOT NULL, -- 学期如“2025-2026-1” UNIQUE KEY uk_class_slot (class_id, slot_id, semester), UNIQUE KEY uk_teacher_slot (teacher_id, slot_id, semester), UNIQUE KEY uk_room_slot (room_id, slot_id, semester), CONSTRAINT fk_schedule_class FOREIGN KEY (class_id) REFERENCES class(class_id), CONSTRAINT fk_schedule_course FOREIGN KEY (course_id) REFERENCES course(course_id), CONSTRAINT fk_schedule_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id), CONSTRAINT fk_schedule_room FOREIGN KEY (room_id) REFERENCES classroom(room_id), CONSTRAINT fk_schedule_slot FOREIGN KEY (slot_id) REFERENCES timeslot(slot_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;上面的建表SQL有几个可以在数据库课程设计答辩环节讲清楚的参数。day_of_week和period_no用TINYINT而不是VARCHAR因为它只需要0到8的区间占1个字节且能参与唯一索引。semester用VARCHAR(20)可以存类似于“2025-2026-1”的学期编码让同一张表支持多个学年的排课数据而不互相干扰。course表里存了teacher_id作为外键但schedule表里又冗余保存了teacher_id。这在第三范式上属于部分冗余但这里是有意留下排课查询经常需要按教师ID直接找时间冲突如果每查一次都要JOIN一次course表SQL在数据量上升后会有明显损耗。课程设计阶段数据量很小冗余代价几乎为零答辩时要能解释这是为查询性能做的反规范化。索引名唯一键组成防止的排课错误uk_class_slotclass_id slot_id semester一个班在同一个时间上两门课uk_teacher_slotteacher_id slot_id semester一个教师在同一个时间跑两个班uk_room_slotroom_id slot_id semester一个教室在同一个时间被两个班占用这三个索引就是排课系统防冲突的最后防线。即使Java排课算法写漏了数据库底层也会抛出DuplicateKeyException测试阶段就能发现问题。2.3 插入顺序与外键约束一个常见的启动报错点课程设计里最常见的报错是往schedule表插入数据时提示外键约束失败。这是因为外键要求被引用的主键必须先行存在。所以初始化顺序应该是先插入班级、教师、教室、时间槽再插入课程最后插入排课记录。另一种做法是临时关闭外键检查再导入但课程设计不建议这么做外键约束本身就是需要演示的数据库知识点。如果评委问“外键会不会拖慢性能”可以回答排课场景写入集中发生在算法执行阶段查询阶段只按班级或教师走索引外键影响很有限。3. JAVA实现排课数据访问层JDBC连接、DAO与事务3.1 准备JDBC驱动与连接参数课程设计要求展示数据库增删改查最直观的做法是用JDBC裸写不引入MyBatis。因为MyBatis的Mapper XML会盖住SQL本身评委让你现场解释一个动态SQL时反而增加回答难度。JDBC的写法固定、逻辑透明也方便在简历上写“熟悉PreparedStatement和事务控制”。只需要依赖一个MySQL Connector/J驱动版本号和你本地的MySQL对应5.7以上用8.x即可。import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbUtil { private static final String URL jdbc:mysql://localhost:3306/school_schedule ?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD your_password; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这里的URL参数值需要注意useUnicodetruecharacterEncodingutf8保证中文字符正确写入serverTimezoneAsia/Shanghai绕开高版本MySQL驱动对时区校验的要求。localhost:3306是默认地址端口换成其他数据库实例时改IP和端口即可。密码不要硬编码到代码里提交课程设计阶段可以临时放配置文件答辩时提一句“正式环境会从环境变量读取”就够了。3.2 用PreparedStatement实现课程表的增删改查以课程表为例实现插入和查询两个方法其余方法同理。核心是用PreparedStatement的?占位符代替字符串拼接。import java.sql.*; public class CourseDao { public void insertCourse(String courseName, int weeklyHours, int teacherId) { String sql INSERT INTO course (course_name, weekly_hours, teacher_id) VALUES (?, ?, ?); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, courseName); ps.setInt(2, weeklyHours); ps.setInt(3, teacherId); int affected ps.executeUpdate(); System.out.println(插入课程成功影响行数 affected); } catch (SQLException e) { System.err.println(插入失败 e.getMessage()); } } public void listCourses() { String sql SELECT course_id, course_name, weekly_hours, teacher_id FROM course; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { System.out.printf(%d %s %d %d%n, rs.getInt(course_id), rs.getString(course_name), rs.getInt(weekly_hours), rs.getInt(teacher_id)); } } catch (SQLException e) { e.printStackTrace(); } } }为什么不用Statement拼接字符串第一PreparedStatement做了参数类型校验可以避免把教师ID传成字符串导致索引失效第二参数作为数据而不是SQL片段传入能直接防止SQL注入。try-with-resources保证Connection、PreparedStatement、ResultSet最终都关闭。executeUpdate返回受影响的行数实际项目中可以在影响0行时抛出业务异常课程设计里输出一句话足够。下面的表格可以放进课程设计文档对应说明增删改查的每个方法侧重点DAO接口方法对应SQL动作需要注意的参数insertCourseINSERTweeklyHours不能为负数updateCourseUPDATEWHERE course_id ? 不能漏deleteCourseDELETE如果schedule表有引用会抛外键异常listCoursesSELECT数据量大时加LIMIT分页3.3 事务控制批量写入排课记录时保证原子性排课算法生成的一组schedule记录必须全部插入或者一条都不插入。如果排到第50条时发现某个时间槽已经冲突前面49条却已进入数据库就会留下一张残缺课表。解决办法是开启事务把整个批次的插入放在同一个事务里public void batchInsertSchedule(ListSchedule list) throws SQLException { String sql INSERT INTO schedule (class_id, course_id, teacher_id, room_id, slot_id, semester) VALUES (?, ?, ?, ?, ?, ?); try (Connection conn DbUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (Schedule s : list) { ps.setInt(1, s.getClassId()); ps.setInt(2, s.getCourseId()); ps.setInt(3, s.getTeacherId()); ps.setInt(4, s.getRoomId()); ps.setInt(5, s.getSlotId()); ps.setString(6, s.getSemester()); ps.addBatch(); } ps.executeBatch(); } conn.commit(); } catch (SQLException e) { // 回滚当前事务避免留下半截课表 e.printStackTrace(); } }setAutoCommit(false)之后即使executeBatch()执行到一半失败程序进入catch分支后调用连接的rollback()即可。更稳妥的做法是在catch中把失败的schedule记录打出来让排课模块提示“失败发生在周一第三节、教室301”方便定位是哪一条数据触发唯一索引冲突。addBatch和executeBatch会在网络层面对多条SQL合并发送适合一次几百条插入的场景。要注意事务开启期间不能让连接被连接池回收课程设计直接用DriverManager不会踩这个坑。4. 排课算法实现用回溯填充避免教师、班级和教室冲突4.1 为什么选择回溯而不是无脑贪心贪心算法按课程列表顺序挑选最早空闲的时间槽初看效率很高问题在于后面的课程可能只剩一个满足教师时间的槽位而那个槽位已经被前面的课占了。要解决只能回退重排。回溯算法保留了这种回退能力当前课程试放一个槽位后会继续递归处理后面的课程填不下时回到当前课程换一个槽位直到找到完整解。可运行的简化模型是这样每门课按每周课时展开成若干次授课每次授课对应一个Assignment对象。例如“高一3班数学每周4节”就生成4个Assignment每个都绑定同一个班级、教师、教室和课程只是还没有指定时间槽。排课就是给每个Assignment分配一个(day, period)。4.2 状态集合与回溯核心代码用三个Set保存占用状态分别对应数据库表上的三个唯一索引。这里把key拼成字符串比写嵌套Map简单代码也容易讲清private static class Assignment { int classId; int teacherId; int roomId; int courseId; } public class Scheduler { private final SetString usedClassSlot new HashSet(); private final SetString usedTeacherSlot new HashSet(); private final SetString usedRoomSlot new HashSet(); private final ListAssignment assignments new ArrayList(); private final MapAssignment, Slot result new LinkedHashMap(); boolean backtrack(int index) { if (index assignments.size()) { return true; } Assignment a assignments.get(index); for (int day 1; day 5; day) { for (int period 1; period 8; period) { String cs a.classId - day - period; String ts a.teacherId - day - period; String rs a.roomId - day - period; if (usedClassSlot.contains(cs) || usedTeacherSlot.contains(ts) || usedRoomSlot.contains(rs)) { continue; } usedClassSlot.add(cs); usedTeacherSlot.add(ts); usedRoomSlot.add(rs); result.put(a, new Slot(day, period)); if (backtrack(index 1)) { return true; } usedClassSlot.remove(cs); usedTeacherSlot.remove(ts); usedRoomSlot.remove(rs); result.remove(a); } } return false; } }usedClassSlot存储的是“哪个班在星期几第几节已被占用”usedTeacherSlot和usedRoomSlot同理。每次试放一个槽位前先查三个Set任何一个组合已存在说明冲突直接continue。试放后递归到下一门课如果递归返回false说明后续课程没有合适位置于是清掉本次试放记录继续尝试下一个时间槽。这种“先占用、递归、失败后还原”的结构是回溯算法的标准写法。参数解释day范围1到5代表周一至周五period范围1到8代表一天8节具体数值可以依照学校作息调整。如果学校上午只有4节课把8改成实际节数即可。回溯的递归深度不会超过课程总节数通常几百次调内就能结束不需要担心栈溢出。4.3 剪枝与顺序调整让回溯跑得更快纯回溯最怕遇到无解场景会遍历大量组合才返回false。常见做法是给assignments做一次预排序把可选槽位少的课排到前面。这个技巧在算法里叫MRV最少剩余值启发式放在这个项目里可以表述为“把教室最固定、老师空闲时间最少的课先排”。下面这个方法用来计算某个Assignment当前还有多少可用槽位private int countFreeSlots(Assignment a, SetString classSet, SetString teacherSet, SetString roomSet) { int count 0; for (int day 1; day 5; day) { for (int period 1; period 8; period) { if (!classSet.contains(a.classId - day - period) !teacherSet.contains(a.teacherId - day - period) !roomSet.contains(a.roomId - day - period)) { count; } } } return count; }在调用backtrack(0)之前把assignments按照countFreeSlots升序排序冲突风险最大的课会先占坑后面的课选择受影响最小。需要注意这个排序只做一次回溯过程中不会重新排序因此不是绝对最优。如果课程数量达到上千节可以在每次递归前对剩余课程重新计算可用槽数量但课程设计阶段没必要。另一个容易踩的坑排课失败时不要在UI上展示残缺课表。backtrack返回false后应该清空result并提示“在第N门课找不到可用时间槽请检查课时配置或调整教室数量”。否则界面显示一半课表答辩老师会认为你的程序没有处理异常分支。5. 验证排课结果与答辩演示三个实用技巧5.1 用LEFT JOIN统计每门课的排课次数仅靠排课模块说“不冲突”还不够还要证明课时数正确。把每个班每门课在schedule里出现的次数和course.weekly_hours对比即可SELECT c.course_id, c.course_name, c.weekly_hours, COUNT(s.schedule_id) AS arranged FROM course c LEFT JOIN schedule s ON s.course_id c.course_id AND s.semester 2025-2026-1 AND s.class_id ? GROUP BY c.course_id, c.course_name, c.weekly_hours HAVING arranged c.weekly_hours OR arranged c.weekly_hours;这里用了LEFT JOIN而不是INNER JOIN目的是把“一门课一次都没排”的记录也带出来。COUNT(s.schedule_id)统计排课次数HAVING专门找课时数与预设值不一致的记录。结果集为空就说明这个班的课都排满且没有排爆。要验证冲突不需要专门写SQL排课表上的三个唯一索引会直接拒绝冲突插入。5.2 输出班级周课表的二维格式Java侧做验证时通常会把某班一周的课表打成5列8行。用两重循环从内存result或数据库查询取数都可以for (int period 1; period 8; period) { for (int day 1; day 5; day) { String subject findSubject(classId, day, period); System.out.printf(%-10s, subject); } System.out.println(); }这里的findSubject可以是在内存里遍历result也可以查一次SQL。输出时可加上“周一第2节”这样的行列表头让每个单元格都有明确归属。把这张二维课表放在答辩现场非常直观老师看到每个格子都不重复再用上面的SQL证明数量不缺就不会在数据完整性上继续追问。5.3 答辩时的讲解顺序建议下面是适合数据库课程设计的5分钟演示顺序每步对应一类知识点场景演示内容要讲清楚的知识点第一步展示ER图实体间1:N、M:N关系如何落地成外键第二步展示建表SQL三个唯一索引与业务冲突对应第三步运行增删改查PreparedStatement、事务与批处理第四步运行排课算法回溯三Set与数据库索引一一对应第五步验证课时SQL使用LEFT JOIN、COUNT和HAVING实际演示时建议提前把MySQL服务和Java环境变量配好把jar包放进项目lib目录数据库脚本放在sql文件里尽量用命令行方式启动。如果老师追问“为什么不引入Spring”回答思路是课程设计重点在数据库原理和基础Java能力用JDBC更能体现对连接、事务和SQL细节的理解。把排课算法里的三个Set和数据库三个唯一索引对应起来讲这门课程设计就基本站得住脚了。本文还有配套的精品资源点击获取
返回列表