ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue前后端分离高校选课系统:乐观锁防超选与JWT鉴权实战

SpringBoot+Vue前后端分离高校选课系统:乐观锁防超选与JWT鉴权实战 简介这套资源是基于SpringBoot与Vue实现的高校学生选课系统完整Java源码面向计算机相关专业毕业设计或需要快速搭建选课平台的开发者。系统采用前后端分离与B/S架构覆盖学生教师账号管理、课程发布、选课冲突检测、结果查询等核心业务并支持多媒体资源关联。资源共695个文件约16.2MB包含131个Java后端源码、98个Vue前端组件、161个SVG图标以及js/css、xml配置、数据库备份zbak等文件目录结构清晰便于定位与二次开发。目前已有74人学习浏览。压缩包内提供install.bat、build.bat等一键运行脚本配合Maven依赖和MyBatisPlus持久层设计可在Eclipse或IDEA中直接导入调试适合作为毕业设计参考或SpringBootVue实战练习。1. 高校学生选课系统SpringBoot与Vue前后端分离的Java源码能直接跑通抢课流程这份毕设源码解决的问题很具体同一门课几十个学生同时抢系统不能超选、不能重复提交学生端要实时看到剩余名额教师端要能导出选课名单。项目用SpringBoot做后端接口、Vue做前端页面、MySQL存数据典型的Java全栈毕设结构。适合两类人一类是正在做毕业设计、想找一套能改能跑的选课系统参考另一类是刚学完Java和前端基础、想看看真实项目里鉴权、事务、分页这些功能是怎么串起来的。这套源码不是玩具demo登录、选课、退课、课表查询、管理员审核这些环节都有完整实现前后端接口对得上导入数据库后能直接跑。2. 系统拆解前后端分离的模块划分与目录结构2.1 技术选型为什么是SpringBoot Vue而不是别的组合高校选课系统这个场景数据量不大但业务规则杂学生要能按学期选课、退课教师要能维护课程信息管理员要管理用户和审核开课。SpringBoot适合这种业务逻辑清晰的系统它把配置收敛在application.yml里不用像SSH框架那样写一大堆XMLMyBatis做数据访问写SQL灵活适合课程筛选这种条件经常变动的查询前端Vue负责渲染和交互ElementUI提供现成的表格、弹窗、表单组件做管理端界面效率高。前后端分离是本项目的核心结构。后端只负责业务逻辑和接口前端页面通过HTTP请求调接口拿数据。这样做的好处是两个端可以独立开发、独立部署而且接口写好了以后以后想换小程序或者App端都能复用同一套后端。对毕设答辩来说前后端分离也是一个值得展开讲的技术点评委普遍会问“为什么要分离、数据怎么传输”。2.2 三大角色与模块地图系统里一共有三类角色权责分得很清楚角色核心操作关键接口学生选课、退课、查看已选课程、查看成绩/api/student/course/select、/api/student/course/drop教师开设课程、维护课程信息、查看选课名单/api/teacher/course/add、/api/teacher/course/list管理员用户管理、课程审核、学期管理/api/admin/user/manage、/api/admin/course/audit模块上拆成四块用户认证模块负责登录和权限校验课程管理模块处理课程的增删改查和审核选课模块是整个系统的核心负责处理选课、退课以及容量校验信息查询模块提供课表和选课名单的导出。这四个模块对应后端的四个包也对应前端的几个独立页面。2.3 项目目录结构前后端代码分别在哪拿到源码后先看目录结构快速定位自己需要改的地方。course-select-system/ ├── backend/ # SpringBoot后端 │ ├── src/main/java/com/course/ │ │ ├── controller/ # 接口层接收前端请求 │ │ ├── service/ # 业务逻辑层事务边界在这里 │ │ ├── mapper/ # MyBatis数据访问接口 │ │ ├── entity/ # 实体类对应数据库表 │ │ ├── common/ # 统一返回体、异常处理、JWT工具类 │ │ └── config/ # 拦截器注册、跨域配置 │ └── src/main/resources/ │ ├── mapper/ # MyBatis的XML文件 │ └── application.yml # 数据库、端口、JWT密钥配置 ├── frontend/ # Vue前端 │ ├── src/ │ │ ├── api/ # 接口封装统一管理请求地址 │ │ ├── router/ # 路由配置含登录守卫 │ │ ├── views/ # 页面组件每个角色一个文件夹 │ │ ├── store/ # Vuex状态管理存用户信息和token │ │ └── utils/request.js # Axios拦截器统一加token │ └── package.json └── sql/ └── course_select.sql # 建库建表脚本含初始数据这种分包方式是SpringBoot项目的标准分层controller不写业务逻辑、service不直接操作数据库每一层只做一件事。改代码的时候遵循这个分层就不会把项目改乱。前端部分api目录是前端工程师最先要看的地方所有接口地址都集中在这里改后端接口地址时只需要动这一个文件。3. 数据库设计选课不超选的根基在三张表和一条乐观锁SQL3.1 核心表结构用户表、课程表、选课记录表选课系统的数据模型不复杂但表之间关系要设计对。用户表存三类角色的公共信息用role字段区分身份课程表存课程基本信息和容量选课记录表是学生和课程的多对多关系表学生选一门课就插入一条记录。-- 用户表学生、教师、管理员共用 CREATE TABLE sys_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, role TINYINT NOT NULL COMMENT 1学生 2教师 3管理员, student_no VARCHAR(20) NULL COMMENT 学号仅学生有, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 课程表 CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, teacher_id INT NOT NULL COMMENT 任课教师ID, credit DECIMAL(2,1) NOT NULL COMMENT 学分, max_students INT NOT NULL COMMENT 课程容量, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, semester VARCHAR(20) NOT NULL COMMENT 学年学期如2024-2025-1, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1已开课 2已结课 ); -- 选课记录表学生与课程的多对多关系 CREATE TABLE student_course ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL COMMENT 学生ID, course_id INT NOT NULL COMMENT 课程ID, select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, UNIQUE KEY uk_student_course (student_id, course_id) );三张表的核心设计点是student_course表的唯一索引uk_student_course它在数据库层面保证了同一个学生只能选同一门课一次。这个索引看着简单但在并发场景下是关键防线。另一个设计点是course表里的selected_count字段每选课成功一次就加一容量校验就是用这个字段和max_students做比较。3.2 防超选原理乐观锁SQL是怎么避免名额超卖的选课系统最怕的问题是超选也就是第50个学生抢课的时候系统以为还有名额但实际已经选了49个人。最常见的错法是先查询剩余名额再插入记录这两步之间存在时间差并发时就会超卖。本项目里用了一个乐观锁思路解决把“校验名额”和“扣减名额”合并成一条UPDATE语句通过影响行数判断是否真的抢到了名额。-- 核心防超选SQL只有当前已选人数小于容量时才更新 UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count max_students AND status 1;如果这条UPDATE语句影响的行数大于0说明更新成功名额扣减成功可以继续插入选课记录如果影响行数是0说明课程已满或者课程状态不对直接返回“选课失败课程已满”。这个做法的精妙之处在于校验和扣减发生在同一条SQL里数据库行锁保证同一时间只有一个事务能更新成功。注意不要用乐观锁的版本号方案来做选课也就是不要在course表里加version字段。版本号方案适合更新不频繁的数据但选课是热点操作会频繁触发版本号冲突反而增加重试次数。用selected_count max_students这个条件做天然校验对选课这种场景更直接。3.3 事务边界选课记录的插入必须和课程名额扣减在同一个事务里课程名额扣减成功之后还要向student_course表插入一条选课记录。这两步必须放在同一个事务里否则会出现扣了名额但没插入记录的情况。我在实际写代码的时候是把UPDATE课程表和INSERT选课记录放在同一个service方法里方法上加Transactional注解。项目源码里的处理方式也一样事务只加在service层controller层不加。如果把事务加到controller上一个请求可能包含多个业务操作事务范围过大会造成长事务影响数据库连接池的回收效率。4. 后端实现JWT登录鉴权、选课接口与分页查询4.1 登录鉴权链路JWT Token的签发与拦截器校验后端接口不能裸奔每次请求都要校验身份。项目用的是JWT方案用户登录成功后后端把用户ID和角色信息加密生成一个Token返回给前端前端把这个Token存在本地之后每次请求都在Header里带上后端通过拦截器统一校验Token的合法性。// 登录成功后签发Token public String generateToken(Integer userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) // 2小时过期 .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }claim相当于Token里的自定义字段这里存了userId和role两个信息。signWith指定了HS256签名算法和密钥密钥放在application.yml配置里不要硬编码在代码中。2小时过期时间适合选课这种短时操作如果做完整的学生系统通常改成7天加自动续期。拦截器的逻辑是重写preHandle方法这是Spring MVC里拦截器的一个标准写法先从请求头里取出Authorization字段解析出用户信息解析失败就返回401成功就把用户信息放进request对象供后续使用。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } }拦截器注册要注意排除登录接口本身不然登录请求也会被拦截导致循环。项目源码里通过WebMvcConfigurer的addInterceptors方法注册excludePathPatterns里放/api/user/login和/api/user/register。4.2 选课接口事务注解、防重校验与业务异常区分选课的service方法体现的是完整业务闭环这一部分把前面数据库设计的思路全部落到了代码上。Transactional(rollbackFor Exception.class) public void selectCourse(Integer studentId, Integer courseId) { // 前置校验检查选课记录是否已存在 int count studentCourseMapper.countByStudentAndCourse(studentId, courseId); if (count 0) { throw new BusinessException(不能重复选同一门课); } // 核心操作乐观锁扣减名额 int rows courseMapper.decreaseStock(courseId); if (rows 0) { throw new BusinessException(课程已满或课程不存在); } // 插入选课记录 StudentCourse record new StudentCourse(); record.setStudentId(studentId); record.setCourseId(courseId); studentCourseMapper.insert(record); }Transactional注解设置了rollbackFor Exception.class这意味着任何异常都会触发事务回滚。这里有个关键点decreaseStock成功但插入选课记录失败时事务回滚会把扣减的名额也一起回滚掉不会出现数据不一致。BusinessException是自定义异常被全局异常处理器捕获后转成{code: 500, message: 课程已满}返回给前端。初学者容易犯的错误是把selectCourse写成先查selected_count再比较再更新。我在很多学生代码里都看到过这种写法一律是需要在并发下翻车的问题。多花点时间理解上面那条UPDATE语句这是项目里最值得背下来的一段SQL。4.3 MyBatis分页PageHelper插件的使用与参数说明课程列表和选课名单都需要分页项目用了PageHelper插件。用法固定需要注意它生效的位置。public PageInfoCourseVO getCourseList(int pageNum, int pageSize, String keyword) { // 启动分页下一行查询语句会被自动拼接LIMIT PageHelper.startPage(pageNum, pageSize); ListCourseVO courses courseMapper.selectByCondition(keyword); return new PageInfo(courses); }PageHelper.startPage(pageNum, pageSize)这一行必须紧跟在要分页的查询语句之前中间不能有其他查询。PageInfo里封装了总条数、总页数、当前页数据前端展示分页组件时直接取这里的字段就行。如果碰到分页不生效的情况九成是startPage和select之间多写了一条SQLPageHelper把分页SQL作用到了错误的那条查询上。5. 避坑指南环境配置、前后端联调、运行期的六个常见问题5.1 SpringBoot版本过高导致的循环依赖与javax包缺失现象导入源码后启动报错提示AllowCircularReferences默认为false或者代码里import javax.servlet找不到类。原因高版本SpringBoot 3.x基于Jakarta EE规范javax.servlet包被替换成了jakarta.servlet同时SpringBoot 2.6版本以后默认禁止循环依赖。这份源码是基于2.x写的直接拿高版本跑必定报错。解决看源码pom.xml里parent标签的版本用对应的JDK和SpringBoot版本。我的习惯是SpringBoot 2.7.x搭配JDK 1.8或11这是目前兼容性最稳的组合SpringBoot 3.x需要JDK 17并且要全局替换javax为jakarta改动量不小。拿到源码第一时间看pom.xml别急着配环境。5.2 Vue前端启动失败Node版本与依赖安装问题现象npm run dev报错或者提示digital envelope routines::unsupported。原因Node 17以上版本和旧版Vue CLI项目存在OpenSSL兼容问题这是Node对Webpack加密库策略调整导致的。解决两种方案任选。方案一是用Node 16版本这是Vue 2项目的黄金版本方案二是在package.json的dev脚本后面加SET NODE_OPTIONS--openssl-legacy-providerWindows环境下用这个方式最省事。另外npm install在中国网络环境下容易卡住用npm install --registryhttps://registry.npmmirror.com可以避开大部分问题。5.3 前后端联调跨域前端代理配置比后端CORS注解更实用现象前端页面能打开但所有接口请求都在浏览器显示CORS error或者请求是发出了但看不到结果。原因前端运行在localhost:8080后端运行在localhost:9090不同端口属于跨域。后端如果配置了CrossOrigin注解开发环境能通但部署上线后域名变化又要改一套。解决项目前端用的是Vue CLI的proxy代理方案在vue.config.js里配置devServer代理把/api开头的请求转发到后端地址。这样做浏览器里看到的请求是同源的完美避开跨域问题。const { defineConfig } require(vue/cli-service) module.exports defineConfig({ devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } })5.4 时区问题导致的时间字段错乱现象选课记录表的select_time比系统时间早8小时MySQL查出来的时间和实际对不上。原因MySQL驱动连接串没有指定时区默认用了UTC而系统是东八区。解决在application.yml的数据库连接URL上加serverTimezoneAsia/Shanghai。这是检查顺序排在前面的一项很多毕设答辩现场打开数据库看到时间不对就是因为这个。5.5 MyBatis的XML文件在打包后丢失现象本地IDEA启动正常打包成jar后运行报Invalid bound statement (not found)。原因Maven打包时默认只打包src/main/resources目录下的内容mapper XML放在java目录下就没被带进去。解决在pom.xml里配置resources把mapper目录的XML文件强制打进去。项目源码如果结构规范mapper XML本来就在resources目录下就不会有这个问题。如果你改过路径一定要记得动pom.xml。5.6 端口冲突导致的后端启动失败现象启动后端时IDE提示Port 9090 was already in use。原因上一次运行没被关掉或者有别的程序占用了端口。解决命令行执行netstat -ano | findstr 9090查出占用进程PID然后在任务管理器里结束对应进程。这个命令对排查端口类问题通用以后做别的项目也会用到。6. 验证与进阶并发压测确认不超选再谈代码演进方向整套环境跑通以后建议按下面这个顺序做一遍验证确认系统真的可靠。第一步启动后端和前端用学生账号登录进入选课页面选择一门容量10人的课程快速连续点击选课按钮页面上应该只有第一次点击生效后续点击被前端loading状态挡住。第二步用JMeter模拟并发请求线程数设成20全部请求同一门剩余名额5人的课程跑完后去数据库执行SELECT selected_count FROM course WHERE id ?确认最终值不超过max_students且选课记录数等于实际成功数。这是检验乐观锁是否生效的最直接方式。第三步验证权限隔离用学生账号请求管理员的接口比如/api/admin/user/list应该返回403同时在用JWT的调试工具篡改Token再访问确认会被拦截器挡在门外。第四步退课测试退掉一门已选课程确认课程表的selected_count减一且student_course表对应记录被删除再重新选这门课应该能成功。如果这些验证全部通过这套系统就达到了可交付的状态。源码层面再往下走有两个方向是毕业答辩时能加分的内容。第一个是引入Redis做库存预热把课程剩余名额提前加载到Redis抢课时先用DECR命令扣减扣减成功再写数据库这个方案能把并发支撑能力提升一个量级也是现在互联网公司秒杀系统的标准做法。第二个是引入RabbitMQ或简单线程池做选课流水异步落库缓解高峰期的数据库压力。这两个方向适合学有余力的情况如果时间紧把现有这套乐观锁方案讲清楚就足够了。做毕设这些年我最深的感受是能跑通只是第一步能讲清楚“为什么这样设计”才是答辩的关键。这套选课系统在数据一致性上的处理思路是除CRUD以外最值得花时间研究的点。如果自己是从零改起强烈建议把我上文提到的那条防超选SQL、JWT拦截器、事务边界这三个部分逐字逐句读懂再动手扩展功能。排版和细节我已经尽量说明白了剩下的就是下载源码后照着跑一遍有问题可以对照避坑章节逐项排查希望帮到你。本文还有配套的精品资源点击获取
返回列表