十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

SpringBoot高校排课系统:从CRUD到多约束资源调度实战解析

SpringBoot高校排课系统:从CRUD到多约束资源调度实战解析 简介面向高校计算机相关专业毕业设计的一款高校排课系统资料包基于SpringBoot框架实现涵盖管理员对专业班级、学生、教师、教室、课程等多维度资源的管理与排课调度。包内共235个文件以56个Java源码、82个HTML页面、31个XML配置、55个class编译文件为主附带SQL数据库脚本及可执行jar包源码结构清晰便于直接导入IDE运行或二次扩展。系统重点解决多条件下课表安排以及学生、教室、教师等资源调度问题适合正在做排课类课题或希望理解SpringBoot前后端整合的学习者参考。压缩包整体仅617KB轻量易下载目前已有1133人学习具有较高的毕业设计参考价值。1. 高校排课系统的需求边界与实体关系梳理高校排课系统听起来像是一个简单的 CRUD 项目但真正动手拆过源码你会发现它的核心难点根本不在增删改查而在「多约束资源调度」。一个学期几十个专业、几百个班级、上千门课程要在教室容量、教师时间、班级空闲段、合班规则四个维度下排出一份没有冲突的课表这比写十个学生管理接口都费脑。这个毕业设计包里的 SpringBoot 源码加数据库 SQL 脚本恰好覆盖了从需求分析到落库的完整链路非常适合作为计算机相关专业毕设的起点或者给想快速搭一套课表管理系统的后端开发者做脚手架。包内包含 MajorController、StudentController 到 TeacherController 等按对象拆分的控制器以及 StudentTest、TeacherTest、RandomName 这类辅助类说明作者已经把常用的资源管理模块抽出来了你拿到手后可以专注于补全排课算法和冲突检测这两块最值钱的部分。2. SpringBoot 分层实现从 Controller 到 Mapper 的调用链2.1 控制层按业务对象拆分的资源封装项目类列表里出现了 MajorController、StudentController、TeacherController、SubjectController、ClassroomController这种命名方式说明作者没有把所有接口塞进一个类而是遵循 REST 风格按业务对象拆分成独立控制器。每个 Controller 只做三件事接收参数、调用 service 层、封装返回结果。以 StudentController 为例常見写法是RestController RequestMapping(/api/student) public class StudentController { private final StudentService studentService; public StudentController(StudentService studentService) { this.studentService studentService; } GetMapping(/list) public Result list(RequestParam(required false) String majorName, RequestParam(defaultValue 1) int page, RequestParam(defaultValue 20) int size) { PageResultStudentVO data studentService.pageQuery(majorName, page, size); return Result.ok(data); } }注意majorName用了required false因为前端可能只按专业过滤也可能不传。page和size给了默认值避免请求参数缺失时直接 500。这里Result是统一返回体一般包含code、message、data三个字段前端只需要判断code是否为 0 就能决定是否提示异常这是 SpringBoot 接口里很标准的约定。2.1.1 权限控制与 AdminController 的设计AdminController 在排课系统里承担的是基础数据维护角色比如新增专业、修改教室容量、重置学期等。这个类必须和 LoginController 配合使用否则任何人都能调用接口删掉课表。常见做法是加一个AdminInterceptor通过 Token 或 Session 判断当前用户是否已登录且角色为管理员。LoginController 登录成功后把用户 Id 和角色写入 Session或者生成一个 JWT 返回前端。拦截器里放行的路径一般只有/api/login其余路径都要校验权限。排课系统里如果少了这一层等于把数据库裸奔在公网上答辩时也是明显扣分点。2.2 Service 层的排课事务边界排课这个动作不是插一条数据那么简单而是一组操作的集合先清空旧学期课表再重新计算所有课程的时间片和教室最后把新课表批量插入。这三个操作必须在一个事务里否则中途出错会导致数据残缺例如只清了旧数据没写入新课表。正确做法是在 Service 方法上加TransactionalTransactional(rollbackFor Exception.class) public boolean schedule(ScheduleRequest request) { scheduleMapper.deleteByTerm(request.getTermId()); ListScheduleItem items buildScheduleItems(request); scheduleMapper.batchInsert(items); classroomMapper.batchUpdateOccupation(items); return true; }这里的deleteByTerm用于整学期重排按termId删除所属记录。buildScheduleItems是核心算法稍后会展开。batchInsert建议使用 MyBatis 的动态 SQL 批量插入不要用 for 循环逐条 insert否则几千条课表数据会慢到让你怀疑数据库性能。batchUpdateOccupation是把教室的占用标记刷新一遍让前端能展示哪些教室已满。2.2.1 MyBatis Mapper 与 XML 的对应关系排课项目通常使用 MyBatis 或 MyBatis-Plus。如果使用 MyBatisMapper 接口与 XML 文件必须保证同名且 namespace 正确。例如Mapper public interface ScheduleMapper { int deleteByTerm(Param(termId) Integer termId); int batchInsert(Param(list) ListScheduleItem items); }对应 XML 里的delete iddeleteByTerm和insert idbatchInsert。这里要特别注意Param注解因为 XML 中的#{list}依赖这个参数名如果不写MyBatis 会把 List 当成单个参数解析报出There is no getter for property named list之类的异常。这个坑在毕业设计里出现的频率极高排查时先看 Mapper 接口参数注解是否齐全。3. 排课核心算法时间片与教室资源的冲突检测3.1 为什么不能先排时间再排教室很多初版排课系统的做法是先给每个班级分配时间片最后再检查教室是否够用。这样做的结果往往是一周课表排完发现周一上午三节课都需要用同一间大教室然后人工调整到崩溃。正确做法是在分配时间片的同时把教室也作为一个约束条件一起参与校验。也就是「班级 教师 教室」三者在同一时间片只能被一门课程占用。排课问题本质上是图着色问题的一种变体每个课程是一个顶点冲突关系是边颜色就是时间片。但完整求解 NP 难毕业设计里没必要追求全局最优。常见做法是贪心算法按课程优先级从高到低依次分配每次分配时检查班级时间、教师时间、教室占用三个维度找到一个可用时间片就立即占用。3.2 时间片编码与冲突检测表我一般把时间片编码成一个整数比如一周五天每天 12 个节次就把时间片编号为 0 到 59。对于普通高校作息一天通常是 1-2 节、3-4 节、5-6 节、7-8 节、9-10 节共五个大节次一周 25 个时间片。用整数编码而不是直接用day_of_week和start_section两个字段好处是在内存里做冲突检测时只要一个整型变量就能表示一个时间片。冲突检测通常用一个MapInteger, SetInteger或者布尔数组来记录占用情况下标是班级 Id值是已占用的时间片集合。同样的结构再维护一份教师维度和教室维度。检测逻辑如下private boolean isAvailable(MapInteger, SetInteger classConflicts, MapInteger, SetInteger teacherConflicts, MapInteger, SetInteger roomConflicts, int classId, int teacherId, int roomId, int slot) { if (classConflicts.getOrDefault(classId, Collections.emptySet()).contains(slot)) { return false; } if (teacherConflicts.getOrDefault(teacherId, Collections.emptySet()).contains(slot)) { return false; } if (roomConflicts.getOrDefault(roomId, Collections.emptySet()).contains(slot)) { return false; } return true; }3.2.1 贪心分配的时间片顺序优化贪心算法的效果很大程度取决于遍历顺序。如果一个专业上午的课排满了下午空着那么应该优先把核心专业课放在上午。我看到一个比较实用的策略是对每个课程计算一个优先级分数先按分数降序排出课程列表再依次分配。优先级分可以这样计算priority 课程权重专业基础课 5 分、公共课 3 分 班级年级系数高年级先选 是否合班合班课优先分配时遍历时间片集合对每个时间片内还要优选合适的教室。如果教室有容量属性应该选「容量大于选课人数且最接近选课人数」的教室这样不会造成大教室资源浪费。代码里可以用一个TreeMapInteger, ListClassroom按容量整理教室然后ceilingEntry(studentCount)找到最合适的容量区间。3.3 合班课的资源冲突处理高校排课里合班课很常见比如两个班或者三个班合上《大学英语》。这时候排课算法不能只核对单个班级的空闲时间还要核对参与合班的每一个班级都不能在该时间片有课。常见做法是把合班信息放在课程表里比如class_ids字段存储多个班级 Id用逗号分隔。检测时遍历这些班级的占用集合任一班级冲突则该时间片不可用。教室分配同理合班课需要的教室容量是多个班级人数之和绝不能拿单个班级人数去匹配。如果项目里把student_count冗余在排课表上那么合班课插入前要确保人数已经累加过否则会出现明明有三个班合班教室却按一个班人数分配最后上课时坐不下。4. 数据库模型与 SQL 脚本排课记录怎么落库4.1 实体关系与数据字典SQL 脚本是这份资源里最直接可用的部分。排课系统的核心表至少包括专业表major、班级表class、学生表student、教师表teacher、课程表subject、教室表classroom、排课表schedule_item、用户表admin。这些表的实体关系大概是major 1 对多 classclass 1 对多 studentteacher 1 对多 subject一个教师可以教多门课subject 多对多 classroom一门课可以在多个教室上课但具体到某个学期排课时只固定一个schedule_item 关联 class、subject、teacher、classroom 和 term排课表是业务核心字段设计直接影响算法复杂度。下面是一份典型的表结构字段名类型说明idbigint主键term_idvarchar学期标识如 2024-2025-1class_idbigint班级 Idsubject_idbigint课程 Idteacher_idbigint教师 Idclassroom_idbigint教室 Idday_of_weektinyint星期几1-5start_sectiontinyint起始节次1 表示第 1-2 节statustinyint0 未确认 1 已发布day_of_week和start_section是课表展示时最关键的字段前端拿到这两个值就能拼出格子位置。term_id用于区分不同学期否则下学期的数据会和上学期混在一起。4.1.1 SQL 脚本里的约束与索引建议看 SQL 脚本时重点检查主键、外键和唯一索引。排课表的class_id、day_of_week、start_section这三个字段组合起来应该建立唯一索引防止同一个班级在同一时间片被插入两条课表记录。但要注意唯一索引不能覆盖合班课的情况因为合班课是多条 record 共享同一个时间片只是 class_id 不同所以这里的唯一索引建议建在classroom_id day_of_week start_section上防止同一教室同一时间被重复占用。这种索引是数据层面的最后一道防线即便算法有 bug数据库也会拒绝冲突数据。另外term_id字段非常建议建普通索引因为整学期重排时DELETE FROM schedule_item WHERE term_id ?会走索引快速定位否则全表扫描在大数据量下会非常慢。4.2 查询课表的 SQL 写法排课系统最常用的查询是「按班级查一周课表」那么 SQL 可以写成SELECT s.day_of_week, s.start_section, sub.subject_name, t.teacher_name, c.classroom_name FROM schedule_item s JOIN subject sub ON s.subject_id sub.id JOIN teacher t ON s.teacher_id t.id JOIN classroom c ON s.classroom_id c.id WHERE s.class_id #{classId} AND s.term_id #{termId} ORDER BY s.day_of_week, s.start_section;这个查询会把一周的课表按星期和节次排序前端拿到后就能直接渲染成表格。如果你的时间片编码已经用了整数slot那么day_of_week和start_section可以由slot整除和取模得到但是不建议在 SQL 里用表达式去算这样无法走索引。更稳妥的做法是排课表同时存储slot、day_of_week、start_section三个字段查询时直接过滤day_of_week减少计算开销。4.3 备份与恢复策略SQL 脚本通常是纯文本文件直接导入数据库即可。但排课系统在学期初排课完成后接下来一整个学期都不会怎么变动课表。这时候可以把这个学期数据单独导出一份 SQL 文件作为备份归档。常见做法是mysqldump -u root -p coursemanager schedule_backup_2024.sql恢复时导入mysql -u root -p coursemanager schedule_backup_2024.sql如果数据库比较小这种全量导出完全够用。如果是大型学校建议按学期分表或者加term_id分区否则每次备份导出全量数据会越来越慢。5. 用测试类和 RandomName 验证排课结果的几个技巧5.1 RandomName 在测试数据填充中的作用源码中包含 StudentTest、TeacherTest 和 RandomName这三个类放在同一个包里说明作者在开发时非常依赖随机数据来模拟真实业务。排课系统在测试时最大的痛点就是没有真实学生和教师名单导致课表排出来之后没法直观看到「哪个班级有哪些老师」。RandomName 类的典型实现是从姓氏和名字数组里随机拼接public class RandomName { private static final String[] SURNAMES {赵, 钱, 孙, 李, 周, 吴, 郑, 王}; private static final String[] GIVEN_NAMES {伟, 芳, 娜, 敏, 静, 磊, 军, 洋, 勇}; public static String generate() { return SURNAMES[(int) (Math.random() * SURNAMES.length)] GIVEN_NAMES[(int) (Math.random() * GIVEN_NAMES.length)] GIVEN_NAMES[(int) (Math.random() * GIVEN_NAMES.length)]; } }这里生成的姓名可能是三个字但一定注意不要在这个类里去和数据库交互保持它是一个纯工具类。StudentTest 中可以用它循环生成几百条学生记录插入测试库用来验证当班级人数达到 60 人时排课算法是否还能分配到足够容量的教室。TeacherTest 则可以用来生成 30 位教师数据模拟一个学院的实际师资规模。5.2 冲突检测脚本的自动化验证排完课后不能只看结果没报错还必须做一次系统性冲突检查。除了代码里已有的isAvailable检测数据库层面还可以用 SQL 验证把那些被算法遗漏的冲突找出来SELECT day_of_week, start_section, classroom_id, COUNT(*) FROM schedule_item WHERE term_id 2024-2025-1 GROUP BY day_of_week, start_section, classroom_id HAVING COUNT(*) 1;这条 SQL 能查出同一个教室在同一时间片是否被分配了多门课。同理可以把classroom_id换成teacher_id或者class_id分别检测教师和班级的冲突。我在实际项目里会把这三条验证 SQL 写进一个测试类在排课任务执行完成后自动运行任何一条返回非空就判定排课失败。这比人眼看课表高效得多也能在答辩时展示你的工程严谨性。5.3 测试数据的清理策略StudentTest 和 TeacherTest 生成的随机数据会留在数据库里如果不做清理后面的正式排课会把随机学生当成真实选课人数导致容量判断错乱。处理方式有两种一种是在测试类里用Transactional注解让测试方法结束后自动回滚另一种是单独写一个清理 SQL删除那些学号或教师工号以某个前缀开头的数据。SpringBootTest Transactional class StudentTest { Test void testInsertStudents() { // 插入后可以在同一个事务里查询验证 } }这个Transactional的坑是如果测试方法里调用了scheduleMapper.batchInsert等需要提交事务才能生效的代码回滚可能让你误以为插入成功但查不到结果。所以我的建议是测试类里就干脆不用事务而是在测试方法结束时显式执行清理这样数据落库后还能用查询验证排课结果是否正确。清理时要注意先删排课表再删学生表否则外键关联会报错。本文还有配套的精品资源点击获取
返回列表