1. 从选题到落地:为什么学生选课系统是SpringBoot+Vue的黄金练手项目
做后端开发这些年,我见过太多人问我同一个问题:“想学SpringBoot全家桶,到底做什么项目才能把技术栈串起来?”我的回答一直是——管理系统类项目,尤其是学生选课系统。原因很简单,它麻雀虽小五脏俱全,覆盖了权限控制、复杂关联查询、事务处理、前后端分离对接这些真实业务中天天要用的硬核知识点。
这次要拆解的项目是一套基于SpringBoot + Vue的学生选课系统源码,持久层用MyBatis,数据库用MySQL。整个系统分为学生端和管理员端:学生可以登录系统、浏览课程列表、选课退课、查看个人课表;管理员维护课程信息、管理学生数据、统计选课情况。听起来功能不算多,但整套代码跑通之后,你对SpringBoot的项目结构、Vue组件的生命周期、MyBatis的动态SQL、MySQL的表设计就会有非常立体的认知。
我自己复现这套系统的时候,最大的感受是:它的代码风格干净,注释完整,没有刻意堆砌花哨的框架,每一处设计都有理由。直接拿源码来跑一遍,比看十篇“SpringBoot入门教程”都管用。不管是准备毕业设计、求职项目经验,还是团队内部培训练手,这套系统都值得花时间撸一遍源码。
2. 整体设计与技术选型:每一层为什么这么搭
2.1 前后端分离架构的核心价值
这个项目采用的是标准的B/S模式,前后端彻底分离。前端跑在Vue的开发服务器上,默认端口8080,通过HTTP请求访问后端接口;后端是SpringBoot内嵌的Tomcat,端口8081,只负责处理业务逻辑和数据库交互,不关心页面长什么样。两端通过JSON格式的数据进行通信。
这种分离方式在实际工程项目里几乎成了标配,好处非常明显:前后端团队可以并行开发,互不等待;后端接口可以复用到小程序、App等不同端;部署时候前端打包成静态文件扔到Nginx,后端打成一个jar包单独部署,扩展和容错都更灵活。我见过不少新手把Vue的axios请求直接写成相对路径,结果前端打包后一换部署环境就全部404,就是因为没有理解“前后端分离”的本质——前端资源和后端服务是两个独立的东西,请求地址必须写清楚。
项目里有一个很聪明的处理:后端通过@CrossOrigin注解或者全局CORS配置解决跨域问题。开发阶段前端跑在8080端口,后端跑在8081端口,浏览器的同源策略会拦截请求,如果不处理跨域,前端根本拿不到数据。这里我建议你实现的时候用全局配置类而不是每个Controller加注解,省事还不容易漏。
2.2 SpringBoot + MyBatis + MySQL:经典组合的默契
后端选型为什么是SpringBoot而不是SSH、SSM的老组合?核心就一个字——快。SpringBoot通过自动配置把以前Spring MVC里繁琐的XML配置全部干掉,约定大于配置,一个main方法启动整个项目。对于选课系统这种规模适中的业务,SpringBoot能让开发者把精力集中在业务逻辑上,而不是耗在配置文件里。
MyBatis在这个项目里承担的是数据库操作层的工作。它的核心优势是SQL由开发者自己掌控,灵活度非常高。选课系统里有大量需要多表连接的查询,比如“查询某学生所有已选课程”需要关联student表、course表和选课记录表,这种场景下MyBatis的<resultMap>映射和动态SQL能写得很清晰、很好调优。对比一下,如果换成JPA/Hibernate,复杂查询反而要被它那套自动生成SQL的机制绕得头疼。
MySQL作为底层数据库,负责持久化存储选课系统的全部数据。选课系统这种业务规模,MySQL的性能完全够用,而且它对事务的支持(InnoDB引擎)非常可靠,正好满足选课这个场景里最核心的需求——高并发下不能超选。
2.3 数据库表设计:五个表如何撑起整个业务
看源码的时候我先翻的是sql目录下的建表脚本,整个系统的数据模型可以拆成五张核心表:
student(学生表):存储学号、姓名、密码、专业、班级等基础信息。teacher(教师表):存储教师信息,用于课程关联授课教师。course(课程表):存储课程编号、课程名称、学分、上课时间、容量等。student_course(选课记录表):学生和课程的多对多关联表,是选课业务的核心表。admin(管理员表):管理员的账号密码。
这里最值得学习的是student_course这张关联表的设计思路。每一条选课记录都绑定了一个学生ID和一个课程ID,两张表的主键在这里变成联合外键,同时记录选课时间。想象一下如果没有这张表,学生和课程的多对多关系根本无法表达。这是一个非常经典的多对多关系建模案例,理解了这张表,你对关系型数据库的核心概念——关联,就真正入门了。
另外我注意到建表脚本里时间字段用的是datetime类型,并且在Java实体类中对应Date类型。这种设计比用字符串存时间更规范,排序、比较、格式化都更安全。
3. 核心模块与关键技术点拆解
3.1 登录鉴权与权限控制的实现逻辑
管理系统类的项目,“谁能干什么”是绕不开的问题。这套选课系统没有引入Spring Security或者Shiro这种重量级安全框架,而是用了一种更适合初学者的方式——拦截器(Interceptor)加Session。
具体做法是:后端写一个LoginInterceptor类,实现HandlerInterceptor接口,在preHandle方法中校验Session中是否存在已登录的用户标识。然后通过WebMvcConfigurer配置类注册拦截器,并指定拦截路径。学生端接口和管理员端接口分别用不同的路径前缀,拦截器根据前缀判断需要何种角色。
比如访问/student/**路径下的接口时,拦截器会检查Session里的用户角色是否为“学生”;访问/admin/**时,检查角色是否为“管理员”。校验不通过直接返回JSON信息提示未登录。
提示:这种基于拦截器的方案轻量、直观,非常适合作学习项目。生产环境建议升级为JWT + Spring Security,但核心思路完全一致——身份认证在前,业务处理在后。
前端这边配合使用Vue Router的全局前置守卫,在路由跳转前检查本地存储(localStorage)里有没有token或用户信息,没有就跳转到登录页。这里前后端各有一次校验,前端的目的是提升用户体验,后端的目的是保证安全,两者缺一不可。
3.2 课程查询与选课退课的动态SQL精髓
如果你只用一个词概括MyBatis的威力,那就是“动态SQL”。选课系统的课程查询功能就把这个能力发挥到了极致。课程列表页面提供了多个筛选条件:按课程名称模糊查询、按授课教师查询、按学分范围查询,这些条件用户可以自由组合。
如果用JDBC写,得用StringBuffer一个个拼接SQL片段,还得小心翼翼处理where 1=1这种尴尬的写法。MyBatis的<where>标签巧妙地解决了这个问题,它会智能地处理条件拼接:没有条件时不生成WHERE子句,有条件时自动去掉第一个多余的AND。
<select id="selectCoursesByCondition" resultMap="CourseResultMap"> SELECT c.*, t.name AS teacher_name FROM course c LEFT JOIN teacher t ON c.teacher_id = t.id <where> <if test="name != null and name != ''"> AND c.name LIKE CONCAT('%', #{name}, '%') </if> <if test="teacherId != null"> AND c.teacher_id = #{teacherId} </if> <if test="minCredit != null"> AND c.credit >= #{minCredit} </if> <if test="maxCredit != null"> AND c.credit <= #{maxCredit} </if> </where> ORDER BY c.id </select>选课操作更是体现了事务控制的重要性。一次完整的选课逻辑是:先检查课程余量是否大于0,再向选课记录表插入一条数据,然后更新课程表的已选人数(selected_count字段加1)。这三步操作任何一步失败,前面的操作都要回滚,否则就会出现“课没选上但已选人数增加了”这种数据不一致的问题。
在SpringBoot里,只需要在Service方法上加@Transactional注解,Spring就会自动处理事务的开始和回滚。源码里这一步做得非常规范,建议你重点看一下CourseServiceImpl中的选课方法实现。我自己测试过,故意在插入选课记录后抛一个运行时异常,数据库里的数据纹丝不动,这就是事务的功劳。
3.3 Vue组件的通信与页面渲染机制
前端部分,这个项目用的是Vue 2 + Element UI的组合。尽管Vue 3已经发布了很久,但Vue 2的生态依然庞大,而且Element UI直接对标Vue 2,上手和学习资料非常多,现阶段用Vue 2来学习管理系统开发是完全没问题的。
项目的目录结构走的是Vue CLI的标准规范——views目录存放页面组件,components目录存放公共组件,router目录管理路由表,api目录统一封装axios请求。这种分层非常清晰:页面组件负责布局,公共组件负责复用,接口层负责和后端打交道。
以课程列表页为例,页面加载时会触发created生命周期钩子,调用API层封装的getCourseList()方法,通过axios发起GET请求。后端返回数据后,组件把结果赋值给data里的courseList数组,然后用Element UI的<el-table>组件渲染表格。这个流程看起来简单,但考的就是你对“生命周期钩子”、“异步请求”、“响应式数据绑定”这三个Vue核心概念的理解。
<template> <el-table :data="courseList" stripe border> <el-table-column prop="name" label="课程名称" /> <el-table-column prop="credit" label="学分" /> <el-table-column prop="teacherName" label="授课教师" /> <el-table-column label="操作"> <template slot-scope="scope"> <el-button type="primary" size="mini" @click="selectCourse(scope.row)">选课</el-button> </template> </el-table-column> </el-table> </template> <script> import { getCourseList, selectCourse } from '@/api/course' export default { data() { return { courseList: [] } }, created() { this.fetchCourseList() }, methods: { fetchCourseList() { getCourseList().then(res => { this.courseList = res.data }) }, selectCourse(row) { selectCourse(row.id).then(() => { this.$message.success('选课成功') this.fetchCourseList() }) } } } </script>注意selectCourse方法在成功后立即重新请求课程列表,才能看到已选人数的实时变化。这种“操作成功后刷新列表”的模式,在管理系统中非常常见,比手动修改当前行的数据要安全和省心得多。
3.4 路由守卫与页面跳转的控制粒度
Vue Router在本项目中的角色不只是页面映射,更重要的是它可以配合登录状态做访问控制。我在复现时把前端的路由表拆成了三个部分:公共路由(登录页)、学生路由(课表、选课、退课)、管理员路由(课程管理、学生管理、统计报表)。
const router = new VueRouter({ routes: [ { path: '/login', component: Login }, { path: '/student', component: StudentLayout, meta: { roles: ['student'] }, children: [ { path: 'courses', component: CourseList }, { path: 'my-courses', component: MyCourses } ] }, { path: '/admin', component: AdminLayout, meta: { roles: ['admin'] }, children: [ { path: 'course-manage', component: CourseManage }, { path: 'student-manage', component: StudentManage } ] } ] }) router.beforeEach((to, from, next) => { const userInfo = JSON.parse(localStorage.getItem('userInfo')) if (to.path !== '/login' && !userInfo) { next('/login') } else if (to.meta.roles && (!userInfo || !to.meta.roles.includes(userInfo.role))) { next('/login') } else { next() } })这种写法有一个很大的好处:新增页面时只需要在路由表里声明路径和角色,权限逻辑自动生效,不需要每个页面自己判断身份。实际项目中我经常看到有人把权限判断写死在每个组件的mounted里,代码重复不说,一旦加一个页面忘了判断,漏洞就出现了。路由守卫一次性解决了所有页面的权限问题。
4. 从零复现:环境准备到跑通全流程
4.1 开发环境搭建的关键细节
在开始跑源码之前,先把环境准备好。这个项目默认使用的开发环境如下:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| JDK | 1.8+ | SpringBoot 2.x 必须用JDK 8以上,不建议用更高版本避免兼容问题 |
| Maven | 3.6+ | 依赖管理和项目构建,IDEA自带Maven也可以 |
| Node.js | 14+ | 前端开发环境,Vue 2 + Vue CLI 4需要 |
| MySQL | 5.7+ / 8.0 | 数据库,注意8.0版本的加密方式需要配驱动参数 |
| IDEA | 任意版本 | 建议2019.3以上,对SpringBoot支持更好 |
这里有一个非常常见的新手问题:MySQL 8.0的驱动类名改成了com.mysql.cj.jdbc.Driver,而且需要额外指定时区参数。如果后端配置文件里还是老版本的com.mysql.jdbc.Driver,启动时大概率会报ClassNotFoundException。源码里如果用的是application.yml,数据库连接URL建议写成这样的标准格式:
spring: datasource: url: jdbc:mysql://localhost:3306/course_selection?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.DriverserverTimezone=Asia/Shanghai这个参数非常关键,不配的话插入时间字段时会报时区相关的错误。用MySQL 5.7的话加不加都能跑,但统一加上更稳妥。
4.2 数据库初始化与项目导入步骤
本项目的sql目录下提供了完整的建库建表脚本,导入方式有两种。如果你用的是Navicat,直接打开脚本文件执行即可;如果你习惯命令行,可以这样做:
mysql -u root -p source /your/project/path/sql/init.sql;脚本执行完毕后,用show tables;确认一下5张核心表都创建成功。为了测试方便,脚本里会默认插入一个管理员账号和一些示例课程数据,登录系统的初始账号密码通常在项目的README.md文件里有注明,这个一定要先读。
后端项目导入IDEA时,选择File -> New -> Project from Existing Sources,然后选中后端目录下的pom.xml,IDEA会自动识别Maven项目并下载依赖。这里提醒一句:依赖下载速度慢的话,可以在Maven的settings.xml里配置阿里云镜像,不然光等依赖就能等一上午。
前端项目导入更简单。用VS Code或者IDEA直接打开前端目录,然后在终端里执行:
npm install如果npm install遇到权限问题或者网络问题,可以换淘宝镜像源:
npm config set registry https://registry.npmmirror.com npm install安装完成后执行npm run serve,看到Compiled successfully就说明前端环境没问题了。
4.3 前后端联调与启动顺序
启动系统要遵守一个顺序:先数据库,再后端,最后前端。
后端启动非常简单,IDEA里找到主启动类(类名通常是CourseSelectionApplication),右键Run即可。看到控制台输出“Tomcat started on port(s): 8081”就是启动成功。如果端口被占用,在application.yml里修改server.port即可。
前端启动后默认跑在8080端口,浏览器访问http://localhost:8080就能看到登录页面。前后端联调的关键是确保前端请求能正确到达后端。在src/utils/request.js里找到axios的baseURL配置,确认它指向的后端地址是http://localhost:8081,如果端口不一致,所有请求都会404或超时。
有个很实用的小技巧:用浏览器的开发者工具(F12)打开Network面板,看请求的状态码。如果是200,说明联调成功;如果是404,检查路径是否匹配;如果是500,去后端控制台看异常日志。这套排查思路适用所有前后端分离项目,很值得养成习惯。
4.4 MyBatis Log与SQL调试工具的使用
这个项目还引入了MyBatis Log插件(IDEA插件市场搜索MyBatis Log安装即可),它可以在控制台直接打印出MyBatis执行的SQL语句和参数值。没有这个插件时,MyBatis默认只输出日志级别为DEBUG的信息,而且看不到完整的SQL语句,排查问题非常痛苦。
装好插件后,SpringBoot项目启动时会自动拦截MyBatis的执行过程,在控制台通过点击插件的开关按钮,就能看到格式化的SQL:
==> Preparing: SELECT * FROM course WHERE id = ? ==> Parameters: 1(Integer) <== Columns: id, name, teacher_id, credit <== Row: 1, Java程序设计, 2, 3.0 <== Total: 1我在复现这个项目的过程中,遇到两次数据查询结果不对的问题,都是靠这个插件直接看SQL才快速定位的。一次是条件参数没有拼接进去,另一次是Score字段类型映射错误。没有调试工具的话,这种问题靠肉眼硬找,效率极低。
4.5 运行效果与功能验证
系统跑通后,用管理员账号登录,看到的首页通常包含左侧菜单和顶部导航栏。管理员端可以进入课程管理页面,执行新增课程、编辑课程、删除课程的操作;学生管理页面可以模糊搜索学生,查看学生选课详情。切换到学生账号登录后,能看到课程列表页,每一门课后面有“选课”按钮,已选过的课程按钮会置灰;在“我的课表”页面可以查看已选课程列表,并支持退课操作。
我建议你在验证功能时,刻意测试几个边界情况。比如选一门容量已满的课程,系统是否给出了友好提示;重复选同一门课,数据库是否会插入两条重复记录。源码在这些地方都做了校验逻辑,测试一遍你能更清楚后端参数校验的重要性。
5. 常见问题和坑:我在复现过程中踩过的雷
5.1 数据库版本引发的时区与驱动问题
开头最容易踩的坑就是MySQL版本差异。如果你本地装的是MySQL 8.0以上版本,还是用了老项目里的com.mysql.jdbc.Driver,项目启动时会直接报ClassNotFoundException: com.mysql.jdbc.Driver。解决办法是把配置文件中的驱动类名换成com.mysql.cj.jdbc.Driver,并且在URL中补上serverTimezone=Asia/Shanghai。如果MySQL是5.7版本,老驱动也能凑合用,但为了统一标准,建议都用新驱动。
还有一个让人头疼的问题是字符集。建表脚本里如果没有显式指定DEFAULT CHARSET=utf8mb4,表默认可能是latin1,存中文时会出现乱码。我教你一个快速判断的办法:登录MySQL后执行show create table course\G,查看表定义里是否有CHARSET=utf8mb4。没有的话执行ALTER TABLE course CONVERT TO CHARACTER SET utf8mb4;即可修复。这个坑我做过项目才碰上,因为本地MySQL默认字符集是latin1,不处理的话前端页面显示中文就全是问号。
5.2 SpringBoot版本太高导致的各种兼容性问题
热词里有“springboot版本太高”这个说法,我深有体会。这套源码基于SpringBoot 2.x开发,如果你把版本升到SpringBoot 3.x,会遇到两个明显的问题:一是javax.servlet包被替换成jakarta.servlet,所有依赖Servlet API的代码编译不过;二是MyBatis Spring Boot Starter的版本也要跟着大升级,否则自动配置完全不生效。
所以我的建议是:在原项目没有升级意图的前提下,别碰高版本。SpringBoot 2.7.x是一个非常稳定的版本,把这个项目跑熟再学更高版本的新特性不迟。毕竟核心技术是业务逻辑和框架思想,版本高低不决定能力。
5.3 前端跨域请求被拦截的问题
如果你按标准流程启动前后端,仍然在浏览器控制台看到CORS error或blocked by CORS policy,说明跨域配置没有生效。检查点有两个:
第一,后端是否正确配置了CORS。可以写一个全局CORS配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:8080") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }第二,前端axios请求是否携带了凭证。使用了Session登录机制的项目,前端请求必须设置withCredentials: true,否则Session Cookie无法传输,后端始终认为你未登录。
const service = axios.create({ baseURL: 'http://localhost:8081', withCredentials: true, timeout: 10000 })这两个问题是我遇到过最多的跨域事故原因,建议排错时先看Network面板里请求的Request Headers有没有Cookie字段,再回去查后端配置。
5.4 经典的SQL重复插入记录问题
选课系统的核心功能是选课,最容易出现的bug是重复选课。如果后端只做了余量检查(selected_count < capacity),没有做唯一性约束,用户疯狂点击选课按钮,数据库就会插入多条记录。
这个问题的标准解法是在建表脚本里给选课记录表添加联合唯一索引:
ALTER TABLE student_course ADD UNIQUE KEY uk_student_course (student_id, course_id);同时在Service层也通过查询先判断一遍是否已选课。两张保护网双管齐下,数据库层从底层杜绝了重复记录的物理可能,应用层则给用户友好的返回提示。这种“数据库约束兜底 + 应用逻辑校验”的思路,今天用得上,以后做其他业务也一样适用。
5.5 Vue组件数据不同步的诡异问题
前端踩过的一个坑是:选课后课程列表的已选人数没有实时刷新。原因是列表页的courseList数据在created钩子里加载了一次,选课成功后虽然重新调了接口,但接口返回的数据格式和之前不一致(比如字段命名从teacherName变成了teacher_name),Vue异步更新数据后表格无法正确渲染。
这种问题排查起来真的很费时间,后来我用console.log(res)打印后端返回才发现了字段映射错位。解决办法有两种:一是让后端严格使用驼峰命名返回JSON,二是前端在拿到数据后做一次字段映射转换。大多数情况下我们选择第一种——后端在MyBatis配置里开启驼峰映射:
mybatis: configuration: map-underscore-to-camel-case: true加上这个配置后,数据库的teacher_name字段会自动映射到Java实体类的teacherName属性,输出JSON自然也就是驼峰格式了。
6. 项目优化与扩展方向:让这套源码更有面试含金量
6.1 增加Redis缓存实现选课防重
如果你想在面试时多讲几个亮点,可以尝试把Redis集成到这个项目中。选课场景天然适合Redis:在选课接口里,先用SETNX命令设置一个以学生ID和课程ID为粒度的锁,设置成功才执行选课逻辑,选课完成后删除锁。这样可以保证同一个学生同时对同一门课发起多次请求时,只有一次能成功选课。
Boolean locked = stringRedisTemplate.opsForValue() .setIfAbsent("lock:course:" + courseId + ":student:" + studentId, "1", 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 执行选课逻辑 } finally { stringRedisTemplate.delete("lock:course:" + courseId + ":student:" + studentId); } }这套代码写出来,面试官听完基本都会眼前一亮。它体现的不是某个框架API的熟练度,而是你对高并发下数据一致性问题的思考。
6.2 使用Logback输出结构化日志
源码的日志输出比较简单,默认只到控制台。如果想让它看起来更专业,可以加一个logback-spring.xml,把日志同时输出到控制台和文件,文件按时长滚动切割。这样系统跑了一段时间后,排查线上问题直接翻日志文件,效率比开调试器高得多。
6.3 前端增加Vuex进行全局状态管理
选课系统前端目前用localStorage保存用户信息,登录后在各个页面手动获取。这种方案的缺点是数据分散,页面刷新后状态丢失。引入Vuex的话,把用户信息、课程状态、选课数量都做成全局state,配合持久化插件,整个前端的状态管理会更清爽、更健壮。
6.4 Docker化部署提升可移植性
项目跑通之后,可以试着给它配一个Dockerfile和docker-compose.yml,把MySQL、后端jar包、前端静态文件全部容器化。一条docker-compose up -d命令就能在任意机器上完整启动整套环境。这个技能现在几乎成了后端岗位的标配,花半天时间折腾一下,绝对值得。
7. 写在最后的经验总结
整套系统复现下来,我最大的收获不是记住了哪个API,而是体会到了“一条完整业务链路”的训练价值。从数据库表设计到后端接口实现,从前端页面编写到前后端联调,每一个环节都在真实地解决业务问题,这种综合能力不是靠刷几道面试题能练出来的。
最后分享两件小事。第一,如果你在跑项目的过程中卡住了,先看日志,再看配置,最后再去翻源码。大部分问题靠这三板斧都能解决,不用急着去网上搜答案。第二,别一次性把全部代码都看完,建议按“登录模块 → 学生选课 → 管理员维护”的顺序,一边跑一边读,遇到不懂的加断点调试,像剥洋葱一样一层层理解下去。
这套系统的完整链路,就是一条打通SpringBoot、Vue、MyBatis、MySQL四位一体的高效学习线,把它的每一个模块都吃透、改过、跑通,你对“前后端分离开发”这个词的理解,一定会比看十篇教程都深刻。