☰
高校学生选课系统毕设实战:SpringBoot+Vue前后端分离与并发控制解析
2026/9/29 18:11:55 网站建设 项目流程

这份选题是Java Web方向里真正能打的“常青树”项目——高校学生选课系统。一个人口不到两千的学校,每学期选课几万条记录,涉及学生、教师、管理员三类角色,包含选课、退课、排课、成绩录入全流程,业务边界清晰,难度又刚好卡在本科毕设的黄金区间:既有CRUD以外的并发逻辑,又不至于让你陷入分布式、高可用的泥潭。

如果你正在为毕设选题发愁,或者已经拿到了这套源码但不知道怎么跑起来、怎么改造成自己的项目,这篇文章会把整个系统的骨架、数据库设计、接口逻辑、环境坑点拆开讲透。我尽量用“带过毕设的师兄”的视角,把文档里不会写的东西也补上。

1. 项目整体拆解:高校选课系统的核心业务闭环

很多人拿到源码第一件事就是启动、点点点、截图,这其实是最亏的打开方式。选课系统这种经典题目,真正的学习价值在于理解它的业务闭环是怎么通过代码落地的。你答辩的时候,老师最常问的一句话就是:“你这个系统的核心业务流程是什么?遇到并发怎么处理?”这两个问题,恰好就是这套系统的命门。

1.1 三个角色的权限边界

高校选课系统的用户模型非常经典,几乎可以作为所有“角色权限”类项目的模板。系统里一共有三种身份:

  • 学生:浏览课程、选课、退课、查看课表、查看成绩、个人信息维护
  • 教师:查看自己开设的课程、录入修改成绩、查看选课学生名单
  • 管理员:学生/教师账号管理、课程信息维护、选课时间窗口配置、系统数据统计

权限设计的核心是后端接口校验,而不是前端隐藏按钮。前端你随便用Vue改个菜单就能把管理员的入口露出来,真正的防线在SpringBoot的拦截器里。这套项目通常会用拦截器读取请求头里的token,解析出用户角色,再比对当前访问接口所需的权限。比SysUserRole表?这类毕设规模完全没有必要,一个role字段就够了,三种角色值:student、teacher、admin。

1.2 选课业务的核心冲突:时间窗口与容量控制

选课系统最关键的业务规则有两条:

第一,选课有时间窗口。管理员设置一个选课开放时间和截止时间,这个配置通常放在数据库一张配置表里,学生在窗口外不允许操作。为什么这个功能必须做?因为没有时间窗口控制,学生可以随便选课甚至半夜改课表,整个学期的教学安排就乱套了。

第二,课程有容量限制。一门课可能只收80人,选满了就不能再选。这个看起来简单的规则,恰恰是并发问题的根源。假如有100个学生同时抢一门只剩5个名额的课,如果代码只是查一下剩余名额、判断大于0就插入选课记录,那5个名额瞬间可以冒出8条甚至更多记录。

1.3 为什么这个题目是Java Web毕设的常青树

我自己带过几届毕业设计,老实说大部分题目都“活不过答辩”。旅游类项目没特色,图书管理类太单调,商城类又容易陷入支付方案的泥潭。选课系统之所以经久不衰,是因为它有三个特质:

  • 业务天然闭环:从创建课程 → 学生选课 → 教师录成绩 → 学生查成绩,每个动作在真实校园里都对应一个场景
  • 难度刚好卡位:增删改查能跑通,够毕业;加上事务、唯一约束、乐观锁,就能把“并发”这个亮点讲出深度
  • 改造成本低:换成实验室预约、场馆预订、在线考试报名,只需要替换实体字段,表结构几乎不用动

比如你把这个系统改造成“高校实验室预约系统”,核心流程完全一致:管理员开放预约 → 学生在时间段内预约 → 实验室管理员审核 → 学生查看预约记录。甚至前端页面都能复用大半。

2. 技术选型背后:SpringBoot + Vue.js前后端分离的取舍

题目已经把所有技术栈写明了:SpringBoot后端 + Vue.js前端 + MySQL + SQL脚本 + 接口文档。这套组合在2024年几乎就是Java Web毕设的默认配置了,但你要能说清楚“为什么选它”而不是“别人都这么选”,答辩才能加分。

2.1 后端为什么是SpringBoot而不是SSH或SSM

很多学校教材还在讲SSH(Struts2 + Spring + Hibernate)或者SSM(Spring + SpringMVC + MyBatis),但实际开发里这些东西已经被淘汰得差不多了。SpringBoot的本质是“Spring的自动配置封装”,它把原来Spring项目里几十行XML配置压缩成了几个注解。

