首页 / 资讯中心 / 文章详情

Spring Boot+Vue选课系统实战:从数据库设计到并发控制全解析

Spring Boot+Vue选课系统实战:从数据库设计到并发控制全解析 ★ FEATURED ARTICLE
高校学生选课系统几乎可以说是Java后端开发里被提到最多、也被做烂了的一个实战项目。每年到毕设季我都能遇到身边同学拿着同一个问题反复来问——Spring Boot 搭 Vue 的选课系统源码、数据库脚本、文档三件套凑齐了怎么才能真正跑起来怎么才能让答辩和后续二次开发都不翻车。这篇就把我实际盘这类项目的完整思路、核心模块拆解和踩过的坑整理出来尽量按能直接参考的标准来写。既然是“源码 数据库 文档”这种交付结构说明它面对的典型场景应该是课程设计、毕业设计或者小团队的内部教学项目。系统面向三类角色学生、教师、管理员。学生要能登录、浏览课程、选课、退课、查课表、看成绩教师要能开设课程、查看选课名单、录入成绩管理员则要做用户管理、课程审核、选课时间段控制、公告维护。功能不算多但涉及权限、事务、并发、前后端联调麻雀虽小五脏俱全。适合正在做Java方向课设的同学也适合刚接触前后端分离开发的初学者对照着抄作业。1. 项目整体设计为什么一个选课系统最考验架构取舍1.1 技术选型为什么是Spring Boot Vue而不是其他组合选课系统这类中后台管理系统有一个非常明显的特征页面多、表单多、表格多但交互不算特别复杂。用 Vue 做前端非常合适因为它的组件化开发方式能让课程列表、选课弹窗、成绩表格这类页面复用起来很顺手。Vue 生态里的 Element 组件库几乎是这类项目的标配表格、表单、分页、日期选择器都现成半天就能把页面骨架搭完。后端选 Spring Boot 的理由更直接开箱即用不搞繁琐的 XML 配置自带内嵌 Tomcat打一个 jar 包就能跑。配合 MyBatis 或者 MyBatis-Plus 操作数据库CRUD 的效率非常高。相比 SSM 时代要手动配一大堆 BeanSpring Boot 把配置收敛到application.yml一个文件里对课设级别的项目来说把精力集中在业务逻辑上才是正事。但技术选型也有一些值得追问的地方。比如为什么不用 Spring Cloud 微服务原因很简单选课系统的业务规模和团队规模都远没到需要微服务的程度。引入注册中心、网关、远程调用只是徒增复杂度跑起来还要多开好几个进程对毕设和课设来说吃力不讨好。单体应用加前后端分离是这个体量下的最优解。1.2 角色与功能模块拆分我从实际功能的视角把系统分成四大块登录认证、学生端选课、教师端教务、管理员端管理。登录认证是门面也是安全底线。系统里三个角色共用一张登录逻辑后端签发 token 后返回给前端前端把 token 存在本地之后每个请求都带上。前端路由守卫根据角色判断能不能进入某个页面后端接口再用拦截器做二次校验。前后端双重校验才不算裸奔。学生端核心是选课中心和个人课表。选课中心展示可选课程带课程名称、授课教师、学分、上课时间、剩余名额这些字段。点击选课按钮后后端先做课程容量判断再做重复选课判断通过后写入选课记录。个人课表按星期几和节次展示这块的难点在于如何把课程时间字段解析成表格的行列坐标。教师端主要做课程维护和成绩录入。开课信息由教师提交管理员审核后才会出现在学生的可选列表里。成绩录入则要根据选课记录把每名学生的成绩回填到成绩表。管理员端是权限最高的模块用户管理、课程管理、选课时间窗口设置以及最基础的公告发布都放在这里。选课时间窗口是一个容易被忽略但很重要的设计系统只允许在指定时间段内开启选课功能通过一张配置表存开始时间、结束时间、是否开放三个字段比用硬编码开关合理得多。1.3 选课状态流转的逻辑选课系统里课程状态是有生命周期的。大致是待审核 - 可选 - 已选满 - 选课结束 - 结课归档。“可选”和“已选满”之间的切换不是靠后台手动改状态而是由已选人数和课程容量的对比动态计算出来的。这里有一个代码习惯问题不要在状态字段里存“已满”这种值而是每次查询时用selected_count capacity判断把剩余名额算出来。原因很简单状态是瞬时结果一旦有人退课他就应该立刻恢复为可选。如果写死在状态字段里退课场景就得多维护一个翻转逻辑很容易出bug。从数据库角度选课记录表应该维护一个状态字段或通过时间判断是否有效从业务角度退课后课程人数要减一这个操作和插入选课记录一样必须包在同一个事务里。这些链路想清楚后再写接口就不容易乱。2. 选课系统数据库设计表结构、唯一约束与并发控制2.1 核心表结构到底该怎么建选课系统数据库表不算多我通常按业务域拆成六张核心表用户表、学生表、教师表、课程表、选课记录表、成绩表再加一张公告表和一张配置表备用。用户表用来支撑登录认证字段一般有 id、用户名、密码、角色、状态。密码必须存加密后的结果明文存储是大忌。学生表和教师表可以看作用户表的扩展表存放学号、姓名、院系、工号、职称这类业务属性。很多人图省事直接用一张用户表加 role 字段搞定但用户表塞太多业务字段会让表变得臃肿还是分成两到三张表更干净。课程表是业务核心。字段包含课程名称、课程编号、授课教师ID、学分、上课时间、上课地点、课程容量、已选人数还可能包含上课周次、考核方式。这里要特别注意课程时间字段的设计方式。常见的做法是用一个字段存“周几第几节”例如1-34表示周一第34节。这种文本方案简单直接但做冲突检测时解析逻辑很痛苦。更好的方案是拆成星期字段、开始节次、结束节次三个字段查询时直接比较数值冲突检测和课表渲染都会容易非常多。选课记录表是另一张关键表。字段包括 id、学生ID、课程ID、选课时间、状态。状态字段可以标记为“已选”“已退”“已结课”这样即便退课了也有历史轨迹。成绩表则关联选课记录表和课程表存分数也可以直接把分数冗余到选课记录表里节省一张表看具体需求。SQL 脚本可以给出课程表的建表参考CREATE TABLE course ( id bigint NOT NULL AUTO_INCREMENT, course_code varchar(32) NOT NULL COMMENT 课程编号, course_name varchar(100) NOT NULL COMMENT 课程名称, teacher_id bigint DEFAULT NULL COMMENT 授课教师ID, credit decimal(3,1) DEFAULT NULL COMMENT 学分, capacity int DEFAULT 50 COMMENT 课程容量, selected_count int DEFAULT 0 COMMENT 已选人数, week_day tinyint DEFAULT NULL COMMENT 星期几1-7, start_section tinyint DEFAULT NULL COMMENT 开始节次, end_section tinyint DEFAULT NULL COMMENT 结束节次, location varchar(100) DEFAULT NULL COMMENT 上课地点, status tinyint DEFAULT 0 COMMENT 审核状态0待审核 1通过 2不通过, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 选课并发数据库层面怎么防超选选课系统最值钱的经验就是并发控制。一个热门课程容量只有60人瞬间有两百个学生同时点选课如果代码只是“查剩余名额、够就插入”那一定会超卖实际选上的人数超过课程容量。数据库层面第一步要做的就是唯一约束。给选课记录表加上(student_id, course_id)唯一索引这样从数据库源头就杜绝了同一学生重复选同一门课的可能。否则应用层就算查过了并发瞬间两次插入也有可能都成功。第二步是解决超卖。我见过不少项目在 Service 方法上直接加synchronized关键字但 JVM 级别的锁只在单实例部署时有效而且加了锁也不一定能阻止跨请求看到的旧值。更务实的做法是使用乐观锁更新已选人数时让 SQL 条件带上当前值校验。UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count capacity;如果影响行数为0说明要么课程不存在要么已经满了业务层直接返回“课程已满”。这一条 SQL 相当于原子地把人数校验和加一操作合并了不用额外加锁性能也好很多。第三整个选课动作要用Transactional包起来。插入选课记录和更新课程人数必须同时成功或同时失败不然会出现选课记录有了但人数没加上去的脏数据。2.3 初始化脚本和演示数据的重要性三件套里的数据库脚本绝对不只是“建表语句”那么简单。一段好的初始化脚本应该包含默认管理员账号、几个学生账号、教师账号、足够数量的课程数据和选课记录。默认账号的密码需要和加密方式保持一致。比如前端用 BCrypt 加密那 SQL 里就要写入 BCrypt 加密后的字符串。这一点是我见过最容易出错的地方很多人导入脚本后登录一直失败最后发现是初始化数据里的密码格式和应用代码的加密算法不匹配。演示数据也要刻意设计。课程时间要覆盖周一到周五的不同节次至少要有两门课在时间上重叠这样前端课表渲染和课程冲突检测的效果才能直观展示出来。文档里如果能配一段“账号对照表”把学生、教师、管理员的示例账号列清楚整个交付质量会立刻上一个档次。3. 前后端核心实现登录鉴权、选课接口与页面联动3.1 后端鉴权链路从一次登录到每次请求登录接口的逻辑看起来就是查用户表、比对密码、返回 token但实际链路里有几个容易翻车的点。第一是密码比对。用户提交的明文密码后端先加密再和数据库里的密文比对。我用 BCrypt 比较多因为它内置盐值每次加密结果都不一样也能防止彩虹表攻击。有人为图省事用 MD5 加盐但 MD5 本身不是为密码存储设计的安全性不如 BCrypt建议直接选后者。第二是 token 签发。可以引入 JWT把用户ID、角色、过期时间写进 token。签发的核心代码类似这样String token Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 86400000)) .signWith(secretKey) .compact();签完之后返回给前端前端存到 localStorage 或者直接放内存里。放 localStorage 的缺点是有 XSS 风险但课设项目里这个取舍可以接受只要清楚风险即可。第三是后端拦截器统一鉴权。写一个 HandlerInterceptor对所有/api/**请求先放行登录接口再校验 header 里的 token 是否有效从 token 里解析出当前用户和角色。这里额外的关键点是不要只校验“有没有 token”还要校验角色是否匹配接口要求的角色。我之前见过一个项目管理员接口只在前端做了按钮隐藏后端接口没有任何角色判断。懂技术的人直接用接口工具带一个普通学生的 token 去请求管理员接口照样能拿到数据。这是个很伤的安全漏洞后端必须做这层校验。3.2 选课接口的业务逻辑与事务边界选课接口是整个系统里业务逻辑最密集的地方。我直接给出核心思路Transactional(rollbackFor Exception.class) public ResultVoid selectCourse(Long studentId, Long courseId) { // 1. 校验选课时间窗 if (!configService.isSelectOpen()) { return Result.error(当前不在选课时间内); } // 2. 查询课程判断是否存在且已审核通过 Course course courseMapper.selectById(courseId); if (course null || course.getStatus() ! 1) { return Result.error(课程不存在或未开放); } // 3. 校验是否重复选课 int count selectCourseMapper.countByStudentIdAndCourseId(studentId, courseId); if (count 0) { return Result.error(你已选过这门课程); } // 4. 乐观锁更新选课人数返回0说明课程已满 int rows courseMapper.increaseSelectedCountIfExists(courseId); if (rows 0) { return Result.error(课程名额已满); } // 5. 插入选课记录 selectCourseMapper.insert(studentId, courseId); return Result.success(选课成功); }这段逻辑的要点在于第4步和第5步都在同一个事务里第4步利用影响行数判断名额。如果第4步成功但第5步失败整个事务回滚人数也一并回滚回去不会出现人数虚高。退课接口则是反过来的操作先删除选课记录再把课程人数减一。步骤看着简单但要注意两个细节。一是只允许删除当前登录学生自己的选课记录必须带上 studentId 条件否则就会变成任何人能退任何人的课。二是退课后释放名额的操作同样要在事务里而且顺序应该是先删记录再减人数。3.3 前端页面结构与路由权限控制前端页面我按“登录页 主布局 子页面”三层来组织。主布局是一个侧边栏加顶栏的结构根据角色不同渲染菜单项。学生看到的是“课程中心”“我的课表”“我的成绩”教师看到的是“课程管理”“选课名单”“成绩录入”管理员看到的是“用户管理”“课程审核”“选课设置”“公告管理”。路由守卫是前端权限控制最重要的代码。Vue Router 的 beforeEach 钩子里先判断有没有 token没有就重定向到登录页。有 token 再判断目标路由要求的角色是否匹配当前用户身份不匹配就重定向到首页或者403页面。axios请求封装也很关键。统一在请求拦截器里加Authorization: Bearer token在响应拦截器里统一处理 401、403 和业务错误码。很多同学在每个页面里单独写请求逻辑不仅重复而且后期改接口上下文时极易漏改。集中封装之后前端代码会清爽得多。这里给出一个常用的 Vue 开发环境跨域代理配置devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/login开发环境会自动转发到http://localhost:8080不需要在后端写花里胡哨的 CORS 配置。等到生产环境前端部署到 NginxNginx 再配置一层反向代理指向后端服务是同一个思路。4. 拿到源码后怎么跑起来环境、配置与联调全流程4.1 环境准备JDK、Maven、Node、MySQL版本怎么选跑这套项目之前先把环境版本对齐否则一步错步步错。后端没有那么挑剔。JDK 8 或者 JDK 17 都能跑Spring Boot 2.x 用 JDK 8 最保险Spring Boot 3.x 则要求 JDK 17 起步。Maven 用 3.6 以上版本基本没问题。MySQL 我推荐 8.0驱动用com.mysql.cj.jdbc.Driver连接串里加上serverTimezoneAsia/Shanghai和characterEncodingutf8能避开时差和中文乱码两个大坑。前端反而更容易出问题。Vue 2 的老项目用 Node 14、16 都行但如果你拿到一个依赖了node-sass的项目Node 版本太高会编译不过。这时候可以看package.json里的依赖版本如果是node-sass建议降到 Node 14 试如果是sass或者dart-sassNode 16 以上通常没问题。Vue 3 项目配合 ViteNode 18 会比较顺手。4.2 数据库导入和后端配置拿到数据库脚本后建一个空库直接导入 SQL 文件。导入之前先检查一下脚本里有没有CREATE DATABASE和USE语句有的话要注意数据库名和代码里的连接串是否一致。我见过有人把脚本导到默认库但代码连的是另一个库结果页面报表全是空的排查半天才找到原因。后端的application.yml是最需要改的文件核心配置如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_select?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver数据库密码、端口、库名按自己环境改。如果接口路径统一带/api前缀注意检查后端有没有配置 context-path别让前端请求路径和后端实际路径对不上。启动后端时最直观的验证方式是看控制台日志有没有报数据库连接错误。启动成功后再用接口测试工具请求一个登录接口传管理员账号和密码能正常返回 token就说明后端链路通了。4.3 前端启动、联调与打包交付前端依赖安装尽量用npm install如果网络环境不好可以用镜像源。装完依赖后启动开发服务访问页面能打开登录界面就进入联调环节。联调阶段优先验证的是登录。登录成功跳转到首页侧边栏菜单按角色渲染这就说明前端的 token 存储、路由守卫、用户信息获取都正常。接着逐角色验证核心链路学生选一门课、课表刷新出结果教师给学生录入成绩管理员把某门课程下线。每验证完一条链路前端控制台和后端日志都确认没有报错这套系统就算真正跑通了。打包交付时要区分开发和生产。开发环境直接npm run dev生产部署先npm run build生成静态文件再让 Nginx 托管静态文件同时把/api请求反向代理到后端的启动端口。后端则用mvn clean package打成 jar 包java -jar启动即可。文档里如果能把这个部署流程写清楚项目就不再只是“本地能跑”而是一个真正可交付的成果。5. 实操中高频踩坑从报错到排查的一次性整理5.1 高频报错对症下药速查表这两年我帮人排查过不下十套选课系统问题和解决方案高度集中。整理成表格方便对照症状常见原因解决方案启动后登录接口404前端请求路径和后端Controller路径不一致或未加/api前缀统一接口路径检查代理配置数据库中文乱码连接串缺少UTF-8参数或建库时默认字符集非UTF-8连接串加characterEncodingutf8建库用utf8mb4选课一直提示“已满”初始化数据里selected_count被误写成等于capacity检查SQL脚本的初始数据前端页面空白白屏依赖没装全或Node版本不兼容或路由history模式刷新404重新npm install检查Node版本刷新问题用hash模式发布到服务器后样式丢失Nginx没配置try_files改Nginx配置让所有路径都回到index.html登录一直失败初始化密码加密格式和代码加密算法不一致用同一套加密方式重新初始化管理员密码这些坑单独看都很小但每个都能卡人一小时以上。所以我把“看报错日志”当成第一原则。前端报错按F12看 Network 和 Console后端报错看控制台堆栈大部分问题都能在十分钟内定位。5.2 排查问题的通用方法从日志到数据层层定位我把排查思路总结成三步。第一步复现问题并确认是前端问题还是后端问题。直接在浏览器 Network 面板里看请求有没有发出去、返回了什么状态码。如果请求都没发出去问题大概率在前端的路由或拦截器如果请求发了但报错就把响应体的错误信息和后端日志对应起来。第二步确认是不是数据库问题。把接口里执行的 SQL 拿出来手动在数据库里跑一遍看返回结果是否符合预期。很多逻辑问题其实出在 SQL 条件写错比如查选课记录时少写了student_id条件导致查出别人的记录重复选课判断就失灵了。第三步确认是不是环境问题。本地能跑生产跑不了十有八九是端口、防火墙、数据库权限、Nginx 代理这几类问题。把生产环境的配置文件逐个对比本地很快能找到差异。5.3 容易被忽略的数据安全细节选课系统涉及学生个人信息和成绩数据哪怕只是一个课设级项目“不裸奔”也是底线。后端接口必须鉴权尤其是以下接口很多人会漏掉。查询成绩列表的接口只能允许学生本人查询自己的成绩或者教师和管理员按条件查询而不能允许任意登录用户直接遍历全表。修改密码接口要校验旧密码。管理员接口必须校验管理员角色不能只靠前端菜单隐藏。课程删除接口要检查是否已经有选课记录如果有最简单的是不允许物理删除改成下架状态不然成绩表和选课记录表会留下孤儿数据。文档里也应该专门补一段“安全说明”哪怕只是简单描述接口鉴权方案答辩时都会显得全面很多。6. 二次开发思路从毕业设计到真实生产环境还能加什么6.1 选课高峰期的性能优化方向如果把选课系统放到真实校园环境选课开始的前几分钟会有高并发请求涌进来。真实的选课系统内部是排队机制公开可选时间一到大量学生同时点选课服务压力非常大。一个工程上常用的优化手段是引入 Redis。选课开始时把课程容量预加载到 Redis 里选课接口先用 Redis 的原子减操作扣减库存扣减成功再落库写选课记录异步同步数据库里的selected_count。这样即使用户量大Redis 单机也能扛住很高的 QPS数据库不会被打崩。更进阶的做法是记录用户请求时间顺序管理员后台可以设置按学号尾号分批开放选课类似“分时段放号”的思路。这对真实教务场景来说比单纯追求接口性能更实用。把这两个优化点写进扩展方案里项目立马就有了层次。6.2 教学管理功能增强排课冲突与通知我实际用下来课程冲突检测是选课系统里非常值得增加的后端逻辑。学生选新课时系统要把新课的时间和已选课程逐条比对如果没有时间重叠才允许提交。实现上可以写一个专门的方法查出该生所有已选课程再和新课逐一比较week_day、start_section、end_section有重叠直接拦截。这一块特别适合在文档里详细展开因为它的业务逻辑比单纯CRUD复杂能体现设计能力。另一种增强是通知推送。管理员发布选课公告时学生登录后能看到未读提醒。简单实现就是在公告表加是否已读字段复杂一点可以接 WebSocket把选课开始时间变化的实时消息推到所有在线学生端。加上这个功能后系统会更像一个完整的教务工具。6.3 移动端适配与数据可视化真实校园里的学生更多会用手机访问选课系统。把现有 Vue 前端适配成移动端或者另做一个基于移动端的简化版本都是常态需求。可以在现有布局上直接引入移动端适配侧边栏折叠成抽屉表格改成卡片式展示课表页面简化成单列列表。门槛不高但体验提升非常明显。数据可视化是另一个加分方向。管理员后台增加统计面板用图表展示各课程选课人数、热门院系选课率、每学期开课数量变化。技术选型上用 ECharts 就能满足大部分需求和 Vue 配合也很好。这些内容不需要改动后端核心逻辑只是新增查询接口和前端展示层最适合作为“下一步工作计划”写进总结文档。折腾完这一整套我最大的感受是选课系统看着简单真正往下挖全是细节。三件套不只是跑通的代码也是一个完整工程习惯的缩影。我自己在后来再看这类项目时会先去看数据库设计里的唯一约束和事务边界再看后端接口的角色校验然后才看页面布局。这也算是一个提醒如果你准备拿这个项目去答辩或者作为入行作品别只停留在能跑通的阶段。把表结构重新画一遍把选课接口的并发控制思路讲清楚再顺手补一个冲突检测或数据统计功能深度立刻就不一样。最后再分享一个小技巧项目里所有密码相关的初始化数据用和代码同一个加密工具生成别再手写固定哈希这个小习惯能给后面的自己省下大量排查时间。
阅读完成 · 觉得有帮助?
咨询建站