SpringBoot+Vue选课排课系统毕业设计:并发控制与排课算法实战

发布时间:2026/10/13 13:00:40
SpringBoot+Vue选课排课系统毕业设计:并发控制与排课算法实战 简介这份资源是面向高校计算机相关专业毕业设计场景的选课与排课管理系统完整方案基于SpringBoot与Vue.js前后端分离架构实现适合正在准备毕设或希望实践Java全栈开发的学习者。压缩包为zip格式整体约17.55MB内含源码、SQL脚本、项目文档与答辩PPT等主要文件类型分别对应系统实现、数据库建表与初始化、架构与功能说明以及汇报演示等用途。系统覆盖用户权限管理、课程信息维护、学生选课、教师排课时段与教室安排、选课数据统计等核心模块后端采用SpringBoot提供RESTful接口并集成JWT身份验证前端借助Vue的响应式绑定与组件化完成交互数据层通过MySQL完成增删改查。目前已有559人学习下载读者可据此快速理解前后端分离项目的目录组织与接口设计并在此基础上按需修改扩展作为毕设起步模板或全栈练手案例均较为实用。1. 选课排课系统为什么总在毕业设计里翻车每年到了毕设季总有一批同学在选课排课管理系统这个题目上栽跟头。表面上看这就是个增删改查的管理系统能有多难但真正动手做过的人都知道选课和排课这两个业务场景藏着大量容易被忽略的约束条件。选课要考虑课程容量、时间冲突、先修课依赖排课要处理教室资源、教师时间、班级课表不重叠。很多同学做到一半才发现自己写的系统只能跑通“一个学生选一门课”的理想情况一旦并发上来或者约束变多逻辑就全乱了。这篇文章面向的是正在做这类毕业设计的同学也适合想拿它当 SpringBoot Vue 全栈练手项目的开发者。我会从技术选型、数据库设计、核心业务实现、前后端联调、部署交付这几个环节把一套能跑通、能答辩、能复现的方案讲清楚。标题里提到的源码、SQL 脚本、文档和 PPT本质上是一套完整的交付物但比交付物更重要的是你得理解每个模块为什么这么设计。下面我按实际开发顺序把关键决策和踩过的坑一个个拆开说。2. 技术选型与项目骨架为什么用 SpringBoot Vue 而不是别的2.1 后端选 SpringBoot 的三个现实理由毕业设计的时间窗口通常只有两三个月后端框架的选型直接决定了你能不能在截止日期前跑通核心业务。SpringBoot 最大的优势是自动配置和起步依赖你不需要像传统 SSM 那样写一堆 XML 配置文件。一个spring-boot-starter-web就能把 Tomcat、SpringMVC、Jackson 全部拉进来对于选课排课这种以 RESTful 接口为主的系统来说开发效率是最高的。另一个容易被忽视的点是生态成熟度。选课排课系统绕不开权限控制、事务管理、分页查询这几个需求SpringBoot 配合 Spring Security 或者 Shiro 做认证授权配合 MyBatis-Plus 做数据访问几乎不需要自己造轮子。我在实际开发中一般会选 MyBatis-Plus 而不是 JPA原因是排课查询经常需要多表联查和动态条件拼接MyBatis-Plus 的 QueryWrapper 写起来更直观生成的 SQL 也更容易调试。第三个理由是社区资料丰富。毕业设计最怕遇到问题搜不到答案SpringBoot 的中文资料量级足够大你遇到的 90% 的报错都能找到现成的解决方案。但要注意不要盲目复制网上的代码尤其是版本不一致的时候SpringBoot 2.x 和 3.x 的依赖坐标差异很大选定了版本就要保持一致。2.2 前端选 Vue 而不是 React 或 Thymeleaf 的考量前端选 Vue 的核心原因是学习曲线平缓。对于大多数只学过 HTML、CSS、JavaScript 的同学来说Vue 的模板语法和指令系统几乎可以无缝上手。选课排课系统的前端交互主要集中在表格展示、表单提交、弹窗确认这几个场景Vue 配合 Element Plus 组件库能快速搭出可用的管理界面。不选 Thymeleaf 这类服务端渲染方案的原因是前后端分离更符合当前企业开发的实际情况。毕业设计如果只写个 Thymeleaf 页面答辩时容易被问到“前后端怎么交互”这种问题而前后端分离的项目天然就有清晰的接口边界答辩时也更好展示技术栈的完整性。Vue 3 和 Vue 2 的选择上我建议用 Vue 3 Vite 的组合。Vite 的冷启动速度比 Webpack 快很多开发体验更好。但要注意Element Plus 是配合 Vue 3 的Element UI 是配合 Vue 2 的不要混用。下面是一个最小化的项目结构参考# 后端项目结构SpringBoot course-scheduling-backend/ ├── src/main/java/com/example/scheduling/ │ ├── controller/ # 接口层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # 数据访问层 │ ├── entity/ # 数据库实体 │ ├── config/ # 配置类跨域、拦截器 │ └── common/ # 统一返回结果、异常处理 ├── src/main/resources/ │ ├── application.yml # 主配置文件 │ └── mapper/ # MyBatis XML如果不用注解 └── pom.xml # 前端项目结构Vue 3 Vite course-scheduling-frontend/ ├── src/ │ ├── api/ # 接口封装 │ ├── views/ # 页面组件 │ ├── router/ # 路由配置 │ ├── store/ # 状态管理Pinia │ └── utils/ # 请求拦截器、工具函数 ├── vite.config.js └── package.json这个结构不是唯一的但它的好处是职责清晰。controller 只负责接收参数和返回结果service 写业务逻辑mapper 只管数据库操作。很多同学把业务逻辑写在 controller 里后期改需求时牵一发而动全身这个坑一定要避开。2.3 数据库选 MySQL 8 的版本注意点MySQL 8 和 MySQL 5.7 在选课排课系统里最明显的差异是窗口函数和 CTE公用表表达式。排课算法中经常需要按教师、班级、教室分组后取最大时间戳或者计算排名MySQL 8 的ROW_NUMBER()和RANK()能省掉很多子查询。另外 MySQL 8 默认字符集是 utf8mb4支持 emoji 存储虽然选课系统不一定用得上但避免乱码问题总是好的。建库时要注意排序规则用utf8mb4_general_ci还是utf8mb4_0900_ai_ci。后者是 MySQL 8 的默认值对中文排序更准确但如果你从 5.7 迁移过来可能会遇到排序结果不一致的情况。毕业设计里我一般直接用默认值减少不必要的麻烦。3. 数据库设计选课排课系统的表结构与约束怎么定3.1 核心表清单与字段设计选课排课系统的数据库设计是整个项目的地基表结构没定好后面写业务逻辑会非常痛苦。下面这张表列出了最核心的几张表及其关键字段实际开发中可以根据需求增减。表名作用关键字段约束说明student学生信息id, student_no, name, class_idstudent_no 唯一索引teacher教师信息id, teacher_no, nameteacher_no 唯一索引course课程信息id, course_no, name, credit, capacity, teacher_idcourse_no 唯一索引capacity 默认 0classroom教室信息id, room_no, capacity, buildingroom_no 唯一索引course_schedule排课记录id, course_id, classroom_id, day_of_week, start_section, end_section联合唯一索引防冲突course_selection选课记录id, student_id, course_id, select_time, statusstudent_id course_id 联合唯一class_info班级信息id, class_name, gradeclass_name 唯一索引这张表里最容易被忽略的是course_schedule的联合唯一索引。排课的核心约束是“同一教室、同一天、同一时间段不能有两门课”这个约束如果不在数据库层面加唯一索引只靠代码判断并发情况下一定会出问题。我一般会建一个UNIQUE KEY uk_room_time (classroom_id, day_of_week, start_section, end_section)让数据库来兜底。course_selection表的status字段用来标记选课状态比如 1 表示已选、2 表示已退、3 表示候补。很多同学只用一个布尔字段表示是否选中后期想加候补功能时就得改表结构不如一开始就留好扩展空间。3.2 时间冲突检测的 SQL 实现选课和排课都绕不开时间冲突检测。学生选课时要判断新选的课是否和已选课程时间重叠排课时要判断教室和教师是否被占用。下面这段 SQL 是检测某个学生在指定时间段内是否已有课程-- 检测学生是否在指定时间段已有课程 -- 参数studentId, dayOfWeek, startSection, endSection SELECT COUNT(1) FROM course_selection cs JOIN course_schedule sch ON cs.course_id sch.course_id WHERE cs.student_id #{studentId} AND cs.status 1 AND sch.day_of_week #{dayOfWeek} AND sch.start_section #{endSection} AND sch.end_section #{startSection};这段 SQL 的逻辑是两段时间重叠的条件是“新课程开始节次小于等于已有课程结束节次且新课程结束节次大于等于已有课程开始节次”。如果返回的 COUNT 大于 0说明存在冲突。参数说明studentId是学生 IDdayOfWeek是星期几1-7startSection和endSection是节次范围。注意status 1只统计已选中的课程退选的课程不参与冲突检测。排课时的教室冲突检测逻辑类似只需要把student_id换成classroom_id并且去掉course_selection的关联直接查course_schedule表即可。这里有个细节如果排课记录允许修改更新时要把当前记录排除掉否则会自己和自己冲突。3.3 外键约束要不要加毕业设计里关于外键的争论一直存在。我的建议是不加物理外键用代码保证引用完整性。原因是物理外键在批量插入和删除时会带来额外的锁开销而且很多同学在测试阶段手动改数据外键约束会导致操作失败增加调试难度。但要在文档里写清楚表之间的逻辑关系答辩时老师如果问到你能说出“逻辑外键”这个概念就行。如果一定要加外键注意级联删除的配置。比如删除课程时关联的排课记录和选课记录应该怎么处理ON DELETE CASCADE会直接删掉关联数据ON DELETE RESTRICT会阻止删除。选课系统里我倾向于用 RESTRICT因为课程删除应该是一个软删除操作直接物理删除会导致历史数据丢失。4. 核心业务实现选课并发与排课算法的代码落地4.1 选课接口的并发控制方案选课系统最典型的场景是一门课容量 50 人开放选课的一瞬间有 200 个请求同时进来。如果不做并发控制数据库里会出现选课人数超过容量的情况。常见的解决方案有三种数据库悲观锁、Redis 分布式锁、乐观锁版本号。毕业设计里我推荐用乐观锁 数据库唯一索引的组合实现简单且不容易出错。乐观锁的实现方式是在course表加一个version字段每次更新容量时检查版本号// CourseServiceImpl.java 选课核心逻辑 Transactional(rollbackFor Exception.class) public Result? selectCourse(Long studentId, Long courseId) { // 1. 查询课程信息带版本号 Course course courseMapper.selectById(courseId); if (course null) { return Result.fail(课程不存在); } // 2. 检查容量 if (course.getSelectedCount() course.getCapacity()) { return Result.fail(课程已满); } // 3. 检查时间冲突调用 3.2 节的 SQL if (hasTimeConflict(studentId, courseId)) { return Result.fail(时间冲突); } // 4. 乐观锁更新已选人数 int updated courseMapper.increaseSelectedCount(courseId, course.getVersion()); if (updated 0) { return Result.fail(选课失败请重试); } // 5. 插入选课记录依赖唯一索引兜底 try { courseSelectionMapper.insert(new CourseSelection(studentId, courseId)); } catch (DuplicateKeyException e) { throw new BusinessException(请勿重复选课); } return Result.success(选课成功); }对应的 Mapper 方法!-- CourseMapper.xml 乐观锁更新 -- update idincreaseSelectedCount UPDATE course SET selected_count selected_count 1, version version 1 WHERE id #{courseId} AND version #{version} AND selected_count lt; capacity /update这段代码的关键点有三个第一WHERE条件里带了version和selected_count capacity保证并发时只有一个请求能更新成功第二course_selection表的student_id course_id联合唯一索引防止重复选课第三整个方法加了Transactional注解任何一步失败都会回滚。参数说明studentId和courseId由前端传入version从查询结果中获取。如果更新返回 0说明版本号已被其他请求修改提示用户重试即可。4.2 排课算法的简化实现排课算法是这类系统的难点完整的排课需要考虑教师时间、教室容量、班级课表、课程优先级等多个维度。毕业设计里不需要做到工业级排课但至少要能自动生成一份无冲突的课表。我一般用贪心算法 回溯的思路按课程优先级排序依次为每门课寻找可用的时间段和教室如果找不到就回溯调整。下面是一个简化版的排课核心逻辑// ScheduleServiceImpl.java 自动排课 public void autoSchedule(Long semesterId) { // 1. 获取本学期所有待排课程按优先级排序 ListCourse courses courseMapper.selectPendingCourses(semesterId); courses.sort(Comparator.comparingInt(Course::getPriority).reversed()); // 2. 获取所有可用时间段周一至周五每天 5 个时间段 ListTimeSlot timeSlots generateTimeSlots(); // 3. 获取所有教室 ListClassroom classrooms classroomMapper.selectAll(); // 4. 贪心分配 for (Course course : courses) { boolean assigned false; for (TimeSlot slot : timeSlots) { for (Classroom room : classrooms) { if (room.getCapacity() course.getCapacity()) { continue; // 教室容量不足 } if (isRoomAvailable(room.getId(), slot) isTeacherAvailable(course.getTeacherId(), slot) isClassAvailable(course.getClassId(), slot)) { // 插入排课记录 scheduleMapper.insert(new CourseSchedule( course.getId(), room.getId(), slot.getDayOfWeek(), slot.getStartSection(), slot.getEndSection() )); assigned true; break; } } if (assigned) break; } if (!assigned) { // 记录排课失败的课程后续人工干预 log.warn(课程 {} 排课失败需要手动调整, course.getName()); } } }这段代码的逻辑说明generateTimeSlots()生成所有可选时间段比如周一第 1-2 节、周一第 3-4 节等。isRoomAvailable、isTeacherAvailable、isClassAvailable分别检查教室、教师、班级在该时间段是否已被占用。参数说明semesterId是学期 IDpriority是课程优先级数值越大越优先capacity是课程容量。这个算法不保证全局最优但能生成一份可用的课表剩下的冲突由人工在界面上调整。4.3 前端选课页面的交互细节前端选课页面有两个容易翻车的地方一是选课按钮的防重复点击二是选课成功后的数据刷新。防重复点击可以用loading状态控制// SelectCourse.vue 选课按钮逻辑 const handleSelect async (course) { if (course.loading) return; // 防止重复点击 course.loading true; try { const res await selectCourseApi({ courseId: course.id }); if (res.code 200) { ElMessage.success(选课成功); await fetchCourseList(); // 刷新列表 } else { ElMessage.error(res.message); } } finally { course.loading false; } };参数说明course.loading是每个课程项独立的加载状态不要用全局的 loading否则一个课程在请求中会导致所有按钮都禁用。fetchCourseList()重新拉取课程列表保证已选人数是最新的。这里有个细节如果选课请求返回“课程已满”前端应该把该课程的按钮置灰而不是等用户再点一次才提示。5. 避坑与排查选课排课系统最常见的五个问题5.1 跨域问题导致前端请求全部 404现象前端启动后调用后端接口浏览器控制台报Access-Control-Allow-Origin错误Network 面板显示请求被拦截。原因前后端分离项目里前端运行在localhost:5173后端运行在localhost:8080端口不同就构成跨域。SpringBoot 默认不允许跨域请求。解决在后端加一个全局跨域配置类不要在每个 Controller 上单独加CrossOrigin那样容易漏掉。配置如下Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) // 允许所有来源 .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns和allowedOrigins的区别当allowCredentials(true)时不能用allowedOrigins(*)必须用allowedOriginPatterns否则 SpringBoot 会报错。5.2 选课人数超卖但数据库没有报错现象课程容量 50但选课结束后发现已选人数是 53数据库里也没有唯一索引冲突的报错。原因并发请求同时读到selected_count 49都判断“未满”然后都执行了更新。如果更新语句没有加selected_count capacity条件就会超卖。解决检查increaseSelectedCount的 SQL确保WHERE子句里有selected_count capacity。另外course_selection表的联合唯一索引必须存在防止同一个学生重复选同一门课。如果用的是 MyBatis-Plus 的updateById它不会带容量条件必须手写 SQL。5.3 排课结果出现教师时间冲突现象自动排课完成后某个教师在周一第 1-2 节同时有两门课。原因isTeacherAvailable方法在判断时只查了course_schedule表但排课过程中新插入的记录可能还没有提交事务导致查询不到。解决把自动排课方法加上Transactional注解保证插入和查询在同一个事务里。另外isTeacherAvailable的查询条件要包含teacher_id、day_of_week、start_section、end_section四个字段缺一不可。如果还是冲突检查一下course_schedule表有没有脏数据先清空再重新排。5.4 前端表格分页参数传错导致数据重复现象课程列表第一页和第二页显示的数据有重复或者翻页后数据不变。原因Element Plus 的el-pagination组件默认页码从 1 开始但后端 MyBatis-Plus 的Page对象当前页也是从 1 开始如果前端传了pageNum 0就会导致查询结果异常。解决在前端请求拦截器里统一处理分页参数确保pageNum最小值为 1。后端接收参数时用RequestParam(defaultValue 1)设置默认值。另外分页查询的 SQL 要加ORDER BY id DESC否则 MySQL 返回的顺序不稳定翻页时会出现数据跳动。5.5 部署到服务器后接口 500 但本地正常现象本地开发环境一切正常打包部署到服务器后部分接口返回 500日志显示Table xxx doesnt exist。原因服务器上的数据库没有导入完整的 SQL 脚本或者数据库名和application.yml里配置的不一致。还有一种可能是 MySQL 版本不同本地是 8.0服务器是 5.7某些语法不兼容。解决部署前先确认服务器 MySQL 版本用SELECT VERSION();查看。导入 SQL 脚本时注意字符集用source /path/to/script.sql命令导入不要直接复制粘贴。application.yml里的url要加上useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai避免时区和编码问题。6. 交付物整理与答辩演示的实用技巧6.1 源码、SQL 脚本、文档、PPT 的整理规范毕业设计的交付物不只是代码能跑就行文档和 PPT 的质量直接影响答辩分数。源码目录里建议加一个README.md写清楚项目启动步骤、依赖版本、数据库配置。SQL 脚本要分成两个文件schema.sql建表语句和data.sql初始数据。初始数据里至少要有管理员账号、几个测试学生和教师、几门示例课程方便答辩时直接演示。文档部分我一般会准备三份需求分析文档、数据库设计文档、接口文档。接口文档可以用 Swagger 自动生成省去手写的麻烦。PPT 控制在 15 页以内重点讲清楚技术选型理由、数据库设计思路、核心业务实现、遇到的难点和解决方案。不要堆砌代码截图用流程图和表格代替。6.2 答辩演示的流程设计答辩演示最怕现场翻车所以提前走一遍完整流程很有必要。我一般会按这个顺序演示先登录管理员账号展示课程管理、教师管理、教室管理然后切换到学生账号演示选课流程重点展示时间冲突检测和容量已满的提示最后回到管理员账号执行自动排课展示排课结果和手动调整功能。演示前要准备一个“后悔药”方案如果自动排课结果不理想提前手动排好一份课表演示时直接展示手动调整后的结果。另外数据库里的测试数据要提前准备好不要现场录入避免手忙脚乱。6.3 一个容易被忽略的细节接口返回的统一格式很多同学写接口时有的返回{ code: 200, data: ... }有的直接返回对象前端处理起来很麻烦。建议定义一个统一的返回结果类public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT fail(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }前端在 axios 响应拦截器里统一判断code如果是 200 就返回data否则弹出错误提示。这样业务代码里就不用每次都写if (res.code 200)减少重复代码。6.4 最后说一个血泪教训做毕业设计最忌讳的是前期不着急后期通宵赶。选课排课系统的业务逻辑比看起来复杂尤其是并发控制和排课算法调试起来很花时间。我的习惯是先把数据库设计和接口定义好用 Postman 把后端接口全部测通再写前端页面。这样前后端联调时问题基本都出在前端排查范围小很多。另外版本控制一定要用 Git每天提交一次。我见过太多同学因为改错一个文件导致整个项目跑不起来又没有备份最后只能重写。Git 的commit信息写清楚改了什么比如“完成选课接口并发控制”比“修改代码”有用得多。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询