具体到选课系统,SpringBoot带来的好处是实实在在的:

  • 内嵌Tomcat:不用再往Tomcat里拷贝war包,mvn spring-boot:run或者直接启动main方法就能跑,这对没配置过服务器的学生来说友好太多
  • application.yml集中配置:数据源、JWT密钥、MyBatis映射、端口号全在一个文件里,清晰明了
  • 起步依赖:引入什么场景就加什么starter,比如spring-boot-starter-web、mybatis-plus-boot-starter,不会像SSM那样手动拷一堆jar包还冲突

这套项目如果用纯SSM写,光配置文件就得写200多行,而且每个配置文件之间的引用关系非常隐蔽,一旦报错新手基本懵圈。SpringBoot把这一层复杂度吃掉了,所以你能把精力花在业务逻辑上——也就是答辩时真正值钱的地方。

2.2 前端为什么是Vue.js + Element UI

Vue.js在这个项目里的角色是“渐进式前端框架”。它不是那种刷新页面跳转的传统网页(JSP),而是整个页面由一个Vue实例接管,后端只提供JSON数据,点击“选课”按钮,前端发一个Ajax请求,局部更新页面状态。

Element UI是Vue.js专用的组件库,相当于给前端提供了一套已经做好样式的控件:表格、表单、分页、日期选择器、弹窗、消息提示。你自己手写一套这样的UI组件,少说也得2000行CSS加JavaScript,用Element UI就是引用组件然后绑数据的事。

前后端分离和传统JSP开发最本质的区别,可以用一个例子说明:传统方式里,页面是后端用Java拼出来的字符串;分离模式下,前端页面是静态文件,后端只返回{ "code": 200, "data": [...] }这种JSON。两边通过网络通信,前端跑在8080端口,后端跑在9090端口,开发时前端加个代理转发请求,避免跨域问题,生产环境则用Nginx把两者合成一个域名。

2.3 接口文档的价值:拿着就能联调

这套项目附带接口文档,这一点很多学生不重视,但它恰恰是项目“完整度”最直接的体现。接口文档解决的问题是:前端和后端怎么约定数据格式。

没有接口文档,你前后端联调时反复问“你这个接口返回的字段是count还是totalCount?”、“日期传字符串还是时间戳?”,一个项目拖一两周。有了文档,前端直接照着写axios请求,后端照着返回,两边不用等对方写代码。

最常见的接口格式是:

{ "code": 200, "message": "操作成功", "data": { "courseId": 1, "courseName": "Java程序设计", "teacherName": "张老师", "credit": 3, "selected": 45, "capacity": 80 } }

code用于标识业务成功与否,data是真正的业务数据,message用于前端弹窗提示。这样一套统一格式,写10个接口和写100个接口,模式都是一样的。

3. 数据库设计与SQL脚本:核心表结构与初始化数据

拿到SQL脚本第一件事,千万不要直接执行完就跑。我见过太多学生执行source course.sql之后,连show tables都没敲过就开始启动后端,结果数据库连接失败都不知道问题在哪。先看建表语句,读懂每张表的作用,后续改需求心里才有底。

3.1 从E-R图到建表语句,五张表怎么撑起一套系统

一个标准的选课系统,最少只需要6张表:

表名作用核心字段
user统一账号表,存所有登录身份id, username, password, role
student学生详细信息user_id, name, student_no, class_name
teacher教师详细信息user_id, name, teacher_no, title
course课程基本信息id, course_name, credit, capacity, selected_count, teacher_id
course_record选课记录表,也叫选课明细id, student_id, course_id, status, select_time
config系统配置表,存选课开放时间等config_key, config_value

学生跟用户是一对一关系,student.user_id关联user.id。选课记录是学生和课之间的“关联表”,这是最核心的设计。为什么选课记录不能被合并到课程表里?因为一门课会关联很多学生、一个学生会选择很多门课,这就是典型的多对多关系,必须拆出一张中间表。

课程表里的selected_count字段很关键,它代表当前已经选了多少人,每次成功选课就+1,退课就-1。这是典型的空间换时间的冗余设计:如果每次要看剩余名额都要去course_record里count(*),课程列表页一次展示10条数据就要跑10次查询,性能很差。

3.2 改造成自己毕设时,SQL脚本需要动哪里

拿到的SQL脚本一定带有原始项目的痕迹,比如表名可能叫t_course、字段里可能有c_id这种缩写风格。改造时我建议统一整理一遍:

  • 把表名改成语义化的英文,比如course_record而不是sc或elective
  • 每个表都加上create_time和update_time字段,哪怕逻辑上暂时用不到,答辩时解释“冗余设计方便后续维护”也能加分
  • 统一主键为id自增,避免UUID主键引发连表查询性能问题
  • 删除你不用的示例数据,比如原始项目里“张三、李四”这种测试学生账号要清干净,换成你自己设计的用户信息

