news 2026/10/21 21:38:03

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前后端分离选课系统全栈实战:SpringBoot+Vue+MyBatis+MySQL

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 MyBatis+MySQL:复杂筛选查询的底层支撑

数据持久层我选了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_week(1-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 -jar,systemd的好处是进程崩溃后可以自动重启,还能统一看日志。

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 选课轮次与抽签机制的设计心得

项目做到这里已经能稳定跑了,但真正常态化的选课系统还需要选课轮次管理。不同年级分批选课、预选和正选分开、超容量课程随机抽签,这些功能我在设计数据库时就预留了轮次表字段。抽签实现思路是:预选阶段允许超容量申请,截止后遍历申请列表,超过容量的教学班按随机顺序筛选,结果写回选课记录的状态字段。这个逻辑不复杂,但一定要放在事务里执行,避免抽签结果和课程容量不一致。在我个人经验里,先设计好选课轮次表和状态机,再来写业务逻辑,会比一开始就只做"能选能退"要稳妥得多,给后续功能扩展留足了余地。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 21:34:53

8GB显卡也能跑H3视频生成?显存优化与自动化管线实战

说实话&#xff0c;看到“8GB显卡跑H3视频生成”这个目标时&#xff0c;我第一反应是不太信的。视频生成模型的门槛肉眼可见地高&#xff1a;参数动辄几十亿甚至更多&#xff0c;推理时不仅要处理文本条件&#xff0c;还要同时对数十帧图像特征做时序建模&#xff0c;显存压力比…

作者头像 李华
网站建设 2026/10/21 21:32:57

自动药片装瓶机控制系统:从PLC架构到故障排查实战

做制药设备这些年&#xff0c;我接触过不少自动药片装瓶机&#xff0c;从单头半自动到高速转塔式&#xff0c;控制架构换了一茬又一茬&#xff0c;但用户问得最多的永远是同一个问题&#xff1a;这台机器到底靠什么保证每一瓶都数得准、装得对、封得严&#xff1f;答案不在凸轮…

作者头像 李华
网站建设 2026/10/21 21:32:37

词嵌入维度越大越好吗?从踩坑到调参的实战指南

你是不是也跟我一样&#xff0c;刚接触词嵌入时听过一句话&#xff1a;维度不就是嵌入向量的长度吗&#xff0c;越长越能装信息。后来我做了几年NLP相关的工程&#xff0c;发现这句话既对也不对。对的地方在于&#xff0c;低维度确实装不下语义&#xff1b;不对的地方在于&…

作者头像 李华
网站建设 2026/10/21 21:32:36

Linux下SVN版本控制服务搭建实战:从安装到Systemd托管与备份

1. 搭建前必须想清楚的事&#xff1a;方案选型与场景匹配聊到Linux下的版本控制&#xff0c;很多人第一反应是Git。但如果你去一些传统行业、外包协作或公司内网环境里转一圈&#xff0c;会发现SVN服务的进程还坚挺地跑着&#xff0c;有些甚至一跑就是七八年没重启过。这不奇怪…

作者头像 李华
网站建设 2026/10/21 21:30:07

MTCNN人脸检测系统源码拆解:多任务级联卷积网络的工程实践

十几年前我第一次在嵌入式板子上做人脸检测的时候&#xff0c;试过一堆主流检测模型&#xff0c;结果不是精度不够就是速度拉胯。后来回头用MTCNN这套多任务级联卷积网络的方案&#xff0c;反倒是稳稳跑起来了。这两年总有人问我&#xff1a;都什么年代了&#xff0c;还有人用M…

作者头像 李华
网站建设 2026/10/21 21:29:29

UE日志等级实战指南:从Fatal到VeryVerbose的选型与避坑

UE 的日志系统看起来简单&#xff0c;无非是 UE_LOG 加一个等级参数&#xff0c;可真到排查问题的时候才发现&#xff0c;日志等级选得不好&#xff0c;比没有日志还难受。我之前在一个项目里接过一次网上反馈的疑难杂症&#xff0c;玩家反馈“进游戏后过几秒黑屏”&#xff0c…

作者头像 李华