拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot+Vue学生选课系统实战:前后端分离与动态SQL解析

1. 从选题到落地为什么学生选课系统是SpringBootVue的黄金练手项目做后端开发这些年我见过太多人问我同一个问题“想学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 11这种尴尬的写法。MyBatis的where标签巧妙地解决了这个问题它会智能地处理条件拼接没有条件时不生成WHERE子句有条件时自动去掉第一个多余的AND。select idselectCoursesByCondition resultMapCourseResultMap SELECT c.*, t.name AS teacher_name FROM course c LEFT JOIN teacher t ON c.teacher_id t.id where if testname ! null and name ! AND c.name LIKE CONCAT(%, #{name}, %) /if if testteacherId ! null AND c.teacher_id #{teacherId} /if if testminCredit ! null AND c.credit gt; #{minCredit} /if if testmaxCredit ! null AND c.credit lt; #{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 :datacourseList stripe border el-table-column propname label课程名称 / el-table-column propcredit label学分 / el-table-column propteacherName label授课教师 / el-table-column label操作 template slot-scopescope el-button typeprimary sizemini clickselectCourse(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 开发环境搭建的关键细节在开始跑源码之前先把环境准备好。这个项目默认使用的开发环境如下组件版本建议说明JDK1.8SpringBoot 2.x 必须用JDK 8以上不建议用更高版本避免兼容问题Maven3.6依赖管理和项目构建IDEA自带Maven也可以Node.js14前端开发环境Vue 2 Vue CLI 4需要MySQL5.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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.DriverserverTimezoneAsia/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.xmlIDEA会自动识别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中补上serverTimezoneAsia/Shanghai。如果MySQL是5.7版本老驱动也能凑合用但为了统一标准建议都用新驱动。还有一个让人头疼的问题是字符集。建表脚本里如果没有显式指定DEFAULT CHARSETutf8mb4表默认可能是latin1存中文时会出现乱码。我教你一个快速判断的办法登录MySQL后执行show create table course\G查看表定义里是否有CHARSETutf8mb4。没有的话执行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_nameVue异步更新数据后表格无法正确渲染。这种问题排查起来真的很费时间后来我用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四位一体的高效学习线把它的每一个模块都吃透、改过、跑通你对“前后端分离开发”这个词的理解一定会比看十篇教程都深刻。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门