资讯动态

前后端分离选课系统全栈实战:SpringBoot+Vue+MyBatis+MySQL

发布时间:2026/10/21 21:38:11 来源:尧图企业网站定制
1. 为什么我会做这么一套选课系统旧方案撑不住了事情得从一次真实经历说起。某高校的教务选课系统每到选课周就进入半瘫痪状态学生这边疯狂刷新页面那边服务器频繁出现超时和报错数据库连接池被瞬时打满后直接拒绝新请求。有人上午选好的课下午再看居然被挤掉了数据对不上。我接手这个问题时最头疼的不是写新功能而是搞清楚旧系统为什么会变成这样。旧选课系统是典型的单体JSP项目所有页面渲染和业务逻辑都堆在服务端。学生端、教师端、管理端全挤在同一套工程里每次改动都要整体停服重新部署。更麻烦的是选课规则的变化频繁跨专业选课限制、先修课校验、容量调整这些需求都靠直接改数据库和硬编码判断来支撑时间一长谁都不敢乱动。投入产出比太低于是我决定用它作为原形重做一套前后端分离的选课系统技术栈锁定 SpringBoot Vue MyBatis MySQL。这套组合不是偶然选的后面我会详细说。想学习选课系统从零搭建、或者准备课程设计/毕业设计参考开源项目的同学这篇文章应该能帮你少走不少弯路我尽量把设计和部署过程中的真实想法都写出来。2. 技术选型的底层逻辑这套组合凭什么撑住选课场景先说说我是怎么定技术栈的。很多人一上来就纠结用这个框架还是那个框架我觉得先别急着比功能要回到场景本身选课系统的核心矛盾是高峰瞬时并发高、规则逻辑复杂、上下学期业务变化多。围绕这三点技术选型的目标就很明确——开发效率高、社区生态成熟、部署不折腾、后期好改。2.1 SpringBoot内置容器和自动配置省了多少事如果还是SSM时代光配置Spring、SpringMVC、MyBatis三套XML就要花很多时间项目结构还容易乱。SpringBoot把这些问题收进自动配置里同时内置了Tomcat本地调试不用单独装容器环境。我当时的感受是从新建工程到能跑通第一个接口十分钟足够这对后期频繁调整选课规则来说太重要了。选课系统的核心是接口逻辑而不是框架本身所以框架越简洁越好。SpringBoot的starter机制让第三方依赖管理变得很干净比如做权限用Spring Security或者JWT做参数校验用Spring Validation这些都是几行依赖就能接入的。对比我见过的老系统光一个登录过滤器就写了几百行配置和代码这种维护成本在业务变化快的时候完全是负担。2.2 Vue三个理由说明它比JSP更契合这个场景前端我选Vue是因为三个实际原因。第一选课页面里有大量联动交互——选了专业筛选条件、课程列表要实时刷新选课按钮的状态要根据是否冲突、是否满员、是否已选动态变化这些场景用数据驱动的框架写起来远比JSP里面写jQuery顺手。第二Vue的组件化让代码能复用比如课程卡片组件、分页组件、弹窗组件管理端和学生端都能共用一套。第三前后端分离之后接口联调可以并行推进前端mock数据先跑着后端接口写好了再对接整体周期能压缩不少。我实际用的是Vue 3配合Vite开发服务器启动很快热更新反应也灵敏。UI方面选的是Element Plus表格、表单、日期选择这些现成组件覆盖了管理端的大部分需求不用自己从零写样式整体观感也稳定。2.3 MyBatisMySQL复杂筛选查询的底层支撑数据持久层我选了MyBatis而不是JPA主要原因还是数据查询的复杂度。选课系统的课程列表需要同时按学院、专业、课程类型、上课时间、剩余容量等多个维度筛选还要做模糊搜索和动态排序。这类需求用MyBatis的XML写动态SQL非常灵活if、choose这些标签可以直接控制查询条件拼装相比之下JPA在动态查询上写起来不够直观调试SQL也不如直接读XML来得快。MySQL不用多说免费、稳定、运维资料多高校场景完全可以覆盖。我比较看重InnoDB的行锁和事务能力这在后面做选课容量校验的时候是基础。3. 数据库设计先想清楚谁在什么时间选了什么课数据库是选课系统最重要的部分因为选课本质上就是一套关系约束——一个学生可以选多门课程一门课程可以被多个学生选而选课记录就是关联这两者的核心实体。关系建模一旦出问题后面写业务逻辑会处处别扭。3.1 七张业务表的字段设计我把表拆成了用户体系、课程体系、选课体系和辅助体系四大块。用户体系直接用两张表用户表存统一登录账号、密码哈希、用户类型学生/教师/管理员、姓名、学号工号角色权限不单独建表用用户类型字段配合枚举在代码里控制因为选课系统的角色很固定过度设计反而浪费。学生和教师共用一个用户表靠用户类型区分学院和班级信息放在学生档案表的扩展字段里。课程体系的表比较多。课程表负责课程的基本信息比如课程编号、课程名称、学时学分、上课地点、课程性质、所属院系课程表不直接存选课容量而是把容量关联到具体的教学班因为同一门课程可能开多个班每班容量不同。教学班表才是选课的主体单位它关联课程ID、教师ID、学期还带一个已选人数字段方便界面上直接显示剩余容量。我额外建了教学班上课时间表存星期几、第几节课、单双周等信息一门课可能有多个上课时段这个表就是用来做冲突检测的。选课记录表的核心字段是选课ID、学生ID、教学班ID、学期、选课时间、选课状态。状态我设计了已选、退选、待定三种待定用于预选阶段选课轮次结束后统一抽签确定结果。3.2 时间冲突检测算法在SQL里的实现学生选课不能同时选两门时间冲突的课程这是业务刚需。检测思路很直白先查出学生当前所有有效选课记录关联到的上课时间集合再把新选教学班的上课时间一条条拿来比对看是否在同一学期内有重叠。注意这里不是简单判断星期几相同就冲突还要比较节次区间比如上午第一、二节和三、四节虽然都在同一半天但并不重叠。我的做法是用时间段的起始和结束字段而不是课节编号。每条上课时间记录存day_of_week1-7、start_section开始节次、end_section结束节次。判断两节课冲突的条件是day_of_week相同且两段区间存在交集SQL里写就是新时间段的start_section小于旧记录的end_section且新时间段的end_section大于旧记录的start_section。这个判断要放在事务里执行因为学生哪怕只有一瞬间做出修改也不能带着冲突选课记录入库。3.3 索引与容量字段选课高并发的性能基础课程列表页查询条件多很容易出现全表扫描。我在教学班表上加了联合索引把学期、学院、课程性质这几个高频过滤条件放进去。选课记录表则建了学生ID学期、教学班ID选课状态这两组索引前者保证学生查看已选课程时快速返回后者保证后端做容量校验和查询课程已选列表时不拖后腿。容量字段的已选人数我放在了教学班表上。有人可能会问为什么不直接对选课记录表做 count因为高并发下 count 每次都要实时扫索引耗费比较大而且维护一个整型字段做加减再配合原子更新可以帮我们在SQL层面锁住并发边界。这个字段在选课成功时UPDATE teaching_class SET selected_count selected_count 1 WHERE id ? AND selected_count capacity等于让数据库帮我们做了容量校验。4. 后端核心逻辑选课不是简单的insert一条记录后端模块我按业务边界拆成了用户、课程查询、选课、退课、教学班管理等。下面重点讲几个容易出问题的地方这些也是整个系统的技术难点。4.1 JWT登录与权限控制登录接口接收账号和密码密码用BCrypt做哈希存储坚决不存明文。登录成功后后端签发JWT令牌令牌里只放用户ID和用户类型不塞敏感信息有效时间设为两小时前端每次请求把令牌放到请求头里由一个拦截器统一解析和校验。权限控制我用的是Spring MVC的拦截器加自定义注解。先把所有人都必须放行的接口列白名单——登录、注册、公开的课程列表——其余接口全部走拦截器。拦截器里解析JWT后取出用户类型再判断当前请求路径是否在该角色的权限范围内。比如学生只能访问/student/**和公共接口教师只能访问/teacher/**管理员可以访问/admin/**。这样做虽然比Shiro和Spring Security轻量但足够应对选课系统的场景代码量也少排查问题的时候一目了然。4.2 选课接口的并发控制与幂等设计选课接口是整个系统压力最大、逻辑最容易出错的地方。假设1000个学生同时抢一个剩余容量只有300的教学班如果每个人都先查容量再insert后查的先插入容量就超了。我选用的方案是事务内先做并发安全的容量扣减再做选课记录插入。具体顺序是开启事务对教学班记录执行带条件更新UPDATE teaching_class SET selected_count selected_count 1 WHERE id #{classId} AND selected_count #{capacity}然后看更新影响的行数。影响行数为零说明已经满了直接抛出容量不足的异常影响行数为1说明容量扣减成功再插入选课记录同时校验时间冲突最后提交事务。这一步把容量判断和扣减合并在一条原子SQL里不需要悲观锁也不需要引入Redis就能在MySQL层面保证不会超卖。还要注意幂等。学生同一时间收到多个请求或者前端按钮被连续点击都可能产生重复选课记录。我在选课记录表给学生ID、教学班ID、学期建了联合唯一索引插入重复记录会触发异常再在业务代码里捕获并转成你已选过这门课的友好提示。4.3 退课、改选与数据一致性处理退课相对简单但同样要放在事务里先删除选课记录再对教学班的selected_count做减一操作。有人会问先删还是先减我的习惯是先删记录后减容量这样如果减容量失败事务回滚时删除操作也会回滚不会出现记录没了但容量没释放的情况。改选则是先退旧课再选新课的组合操作。这里要注意如果先用退课接口释放容量再调用选课接口抢占新课在高并发下容量可能被别人抢走。我在系统里提供了一个统一的换课接口在一个事务里完成退旧选新保证两步操作要么都成功要么都失败。用户界面上则不直接开放单个退选和选课按钮的组合操作而是用一个换课入口引导操作。5. 前端页面从零到能用的过程记录前端部分我按角色视角拆成了三个界面学生端选课大厅、教师端教学班管理、管理员端基础数据维护。整体都基于Vue 3项目搭建下面挑几个关键实现讲讲。5.1 项目搭建与路由权限设计项目用Vite初始化参考结构大致是src/views下按角色分目录student、teacher、admin各放各的页面公共组件抽到components里。路由权限用了路由守卫。全局前置守卫里每次跳转前先读本地存储的JWT和用户类型如果没有令牌就重定向到登录页有令牌但访问的角色路径不匹配就重定向到当前角色对应的首页。页面侧边菜单也根据用户类型动态渲染学生看到的是课程大厅、我的课表、选课结果教师看到的是我的课程、选课名单管理员看到的是学生管理、课程管理、教学班管理、选课轮次管理。5.2 核心页面组件拆解课程大厅是整个系统交互最复杂的页面。顶部是筛选栏按学期、学院、课程性质、是否仅显示可选等条件筛选。中间是课程列表每一行课程卡片展示课程名、教学班信息、教师、上课时间地点、剩余容量和选课按钮。选课按钮的状态逻辑很关键已满时置灰显示已满已选时置灰显示已选与已有课程时间冲突时置灰显示时间冲突只有真正可选时才高亮显示选课。我写了一个computed函数处理按钮状态它会读取已选课程的时间集合实时和当前课程对比。页面不做整页刷新选课成功后直接更新局部状态比如调用接口返回后重新拉取课程列表或更新当前行的剩余容量。5.3 Axios封装与Token处理前端请求统一通过封装好的Axios实例发送。我在request拦截器里统一加Authorization请求头从localStorage取JWT在response拦截器里统一处理后端返回的code比如未登录或令牌过期时清掉本地登录态跳回登录页网络错误时统一弹出Message提示不让每个页面各自写一遍错误处理。实际开发里我还加了一个小的优化选课接口返回后前端如果发现剩余容量已经变成0会把所有卡片里该教学班的按钮状态一次性置灰避免用户再去点击一个必然失败的按钮这个小细节能明显降低无效请求量。6. 手动部署的完整链路从本地跑通到Linux上线部署是很多同学卡住的地方本地跑得好好的一到服务器就各种问题。我在这里把完整链路列一遍按顺序操作基本不会出错。6.1 环境准备版本搭配别踩坑我使用的版本组合是JDK 1.8、Maven 3.8、Node 16、MySQL 8.0。SpringBoot版本选2.7.x对应MyBatis-Starter用2.x系列pom.xml里加上spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-j、spring-boot-starter-validation这几个依赖就够了。服务器上需要先装好JDK和MySQL。MySQL在Linux下安装完要记得修改root密码并给项目单独建一个数据库用户不要用root直接连业务库。数据库创建好后导入我提供的schema.sql里面包含了全部建表语句和初始数据包括管理员账号、测试学生账号和几门示例课程。6.2 后端打包与systemd服务配置后端打包是标准的Maven流程在项目根目录执行mvn clean package -DskipTests成功后在target目录下生成 jar 包。注意数据库连接信息在application.yml里改成本机实际地址和账号我习惯把关键配置用环境变量覆盖比如DB_HOST、DB_PASSWORD这样换环境部署不用改代码重建。部署时我用systemd托管jar进程新建一个service文件指向Java路径和jar包路径然后systemctl enable开启开机自启systemctl start启动。相比直接用nohup java -jarsystemd的好处是进程崩溃后可以自动重启还能统一看日志。6.3 前端构建与Nginx反向代理前端构建很直接在项目目录里创建.env.production把接口地址写成一个环境中容易改的变量比如VITE_API_BASE_URL/api然后执行npm run build生成dist静态目录。Nginx配置核心有两段。一段是静态资源服务location /指向dist目录并配置try_files规则保证前端路由刷新时不出现404。另一段是反向代理location /api/把请求代理到后端服务比如proxy_pass http://127.0.0.1:8080;同时注意替换/api前缀让后端接口路径正确匹配。我这里用/api作为统一前缀前端所有请求都走相对路径这样前端打包出来的资源可以放在任意服务器上只要代理配置保持一致即可。7. 上线后的实测结果与排坑心得系统真正上线运行之后我才发现很多在测试环境暴露不出来的问题。这里挑三个让我印象最深的也是后续做类似项目一定会遇到的坑。7.1 真实选课场景下的性能表现上线第一轮选课时我预判并发会很高提前做了压测。在500人同时在线选课的压力下接口响应时间稳定在200毫秒以内数据库连接池用的是默认的HikariCP配置最大连接数调到60后没有出现连接耗尽。靠的就是前面提到的原子扣减容量SQL和联合唯一索引这两招让MySQL在极端情况下也没有出现超卖。但有一个细节值得强调选了课后学生端的课表接口在高峰期也会被频繁请求我的课表查询逻辑里加了Redis缓存把学生每周的课表数据缓存60秒高峰期这部分压力直接降下来了。如果不想引入Redis也可以在前端做缓存但服务端缓存是更稳妥的方案。7.2 三个让我印象深刻的Bug复盘第一个Bug是事务失效。我一开始把选课逻辑写在Service里然后在同类的另一个方法里调用结果事务完全没生效出现容量已扣但选课记录没插上的情况。原因是Spring事务是通过AOP代理实现的同类内部自调用是不走代理的。当时排查了半天最后把事务边界拆到独立Service类里问题才解决。第二个Bug是时间冲突漏判。我的上课时间表支持一门课多个时段一开始只在教学班表上存了一个字符串字段类似周一3-4节导致冲突检测需要解析字符串又难维护又容易错。后来改成独立的上课时间明细表用数值字段存储节次和星期SQL比较就变得简单可靠。这个教训是会变化的数据不要塞进单个字段里。第三个Bug是前端按钮连点。学生双击选课按钮会产生两个几乎同时到达的请求后端虽然有唯一索引兜底但会向用户返回一个不太友好的错误提示。我在前端做了按钮提交状态的锁定请求发出后按钮立即置为提交中并禁用几秒钟内不允许重复点击这样既保证了体验也减少了无谓的数据库访问。7.3 选课轮次与抽签机制的设计心得项目做到这里已经能稳定跑了但真正常态化的选课系统还需要选课轮次管理。不同年级分批选课、预选和正选分开、超容量课程随机抽签这些功能我在设计数据库时就预留了轮次表字段。抽签实现思路是预选阶段允许超容量申请截止后遍历申请列表超过容量的教学班按随机顺序筛选结果写回选课记录的状态字段。这个逻辑不复杂但一定要放在事务里执行避免抽签结果和课程容量不一致。在我个人经验里先设计好选课轮次表和状态机再来写业务逻辑会比一开始就只做能选能退要稳妥得多给后续功能扩展留足了余地。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