新闻详情

新闻详情

首页 / 资讯中心 / 详情

SpringBoot+Vue.js高校选课系统:并发控制、权限设计与部署实践

发布时间:2026/10/1 3:42:06来源:尧图网络
SpringBoot+Vue.js高校选课系统:并发控制、权限设计与部署实践
这套基于SpringBootVue.js的高校学生选课系统算是我经手课设项目里结构比较完整的一套。后端用SpringBoot整合MyBatis操作MySQL前端用Vue.js配合Element UI做页面前后端通过JSON交互既覆盖了学生选课、退课、查成绩也覆盖了教师开课、录成绩和管理员维护课程这些常规流程。如果你正准备做毕业设计、课程设计或者想找一个能写进简历的实战项目这套源码非常值得花点时间拆开看看。我先把话放前面选课系统的难点从来不是CRUD而是选课瞬间的并发控制、权限隔离以及前后端联调时那堆让人头疼的跨域和状态同步问题。这篇文章会把数据库设计、核心接口、前端联调和部署避坑一并展开里面的代码和配置都是我反复验证过能跑的照着改就能用。1. 项目定位与技术选型背后的逻辑1.1 它是为谁写的学生、教师、管理员各自的闭环高校选课系统表面上是一个信息管理项目实际上是一个典型的角色权限系统。你打开这套源码首先应该去看的是登录接口因为登录后的身份直接决定了你能看到哪些菜单、能请求哪些接口。学生进来要能浏览本学期开课列表、选课、退课、查看自己的课表和成绩教师登录后要能看到自己讲授的课程、查看选课学生名单、录入或修改成绩管理员承担的则是基础数据维护比如新增和关闭课程、管理教师和学生账号、设定选课时间段、做简单的统计导出。角色拆分清楚之后前后端的功能页面就很好规划了。前端路由按角色做动态加载后端接口按角色做权限拦截数据库设计也跟着分成用户表、学生表、教师表、课程表、选课表这几类。很多新手一上来就写登录写完登录直接写CRUD结果做到选课模块才发现不知道该把业务规则放哪层比如限选人数、选课时间窗口、退课截止时间这些都应该在后端Service层统一校验不能靠前端按钮禁用来控制。这套源码里最大的看点之一就是这些规则放对了位置。1.2 为什么偏偏是SpringBootVueMyBatisMySQL这个组合在2025年的校园项目里依然高频出现不是没道理的。SpringBoot把配置简化到了极致内嵌Tomcat一个jar包直接跑起来不像传统SSH项目还要部署WAR包、配一堆XMLVue.js做前后端分离配合Element UI写出来的界面在答辩时观感明显好一截MyBatis作为半自动ORMSQL手写可控尤其在选课系统这种需要精确控制更新语句、乐观锁条件的场景反而比全自动的JPA更好用MySQL则是目前教学环境里最通用的存储方案安装配置资料多遇到问题也容易搜到答案。另外还有一个实际原因这套组合的“人才密度”高。在校生几乎人人会一点SpringBoot和Vue技术栈不冷门意味着你接手源码、改功能、找人讨论的成本都低。如果换成其他冷门框架哪怕功能做得再花哨答辩老师不认识团队成员不熟悉项目后期维护反而是灾难。2. 数据库设计选课系统的地基2.1 核心表拆解数据库是整个系统的地基很多所谓“运行报错”的问题最后追根溯源都是表结构设计不合理造成的。这套源码里的核心表大概分三类人员类、课程类和关系类。人员类主要是用户表(user)、学生表(student)、教师表(teacher)。用户表存user_id、username、password、role、status这类公共字段学生表和教师表再通过user_id关联用户的扩展信息比如学号、院系、专业、年级或者教师所属学院和职称。这样设计的理由是登录认证只需要查一张用户表个人信息展示再按角色去扩展表查避免把教务字段全部堆在一张表里。课程类是课程表(course)字段至少要包含course_code、course_name、teacher_id、credit、total_capacity、selected_count、class_time、class_location、status。注意selected_count这个字段非常关键它记录了当前已选人数选课接口每次更新它都要用原子性的SQL条件更新后面我会详细说。关系类就是选课表(select_course)一般接触的字段是id、student_id、course_id、select_time、status、score。status用来区分正常选课、退课、待录入成绩等状态score则用于教师录入成绩。选课表在多对多关系里属于典型的中间表但它不是简单的关联表还承担了业务状态流转所以它的索引和约束设计必须单独抠。2.2 选课关系表如何防止重复选课这是我每次讲这个项目都要强调的一段选课表上必须建联合唯一索引把student_id和course_id绑在一起。建表SQL里大概是这样的CREATE TABLE select_course ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, select_time DATETIME NOT NULL, status TINYINT DEFAULT 1, score DECIMAL(5,2) DEFAULT NULL, UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;如果你不加这个唯一索引就算Service层写了判断“该学生是否已选这门课”在高并发下依然会出现两个请求同时通过校验、然后插入两条重复记录的情况。原因很简单判断和插入是两个步骤中间存在时间窗并发请求完全可能交错执行。而数据库唯一索引是从底层强制保证的一旦重复就抛DuplicateKeyException这时候就可以在Service层捕获这个异常返回“请勿重复选课”。2.3 从XMLConfigBuilder理解MyBatis的启动过程很多人看这套源码时会遇到一个问题MyBatis的Mapper接口明明没写实现类为什么Spring容器能注入这就得说MyBatis的启动流程了。在SpringBoot环境下mybatis-spring-boot-starter会自动扫描标注了Mapper的接口然后通过MapperFactoryBean生成代理对象代理对象每次执行方法时会去加载对应的XML映射文件中的SQL。这里有个常被面试官追问的概念叫XMLConfigBuilder。它负责读取MyBatis的核心配置文件比如设置settings、类型别名、类型处理器typeHandlers、插件plugins等最终构建出Configuration对象。Configuration对象里维护了MappedStatement、ResultMap这些重要的内部结构。你可以把XMLConfigBuilder理解成整个MyBatis的“初始装配工”它把零散的配置解析成运行时需要的对象模型。看这套源码时如果遇到自定义TypeHandler或者自定义插件不知道往哪配第一反应就是去看Configuration里有没有对应注册入口。3. 后端模块实现认证、并发选课和成绩管理3.1 登录与JWT身份认证现阶段的SpringBoot项目基本都已经转向无状态登录这套源码用的也是JWT方案。用户登录成功后后端生成一个token返回给前端前端把它存在localStorage里之后每次请求在Header带上Authorization字段后端通过拦截器解析token并取出用户角色。具体实现上有几个细节值得注意。第一拦截器要放行登录接口、验证码接口等白名单路径其余接口全部拦截第二解析完token后要把用户信息放进Request上下文里比如通过ThreadLocal或自定义Attribute方便Service层直接获取当前登录人第三角色权限可以用HandlerInterceptor做一次粗粒度拦截比如以/admin/开头的接口要求管理员身份以/teacher/开头的接口要求教师身份这样比在Controller里每个方法都判断角色要清爽得多。需要注意的是JWT是无状态的token一旦签发在过期前无法主动撤销所以对修改密码、禁用账号这类场景通常还要搭配Redis黑名单或用户状态校验否则会有一个时效窗口的风险。3.2 选课提交的并发控制与事务选课接口是整个系统技术含量最高的地方。先说我见过的问题代码先查询课程剩余容量剩余大于0就insert选课记录然后update课程表减少容量。这段逻辑如果在并发环境跑很容易超选。要解决这个问题最简单的办法是把检查和更新合并成一条原子SQLTransactional public boolean selectCourse(Long studentId, Long courseId) { // 1. 插入选课记录利用唯一索引捕获重复 try { selectCourseMapper.insert(studentId, courseId); } catch (DuplicateKeyException e) { throw new BizException(请勿重复选课); } // 2. 原子扣减容量 int affected courseMapper.increaseSelectedCount(courseId); if (affected 0) { throw new BizException(课程容量已满); } return true; }对应的SQL是UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count total_capacity这里的关键是update语句自带条件affected0就说明当前已经没有剩余容量。相比先查再改这种条件更新的写法在并发下才不会因为读到了过期数据而超卖。当然事务必须开着否则插入成功后更新失败就会造成数据不一致。这里我额外强调一点事务注解默认只能拦截RuntimeException如果你抛的是自定义受检异常需要指定rollbackFor参数否则数据会悄悄提交这个坑很多老手都踩过。3.3 成绩录入、统计和登录状态保持成绩模块相对简单教师端按课程查出选课学生列表逐条录入或批量导入分数。但这里同样有一个事务细节成绩一旦录入课程状态就可能从“进行中”变成“已结课”此时选课和退课入口都必须关闭。也就是说成绩录入操作会同时修改select_course表的score字段和course表的status字段这两个动作必须放到同一个事务方法里并且要在状态变更前检查是否还有人正在选课避免出现边改成绩边选课的中间状态。统计功能一般包括课程选课人数统计、院系选课排行、及格率统计等。写这类SQL时可以先用GROUP BY聚合出基础数据再用LEFT JOIN补全课程和教师信息。要注意MySQL的sql_mode如果开启了ONLY_FULL_GROUP_BY查非聚合列必须走聚合函数或关联子查询否则控制台直接报错。这套源码里的统计SQL如果报这个错很多人第一反应是SQL写错了其实只是数据库严格模式的原因。4. Vue.js前端实现与联调细节4.1 前端工程结构与页面划分Vue前端通常分两种形态Vue2配Element UI或者Vue3配Element Plus。这套源码如果你拿到的是Vue2版本那就需要Node 16/14这类较旧版本环境启动的时候经常会在node-sass上翻车如果是Vue3版本对Node版本要求宽松很多直接用最新LTS版本一般没问题。拿到源码后先看package.json里的依赖再决定本机Node版本这是省时间的第一步。页面结构按角色来划分会很清晰src/views下面建admin、teacher、student三个目录分别放对应的页面组件src/router里配置动态路由登录后根据角色加载对应路由表src/utils里放request.js统一封装Axios设置baseURL和请求拦截器。组件之间如果存在跨页面共享的状态比如当前用户信息、页面Tab缓存可以用Vuex或Pinia但只有少数场景真正需要千万别为了“工程化”硬上状态管理反而增加不必要的复杂度。4.2 Axios拦截器和路由守卫Axios拦截器是联调里最实用的工具。请求拦截器负责把localStorage里的token取出来加到Header里响应拦截器负责统一处理后端返回的code比如token过期就清空本地登录信息、跳转登录页。这里有个经验后端接口返回的数据别只靠HTTP状态码判断业务成功因为很多项目为了前端拿到报错信息会统一返回200加业务code比如{code:200,message:成功}。所以前端判断逻辑要跟后端约定好到底是看HTTP状态还是看业务code两边不一致就会莫名其妙地出现“明明没报错却没数据”的情况。路由守卫的作用是控制页面访问权限。通常在router.beforeEach里判断逻辑顺序是没有token就直接去登录页有token但当前路由不在权限列表里就拉取当前用户的角色和动态路由表再放行。注意不要在路由守卫里重复请求用户信息可以把用户信息放到全局状态或localStorage里缓存否则每次刷新页面都要重新拉一次接口压力大不说页面切换还会闪白屏。4.3 选课列表的状态细节选课页面前端要注意几个状态细节。第一个是选课按钮的动态禁用一门课程如果selected_count已经等于total_capacity按钮就要显示“已满”并禁用如果当前学生已经选过这门课则显示“已选”。第二个是操作反馈选课成功后不能只alert一下要重新拉取最新的选课列表和课程列表因为两边的数字都会变化。第三个是防重复点击按钮点击后到接口返回之前要加一个loading状态避免用户连点导致后端同一时间收到多个请求。还有一个容易被忽略的细节退课之后容量要回补前端要考虑到退课操作同样会影响课程列表的total_count所以页面数据刷新要统一处理好loading和错误提示不要让用户看到旧的课程容量数据。前端这块说白了就是“状态同步”的艺术页面之间通过接口拿数据一定要在关键操作后主动重新拉取而不是本地改一个数字假装同步。5. 源码导入、环境配置和打包部署5.1 从Maven构建到MySQL初始化拿到源码第一步不是打开IDE写代码而是先检查本机环境。JDK建议用1.8或11Maven用3.6.x以上MySQL用5.7或8.0均可。如果你用的是SpringBoot 2.x版本JDK8完全够用SpringBoot 3.x则强制要求JDK17同时javax包名也变成了jakarta很多老教材里的import javax会直接编译报错。所以拿到源码先看pom.xml里SpringBoot的版本号再对应选JDK版本。数据库初始化的SQL脚本一般在项目根目录下的sql文件夹里文件名可能是init.sql或者school.sql。用Navicat连接MySQL后先单独建一个数据库比如school_select再运行SQL脚本。导入时要注意编码脚本里有中文注释的话连接编码设置成utf8mb4否则会出现乱码表结构建出来字段名也一切正常但数据全是问号。5.2 application.yml配置要点SpringBoot项目的核心配置在application.yml里你需要改的地方主要是数据库连接和MyBatis配置spring: datasource: url: jdbc:mysql://localhost:3306/school_select?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.select.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl连接串里的serverTimezone一定不能省MySQL 8.0默认时区是UTC不设置的话日期时间字段会出现8小时偏移。useSSLfalse是为了避开SSL握手报错allowPublicKeyRetrievaltrue则是为了规避MySQL 8.0的“Public Key Retrieval is not allowed”报错。如果你的项目是SpringBoot 2.4以上还用到了spring.config.import或者不同环境的配置那还要检查一下是否有activeProfile指向了bootstrap.yml。5.3 Vue前端启动、打包和后端交互前端启动前先修改接口地址的配置一般是src/utils/request.js里的baseURL。开发环境下后端服务跑在8080前端dev server跑在8081所以必须配置跨域代理。最简单的做法是在vue.config.js里配置proxymodule.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端请求/api开头的接口就会自动转发到后端避免了浏览器跨域限制。如果后端也配置了CorsFilter两者可以共存但开发时优先用proxy部署后再统一通过Nginx反代。打包部署这块后端执行mvn clean package -DskipTests生成target目录下的jar包前端执行npm run build生成dist静态目录。生产环境下可以把dist目录里的文件放到Nginx的html目录再把/api请求反向代理到后端的8080端口。如果是在同一台服务器上运行jar包直接用java -jar就行但记得生产环境的mysql密码、数据库名都要改成线上的不能沿用开发环境的本地配置。5.4 收到只有jar包的情况下如何还原可读工程有时候资源包里只提供了一个编译好的jar包没有源码那你需要做反编译来还原工程。SpringBoot的jar包去掉Boot-INF/classes目录后里面就是标准的class文件可以用CFR或Fernflower这类反编译工具还原Java源码。IDEA自带的反编译器比较简单直接把class文件拖进编辑器就能看也可以用java -jar cfr.jar xxx.jar --outputdir ./src来批量导出。还原出来的代码可以正常阅读逻辑但注解和泛型信息可能部分丢失且Lombok相关的getter/setter会变成原始写法。要注意的是反编译只能用于学习自己拥有或有权的项目或者分析开源项目别拿它去做违反版权的事。如果你只是为了修复一个线上bug反编译是能救急的但后续最好及时把源码补回来否则维护成本会被拉得很高。6. 运行过程中的常见问题与避坑清单6.1 MyBatis缓存把数据搞“脏”了MyBatis自带一级缓存和二级缓存很多人没搞清楚就直接开启结果改了数据库后查询结果还是旧的。一级缓存是SqlSession级别的同一个SqlSession内执行两次相同查询会走缓存但SpringBoot整合MyBatis后每次Mapper调用通常都是新SqlSession所以一级缓存影响不大真正容易踩的是二级缓存。二级缓存是Mapper级别的默认关闭但如果某张表启用了并且关联表执行了更新操作就可能出现脏数据。比如选课系统里查询课程列表时缓存了结果此时另一个线程更新了selected_count但缓存不会自动失效页面展示还是旧数据。我的建议是这种实时性要求高的模块一律不要开二级缓存或者在更新语句后显式调用sqlSession.clearCache()。选课系统这种写多读少、数据一致性敏感的业务缓存不应该是首选方案Redis如果要加也只加给真正不变的基础数据比如院系列表、课程类型字典这类。6.2 前端跨域、端口冲突和登录失效跨域问题最常见的表现形式是浏览器控制台报“Access-Control-Allow-Origin”或者“CORS错误”。如果你用了代理先确认请求是不是真的走了代理路径看Network里的请求URL有没有变成后端地址如果没走代理大概率是baseURL没配置对。如果决定在后端用CorsFilter注意allowedOrigins不能写*要和allowCredentials同时存在时会冲突SpringBoot较新的版本会直接拒绝启动这时候应该用allowedOriginPatterns。端口冲突也是高频问题。SpringBoot默认8080如果本机MySQL占用了或者之前有其他项目占用启动直接报Port already in use。这时候快速改掉server.port配置即可。前端登录失效也很常见表现为“请求夹带token但后端始终未认证”。优先看Authorization这个Header有没有被跨域请求预检拦截跨域OPTIONS请求通常不会带自定义Header所以拦截器要放行OPTIONS请求否则GET/POST正请求都发不出去。6.3 速查表十个高频报错我把这套源码运行中容易出现的问题整理成了一张速查表你按表里的顺序排查基本能覆盖90%的坑。报错现象可能原因解决方法Access denied for userMySQL密码不正确或用户权限不足在application.yml里改成实际账号密码并确认允许localhost访问Public Key Retrieval is not allowedMySQL 8.0的SSL机制导致连接串加allowPublicKeyRetrievaltrueServer returns invalid timezone时区未指定连接串加serverTimezoneAsia/ShanghaiPort already in use端口被占用改spring.server.port或杀掉占用进程Failed to configure a DataSource没加数据库连接配置检查application.yml的spring.datasource配置Field xxx in com.xxx required a beanMapper没扫描到启动类加MapperScan或在Mapper接口加MapperConsider defining a field of type RedisTemplateRedis依赖没有配置如果没用到Redis去掉redis依赖并排除自动配置Invalid bound statementMapper接口与XML文件不匹配检查mapper-locations路径和XML命名空间Node Sass could not find a bindingnode-sass版本不兼容删掉node_modules用Node14/16重新installAxios request failed with status code 404请求路径或Web上下文不对检查后端Controller路径和baseURL是否一致还有一类隐含问题必须提醒当你在别人的源码上改造时不要把前端路由的path和后端接口的URL搞混。前端的/api前缀是给代理用的后端接口本身如果用的是RequestMAPPING(/api/...),那代理配置就要写成/api打头的都能转过去否则会转发到不存在的接口。面试时如果被问到这台项目怎么上线你就把代理、Nginx反代、前后端分离这套逻辑讲清楚基本上就能说服面试官你是真做过项目。整个项目里最值得你反复推敲的就是选课那几行更新语句和事务搭配。把这个地方吃透了不仅是会跑通这个项目更是理解了企业里对数据一致性最基本的要求。我自己当初就是把这段代码里的锁和事务关系想清楚后才真正觉得自己算是入了后端开发的门。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

DQN深度Q网络入门:目标网络与经验回放实战 2026/10/1 4:39:33

DQN深度Q网络入门:目标网络与经验回放实战

1. DQN到底在解决什么问题:从"查字典"到"猜答案"的转变DQN,全称Deep Q-Network,中文一般叫深度Q网络。如果你之前接触过强化学习,大概率是从Q-Learning入的门,那套东西简单说就是在内存里维护一张…

阅读更多 →
Vue首屏优化指南:4种骨架屏实现方案,告别白屏 2026/10/1 4:39:26

Vue首屏优化指南:4种骨架屏实现方案,告别白屏

跟UI撕需求、跟后端对接口、跟网络环境死磕,每一个做Vue首屏优化的前端都会遇到同一个问题:白屏。用户打开页面,地址栏已经变了,但页面空空如也,转圈、闪烁、卡顿,这些体验往往不是功能问题,而是…

阅读更多 →
精品巧克力配方结构:从比例计算到稳定复现 2026/10/1 4:39:19

精品巧克力配方结构:从比例计算到稳定复现

做精品巧克力做到中级阶段,你会发现一个很明显的分水岭:同样标着“70%黑巧克力”,有些人每批做出来的风味、流动性、脱模状态都像开盲盒,有些人却能照着配方设计稿稳定复现,连切面光泽都大差不差。差距不在原料贵不贵、…

阅读更多 →
PyTorch手写数字识别实战:Tkinter GUI+实时预处理+ResNet18微调 2026/10/1 4:39:19

PyTorch手写数字识别实战:Tkinter GUI+实时预处理+ResNet18微调

简介:本资源是一套基于PyTorch实现的手写数字识别完整项目,面向Python与深度学习初学者、课程设计学生及AI入门实践者,解决从模型训练到交互式部署的一站式学习需求。压缩包共5个文件(4个Python源码1个预训练.pth模型)…

阅读更多 →
C++ unordered_map底层原理:哈希冲突、负载因子与rehash全解析 2026/10/1 4:39:19

C++ unordered_map底层原理:哈希冲突、负载因子与rehash全解析

能坚持把unordered_map的底层搞明白的人,通常都是被面试题狠狠教育过一轮之后才下的决心。市面上聊 C 哈希表的文章不少,但大多数要么只讲 API 用法,要么直接把源码怼你脸上,看完还是不知道这玩意到底为什么快、什么时候会变慢、自…

阅读更多 →
2026实木家具品牌红榜盘点:选购避坑与材质鉴别指南 2026/10/1 4:39:19

2026实木家具品牌红榜盘点:选购避坑与材质鉴别指南

先别急着看榜单。买实木家具这几年,我身边翻车的案例多得能写成一本书——有人花两万买了“橡木床”,到家发现是橡胶木贴皮;有人在全屋定制店交了定金,合同只写“实木”,最后送来一堆密度板。这次聊的《20大实木家具品…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