还有一个很多学生会忽略的细节:字符集。建表语句里指定ENGINE=InnoDB DEFAULT CHARSET=utf8mb4,如果你图省事用默认的latin1,你插入中文字段就会变成一堆问号。我在实战中看到至少三分之一的学生栽在这个问题上。

3.3 初始化数据必须手工造

SQL脚本里的INSERT INTO语句一定不能全盘信任,因为原始项目的数据可能跟你的业务对不上。但种子数据又必须有,否则系统登录进去空空如也,截图都不好看。我是这么处理的:

管理员账号:admin / admin123,密码用MD5加密后写入数据库(注意SpringBoot项目里多半用了加盐加密,需要看下PasswordEncoder的实现)

学生账号:造10个,账号规则用学号,例如2021010101 / 123456教师账号:造4个,分布在不同的二级学院 课程数据:一个学期12门课,覆盖周一至周五、每天1-8节,容量从30到120不等

初始化数据时有个小技巧:把最热门的那门课容量设置为80,但selected_count预先填入78,这样你演示选课的时候,滑块或者前端进度条会有“抢最后一两个名额”的紧张感,演示效果比选择一架空空荡荡的课程好得多。

4. 接口文档与核心API实现:登录、选课、退课背后的事

接口文档不是摆设,它是你做二次开发的地图。选课系统里最核心的几个接口,每一个都有值得展开讲的细节。作为毕设,你不需要把每个接口都背下来,但接口设计的思想和核心逻辑能讲清楚,就足够优秀了。

4.1 登录鉴权:Shiro还是JWT,这个项目怎么选

传统Java Web项目经常用Session保持登录状态,前后端分离项目则普遍使用JWT(JSON Web Token)。流程是这样的:

  1. 用户提交账号密码
  2. 后端校验通过,生成一段带签名信息的Token字符串
  3. 前端拿到Token存在localStorage里,之后每次请求都在Header里带上Authorization: Bearer <token>
  4. 后端过滤器拦截请求,解析Token,把用户身份还原出来放到上下文里

JWT的好处是服务端不用存Session,天然适合前后端分离。毕设项目里你只需要一个拦截器加一个工具类就能实现,代码量很小。比较常见的实现方式是用io.jsonwebtoken:jjwt这个库,生成Token的代码就几行:

