☰
学生选课系统全栈开发实战:SpringBoot+Vue+MyBatis+MySQL设计部署详解
2026/10/30 22:36:21 网站建设 项目流程

做学生选课系统,说到底是把一条条课程数据、一个个学生身份、一节节时间资源串起来,把"谁在什么时间选了哪门课"这件事管明白。这篇博文围绕SpringBoot+Vue+Vue+MyBatis+MySQL这套企业级前后端分离架构,完整拆解一个学生选课管理系统的源码设计、数据库建模、后端接口实现、前端页面交互以及真实部署中会遇到的坑。无论你是正在做毕业设计、课程设计,还是想快速上手SpringBoot后端+Vue前端的全栈开发套路,这篇内容都能帮你少走不少弯路。

先说结论:这套系统跑通不难,但要把选课容量、防重复选、退课名额释放、角色权限控制这些细节做干净,还是有不少门道。下面我按实际开发顺序,从技术选型、数据库设计、后端核心逻辑、前端对接一直讲到部署排错,把每个环节的关键代码和思路都梳理出来。

1. 系统整体设计与思路拆解

1.1 为什么选SpringBoot+Vue+MyBatis+MySQL这套组合

学生选课系统不算复杂,但胜在功能典型:用户登录、角色权限、数据列表、增删改查、业务状态流转,这些几乎是所有管理系统的共性需求。选这套技术栈,不是因为它最花哨,而是因为它足够"标准",踩坑资料多、上手成本低、维护成本也低。

SpringBoot解决了传统SSH/SSM项目配置繁琐的问题。不用再写一堆XML配置文件,起步依赖自动管理jar包版本,内嵌Tomcat让项目直接通过main方法就能启动。对初学者来说,最大的感受就是"原来启动一个Web项目可以这么快"。

Vue负责前端页面。菜鸟教程式的渐进式框架,组件化开发让页面拆成独立的.vue文件,路由控制页面跳转,配合Element UI这样的组件库,表格、表单、弹窗、分页这些后端管理系统最常见的交互,半天就能搭出像模像样的界面。前后端分离的好处也很明显:前后端各干各的,接口约定好,调试互不干扰。Vue 3也可以,但考虑到生态成熟度和学校主流教材,Vue 2 + Element UI仍然是不少毕设项目的稳妥选择。

MyBatis作为持久层框架,最大的优势是SQL可以完全自己控制。选课系统里有大量涉及多表关联、条件统计、动态查询的场景,比如查某个学生已选的课程列表、查某门课的已选人数、按学院/学分/上课时间筛选课程。这些SQL写起来灵活,调试也直观,比全自动ORM更容易排查问题。

MySQL作为数据存储端,免费开源,性能足够,和SpringBoot、MyBatis的兼容性也经过了大量项目验证。整套组合下来,就是一个"经典到不能再经典"的中小规模管理系统标配。

1.2 角色权限与功能模块的划分逻辑

这个系统的核心角色有三种:管理员、教师、学生。为什么分成三种,而不是一张用户表通吃?从业务上来讲,三种角色的操作边界完全不同。

  • 学生:注册/登录、浏览课程列表、按条件查询课程、选课、退课、查看已选课程、查看个人成绩
  • 教师:登录、发布课程、修改课程信息、查看选课学生名单、录入学生成绩
  • 管理员:学生管理、教师管理、课程管理、选课数据统计、系统公告

如果用一张user表加一个role字段去区分,倒也能跑,但代码里到处都要加"if (role == 1)"的判断,逻辑会越来越乱。更合理的做法是独立出一个角色字段,配合后端拦截器做权限校验——每个接口在进入Controller之前先判断登录用户角色,不符合直接返回403。前端再根据角色动态渲染菜单和按钮,做到后端兜底、前端体验的双重安全。

在实际源码里,用户表(sys_user)通常保留username和password,再用role字段区分角色。也有项目把学生、教师、管理员拆成三张独立表,但这样登录逻辑就要分别处理,反而麻烦。基于角色的单表+字段区分,更符合中小型系统的简洁原则。

