简介:基于Java与SpringBoot开发的学生网上选课系统毕业设计源码包,适合计算机相关专业学生用于毕设参考,也适合希望快速搭建后台管理系统的开发者。系统采用SpringBoot加Vue.js的前后端分离架构,完整实现教室管理、教师管理、课程管理、教学计划、选课管理、成绩管理、学生管理以及角色权限控制等核心模块,并提供了清晰的分层代码与可直接运行的工程环境。压缩包内含417个文件,其中有109个Java源文件、56个Vue组件和17个JavaScript文件,同时还包含SVG图标、PNG/JPG图片、SQL数据库脚本、启动与构建批处理、说明文档及演示视频等,整体大小约22.15MB,便于快速部署和二次开发。目前已有114人学习浏览,对于想掌握SpringBoot和Vue全栈实践、理解选课业务流程并高效完成毕业设计的读者,它能提供直接运行的完整工程、配套说明文档和数据库初始化脚本,是一份值得参考的实战资料。
1. 这个毕设题目的真实分量
实验室里最典型的翻车现场是:选课页面很流畅,点确定选课也提示成功,后台一查,120人的课被选了150人。这个基于java+springboot的学生网上选课系统,表面是一堆增删改查,真正的内核却藏在“同一时间很多人抢同一门课”这一下子里。标题里那个源码.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 '登录密码' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表:容量和已选人数放在一起,后续并发控制全靠它 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 '截止选课时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选课记录表:学生和课程的关联,唯一索引挡住重复选课 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里最容易犯的错误是把选课记录直接写成两个字段的关联表,后面想记录退课、想统计选课时间就全要改表。单独拆出来之后,status字段可以让退课不删记录,保留完整操作历史,答辩时这一句就能解释为什么不做成简单的多对多。
course表里的selected_count是冗余字段,严格来说可以用count(*)替代。保留它的核心原因是让“选课”这个动作能通过一条UPDATE原子地完成名额扣减,而不是先查再判再写。第4章会专门展开这个并发问题。字段命名建议统一用下划线,配合 MyBatis 的驼峰映射,Java 实体里写selectedCount就能直接对应,省掉一堆resultMap。
2.2 技术选型:为什么站在Spring Boot这一边
这个标题把 Spring Boot 写在了最前面,说明技术栈已经是定死的方向,但很多同学拿到源码后并不知道为什么是它。对比十年前毕设里常见的 SSH 组合(Struts2 + Spring + Hibernate),Spring 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.8,Maven 的 JRE 设置里也选 1.8,三处统一才能避免“本地能跑,换个电脑就炸”的尴尬。
| 工具 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | Spring Boot 2.x 兼容性最好,毕设代码大多按 JDK8 写 |
| Maven | 3.6.x | 与 spring-boot-starter-parent 配合稳定 |
| MySQL | 5.7 或 8.0 | 5.7 坑更少;8.0 需要在连接串里指定时区 |
| IDE | IntelliJ 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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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后半段的参数每一个都有用处:useUnicode=true&characterEncoding=utf8管住中文不乱码,serverTimezone=Asia/Shanghai解决 MySQL 8 的时区报错,useSSL=false是为了省去本地连接的证书校验。driver-class-name用com.mysql.cj.jdbc.Driver对应 MySQL 8,如果本机是 MySQL 5.7 且驱动版本老,就改回com.mysql.jdbc.Driver。
thymeleaf.cache=false在开发时一定要开,这样修改 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.port=8081。
然后浏览器访问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 id="increaseSelectedCount"> UPDATE course SET selected_count = selected_count + 1 WHERE id = #{courseId} AND selected_count < capacity </update>@Transactional(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 里小于号必须写成<,不然 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 id="countOpenCourse" resultType="int"> SELECT COUNT(*) FROM course WHERE id = #{courseId} AND begin_time <= NOW() AND end_time >= NOW() </select>if (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 连接串里加serverTimezone=Asia/Shanghai,并按 MySQL 版本选择驱动类名。MySQL 8 用com.mysql.cj.jdbc.Driver,MySQL 5.7 及配套老驱动用com.mysql.jdbc.Driver,混用会出现ClassNotFoundException。如果数据库已经写入了乱掉的时间,别在代码里强行加 8 小时,先把现有数据修正,再改连接串,否则会出现双重补偿。
4.5 页面和数据库的双向中文乱码
现象:添加课程时填写中文课程名,保存后页面显示问号;或者数据库里是正常中文,页面渲染出来却是乱码。
原因:乱码很少只有一个来源,常见的组合是建库时字符集不是utf8mb4、JDBC 连接串没有characterEncoding=utf8、HTML 页面没有声明UTF-8。三个环节断掉任何一个,都会在某个方向出现乱码。
解决:按三个位置逐项排查。第一步,确认建库语句里带DEFAULT CHARACTER SET utf8mb4,已经建好的库用ALTER DATABASE course_select CHARACTER SET utf8mb4;补救;第二步,确认application.yml的 url 里有useUnicode=true&characterEncoding=utf8;第三步,确认 Thymeleaf 模板文件保存时就是 UTF-8 编码,并且页面里有<meta charset="UTF-8">。注意 IDEA 默认文件编码可能不是 UTF-8,右下角把 file encoding 统一改成 UTF-8,这个配置能省掉一半乱码问题。
5. 答辩前按这条清单过一遍:从固定路径到加分改造
5.1 现场演示先走这段固定路径
答辩现场时间紧、容易慌,别临时发挥。我习惯把演示路径固定成一条“闭环”:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 管理员登录,添加一门容量为2的课程 | 课程出现在列表 |
| 2 | 学生账号A登录,选这门课 | 已选人数显示1 |
| 3 | 学生账号B登录,选同一门课 | 已选人数显示2 |
| 4 | 学生账号C登录,选同一门课 | 提示课程已满 |
| 5 | 账号A退课 | 已选人数回到1 |
| 6 | 账号C再选一次 | 选课成功 |
这条路径把所有核心功能都串起来了,尤其是第4步,直接把你学到的并发控制讲成“防止超选”的现场证据。数据库要提前恢复初始状态,别用已经选满的库演示。
5.2 加分改造一:把裸update升级成乐观锁版本号
如果老师追问“selected_count < capacity这种方式缺口在哪”,可以提版本号方案。给course表加一个version字段,每次更新带上旧版本号:
ALTER TABLE course ADD COLUMN version INT NOT NULL DEFAULT 0;<update id="selectCourseWithVersion"> UPDATE course SET selected_count = selected_count + 1, version = version + 1 WHERE id = #{courseId} AND selected_count < capacity AND version = #{version} </update>Java 侧先查出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 和事务的边界条件,代码能跑只是及格线,边界稳才是真正能拿去交差的东西。希望帮到你。
本文还有配套的精品资源,点击获取