Spring Boot学生选课系统毕设全解析:并发、事务与防超选实战
发布时间:2026/9/26 14:47:59来源:尧图网络
简介基于Java与SpringBoot开发的学生网上选课系统毕业设计源码包适合计算机相关专业学生用于毕设参考也适合希望快速搭建后台管理系统的开发者。系统采用SpringBoot加Vue.js的前后端分离架构完整实现教室管理、教师管理、课程管理、教学计划、选课管理、成绩管理、学生管理以及角色权限控制等核心模块并提供了清晰的分层代码与可直接运行的工程环境。压缩包内含417个文件其中有109个Java源文件、56个Vue组件和17个JavaScript文件同时还包含SVG图标、PNG/JPG图片、SQL数据库脚本、启动与构建批处理、说明文档及演示视频等整体大小约22.15MB便于快速部署和二次开发。目前已有114人学习浏览对于想掌握SpringBoot和Vue全栈实践、理解选课业务流程并高效完成毕业设计的读者它能提供直接运行的完整工程、配套说明文档和数据库初始化脚本是一份值得参考的实战资料。1. 这个毕设题目的真实分量实验室里最典型的翻车现场是选课页面很流畅点确定选课也提示成功后台一查120人的课被选了150人。这个基于javaspringboot的学生网上选课系统表面是一堆增删改查真正的内核却藏在“同一时间很多人抢同一门课”这一下子里。标题里那个源码.zip是给你省时间的但答辩老师不只看代码能不能跑更看你能不能把“剩最后一个名额时两个学生同时点了选课”这件事讲圆。适合刚学完Java和MySQL、准备交课程设计或毕设的在校生也适合想拿一个完整项目练手、搞清楚Spring Boot分层和事务怎么写的人。这篇文章从标题往下拆领域模型怎么建、项目怎么启动、选课并发怎么处理、演示时有哪些坑。目的是让你拿到zip后一个下午跑通并且每个被问到的地方都能接得住。2. 从标题拆需求三张核心表如何撑起选课业务2.1 学生、课程、选课记录三张表的字段设计怎么互相咬合网上选课系统不管页面做成什么样底层都是三个核心对象在转学生、课程、选课记录。学生和课程之间是多对多关系但如果只做两张表加一个中间关联选课时间、退课状态这些业务字段就无处安放。常见做法是单独建一张course_selection表把每一次选课行为本身当作一条业务记录。-- 学生表登录和权限判断都从这里取 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, student_name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) COMMENT 班级, major VARCHAR(50) COMMENT 专业, password VARCHAR(100) NOT NULL DEFAULT 123456 COMMENT 登录密码 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表容量和已选人数放在一起后续并发控制全靠它 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名, teacher VARCHAR(50) DEFAULT NULL COMMENT 授课教师, credit DECIMAL(3,1) DEFAULT 0 COMMENT 学分, course_time VARCHAR(100) DEFAULT NULL COMMENT 上课时间如周一3-4节, classroom VARCHAR(50) DEFAULT NULL COMMENT 教室, capacity INT NOT NULL DEFAULT 60 COMMENT 课程容量, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, begin_time DATETIME DEFAULT NULL COMMENT 开放选课时间, end_time DATETIME DEFAULT NULL COMMENT 截止选课时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表学生和课程的关联唯一索引挡住重复选课 CREATE TABLE course_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0已退课, UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里最容易犯的错误是把选课记录直接写成两个字段的关联表后面想记录退课、想统计选课时间就全要改表。单独拆出来之后status字段可以让退课不删记录保留完整操作历史答辩时这一句就能解释为什么不做成简单的多对多。course表里的selected_count是冗余字段严格来说可以用count(*)替代。保留它的核心原因是让“选课”这个动作能通过一条UPDATE原子地完成名额扣减而不是先查再判再写。第4章会专门展开这个并发问题。字段命名建议统一用下划线配合 MyBatis 的驼峰映射Java 实体里写selectedCount就能直接对应省掉一堆resultMap。2.2 技术选型为什么站在Spring Boot这一边这个标题把 Spring Boot 写在了最前面说明技术栈已经是定死的方向但很多同学拿到源码后并不知道为什么是它。对比十年前毕设里常见的 SSH 组合Struts2 Spring HibernateSpring Boot 最大的优势是让“跑起来”的成本趋近于零内嵌 Tomcat、starter 依赖、自动配置java -jar一条命令就能启动。选课系统并不复杂用不用微服务、分布式那一套都是加分项而不是必需品。真正需要动脑的是数据库操作。为什么不选 Hibernate因为选课扣名额这件事要精准控制 SQL 的条件和受影响行数比如UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity。这种语句在 MyBatis 的 mapper.xml 里写出来一眼能看懂出了并发问题也容易排查Hibernate 虽然也能做但隔离级别、缓存、flush 时机这些概念对毕设来说学成本太高。前端这里有个分岔用 Thymeleaf 做服务端渲染还是用 Vue 做前后端分离。我的建议是如果目标是“尽快跑通、稳过答辩”选 Thymeleaf 更划算。控制器里return course/list就直接对应templates/course/list.html不用配跨域、不用单独部署前端、不用准备两份启动说明。前后端分离在这个体量下带来的工程复杂度远大于收益而且答辩现场最怕的是前端静态文件路径错了一个白屏半天找不出原因。2.3 Maven目录结构解压zip后怎么一眼找对包拿到 zip 解压后先别急着用 IDEA 打开看一眼目录结构心里就有数了。常见的 Maven 项目长这样course-selection/ ├── pom.xml ├── sql/ │ └── course.sql └── src/main/ ├── java/com/example/course/ │ ├── CourseApplication.java # Spring Boot 启动类 │ ├── common/ # 统一返回结果、异常处理 │ ├── config/ # 拦截器、WebMvc 配置 │ ├── controller/ # 页面跳转与 JSON 接口 │ ├── service/ # 业务逻辑 │ ├── mapper/ # MyBatis 接口 │ └── entity/ # 表对应实体类 └── resources/ ├── application.yml # 端口、数据库、MyBatis 配置 ├── mapper/ # MyBatis XML 文件 └── templates/ # Thymeleaf 页面重点看三样东西application.yml、sql目录、mapper目录。application.yml决定端口和数据库连接能不能对上sql目录里是初始化脚本决定了库表结构从哪来mapper目录里的 XML 决定了选课、退课这些关键 SQL 长什么样。只要这三处看懂这个项目你已经掌握一半。common包里通常有一个Result类统一封装code / message / data。毕设里这个类被问到的频率很高因为它能说明你有没有“接口返回规范”的意识。controller里我习惯把页面跳转和接口分开比如/page/courseList返回视图/api/course/list返回 JSON职责清晰面试问到你也能讲出理由。3. 把zip跑通Spring Boot选课系统的最小启动路径3.1 环境版本对齐JDK8、Maven、MySQL怎么配不翻车拿到源码后第一步不是双击跑而是把环境固定住。Spring Boot 2.x 搭配 JDK 8 是最稳的组合很多毕设源码里用了javax.*包路径和 JDK 8 语法直接上 JDK 17 会因为包变更编译报错。如果电脑上已经装了多个 JDK在 IDEA 里把 Project SDK 和 Module SDK 都指到同一个 1.8Maven 的 JRE 设置里也选 1.8三处统一才能避免“本地能跑换个电脑就炸”的尴尬。工具推荐版本说明JDK1.8Spring Boot 2.x 兼容性最好毕设代码大多按 JDK8 写Maven3.6.x与 spring-boot-starter-parent 配合稳定MySQL5.7 或 8.05.7 坑更少8.0 需要在连接串里指定时区IDEIntelliJ IDEA自带 Maven 和 Git 集成开箱即用Maven 依赖下载慢是国内环境的老问题我一般会在settings.xml里配一个阿里云镜像把中央仓库替换掉。这一步不是玄学很多“启动就报找不到类”的问题根因都是依赖下载失败了一半。配完镜像后重新reimport一次看右边的 Maven 面板里依赖有没有红色波浪线就能判断是不是下载出了问题。3.2 导入数据库sql脚本和初始账号密码数据库导入是启动前最关键的一步。zip 里一般会有sql/course.sql或db/目录里面是建库建表脚本和数据初始化语句。我习惯先手动建库再往库里导数据这样字符集可以按自己的意愿设成utf8mb4避免后面中文乱码。mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS course_select DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p course_select 项目目录/sql/course.sql第二条命令里的course_select是目标库名符号把 sql 文件内容重定向到 MySQL 客户端执行。如果项目里给的建库语句用的是CREATE DATABASE IF NOT EXISTS可以直接不建库执行文件。执行完用SHOW TABLES;看一眼应该能看到student、course、course_selection三张表再查一下student表里的初始账号通常是admin或某个学号密码基本都是123456。注意course.sql里的INSERT INTO student语句可能会用明文密码也可能会用 MD5 加密后的密文。如果是密文登录逻辑里就要有对应的MD5加密校验强行把密文改成明文反而会导致登录失败。判断方法很简单看到字符串是e10adc3949ba59abbe56e057f20f883e这种 32 位串就是 MD5 后的123456不需要动它。3.3 application.yml里的5个关键配置项把所有配置集中在一个文件里是 Spring Boot 的优势但这个文件也是启动报错的高发区。以下配置基本是选课系统的标准形态逐个参数过一遍就懂server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_select?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.course.entity configuration: map-underscore-to-camel-case: trueurl后半段的参数每一个都有用处useUnicodetruecharacterEncodingutf8管住中文不乱码serverTimezoneAsia/Shanghai解决 MySQL 8 的时区报错useSSLfalse是为了省去本地连接的证书校验。driver-class-name用com.mysql.cj.jdbc.Driver对应 MySQL 8如果本机是 MySQL 5.7 且驱动版本老就改回com.mysql.jdbc.Driver。thymeleaf.cachefalse在开发时一定要开这样修改 HTML 模板后刷新页面就能看到效果不用重启应用。map-underscore-to-camel-case让student_no自动映射到 Java 实体的studentNo字段这也是为什么建表用下划线命名、Java 用驼峰命名两者能无缝接上的核心配置。密码、端口、数据库名这三处是每个拿到源码的人都会改的地方。改完别急着启动先在 IDEA 的 Database 面板里用同样的账号密码连一下库能连通再启动 Spring Boot能把问题定位到“配置错了”而不是“代码错了”。3.4 启动命令与浏览器里的第一条选课路径环境配好后启动方式有两种效果等价mvn spring-boot:run或先打 jar 包再运行mvn clean package -DskipTests java -jar target/course-0.0.1-SNAPSHOT.jar第一种适合开发调试日志直接打在控制台第二种适合答辩前确认产物可用。日志里出现Tomcat started on port(s): 8080就代表起来了。如果 8080 被占用改application.yml里的server.port或者启动命令加--server.port8081。然后浏览器访问http://localhost:8080/login。我习惯先执行一条核心业务路径学生登录 → 查看课程列表 → 选课 → 查看已选课程 → 退课。这条路径能覆盖登录拦截、列表查询、事务写入、状态更新四类代码任何一步报 500 都能快速定位是 Controller、Service 还是 Mapper 层的问题。4. 选课系统最常见的5个坑从超选到中文乱码4.1 超选同一个名额被两个学生同时抢到现象两个浏览器同时登录不同账号最后一门课只剩一个名额两个账号都提示选课成功course表里selected_count变成超出capacity的值。原因典型的“先查询再更新”竞态。代码先select selected_count from course判断是否小于容量再执行update这两步之间有间隙第二个请求读到的是旧数据。而 jdbc 默认的REPEATABLE_READ隔离级别下行锁只在update时才生效查询不加锁自然拦不住。解决把“判断容量”和“扣减名额”合并进同一条update语句靠数据库行锁保证原子性update idincreaseSelectedCount UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count lt; capacity /updateTransactional(rollbackFor Exception.class) public void selectCourse(Long studentId, Long courseId) { // 时间窗口校验、重复选课校验在此省略 int rows courseMapper.increaseSelectedCount(courseId); if (rows 0) { throw new RuntimeException(课程已满选课失败); } selectionMapper.insertSelection(studentId, courseId); }rows 0说明没有任何一行满足“当前人数小于容量”的条件也就是名额已经没了直接抛异常回滚。注意 XML 里小于号必须写成lt;不然 XML 解析直接报错。这也是 java 面试八股文里常问的“乐观锁与悲观锁”最落地的一个例子答辩时能主动讲出这一步比堆十个功能都管用。4.2 Transactional失效同一个Service里写自调用就开始玄学现象把选课逻辑写在同一个类里类内部this.selectCourse()调用另一个方法事务好像失效插入了一条脏数据却没回滚。原因Spring 事务默认通过 AOP 动态代理实现代理只对“外部调用”生效。类内部用this调用目标方法绕过了代理对象事务注解被直接忽略。这个坑在毕设项目里特别常见因为大家习惯把选课和退课塞进同一个 Service 类方法互相调用。解决最省事的方案是改用TransactionTemplate编程式事务不依赖代理输在明处Service public class SelectionService { Autowired private TransactionTemplate transactionTemplate; public void selectCourse(Long studentId, Long courseId) { Integer result transactionTemplate.execute(status - { int rows courseMapper.increaseSelectedCount(courseId); if (rows 0) { status.setRollbackOnly(); return 0; } selectionMapper.insertSelection(studentId, courseId); return 1; }); if (result null || result 0) { throw new RuntimeException(课程已满选课失败); } } }setRollbackOnly()标记当前事务只能回滚返回 0 给外层由外层决定抛什么异常。这样即使以后有人重构代码也不会因为类内自调用再次踩坑。另一个常见病根是异常被吞掉或者被转换成自定义异常但没有rollbackFor指定Transactional默认只在遇到RuntimeException和Error时才回滚检查异常不会触发回滚所以rollbackFor Exception.class是一个保险写法。4.3 改前端时间就能提前选课现象通过浏览器开发者工具把“选课按钮”的 disabled 属性删掉或者把页面上的日期 input 改到开放时间之前居然能选课成功。原因前端的时间控制只是用户体验不是安全边界。如果后端在接收请求时不重新校验当前时间是否在begin_time和end_time之间就等于把“什么时候能选课”的决定权交给了浏览器。解决在 Service 层选课前查一次时间窗口用数据库时间而不是客户端传的时间select idcountOpenCourse resultTypeint SELECT COUNT(*) FROM course WHERE id #{courseId} AND begin_time lt; NOW() AND end_time gt; NOW() /selectif (courseMapper.countOpenCourse(courseId) 0) { throw new RuntimeException(当前不在选课时间窗口内); }把NOW()放在 SQL 里是因为它取的是数据库服务器时间比取System.currentTimeMillis()更权威也不会因为应用服务器和数据库服务器时钟不一致而误判。这一点在 springboot 面试题里经常作为“后端校验不能省”的例子被问到。4.4 MySQL 8的时区与驱动差异现象启动时报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者数据库里存的时间比北京时间慢了 8 小时。原因MySQL 8 的 JDBC 驱动默认要求连接串里指定时区而系统时区是中文描述驱动解析不了。时间慢 8 小时的则是服务器时区设成了 UTC存进去的是 UTC 时间页面展示成了本地时间。解决统一在 JDBC 连接串里加serverTimezoneAsia/Shanghai并按 MySQL 版本选择驱动类名。MySQL 8 用com.mysql.cj.jdbc.DriverMySQL 5.7 及配套老驱动用com.mysql.jdbc.Driver混用会出现ClassNotFoundException。如果数据库已经写入了乱掉的时间别在代码里强行加 8 小时先把现有数据修正再改连接串否则会出现双重补偿。4.5 页面和数据库的双向中文乱码现象添加课程时填写中文课程名保存后页面显示问号或者数据库里是正常中文页面渲染出来却是乱码。原因乱码很少只有一个来源常见的组合是建库时字符集不是utf8mb4、JDBC 连接串没有characterEncodingutf8、HTML 页面没有声明UTF-8。三个环节断掉任何一个都会在某个方向出现乱码。解决按三个位置逐项排查。第一步确认建库语句里带DEFAULT CHARACTER SET utf8mb4已经建好的库用ALTER DATABASE course_select CHARACTER SET utf8mb4;补救第二步确认application.yml的 url 里有useUnicodetruecharacterEncodingutf8第三步确认 Thymeleaf 模板文件保存时就是 UTF-8 编码并且页面里有meta charsetUTF-8。注意 IDEA 默认文件编码可能不是 UTF-8右下角把 file encoding 统一改成 UTF-8这个配置能省掉一半乱码问题。5. 答辩前按这条清单过一遍从固定路径到加分改造5.1 现场演示先走这段固定路径答辩现场时间紧、容易慌别临时发挥。我习惯把演示路径固定成一条“闭环”步骤操作预期结果1管理员登录添加一门容量为2的课程课程出现在列表2学生账号A登录选这门课已选人数显示13学生账号B登录选同一门课已选人数显示24学生账号C登录选同一门课提示课程已满5账号A退课已选人数回到16账号C再选一次选课成功这条路径把所有核心功能都串起来了尤其是第4步直接把你学到的并发控制讲成“防止超选”的现场证据。数据库要提前恢复初始状态别用已经选满的库演示。5.2 加分改造一把裸update升级成乐观锁版本号如果老师追问“selected_count capacity这种方式缺口在哪”可以提版本号方案。给course表加一个version字段每次更新带上旧版本号ALTER TABLE course ADD COLUMN version INT NOT NULL DEFAULT 0;update idselectCourseWithVersion UPDATE course SET selected_count selected_count 1, version version 1 WHERE id #{courseId} AND selected_count lt; capacity AND version #{version} /updateJava 侧先查出version更新时把查到的值作为条件传回去受影响行数为 0 就重试。这个改造能说明你理解 ABA 问题和乐观锁比单纯背概念更有说服力。基础版本不需要真改把 SQL 和原理讲清楚就够加分。5.3 加分改造二用EasyExcel导出选课名单“导出功能”是选课系统里性价比最高的一项加分。用 EasyExcel 比 POI 手写快得多String fileName 选课名单.xlsx; EasyExcel.write(fileName) .sheet(选课名单) .head(SelectionExcelVO.class) .doWrite(list);POI 写一个导出要手动创建 Workbook、Row、Cell 循环赋值EasyExcel 一个类注解加一行doWrite就完成。我毕业那年就是因为只做了基础 CRUD答辩被问到“这个系统还能扩展什么”时没答好后来才懂老师说到底是看你对业务和工程的思考不在乎你用了多少框架。现在每拿到一个 Spring Boot 项目我第一件事永远是看 SQL 和事务的边界条件代码能跑只是及格线边界稳才是真正能拿去交差的东西。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网