多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

中学排课管理系统:数据库设计、排课算法与JDBC事务

中学排课管理系统:数据库设计、排课算法与JDBC事务 简介一套基于Java的中学排课管理系统完整源码面向数据库课程设计场景适合计算机相关专业学生参考学习也可供教务系统开发者复用核心排课逻辑。系统实现班级、课程、学生、教师等基本信息管理重点支持班级课程任课教师排课并设计存储过程检测指定教师指定节次是否冲突、按班级或教师自动生成课程表同时建立表间参照完整性约束体现从数据库表设计到存储过程应用的完整思路。源码包共79个文件压缩包仅202KB以39个Java后端类为主配合11个Vue前端页面、XML/JSON配置文件、Gradle构建及数据库脚本前后端分层清晰便于导入IDE运行和二次扩展。目前已有510人学习下载对需要完成数据库课程设计、理解排课算法与存储过程结合的读者是较完整的实践参考。1. 为什么课设选“排课系统”比选“图书管理”更划算数据库课程设计最常见的开头是做一个“图书管理系统”全班三十人同题答辩时老师看完外键就没话可问了。而中学排课管理系统不一样它看起来也是增删改查却自带两个值得被追问的点多表关联和排课冲突检测。所谓排课就是给每门课安排“星期几第几节哪间教室”同时保证同一时间老师不重、班级不重、教室不重。这个系统正好覆盖数据库设计、JDBC 事务、Java 算法三块内容对新手是有一点挑战但跳一跳够得着的量级。下面按 Java JDBC MySQL 的落地写法从五张表讲到排课算法、管理端事务和常见坑想找 java 课程设计案例源码做参考的同学可以直接照做。2. 先建好数据库再写代码五张表的关系建模与建表脚本写 Java 代码之前先把表结构定下来。课设翻车大概有一半是表结构不对引发连锁问题——Java 代码写得再漂亮表设计不合理后面所有查询都是黑匣子。先讲清楚数据模型再动手建表能省下后面大量的返工时间。2.1 排课管理系统的数据需求拆解五张表分别管什么中学排课管理系统要管五类东西教师、班级、教室、课程、排课结果。对应五张表teacher、clazz、classroom、course、timetable。字段怎么定课设的核心是简化但不失真。teacher 表id、name、title。title 表示职称比如“高级”“一级”做下拉框展示时可以增加一点真实感。clazz 表id、name、grade、student_count。name 存“高一(3)班”这种完整名称grade 单独存年级方便按年级筛选班级。classroom 表id、name、building、capacity。排课的时候要根据班级人数和教室容量做容量判断这个字段别省。course 表是业务核心id、name、teacher_id、clazz_id、hours_per_week、consecutive、credits。consecutive 是这节课要不要连排两节比如数学、物理往往需要连排这个字段排课算法里直接参与冲突判断。timetable 表id、course_id、weekday、period、classroom_id。它只存“某门课在星期几的第几节、用哪间教室”本身不带业务冗余。这里有一个答辩时可以主动讲的设计点为什么 timetable 不直接冗余 teacher_id 和 clazz_id因为它的定位是事实表越窄越不容易产生不一致。查询“某老师周五有没有课”时只需要 join course 拿到 teacher_id 再过滤课设数据量在几百行级别这种 join 性能完全没有压力。规范化和查询性能的取舍在作业里讲清楚“我选择了什么、为什么”比闷头写代码拿到的分更高。2.2 MySQL 建表脚本字段类型、外键约束与字符集建表脚本直接照下面这份写数据库名建议就叫 school_timetable。注意几个细节表名用 clazz 而不是 class因为 class 是 MySQL 的保留字很多第一次跑脚本的同学就挂在这一点上DROP 顺序要先删子表后删父表否则外键约束会报错。-- 1. 教师表 DROP TABLE IF EXISTS timetable; DROP TABLE IF EXISTS course; DROP TABLE IF EXISTS classroom; DROP TABLE IF EXISTS clazz; DROP TABLE IF EXISTS teacher; CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 教师ID, name VARCHAR(50) NOT NULL COMMENT 教师姓名, title VARCHAR(20) DEFAULT 初级 COMMENT 职称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE clazz ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 班级ID, name VARCHAR(50) NOT NULL COMMENT 班级名称如高一(3)班, grade VARCHAR(10) NOT NULL COMMENT 年级, student_count INT DEFAULT 0 COMMENT 学生人数 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE classroom ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 教室ID, name VARCHAR(50) NOT NULL COMMENT 教室名称如A-101, building VARCHAR(50) COMMENT 所在楼栋, capacity INT DEFAULT 60 COMMENT 教室容量 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 2. 课程表依赖 teacher、clazz CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 课程ID, name VARCHAR(50) NOT NULL COMMENT 课程名如数学, teacher_id INT NOT NULL COMMENT 授课教师ID, clazz_id INT NOT NULL COMMENT 授课班级ID, hours_per_week INT DEFAULT 2 COMMENT 每周课时数, consecutive INT DEFAULT 1 COMMENT 是否连排1表示两节连排, credits DECIMAL(3,1) DEFAULT 1.0 COMMENT 学分, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ON DELETE RESTRICT, CONSTRAINT fk_course_clazz FOREIGN KEY (clazz_id) REFERENCES clazz(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 3. 排课结果表依赖 course、classroom CREATE TABLE timetable ( id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL COMMENT 课程ID, weekday TINYINT NOT NULL COMMENT 星期几0-4代表周一至周五, period TINYINT NOT NULL COMMENT 第几节0-7代表第1-8节, classroom_id INT NOT NULL COMMENT 教室ID, UNIQUE KEY uk_time_class (weekday, period, classroom_id), CONSTRAINT fk_tt_course FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE, CONSTRAINT fk_tt_classroom FOREIGN KEY (classroom_id) REFERENCES classroom(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逐个拆一下参数选择。weekday 和 period 用 TINYINT 而不是 INT是因为取值范围固定TINYINT 省空间并且让索引更紧凑。uk_time_class 这个联合唯一键是给算法兜底的即使 Java 代码里冲突检测写漏了同一间教室同一时间被插入两条记录时MySQL 会直接拒绝。外键策略是故意做区分度的course 的 teacher_id 用 ON DELETE RESTRICT老师被误删时课程还在数据库会拦下这次删除相当于上保险clazz_id 用 ON DELETE CASCADE班级删了这个班的所有课程一起清掉符合业务直觉。timetable 对 course 用 CASCADE删课程连带清排课结果。字符集统一 utf8mb4连接 URL 里还要再配一次 characterEncodingutf8这是后面乱码问题的最早防线。如果课设做到一半想加字段比如给课程表加一个 exam_type 列直接用 MySQL 的修改表结构命令ALTER TABLE course ADD COLUMN exam_type VARCHAR(10) DEFAULT 闭卷;Java 实体类同步加一个属性DAO 里的查询列和插入列补上这个字段页面加一列表单就完事。这是“数据库修改结构”在课设里最常遇到的实际操作不用删表重建别慌。2.3 为什么不用存储过程JDBC 在课设答辩时更占便宜很多课设教程会把排课逻辑写进 MySQL 存储过程理由是“数据库课程设计应该多用数据库功能”。我一般建议别抄这个方案。存储过程把算法固化在数据库里Java 侧退化成一句 CALL答辩老师追问“你的排课算法在哪里体现”的时候你反而拿不出 Java 代码。对比项JDBC Java 算法存储过程排错方式IDE 断点、日志打印命令行报错查看不方便答辩讲解方法级拆讲直观黑匣子难现场解释算法复用可写测试类反复验证测试困难换数据库只改 JDBC 连接存储过程语法要重写数据库课程设计的重点是“数据库设计”不是“数据库编程”。用 JDBC 写增删改查能展示对 SQL 和连接管理的理解把排课算法放在 Java 侧又覆盖了 Java 基础。两边都沾答辩素材更多。存储过程当然也是数据库能力但短时间的课设答辩里没法现场 debug老师问“你是怎么调通的”只能回答“在命令行里执行”没有过程感。JDBC 的核心三板斧是 Connection、PreparedStatement、ResultSet第 4 章写课程增删改查时会把这三者完整走一遍。现在只需要记住结论课设主体用 JDBC存储过程可以作为“我还了解过它”的加分话术。3. 排课核心算法时间片分配与冲突检测的 Java 实现表建好之后排课系统开始进入真正值钱的部分。先定义一周的时间轴周一到周五每天第 1 到第 8 节一共 5×840 个时间片。每间教室每周能承载 40 节课每个班每周也有 40 个可用时间片。排课就是给每门课从这 40 个位置里挑出不冲突的若干个。3.1 把“能不能排”拆成四个约束第一个约束是教师约束。同一个老师同一时间只能在一间教室上课所以需要一个布尔矩阵 teacherBusy[weekday][period]值 true 表示这个老师在这个时间片已经被占用。第二个约束是班级约束。一个班同一时间只能上一门课用 clazzBusy[weekday][period] 表示。第三个约束是教室约束。一间教室同一时间只能安排一个班用 roomPlan[classroomId][weekday][period] 存课程 ID0 表示空闲。第四个约束是连排约束。如果 course.consecutive 为 1这门课必须连续占用 period 和 period1 两个时间片。连排不是简单多占一个格子它要求这两个格子同时空闲并且 period 不能是当天的最后一节。为什么不把教师和班级合并成一个三维数组因为三个维度的语义不同。teacherBusy 按教师索引clazzBusy 按班级索引roomPlan 按教室索引强行合并成一个 int[][][] 反而增加取值的出错概率。三个二维数组各管一个“资源”排课系统本质是三种资源的时间表互不重叠。3.2 用时间片矩阵做冲突检测Java 方法怎么写下面这段是排课系统最核心的方法建议单独放在 TimetablePlanner 类里不要和 DAO 混在一起。// 排课规划器用数组记录三类资源的占用状态 public class TimetablePlanner { // 假设 6 间教室、5 个工作日、每天 8 节 private int[][][] roomPlan new int[6][5][8]; // 值为 courseId0 表示空闲 private boolean[][] teacherBusy new boolean[5][8]; private boolean[][] clazzBusy new boolean[5][8]; // 判断 course 能否放在指定的时间片和教室 public boolean canPlace(Course course, int weekday, int period, int classroomId) { if (teacherBusy[course.getTeacherId()][weekday][period]) { return false; // 老师这个时间已有课 } if (clazzBusy[course.getClazzId()][weekday][period]) { return false; // 班级这个时间已有课 } if (roomPlan[classroomId][weekday][period] ! 0) { return false; // 教室这个时间已被占用 } // 连排课程下一节也必须空闲且不能是当天的第 8 节 if (course.isConsecutive() (period 7 || roomPlan[classroomId][weekday][period 1] ! 0 || teacherBusy[course.getTeacherId()][weekday][period 1] || clazzBusy[course.getClazzId()][weekday][period 1])) { return false; } return true; } }canPlace 是冲突检测的全部逻辑答辩时先讲三个 if再讲连排逻辑是一层一层递进的。参数含义weekday 从 0 到 4period 从 0 到 7classroomId 从 0 到教室总数减一。数组越界是最容易犯的错尤其是连排判断里的 period 1所以先判 period 7。查完冲突之后真正排课时要同步更新三个数组状态// 排课成功后同步占用状态注意连排要占两个时间片 public void occupy(Course course, int weekday, int period, int classroomId) { roomPlan[classroomId][weekday][period] course.getId(); teacherBusy[course.getTeacherId()][weekday][period] true; clazzBusy[course.getClazzId()][weekday][period] true; if (course.isConsecutive()) { roomPlan[classroomId][weekday][period 1] course.getId(); teacherBusy[course.getTeacherId()][weekday][period 1] true; clazzBusy[course.getClazzId()][weekday][period 1] true; } }这里有一个容易写错的点occupy 和 canPlace 里的连排判断必须保持一致。如果 canPlace 判断了 period1 空闲occupy 却不写入 period1下一次判断时这个连排课程就“凭空消失”了后面排进来的课会产生隐形冲突。3.3 两遍扫描的贪心排课参数怎么定排课算法我建议做两遍扫描不写递归回溯。第一遍按每周课时数降序处理课程优先占坑第二遍处理失败队列允许拆开连排。// 按每周课时数降序排序课时多的课程优先排 ListCourse courses courseDao.findAll(); courses.sort((a, b) - b.getHoursPerWeek() - a.getHoursPerWeek()); ListCourse retryQueue new ArrayList(); for (Course course : courses) { boolean placed false; for (int weekday 0; weekday 5 !placed; weekday) { for (int period 0; period 8 !placed; period) { for (int roomId 0; roomId ROOM_COUNT !placed; roomId) { if (planner.canPlace(course, weekday, period, roomId)) { planner.occupy(course, weekday, period, roomId); placed true; } } } } if (!placed) { retryQueue.add(course); } } // 第二遍拆连排兜底 for (Course course : retryQueue) { course.setConsecutive(false); // 重新执行一遍相同的三重循环找到位置就排 }排序这行代码很关键。课时多的课程先排是贪心算法里“最紧约束优先”的变体。如果先排每周只上一节的音乐课后面的数学课很容易找不到连续两个空位排课成功率会明显下降。几个默认参数按表里这样设置就够了参数默认值说明WEEKDAYS5周一到周五不含周末PERIODS_PER_DAY8每天 8 节ROOM_COUNT6必须和 classroom 表行数一致MAX_ATTEMPTS200单门课尝试上限超过就放弃ALLOW_SPLITtrue第二遍允许拆开连排ROOM_COUNT 这个常量特别提醒一下它是数组维度也是教室里表里的数据量。如果 classroom 表只有 4 条记录但代码里写 6遍历时数组不会越界但会查出一堆“占用了不存在的教室”的脏数据。建议从 classroomDao.count() 里取不要写死。4. 管理端功能实现登录、增删改查与事务回滚业务表建好、算法核心写完到这一步系统已经有一半能跑了。剩下的管理端功能听起来全是“增删改查”但要写出能在答辩现场扛住追问的实现有几个细节值得认真做。4.1 JSP Servlet 的分层写法Model 只管数据Servlet 只管路由数据库课程设计的环境里我一般都建议用传统的 JSP Servlet JDBC而不是一上来就套 spring boot 全家桶。原因是课设要求“课堂演示”和“改数据库看页面变化”传统三层结构每一层都是透明的出了问题能直接定位。包结构建议这样分com.school.modelCourse、Teacher、Clazz、Classroom 四个实体类字段和表一一对应。com.school.daoCourseDao、TimetableDao封装全部 JDBC 操作。com.school.servletCourseListServlet、CourseAddServlet、TimetableGenServlet 等。webapp 目录下放 JSP 页面course_list.jsp、course_add.jsp、timetable_view.jsp。Servlet 只做三件事调 Dao 拿数据、往 request 里塞数据、转发给 JSP。下面这个课程列表的 Servlet 就是标准模板WebServlet(/course/list) public class CourseListServlet extends HttpServlet { private CourseDao courseDao new CourseDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListCourse list courseDao.findAll(); req.setAttribute(courseList, list); req.getRequestDispatcher(/course_list.jsp).forward(req, resp); } }写代码时记住一条原则SQL 永远不进 Servlet。如果把 JDBC 写在 doGet 里答辩时老师会沿着这条线往下追问三层结构你越讲越被动。另外登录功能建议做个最简单的 Session 拦截登录成功后把用户 id 塞进 session用一个 Filter 检查未登录请求直接重定向到 login.jsp。admin 表单独建一张只有 id、username、password它和排课业务没有关系不影响前面五张表的完整性。4.2 课程管理模块的完整实现JDBC 增删改查与 SQL 注入防护课程管理是整个管理端最典型的模块查询、新增、修改、删除四件套写全其他模块照抄改表名就行。下面是 CourseDao 里最常用的两个方法public class CourseDao { private Connection getConnection() throws SQLException { String url jdbc:mysql://localhost:3306/school_timetable ?useSSLfalsecharacterEncodingutf8; return DriverManager.getConnection(url, root, 123456); } public ListCourse findAll() throws SQLException { ListCourse list new ArrayList(); String sql SELECT id, name, teacher_id, clazz_id, hours_per_week, consecutive FROM course ORDER BY id; try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Course c new Course(); c.setId(rs.getInt(id)); c.setName(rs.getString(name)); c.setTeacherId(rs.getInt(teacher_id)); c.setClazzId(rs.getInt(clazz_id)); c.setHoursPerWeek(rs.getInt(hours_per_week)); c.setConsecutive(rs.getInt(consecutive) 1); list.add(c); } } return list; } public void insert(Course course) throws SQLException { String sql INSERT INTO course(name, teacher_id, clazz_id, hours_per_week, consecutive) VALUES(?,?,?,?,?); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, course.getName()); ps.setInt(2, course.getTeacherId()); ps.setInt(3, course.getClazzId()); ps.setInt(4, course.getHoursPerWeek()); ps.setInt(5, course.isConsecutive() ? 1 : 0); ps.executeUpdate(); } } }写 JDBC 增删改查有三个老生常谈但必须守住的点。第一永远用 PreparedStatement 的 ? 占位符不要用字符串拼接 SQL这是防 SQL 注入的基本功也是 java 面试题里反复被问到的点。第二用 try-with-resources 自动关闭连接、语句、结果集课设代码能少写一堆 finally。第三getConnection() 写在每个方法里会重复更规范的做法是抽一个 DBUtil 单例但课设演示阶段这么写逻辑更直白。关于连接 URL 里的两个参数再多说一句useSSLfalse 是为了避免本地连接 MySQL 8 时弹出 SSL 告警characterEncodingutf8 是中文不乱码的底线配置。这两个参数缺一个第 5 章的乱码坑就会提前找你。4.3 事务边界排课保存时的原子性为什么必须靠 Connection管理端单个表单的新增和修改都不需要事务真正必须用事务的是“一键排课保存”。排课算法在内存里生成了几十条 timetable 记录要一次性写进数据库。如果不用事务排到第 20 条因为外键或唯一键冲突报错数据库里会残留前 19 条下一次重跑时就重复了。标准写法是关闭自动提交全部成功再 commit任何一条失败就 rollbackpublic void saveAll(ListTimetableItem items) throws SQLException { String sql INSERT INTO timetable(course_id, weekday, period, classroom_id) VALUES(?,?,?,?); try (Connection conn getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (TimetableItem item : items) { ps.setInt(1, item.getCourseId()); ps.setInt(2, item.getWeekday()); ps.setInt(3, item.getPeriod()); ps.setInt(4, item.getClassroomId()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } }setAutoCommit(false) 之后commit 之前的所有操作默认不会持久化任何一个 executeBatch 抛异常rollback 都能把数据恢复到调用前。executeBatch 把几十条 insert 打包发给 MySQL比循环里一条条 executeUpdate 快得多而且和事务机制不冲突。事务边界要清楚这个事务只覆盖“写入 timetable”不覆盖“排课算法计算”。算法在内存里跑不碰数据库只有最终结果需要落库时才开事务。把算法也包进事务里是常见的误用事务包得越大连接占用时间越长并发一高就会出现锁等待。5. 排课系统避坑指南课设过程中最容易翻车的 5 个地方走到这一步系统应该能跑起来了。但课设提交前下面这 5 个坑都是真实发生过的翻车现场每个都按“现象 → 原因 → 解决”讲清楚对照排查一遍能省下大量调试时间。5.1 Tomcat 部署后访问 404多半不是路径错而是 class 没编译现象IDEA 里项目能启动Tomcat 管理器页面也正常但访问 http://localhost:8080/course/list 报 404控制台没有任何异常输出。原因Web 项目的 Java 类没有被编译到 WEB-INF/classes 目录或者 WebServlet 注解扫描被关掉了。新手常犯的一个是把 Servlet 类编译到了 target/classes但 Tomcat 部署的是 Web 目录另一个是 web.xml 里写了 metadata-completetrue让容器忽略了注解。解决先检查部署目录里有没有 CourseListServlet.class再检查注解和 web.xml 的配置。更稳妥的做法是把 Servlet 映射写死在 web.xml 里注解和 web.xml 并存时以 web.xml 为准不要在启动机制上赌运气。404 是课设里最没有技术含量但最耗时的错早检查这两处能少走两小时弯路。5.2 数据库中文全部变问号三层乱码只修一层没用现象页面显示、数据库表、前后端交互都是中文但查出来的数据是 ??或者在表里看是 ??页面上也是 ??。原因乱码是链路问题。JSP 页面编码、请求编码、响应编码、JDBC 连接字符集、MySQL 表字符集五处里任何一处不是 UTF-8就会在某一段变成问号。最常见的漏网之鱼是 JDBC URL 少了 characterEncodingutf8其次是 JSP 没写 pageEncoding。解决建库时指定 utf8mb4JDBC URL 配 characterEncodingutf8Servlet 的 doPost 开头加 request.setCharacterEncoding(UTF-8)响应加 response.setContentType(text/html;charsetUTF-8)JSP 第一行写全 contentType 和 pageEncoding。排查时先在页面写死一段中文比如把“数学”写死在 JSP 里看它能不能正常显示能显示就说明页面编码没问题再逐层往后查。不要五处同时改完发现还是乱码因为那样你根本不知道是谁修好的。5.3 连排排到一天第 8 节自动“跨天”接上第 9 节现象consecutive1 的课程被排到周三第 8 节代码里的 period18检查竟然通过了生成的课表出现“周三第 9 节”这种不存在的时间。原因把时间片建模成一维数组用 index weekday * 8 period然后对 index1 做检查。结果周三第 8 节的 index 加 1 变成了周四第 1 节的 index跨天问题被隐藏了。解决连排判断必须同时检查 period 小于当天最后一节并且 period1 仍在同一天。防御性写法是if (course.isConsecutive()) { if (period PERIODS_PER_DAY - 1) { return false; } if (teacherBusy[teacherId][weekday][period 1]) { return false; } if (clazzBusy[clazzId][weekday][period 1]) { return false; } if (roomPlan[classroomId][weekday][period 1] ! 0) { return false; } }这是排课算法里最容易踩的隐性错误。它不涉及复杂逻辑但一旦错整张课表的可信度就没了。写完连排逻辑后建议故意构造一个“只有第 8 节空闲”的用例去测看代码会不会错误放行。5.4 删课程报“外键约束失败”先删子表还是先删父表现象课程管理页面点删除控制台报 Cannot delete or update a parent row: a foreign key constraint fails课程删不掉。原因timetable 表里还保留着该课程的外键记录。虽然建表时给 timetable 的 course_id 配了 ON DELETE CASCADE但如果级联删除因为另一个外键路径被 RESTRICT 挡住删除就会失败。解决删除课程前先手动删除它的排课结果再删除课程本体。用事务包住两步保证中间不出脏数据public void deleteCourseWithTimetable(int courseId) throws SQLException { String delTt DELETE FROM timetable WHERE course_id?; String delCourse DELETE FROM course WHERE id?; try (Connection conn getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(delTt)) { ps1.setInt(1, courseId); ps1.executeUpdate(); } try (PreparedStatement ps2 conn.prepareStatement(delCourse)) { ps2.setInt(1, courseId); ps2.executeUpdate(); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } }这个坑的根源是“把级联删除当成万能”实际上显式控制删除顺序更可控。用 Navicat、dbx 这类图形工具操作时也一样先展开子表删对应行再删父表行顺序反了外键一定报错。5.5 排课死循环或者某门课永远排不上现象点“自动排课”按钮页面一直转圈或者某门课反复提示“无法排入”但人工看明明有空闲时间片。原因递归回溯或 while 循环里没有设置最大尝试次数。排课冲突是动态的前面十几门课占满了核心时间段最后一门课要连排且只剩下的空位是分散的单节算法反复尝试都冲突又没有退出条件于是死循环。解决给每门课设置最大尝试次数超过就放弃并加入失败队列对失败队列的课程第二遍允许拆开连排再扫一轮仍然失败的直接输出“冲突课程清单”提示用户手动调课。尝试次数不必太大200 次足够判断一门课在当前剩余空间里是否可排。真正能提高成功率的不是无限重试而是排课顺序先排语数英这类课时多、连排要求高的课再排音乐美术这些灵活性高的副课。这也是 java 面试里常聊的贪心话题课设做两层贪心加失败队列已经比大部分同学细致。6. 从课设到加分给排课系统加一个“先判断后动手”的自动调课接口排课系统做完如果还想再加一个亮点做自动调课是性价比最高的选择。典型场景是老师临时请假需要把某门课从周二上午换到周四下午系统自动找一个不冲突的新位置并把旧位置释放。6.1 自动调课的输入与输出输入只需要三门课程 ID 和目标时间片课程 ID、目标 weekday、目标 period。输出就一个布尔值成功或失败。判断成功的标准很简单——新位置和原来的位置不能重叠被挪走之后原来占用的时间片要释放。6.2 先确认能放再释放旧位置核心方法只有十几行顺序是唯一的重点public boolean autoAdjust(int courseId, int targetWeekday, int targetPeriod) { Course course courseDao.findById(courseId); // 先判断新位置能不能放下 if (!planner.canPlace(course, targetWeekday, targetPeriod)) { return false; } // 能放下才释放旧时间片 releaseCourse(course); planner.occupy(course, targetWeekday, targetPeriod); timetableDao.updateTimetable(course.getId(), targetWeekday, targetPeriod); return true; }先判断、后释放这个顺序就是给系统留的后悔药。如果先释放旧位置再判断新位置新位置放不下时旧位置已经没了课程就会凭空消失。自动调课的成功与否不只取决于“调得快”更取决于“调完之后还是一张合法课表”。6.3 验证方法每次排完先自检一遍写完调课接口后建议再加一个 validateTimetable()遍历所有 timetable 记录按教师、班级、教室三个维度复查是否有同时间片重叠任何一组重叠都输出冲突明细。每次排完课或者调完课先跑一遍校验再落库。我做这个系统时因为图省事用过一维索引结果连排跨了天重排了三次才看出规律。后来我把 validateTimetable() 写进每次保存前的流程里排课质量稳定了很多。答辩时顺口说一句“系统每次排完会自检”这句话的效果不亚于 PPT 里画三页算法图。希望帮到你。本文还有配套的精品资源点击获取
返回列表