public String generateToken(User user) { return Jwts.builder() .claim("userId", user.getId()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 86400000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }

Token有效期设置成24小时比较合理。太短学生没上一次课就掉线,太长又有安全隐患。

4.2 选课和退课的核心逻辑,这段代码是答辩重点

选课接口是整个系统最核心的“亮点代码”。基础版逻辑很多人能写出来,但带上事务和并发控制的版本才是真正值钱的:

@Transactional(rollbackFor = Exception.class) public Result selectCourse(Long studentId, Long courseId) { // 1. 判断是否在选课时间窗口内,从config表读取 LocalDateTime now = LocalDateTime.now(); if (now.isBefore(selectStartTime) || now.isAfter(selectEndTime)) { return Result.error("当前不在选课时间内"); } // 2. 判断是否已经选过这门课,防止重复选课 Integer count = courseRecordMapper.countByStudentAndCourse(studentId, courseId); if (count > 0) { return Result.error("请不要重复选课"); } // 3. 使用乐观锁更新课程容量:where条件里带上selected_count int rows = courseMapper.decreaseCapacity(courseId); if (rows == 0) { return Result.error("课程已经选满"); } // 4. 插入选课记录 CourseRecord record = new CourseRecord(); record.setStudentId(studentId); record.setCourseId(courseId); record.setStatus(1); courseRecordMapper.insert(record); return Result.success("选课成功"); }

这段代码最精妙的地方在第3步。decreaseCapacity对应的SQL是:

UPDATE course SET selected_count = selected_count + 1 WHERE id = #{courseId} AND selected_count < capacity

这里的AND selected_count < capacity就是乐观锁。它保证数据库在更新的时候再次校验容量,即使100个并发请求同时到达,数据库的行级锁也会让它们排队执行,只有前80个能把selected_count从79更新到80,后面的更新因为条件不成立而影响行数为0。

@Transactional保证第3步和第4步要么一起成功,要么一起失败。如果插入选课记录失败,容量更新会一并回滚,不会出现“课没选上但名额被占了”的情况。

4.3 接口文档里的字段命名,有坑

接口文档你仔细看会发现,有些字段名不是Java风格的驼峰命名,而是下划线风格,比如student_id、course_name。这是MyBatis或者MySQL字段自动映射的结果。如果后端返回JSON给前端,建议用@JsonProperty或者配置MyBatis的驼峰自动匹配:

mybatis-plus: configuration: map-underscore-to-camel-case: true

这么配置之后,数据库里的course_id字段会自动映射到Java实体的courseId上,JSON返回给前端也自然变成了courseId。如果不配这个,你会在前端看到course_id这种怪字段,运气好能跑通,但在明眼人看来这就是基本功不扎实。

5. 项目运行与环境踩坑实录

实话实说,折腾环境这一步比写代码还劝退人。我见过太多学生卡在“项目跑不起来”最后找我救火,其实大部分问题到了就那几个固定的坑。我把从零启动这套项目的完整过程和常见坑一次性整理出来,照着做能省很多时间。

5.1 三步把前后端项目跑起来

第一步:准备数据库环境

先确认本机MySQL版本是5.7以上,创建数据库:

CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4; USE course_system; SOURCE course_system.sql;

注意执行SQL脚本之前,检查脚本里有没有DROP TABLE IF EXISTS语句。如果脚本没有,而你之前已经执行过一次,再执行会报表已存在,手动DROP即可。

第二步:启动SpringBoot后端

用IDE打开后端项目目录,等待Maven把依赖下载完。修改application.yml里的数据源配置:

spring: datasource: url: jdbc:mysql://localhost:3306/course_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码

这里最大的坑是serverTimezone不匹配导致的时间报错,加上Asia/Shanghai基本稳妥。然后直接运行CourseSystemApplication的main方法,看到日志输出“Tomcat started on port(s): 9090”就代表后端启动成功。

第三步:启动Vue前端

进入前端目录,先安装依赖:

npm install npm run serve

如果npm install卡住或者报错,看看是不是npm源的问题,换成国内镜像源:

npm config set registry https://registry.npmmirror.com

启动成功后终端会给一个本地地址,通常是http://localhost:8081,浏览器打开,用admin/admin123登录即可。

5.2 版本匹配问题,一个表格说清楚

这套项目因为年代变化,依赖版本可能有差异,最常见的版本不兼容问题列在下面:

组件推荐版本坑点说明
JDK8 或 11JWT库和高版本JDK不兼容,会报NoSuchMethodError
Maven3.6+3.8+对镜像源有默认限制,需要手动配国内仓库
Node.js14 或 16Vue CLI 4.x在Node 18以上会报OpenSSL错误
MySQL5.7 或 8.0驱动版本跟数据库版本要配套
MyBatis Plus3.4.x3.5.x改了部分API,旧代码可能报类型转换错误

Node 18跑旧版Vue项目报错是重灾区,报错信息通常是error:0308010C:digital envelope routines::unsupported。解决办法是更改启动命令:

set NODE_OPTIONS=--openssl-legacy-provider npm run serve

5.3 常见问题排查速查表

前端登录不上,提示跨域错误

前后端分离开发时跨域问题几乎人人遇到。检查后端有没有跨域配置类,或者单独加一个@Configuration类实现WebMvcConfigurer的addCorsMappings方法。开发阶段最简单的方式是用Vue的代理:

// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } } }

前端请求路径写成/api/login,代理到后端http://localhost:9090/login,浏览器角度看是同一个域,不存在跨域问题。

启动报错:Access denied for user

八成是数据库账号密码不对,或者远程MySQL没开权限。检查application.yml里的password字段,确认和本地MySQL一致。如果是8.0版本MySQL,还要确认驱动版本对应:

com.mysql:mysql-connector-j:8.0.33

选课成功后刷新课程列表,人数没变化

上面的SQL逻辑里已经提到这一点,常见原因是前端列表接口查的是课程表里的一个“残留字段”——比如你数据库里既有selected_count字段,又通过连表查询course_record去统计人数,两边的数据对不上。统一用selected_count这个冗余字段来展示即可。

5.4 答辩现场最容易翻车的地方,提前防一手

根据我的经验,答辩演示时出最多问题的是演示数据和网络环境。

演示数据的问题在于,你可能在原系统里选满了所有课程,演示“选课”时已经无课可选。建议在答辩前一天重置数据库,并且准备一个专门的演示账号,只让它选了一门课,剩下的课程全部留空。

网络环境的问题在于,答辩现场可能没网。提前把所有依赖包下载好,离线情况下Maven仓库和node_modules都在,项目就能本地启动。同时建议数据库用本地MySQL而不是教室里的远程数据库,否则服务器一挂,全场静默。

最后再分享一个小技巧

整套项目跑通之后,改造成自己的毕设时,最讨巧的做法不是从零重写,而是在现有代码上做业务替换。找一张业务表,把字段换成本身课题的实体,比如“课程表”换成“赛事报名表”,把“选课”语义替换成“报名”,然后改前端菜单文本、按钮文案和部分表单字段。你会发现主体逻辑全部复用。

我自己带学生实操的经验是:先花一天把项目完整跑通,再花两天读清楚核心表结构和选课接口,第三周开始动刀改造。绝大多数人一个礼拜就能把这套项目变成属于自己的系统,剩下时间全部用来打磨PPT和准备答辩问题。这比关在宿舍里从空项目起手写三个月要高效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询