1.3 版本选型与踩过的版本坑

版本选型看着不起眼,但在实际开发中坑不少。这里直接给出一套我验证过的稳定组合,照着用基本不会出幺蛾子。

组件推荐版本说明
JDK1.8兼容性最好,网上资料最多,不推荐强上JDK 17
Spring Boot2.7.x稳定版,和JDK 8完美配合,自带Tomcat 9
MyBatis Starter2.1.x或者2.3.x由Spring Boot 2.7.x自动管理版本
MySQL8.0.x 或 5.7建议8.0,注意时区参数
Maven3.6.3+避免太老版本导致依赖解析失败
Node.js14.x或16.x跑Vue 2项目的工程化环境
Vue2.6.x + Element UI 2.15.x前端框架,组件库稳定
Axios0.21.x前端HTTP请求库

版本适配是我踩过最惨的坑:最开始用Spring Boot 3.0 + JDK 17,结果MyBatis的starter版本不兼容、第三方工具类全是基于javax包的,而Spring Boot 3把javax换成了jakarta,一堆代码改到头大。后来全部回退到2.7.x + JDK 8,几分钟就全部跑通了。老实说,做这种管理系统,真的没必要追求最新版本——稳定跑通、资料好查才是第一位的。

2. 数据库设计与持久层实现

2.1 核心表结构与建表SQL

数据库是整个系统的地基,表设计好不好,直接决定后面SQL写起来痛不痛苦。核心表归纳下来是五张:用户表、课程表、选课记录表、成绩表、公告表(可选)。

学生和教师统一放在sys_user表里,通过role字段区分。核心字段要包括id、username、password、real_name、role、create_time。密码存储我不建议用明文,用MD5加盐或者BCrypt加密,安全性高出一大截。

课程表是course,字段包括id、course_name、teacher_id、credit、capacity、selected_count、class_time、location。其中selected_count表示已选人数,这个字段在并发选课的场景下需要谨慎处理,后面会专门讲。

选课记录表用sc命名(student_course),主键设计为联合主键(user_id, course_id),防止同一个人选同一门课两次,这是数据库层面最朴素也最有效的防重复手段。

下面是精简版的建表SQL:

CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4; USE course_system; CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role TINYINT NOT NULL DEFAULT 2 COMMENT '0-管理员 1-教师 2-学生', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE course ( id BIGINT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, teacher_id BIGINT NOT NULL, credit INT DEFAULT 2, capacity INT DEFAULT 60, selected_count INT DEFAULT 0, class_time VARCHAR(100) COMMENT '上课时间,如 周一3-4节', location VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sc ( user_id BIGINT NOT NULL, course_id BIGINT NOT NULL, score DECIMAL(5,2), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, course_id), CONSTRAINT fk_sc_user FOREIGN KEY (user_id) REFERENCES sys_user(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) );

几个关键细节解释一下:

第一,字符集必须用utf8mb4,不然用户在备注里输入个emoji表情,就会报"Data too long"或乱码。第二,联合主键是防重复选课的底线,就算后端代码有漏洞,数据库也会拦住重复插入。第三,成绩字段直接放进选课表里,教师录入成绩其实就是更新sc表的score字段,不需要单独建一张成绩表。

2.2 MyBatis核心SQL写法与动态查询

MyBatis的XML配置和注解方式我都有用过,选课系统这种多表查询频繁的项目,更推荐XML方式。注解方式适合简单SQL,一旦SQL超过三行、里面还有动态条件,可读性会直线下降。XML把SQL集中管理,后期排查问题也方便。

课程列表查询是最典型的场景:学生进入选课页面,需要看到课程列表、授课教师姓名、当前已选人数/容量。这里必然涉及course表和sys_user表的联查。

<select id="selectCourseList" resultType="com.example.vo.CourseVO"> SELECT c.id, c.course_name, c.credit, c.capacity, c.selected_count, c.class_time, c.location, u.real_name AS teacher_name FROM course c LEFT JOIN sys_user u ON c.teacher_id = u.id <where> <if test="courseName != null and courseName != ''"> AND c.course_name LIKE CONCAT('%', #{courseName}, '%') </if> <if test="teacherName != null and teacherName != ''"> AND u.real_name LIKE CONCAT('%', #{teacherName}, '%') </if> </where> ORDER BY c.id </select>

这段SQL用到了MyBatis动态SQL的if+where标签。where标签有个小细节值得注意:它会自动去掉第一个多余的AND/OR,避免代码里拼SQL时还要纠结"前面要不要加WHERE"的经典问题。这是MyBatis相比直接JDBC拼接SQL的巨大优势。

除了动态条件,查询已选课程列表也是高频SQL。核心逻辑就是通过sc表反查course表:

<select id="selectSelectedCourses" resultType="com.example.vo.CourseVO"> SELECT c.id, c.course_name, c.credit, c.capacity, c.selected_count, c.class_time, c.location, u.real_name AS teacher_name FROM sc JOIN course c ON sc.course_id = c.id JOIN sys_user u ON c.teacher_id = u.id WHERE sc.user_id = #{userId} </select>

这里有一个很容易踩的映射问题:数据库字段是course_name,而Java实体属性是courseName,直接查询返回的结果会是null。解决方案是在MyBatis全局配置中开启驼峰映射:

mybatis: configuration: map-underscore-to-camel-case: true

这个配置我已经不知道救了多少人,不开它,联查出来的字段全是空值,排查半天往往就是这个原因。

2.3 选课容量与并发控制的常见设计

选课系统最容易出问题的场景就是并发:选课高峰时期,多个学生同时对同一门课发起选课请求,如果代码没有做好并发控制,课程容量只有60人,结果选了65人进去,数据就脏了。

最简单的方案是"先查再插"的事务控制:选中课程之前查一下selected_count,小于capacity就插入选课记录,同时把selected_count加1。但在高并发下,两个请求同时查到count=59,然后同时插入,最后一个课程就多了一个人。

解决办法有几个方向:

第一种是数据库层面的乐观锁。在course表加一个version字段,更新时带着version条件,影响行数为0说明version被改了,就提示选课人数已满。

<update id="increaseSelectedCount"> UPDATE course SET selected_count = selected_count + 1, version = version + 1 WHERE id = #{courseId} AND version = #{version} AND selected_count &lt; capacity </update>

这里一定要在WHERE条件里加上selected_count < capacity,通过数据库行锁的机制,保证只有在未满员的情况下才会加人数。Java代码中判断update返回的受影响行数,为1就表示抢占成功,继续执行插入选课记录的SQL;为0就说明课程满了或version冲突,直接返回"选课失败"。

第二种是直接给课程行加锁(SELECT ... FOR UPDATE)。这种方式在编写上更直观,但死锁风险需要仔细测试,性能也有一定消耗。对毕设和中小型系统来说,乐观锁+条件更新的方案最稳妥。

我的建议是:在线程并发量不夸张的场景下,直接把容量判断和更新人数合并为一条UPDATE语句,配合insert选课记录,再用一个事务包起来,基本就够用了。如果选课系统真的要支撑上千人同时抢课,那就要引入Redis分布式锁或消息队列了,但这就超出了学生选课系统的范畴了。

3. 后端核心逻辑与接口实现

3.1 登录认证与权限拦截的实现思路

登录认证几乎是所有管理系统的第一道门槛,选课系统也不例外。常规做法有两种:Session方式和JWT Token方式。

Session方式是传统方案,登录成功后将用户信息存入Session,后续请求通过Cookie携带SessionID来识别身份。实现简单,但前后端分离时,Session共享和跨域Cookie处理比较麻烦。前后端分离的项目更推荐JWT(JSON Web Token)方案。用户登录成功后,后端签发一个包含用户ID、用户名、角色、过期时间的Token字符串,前端拿到后存在localStorage里,之后每次请求在请求头加上Authorization: Bearer 。后端通过拦截器或Spring Security解析Token,识别当前登录用户身份和角色。

对于学生选课系统,我认为直接从简单入手即可,不引入Spring Security这种重型框架,而是利用SpringBoot的HandlerInterceptor实现一个自定义拦截器。核心逻辑如下:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } try { // 解析token,把userId和role放进request属性,方便后续Controller使用 Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); return false; } } }

这里把Token解析出的userId和role塞进request属性,后续Controller方法里直接用@RequestAttribute就可以取出来,非常方便。角色权限校验还可以再写一个RoleInterceptor,按不同接口路径匹配不同角色,实现思路类似,都是继承HandlerInterceptor然后重写preHandle。

为了减少代码侵入,登录接口本身不需要拦截,可以用WebMvcConfigurer的addInterceptors方法配置拦截路径白名单:

registry.addInterceptor(authInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register");

这个白名单的设计要仔细。比如教师录入成绩的接口,必须要求老师权限;学生选课接口,必须要求student角色。如果权限控制做得比较粗,会出现学生直接调管理员的接口删课程,虽然前端页面没这个按钮,但接口是裸奔的——所以一定要在服务端校验角色,不能只靠前端隐藏按钮。

3.2 选课接口的完整处理流程

选课是系统的核心接口,逻辑上要比普通接口多几步。我把它拆成四个关键步骤:

第一步,从Token或请求参数中获取当前学生ID。注意:学生ID不能从前端传来的JSON里读取,否则学号是A的同学可以伪造请求帮学号是B的同学选课。必须通过Token解析出来的身份来确定用户。

第二步,校验课程状态。查出课程信息,确认课程存在、已选人数小于容量。同时校验选课时间是否在系统开放时间内(如果有的话)。

第三步,执行选课。插入sc记录。这一步要利用数据库的联合主键防重复。

第四步,更新课程已选人数。乐观锁UPDATE。

如果用事务统一管理,核心代码大致是这样:

@Service public class CourseServiceImpl implements CourseService { @Autowired private CourseMapper courseMapper; @Autowired private ScMapper scMapper; @Transactional(rollbackFor = Exception.class) @Override public Result selectCourse(Long userId, Long courseId) { // 1. 判断是否已经选过 int existCount = scMapper.countByUserIdAndCourseId(userId, courseId); if (existCount > 0) { return Result.error("你已选过该课程,请勿重复选择"); } // 2. 乐观锁占坑:只有未满才更新成功 int rows = courseMapper.increaseSelectedCount(courseId); if (rows == 0) { return Result.error("该课程人数已满,选课失败"); } // 3. 插入选课记录 scMapper.insert(userId, courseId); return Result.success("选课成功"); } }

这里有个小细节:步骤1和步骤2在并发情况下可能会有间隙,但并不影响最终一致性。因为即使两个请求同时通过步骤1,步骤2的乐观锁UPDATE只会有一个成功,另一个rows=0直接报满。所以在高并发下,偶尔会有学生明明没选过却提示"人数已满",这并不会造成数据错误,属于可接受的业务提示。

退课接口的逻辑就相反了:先删除sc记录,再把course表的selected_count减1。删除时也要判断记录是否存在,不要出现"删除0行却返回成功"的情况。

3.3 Service层与Controller层的职责划分

很多新手写代码喜欢把业务逻辑全堆在Controller里,一个接口对应一个方法,几十行逻辑塞进去虽然能跑,但后期维护极其痛苦。我更推荐严格分层:

Controller层只做三件事:接收参数、调用Service、返回统一结果封装。Controller里不写任何if-else业务判断。

Service层负责核心业务逻辑:事务管理、业务流程编排、业务规则校验。像选课、退课、成绩录入这些核心操作,都放在Service里。

Mapper层(DAO层)只负责数据库交互,一个方法对应一条SQL,不做业务处理。

统一结果返回结构也很重要。所有接口统一返回Result对象,一般包含code、message、data三个字段。前端根据code判断请求成功与否,避免出现"HTTP 200但业务失败"还要解析data里的值来猜的情况。

public class Result { private Integer code; // 200 成功,500 业务失败,401 未登录 private String message; private Object data; public static Result success(Object data) { Result r = new Result(); r.setCode(200); r.setData(data); return r; } public static Result error(String message) { Result r = new Result(); r.setCode(500); r.setMessage(message); return r; } }

这个Result类看着简单,实际使用中价值非常大。前端Axios拦截器可以直接根据code做统一错误提示,不用每个接口单独写一遍"你已选过该课程""人数已满"这类提示逻辑。

4. 前端Vue实现与接口对接

4.1 前端工程结构与路由设计

Vue前端项目一般用Vue CLI(或Vite)创建。以Vue 2为例,工程创建命令:

vue create course-system-frontend

创建完成后在项目根目录安装Element UI和Axios:

npm install element-ui -S npm install axios -S

main.js中引入Element UI:

import Vue from 'vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import App from './App.vue' import router from './router' Vue.use(ElementUI) new Vue({ router, render: h => h(App) }).$mount('#app')

路由设计上,按角色划分模块。为了减少首屏加载时间,用Vue Router的懒加载模式。路由懒加载按需加载对应的组件,页面比较多时能显著提高首屏速度。

在路由守卫中做登录校验,是前端控制访问权限最直接的方式:没有token就跳转到登录页,有token但角色不匹配就跳转到对应角色的首页。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() return } if (!token) { next('/login') return } next() })

另外,导航守卫里还可以根据当前用户的role动态生成菜单。比如管理员路由包含"用户管理",学生路由没有这个菜单。虽然真正的权限控制要靠后端拦截器,但前端动态菜单能让界面更清爽。

4.2 Axios请求封装与拦截器设计

前端所有接口请求都建议封装在统一模块中,而不是每个页面直接调用axios.get。这样在统一处理Token、统一处理错误码、统一加载状态时能省掉大量重复代码。

先封装一个request.js模块:

import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动携带token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) // 响应拦截器:统一处理业务错误码 request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } else if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('未登录或登录已过期')) } else { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } }, error => { Message.error('网络连接异常') return Promise.reject(error) } ) export default request

封装好之后,页面里调用接口就非常干净了:

import request from '@/utils/request' export function selectCourse(courseId) { return request.post('/course/select', { courseId }) } export function getCourseList(params) { return request.get('/course/list', { params }) }

这种封装最大的价值在于:新增一个接口时只需写一行函数定义,接口异常统一弹Message提示,代码可读性也高很多。实际开发中我还发现一个细节:baseURL用'/api'这种相对路径,开发时通过Vue CLI的proxy配置将/api转发到后端localhost:8080,避免跨域。部署时再用Nginx反向代理到后端服务,前端代码里不需要写死IP和端口,非常灵活。

4.3 选课页面的核心交互实现

选课页面是最核心的前端页面,用Element UI的Table组件展示课程列表,加上搜索条件和分页。

table的核心列设计一般包括:课程名称、授课教师、学分、上课时间、上课地点、容量、已选人数、操作按钮。操作按钮需要根据状态显示不同的文字和样式:如果已选,显示"已选"并且置灰;如果满员,显示"已满";否则显示"选课"。

具体实现时,课程列表接口返回的数据里包含一个字段isSelected,这是后端在当前用户登录状态下特别查询返回的,方便前端直接判断按钮状态。

<el-table :data="courseList" v-loading="loading"> <el-table-column prop="courseName" label="课程名称" width="200"></el-table-column> <el-table-column prop="teacherName" label="教师" width="150"></el-table-column> <el-table-column prop="credit" label="学分" width="80"></el-table-column> <el-table-column prop="classTime" label="上课时间" width="180"></el-table-column> <el-table-column prop="capacity" label="容量" width="80"></el-table-column> <el-table-column prop="selectedCount" label="已选人数" width="100"></el-table-column> <el-table-column label="操作" width="150"> <template slot-scope="scope"> <el-button type="primary" size="small" :disabled="scope.row.selectedCount >= scope.row.capacity || scope.row.isSelected" @click="handleSelect(scope.row)" >{{ scope.row.isSelected ? '已选' : '选课' }}</el-button> </template> </el-table-column> </el-table>

选课按钮点击后调用后端接口,成功会刷新列表并弹成功提示,失败则弹错误消息。我在实际开发中遇到一个体验问题:选课成功后如果不刷新列表,isSelected还是旧的false,需要重新拉一遍课程列表。这个小细节处理好之后,选课成功和已选状态切换就很丝滑了。

页面加载时调用分页接口:

getCourseList({ pageNum: this.pageNum, pageSize: this.pageSize, courseName: this.keyword }) .then(res => { this.courseList = res.data.list this.total = res.data.total })

这里分页参数和后端保持一致,后端使用PageHelper插件可以非常方便地实现分页查询,设置PageHelper.startPage(pageNum, pageSize)之后,紧接着的SQL查询就会自动拼接LIMIT语句,同时返回的总条数也会被拦截器自动处理。

4.4 我的课程页面与成绩查看功能

学生选完课后,需要有一个"我的课程"页面来查看已选课程列表。这个页面的表格和选课页面类似,但操作按钮变成"退课"和"查看成绩"。退课操作需要一个二次确认弹窗,用Element UI的MessageBox做确认,防止学生误点:

this.$confirm('确定要退掉这门课程吗?', '提示', { confirmButtonText: '确定退课', cancelButtonText: '取消', type: 'warning' }).then(() => { cancelCourse(courseId).then(() => { this.$message.success('退课成功') this.loadCourses() }) })

教师端查看选课学生名单和录入成绩,页面交互逻辑类似,只是数据源不同。录入成绩用el-input-number或者el-input,做成可编辑的单元格或者弹窗表单,提交后调用录入成绩接口。

5. 项目部署运行与常见问题排查

5.1 本地从零启动完整流程

假如你拿到一套完整的源码,怎么把它跑起来?我按实操顺序整理一下。

第一步,准备环境。安装JDK 8、Maven 3.6+、MySQL 8.0、Node.js 14+,并配置好环境变量。

第二步,导入数据库。用Navicat或其他MySQL客户端执行项目提供的前置SQL脚本。如果没有现成脚本,就按前面的建表SQL手动建库建表,再插入几条测试数据。

第三步,修改配置文件。打开后端项目的application.yml,检查这几项:数据库地址、端口、账号密码。很多新手启动报错,80%是这一步骤出了问题。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true

注意url里一定要加上serverTimezone=Asia/Shanghai,否则MySQL 8.0会报时区错误。

第四步,启动后端。在项目根目录执行:

mvn spring-boot:run

等待控制台出现"Started Application in x.xxx seconds"字样就说明后端启动成功。

第五步,启动前端。前端项目目录下安装依赖并启动开发服务器:

npm install npm run serve

控制台会出现访问地址,通常是http://localhost:8081。默认端口如果冲突,在vue.config.js里配置devServer.port,同时配置代理:

module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

到这里,浏览器打开前端地址,注册一个学生账号,就能体验完整的选课流程了。

5.2 常见问题速查表

我把反复遇到的几类问题整理成表格,按照排查优先级排列:

问题现象根本原因解决办法
启动后端报错ConnectException: Connection refusedMySQL没启动或端口不对检查MySQL服务状态,确认3306端口可连接
报错Unknown database 'course_system'数据库没创建或名字不一致执行建库SQL,核对application.yml里的库名
报错Public Key Retrieval is not allowedMySQL 8.0连接参数缺少allowPublicKeyRetrieval在JDBC URL中追加allowPublicKeyRetrieval=true
前端访问后端接口报404代理路径或接口路径不匹配检查vue.config.js的proxy配置和请求的baseURL
前端接口返回数据里外键字段为null没有开启驼峰映射在mybatis配置中设置map-underscore-to-camel-case=true
中文乱码数据库字符集不是utf8mb4修改库/表字符集为utf8mb4
端口被占用8080被其他程序占用换端口或杀死占用进程
Maven依赖下载失败网络问题或仓库源问题配置阿里云镜像仓库

这些坑基本覆盖了我在实际部署中遇到过的情况。其中"Public Key Retrieval is not allowed"是MySQL 8.0特有的,很多人在MySQL 5.7上能跑通的连接配置,到8.0就报这个错,解决方案就是URL添加上面说的参数,或者修改MySQL用户的加密方式。

5.3 容易忽略但很实用的几个细节

第一个是IDE的Maven配置。很多学生遇到依赖下载失败,其实不是代码问题,而是Maven用了默认的中央仓库,国内访问不稳定。在settings.xml中配置阿里云镜像基本能解决:

<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>

第二个是Lombok的使用。Lombok能省掉大量getter/setter代码,但需要IDEA插件支持,而且IDEA版本和Lombok版本存在兼容性问题。如果实体类上加了@Data,但项目里调用getId()却编译报错,多半是IDEA的Lombok插件没装或版本不对。

第三个是前端npm install时报node-sass安装失败。老项目里常遇到node-sass,它与Node版本绑定,Node版本太高太低都会挂。2025年的视角下,建议优先选sass(dart-sass)而不是node-sass,或者在项目依赖里把node-sass替换成sass。这也是很多Vue 2旧项目的一个历史包袱。

第四个无关技术但非常关键:密码处理。我见过很多毕设项目把用户密码明文存在数据库里,甚至登录验证直接"SELECT * FROM user WHERE password = 'xxx'",这非常危险。至少也要做MD5+盐存储,或者直接使用BCrypt。这个习惯不光是项目要求,更是未来工作的职业底线。

6. 二次扩展与项目进阶方向

如果源码跑通之后想加点亮点,让系统在答辩或实际使用中更有说服力,这几个方向我建议优先考虑。

第一个是引入Redis做课程缓存和选课计数器。课程列表是查询频率最高的接口,用Redis缓存课程列表数据可以显著降低数据库压力。选课人数也可以直接在Redis里做原子自增(INCR命令),选课成功后异步同步到MySQL,这样并发支持能力会上一个档次。实际项目里这个方案很常见,能让面试官觉得你理解了缓存和数据库的一致性问题。

第二个是使用Spring Boot的定时任务实现选课开放与关闭。比如在配置表中设置选课开始时间和结束时间,然后写一个定时任务自动切换课程状态。这比手动在代码里判断当前时间要优雅得多,也更接近真实教务系统的行为。

第三个是增加数据导出功能。学生成绩单、选课名单,用Hutool或Easy Excel直接导出成Excel文件。这个功能很多系统都没有,但实际使用中需求量很大,尤其是教师端导出选课学生名单,属于加分项。

第四个是引入Logback日志配置。把操作日志、异常日志按级别输出到文件,方便线上问题排查。毕设答辩时,面试官问"系统出了异常你怎么排查",拿出日志文件来谈,会显得专业得多。

我自己实际做完这套系统之后的体会是:技术本身并不难,难的是把边界情况想全、把每个按钮的状态判断清楚、把每条SQL在各种场景下都验证到位。选课系统麻雀虽小,五脏俱全,做完它,你对SpringBoot、Vue、MyBatis、MySQL整套技术栈的理解会从"学过"变成"会做",这种转变才是最值钱的部分。

最后再分享一个小技巧:开发阶段用Postman把每个接口的请求参数和返回结果整理成文档,前端对接时会给对方省下大量沟通成本。接口文档不规范导致的前后端扯皮,是我见过最消耗项目进度的事情,没有之一。把接口定义清楚、状态码约定好,你会发现整个开发过程顺畅太多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询