
1. 选题背景与项目定位每年毕业季都能看到一堆选课系统、图书管理系统、宿舍管理系统说句实在话这类题目在计算机毕业设计里属于“常青树”。学生选课系统之所以这么多人选核心原因就两个字稳。Spring Boot Vue的组合前端后端都能展示业务逻辑不会太复杂到做不完也不会简单到没东西可写再加上选课系统本身带有的并发、事务、权限这些经典考点无论是答辩还是写论文都有足够多的素材可以展开。我接触过不少做这个题目的同学最常见的误区是上来就找一套所谓的“完整源码”改个名字就交差。这么做最大的问题不是学术诚信而是到了答辩环节老师随便问一句“你的选课事务是怎么控制的”“如果选课人数满了怎么办”“你的权限校验在哪里做的”基本就卡住了。所以说与其找一份别人的代码硬背不如自己把这个系统从设计到实现完整走一遍哪怕有的功能实现得并不完美但至少每行代码都是自己写的每张表为什么这么设计都能说清楚这才是毕业设计真正的价值所在。这个项目适合三类人参考第一类是计算机、软件工程专业的应届毕业生需要完成毕业设计第二类是菜鸟程序员想通过一个完整的全栈项目串一遍Spring Boot和Vue的核心用法第三类是正在自学Java后端的人想找一个结构清晰、规模适中的实战项目练手。这篇文章我会把整个系统的设计思路、数据库表结构、后端核心逻辑、前端页面结构、部署方式以及常见问题排查全部拆开讲尽量做到拿来能看、看了能做、做了能过。先把这个项目解决的核心需求讲明白。学生选课系统的使用者一般有三类角色管理员、教师、学生。管理员负责基础数据的维护比如学期管理、课程信息的上下架、学生和教师账号的分配教师负责开设课程、设定选课容量、录入和发布成绩学生负责查看课程列表、选课、退课以及在选课结束后查看自己的成绩。这样一个三条业务线交织在一起的系统正好把权限管理、关联表设计、事务控制、前后端交互这些基本功全部覆盖到了作为毕业设计来说题目大小和深度都刚刚好。2. 技术选型与整体架构设计2.1 为什么是Spring Boot Vue而不是其他组合很多同学在选题时会纠结技术栈比如用SSH老架构、用Python的Django、用PHP或者前后端不分离直接在JSP里写。咱们逐个分析一下。Spring Boot是目前Java后端求职和毕业设计中使用率最高的框架之一它的核心优势是“约定大于配置”不需要像早期SSH那样写一大堆XML配置文件内嵌Tomcat使得启动和部署也简单很多。对于做毕业设计来说选一个市面认可度高、资料丰富的框架意味着遇到问题能搜到答案的概率大幅提升这本身就是一种隐性优势。Vue作为前端框架它的渐进式设计非常适合毕设这种规模的项目。你不需要一上来就接触复杂的工程化配置用Vue CLI或Vite搭一个项目组件化开发、Vue Router做页面跳转、Vuex或Pinia做状态管理学习曲线相对平缓。更重要的是Vue的双向数据绑定特性在做表单提交、列表渲染这种场景时代码量比原生JS少很多也更容易讲清楚。可能有人会问为什么不用前后端不分离的传统模式我的看法是既然题目都已经明确写了Spring Boot Vue那就说明这是一个前后端分离的项目。前后端分离模式下后端只负责提供JSON格式的RESTful API前端通过axios调用接口渲染页面这使得项目结构更清晰代码的可维护性更强在论文里也更好画出架构图。2.2 系统整体架构与请求流转过程这个项目的整体架构可以理解为标准的B/S架构加前后端分离模式。浏览器端是Vue构建的单页应用通过HTTP请求访问后端接口后端Spring Boot接收请求后通过Service层处理业务逻辑再通过Mapper层操作MySQL数据库最终将处理结果以JSON格式返回给前端渲染。请求流转的完整过程是这样的用户在浏览器里输入账号密码点击登录前端把表单数据POST到后端的/api/auth/login接口后端先通过JWT工具类生成Token返还给前端前端把它存在localStorage里。接下来前端每次请求都会在axios的请求拦截器中附带这个Token后端通过Spring Security或拦截器校验Token的合法性再决定是否放行请求。这样的设计把“认证”和“授权”分开处理认证是确认“你是谁”授权是确认“你能干什么”这也是答辩时老师比较关注的点。后端内部的层次划分我建议严格遵循Controller-Service-Mapper三层架构。Controller层只负责接收参数和返回结果不写任何业务逻辑Service层处理具体的业务流程比如选课时的冲突检测和事务控制Mapper层通过MyBatis或MyBatis Plus操作数据库。这样的分层虽然看起来多写了一些代码但每一层的职责非常清楚出了Bug也容易定位。我在帮学生排查问题的时候经常发现有些人把所有逻辑都写在Controller里一个方法几百行最后自己都看不懂这就是没有遵守分层规范造成的。2.3 开发环境与版本选型关于版本选型这里给一个经过验证的推荐组合避免很多新手在环境配置上浪费太多时间。JDK推荐1.8或11这两个LTS版本Spring Boot推荐2.7.x系列不用太执着于追求最新的3.x版本因为3.x对JDK版本有硬性要求17而且一些第三方整合包的兼容性还需要踩坑。Vue这边推荐Vue 2.6或Vue 3.2如果对Vue还不熟悉直接用Vue 2也能顺利完成项目只是要注意组件库的对应关系。前端UI框架推荐Element UI对应Vue 2或Element Plus对应Vue 3这套UI库在国内用得最多组件丰富文档也是中文的非常适合毕设项目。MySQL推荐5.7或8.0如果电脑配置允许直接用8.0因为8.0在性能和安全方面都有提升。IDEA使用2021之后的版本就行VS Code负责写前端Navicat或DataGrip负责管理数据库。在实际搭建环境的过程中最容易出问题的环节就是Maven依赖下载和Node依赖安装国内的同学建议把Maven仓库镜像和npm镜像切成阿里云源速度差别非常大这一步可以省下不少等待时间。3. 数据库设计与核心表结构3.1 从需求分析到ER模型数据库设计是整个系统的地基地基没打好后面写代码全是补丁。第一步要做的是梳理清楚实体和实体之间的关系。这个系统里的核心实体有用户、角色、课程、选课记录、成绩、学院、学期。实体间的关系用大白话说就是一个用户学生角色可以选多门课程一门课程也可以被多个学生选所以学生和课程之间是多对多的关系而这个多对多关系是通过选课记录这张中间表来维护的。一个教师可以开设多门课程但一门课程通常对应一个教师所以是一对多的关系。一个学院下面有多个专业一个专业下面有多个学生学院到学生也是一对多的层级关系。画ER图的时候建议先画一个粗粒度的框架把核心实体和关系摆出来再去细化每个实体的字段。很多同学习惯直接上手建表建到一半发现字段对不上又回头改反而浪费时间。用draw.io或者ProcessOn画一张ER图不光是给自己看的后期论文里也需要插图这一步做一遍能省后面不少事。3.2 各表字段设计与设计意图我把关键表的结构和设计理由依次列出来重点说明为什么要这么设计而不是直接甩一堆SQL让读者去抄。用户表是整个系统的入口包含了用户名、密码、真实姓名、角色类型、邮箱、手机号、状态等字段。有一个细节需要注意密码绝对不能明文存储要用MD5加盐或者BCrypt加密。这里推荐BCrypt因为它是自带盐值的哈希算法同样的明文每次加密结果都不同安全性远高于MD5。角色字段我这里建议用整数类型0代表学生、1代表教师、2代表管理员一方面存整数比存字符串更高效另一方面在代码里用常量或枚举去语义化比在数据库里直接写“student”“teacher”更好维护。课程表需要包含课程编号、课程名称、学分、学时、上课时间、上课地点、教师ID外键、所属学院、选课容量、已选人数、开课学期、课程状态等字段。这里有一个重要的设计点selected_count这个字段是冗余字段它记录的是当前已选人数。为什么不直接用SELECT COUNT(*)去统计选课记录表因为选课是一个高频操作如果每次都去关联查询计数数据量大了以后性能会明显下降。在选课的时候对这个字段做UPDATE ... SET selected_count selected_count 1 WHERE id ? AND selected_count capacity这样的原子操作既能防止超选又能减少一次查询。选课记录表是学生和课程的关联表包含ID、学生ID、课程ID、选课时间、状态已选、已退、成绩等字段。一张表同时承担了选课记录和成绩记录两个职责这是合理的因为成绩本质上就是“某个学生选了某门课之后得到的分数”。但要注意退课的时候应该从这张表里把状态改成“已退”而不是物理删除因为成绩、选课历史这些数据都是有追溯价值的。3.3 建表SQL与初始化数据下面是建表SQL的核心部分我按模块整理好注意字段类型和约束的写法。这里的SQL可以直接复制到Navicat或命令行工具里执行。-- 用户表 CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码(BCrypt加密), real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 角色 0-学生 1-教师 2-管理员, email varchar(100) DEFAULT NULL COMMENT 邮箱, phone varchar(20) DEFAULT NULL COMMENT 手机号, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态 1-正常 0-禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT系统用户表; -- 学院表 CREATE TABLE sys_college ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, college_name varchar(100) NOT NULL COMMENT 学院名称, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学院表; -- 学生信息扩展表 CREATE TABLE stu_student ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, user_id bigint(20) NOT NULL COMMENT 关联用户表ID, student_no varchar(20) NOT NULL COMMENT 学号, college_id bigint(20) DEFAULT NULL COMMENT 所属学院ID, grade varchar(10) DEFAULT NULL COMMENT 年级, major varchar(50) DEFAULT NULL COMMENT 专业, class_name varchar(50) DEFAULT NULL COMMENT 班级, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; -- 教师信息扩展表 CREATE TABLE tea_teacher ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, user_id bigint(20) NOT NULL COMMENT 关联用户表ID, teacher_no varchar(20) NOT NULL COMMENT 工号, college_id bigint(20) DEFAULT NULL COMMENT 所属学院ID, title varchar(30) DEFAULT NULL COMMENT 职称, PRIMARY KEY (id), UNIQUE KEY uk_teacher_no (teacher_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT教师信息表; -- 课程表 CREATE TABLE cou_course ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, course_no varchar(20) NOT NULL COMMENT 课程编号, course_name varchar(100) NOT NULL COMMENT 课程名称, credit decimal(3,1) DEFAULT 0.0 COMMENT 学分, hours int(11) DEFAULT 0 COMMENT 学时, teacher_id bigint(20) DEFAULT NULL COMMENT 教师用户ID, college_id bigint(20) DEFAULT NULL COMMENT 开课学院ID, class_time varchar(100) DEFAULT NULL COMMENT 上课时间(如 周一3-4节), class_location varchar(100) DEFAULT NULL COMMENT 上课地点, capacity int(11) NOT NULL DEFAULT 0 COMMENT 选课容量, selected_count int(11) NOT NULL DEFAULT 0 COMMENT 已选人数, semester varchar(20) DEFAULT NULL COMMENT 开课学期(如 2024-2025-1), status tinyint(4) NOT NULL DEFAULT 1 COMMENT 课程状态 1-可选 0-已关闭, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_course_no (course_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 选课记录表(也承载成绩) CREATE TABLE stu_course_selection ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, student_id bigint(20) NOT NULL COMMENT 学生用户ID, course_id bigint(20) NOT NULL COMMENT 课程ID, select_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态 1-已选 2-已退 3-已结课, score decimal(5,2) DEFAULT NULL COMMENT 成绩, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course_id (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课记录表;初始化数据的部分建议准备一些有代表性的测试数据两个学院、三四个教师、十个左右学生、十来门课程。密码统一用BCrypt加密后的值这里提供一个技巧可以在单元测试里写一段代码用Spring的BCryptPasswordEncoder生成加密串然后把生成结果粘贴到SQL里比在线工具更可靠。4. 后端核心模块设计与实现4.1 登录认证与JWT权限控制登录认证这块是整个后端最容易被问、也最容易出问题的地方。传统的Session方案在前后端分离场景下跨域处理比较繁琐而JWTJSON Web Token方案天然适合这种架构。JWT的本质是一个包含用户信息和过期时间的加密字符串服务端不保存登录状态每次请求时验证Token的签名和有效期即可这种无状态的设计在水平扩展时非常友好。JWT的生成逻辑也很直观用户提交用户名和密码后端校验通过后将用户ID、用户名、角色等信息放进Token的claim里用指定的密钥签名设置过期时间比如24小时。前端拿到Token后保存到localStorage或者pinia中每次请求在axios拦截器里带上Authorization: Bearer token后端解析并校验。在Spring Boot里实现JWT认证有两条路。一条是引入Spring Security配合JWT过滤器功能强大但配置门槛高很多新手配到一半就被Security的过滤器链搞晕了另一条是只写一个HandlerInterceptor拦截器手动校验Token代码量少很多也容易理解。我个人建议毕业设计选第二种够用且好讲。实现上就是写一个AuthInterceptor实现了HandlerInterceptor的preHandle方法在方法里获取请求头中的Token解析校验通过后就放行否则返回401状态码。针对不同角色限流的接口比如只有管理员能调用的用户管理接口可以在Controller方法上加一个自定义的RequireRole(admin)注解然后在拦截器里读取注解做判断。这样既实现了权限控制也保持了代码的简洁。4.2 选课核心逻辑事务与并发控制选课是系统里最核心、也最考验并发处理能力的接口。用一句话描述需求就是同一时间可能有很多学生都去抢同一门课系统必须保证不会出现选了超过课程容量的情况。解决这个问题的思路不能是“先查已选人数再判断是否小于容量然后插入记录”因为这个流程在并发场景下会存在竞态条件。两个请求同时查到已选人数是29容量30然后同时通过校验同时插入记录结果选课记录表里多出两条记录课程表里已选人数变成了31这就是典型的超卖问题。解决超卖问题的标准做法是在数据库层面做原子操作。我把选课的核心实现拆成三步第一步在选课记录表里插入记录前先判断该学生是否已经选过这门课这个判断可以利用uk_student_course唯一索引插入时如果冲突会抛出DuplicateKeyException捕获到就返回“不能重复选课”第二步更新课程表的已选人数SQL写成UPDATE cou_course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity如果受影响行数为0说明容量已满或课程不存在直接返回“课程已满”第三步在同一个事务里完成以上两个操作如果第二步失败就回滚第一步的插入。整个过程的代码如下Service public class CourseServiceImpl implements CourseService { Autowired private CourseMapper courseMapper; Autowired private SelectionMapper selectionMapper; Override Transactional(rollbackFor Exception.class) public ResultVO? selectCourse(Long studentId, Long courseId) { // 1. 尝试插入选课记录(student_id, course_id) // 注意数据库存在唯一索引重复选课会抛异常 try { selectionMapper.insert(studentId, courseId); } catch (DuplicateKeyException e) { return ResultVO.error(不能重复选课); } // 2. 原子更新已选人数只有人数未满时更新成功 int rows courseMapper.increaseSelectedCount(courseId); if (rows 0) { // 容量已满抛异常回滚插入 throw new BizException(课程已选满); } return ResultVO.success(null); } }这里有个细节值得展开为什么先插入记录再更新人数如果把顺序反过来先更新人数为30再插入记录时抛出重复键异常那么事务回滚会把人数更新也回滚掉这个逻辑其实也成立。但先插入后更新有一个好处就是可以更早地暴露“重复选课”这个问题避免无谓的更新操作。实际项目中两个顺序都能达到最终一致只要两步在同一个事务里并且更新成功与否通过受影响行数来判断即可。事务的Transactional注解一定要加上并且要理解它的作用在整个方法结束前任何一个抛出异常都会导致前面的SQL全部回滚这是保证数据一致性的核心。4.3 后端接口设计与分层实践接口设计遵循RESTful风格但也不用追求教科书式的绝对规范毕竟毕业设计的核心是把业务讲清楚。我推荐的接口路径如下用户端接口包括POST/api/auth/login登录、GET/api/course/list课程列表、POST/api/course/select选课、POST/api/course/cancel退课、GET/api/student/my-courses我的课程、GET/api/student/my-scores我的成绩。教师端接口包括POST/api/teacher/course/save开课/修改课程、GET/api/teacher/course/list我的课程列表、POST/api/teacher/course/status关闭或开启课程选课、POST/api/teacher/score/save录入成绩。管理端接口包括用户管理、学院管理、学期管理等。Controller层的写法有一个很常见的坑就是参数校验和业务判断都堆在Controller里。正确的做法是Controller只做三件事接收参数、调用Service、返回统一结果。Service层去处理业务校验和规则。这样后面答辩被问到“你代码里事务边界怎么控制”的时候你就能直接说“我在Service层的selectCourse方法上加了Transactional”这就是分层设计的好处。统一返回结果也是一个值得自己动手实现的设计点。可以定义一个ResultVO类包含code、message、data三个字段成功时code为200业务失败时可以是500或者其他自定义编码。所有接口都返回这个结构前端就可以在axios响应拦截器里统一处理错误提示代码整洁度能提升不少。4.4 MyBatis Plus的运用与代码生成对于毕设项目来说MyBatis Plus会比裸的MyBatis更省力。MP内置了大部分单表CRUD方法比如selectById、selectPage、updateById你不用写XML也能完成基本操作。复杂的多表关联查询比如查询课程列表时需要连出教师姓名、学院名称则需要写自定义SQL。这里有一个实践技巧简单操作用MP的内置方法复杂查询写在Mapper的注解里或XML中动静分离各取所长。Mapper public interface CourseMapper extends BaseMapperCourse { Select(SELECT c.*, u.real_name AS teacher_name, col.college_name FROM cou_course c LEFT JOIN sys_user u ON c.teacher_id u.id LEFT JOIN sys_college col ON c.college_id col.id WHERE c.semester #{semester} AND c.status 1) ListCourseVO selectCourseList(String semester); }启用MP的分页插件也是必做的一步选课列表、成绩列表这些场景都要用分页。分页插件的配置方式在MP官方文档里写得很清楚按照文档配置一个MybatisPlusInterceptor的Bean并添加PaginationInnerInterceptor即可。分页的好处不只在于性能更在于前端能够基于分页组件实现有效的交互像Element Plus的el-pagination组件就是直接对接这种数据格式的。5. 前端Vue页面设计与交互实现5.1 前端工程化结构与路由设计前端项目的目录结构按照Vue官方推荐的方式组织src/api存放所有接口请求方法src/router存放路由配置src/store存放全局状态src/views按页面存放各视图组件src/components存放公共组件。路由的配置用Vue Router实现。这里有一个经验之谈路由守卫是必须做的。在router.beforeEach钩子里读取localStorage中的Token如果没有Token且要访问的是需要登录的页面就重定向到登录页。有了路由守卫用户即使手动改URL也进不了未授权的页面这个细节在答辩时拿出来讲老师一般都会认可。还有一点是路由的懒加载使用component: () import(../views/StudentCourseList.vue)这种写法首屏加载会快一些代码拆分也更合理。5.2 页面核心功能拆解整个前端页面主要分登录页、学生端布局、教师端布局、管理端布局几块。学生端的核心页面是课程列表页和“我的课表”页。课程列表页用el-table展示课程信息每一行放一个“选课”按钮。选课之前要确认两件事学分是否已经超标、上课时间是否冲突。可以前端校验也可以后端校验我更推荐两边都做。前端校验的好处是用户体验好不用发请求就知道结果后端校验是安全底线防止有人绕过前端直接调API。“我的课表”页面就是展示当前学生已选课程的列表点击退课按钮时弹出确认框确认后调用退课接口。退课接口核心逻辑和选课类似也有一个原子更新的操作UPDATE cou_course SET selected_count selected_count - 1 WHERE id ?同时把选课记录状态改为已退。要注意的是如果这门课已经录入了成绩就不能再退了这个规则要在Service层判断。教师端的核心页面是课程管理页和成绩录入页。成绩录入页的表格整体做成可编辑的模式教师逐行输入分数点击保存时一次性提交给后端。后端接收成绩列表后对每一条记录执行更新操作并校验分数范围在0到100之间。管理端的核心页面是用户管理和课程审核。用户管理页面提供学生和教师账号的创建、重置密码、禁用等操作。课程审核这个功能视具体需求而定有些学校的选课流程是教师开课、管理员审核后才开放选课这种情况下课程表里需要增加一个审核状态字段这个可以根据实际业务来决定要不要做。5.3 axios封装与状态管理axios封装有一个必须做的事请求拦截器里统一添加Token响应拦截器里统一处理401和业务错误码。实现代码如下// src/utils/request.js import axios from axios import { ElMessage } from element-plus 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) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default request状态管理如果用的是Vue 3我建议直接上Pinia它比Vuex的API更简洁去掉了mutation的概念直接在store里定义state和action就行。用户信息、登录状态、当前角色这些全局数据就放在store里页面间共享非常方便。5.4 前后端联调与跨域处理前后端分离最经典的一个坑就是跨域。因为前端开发服务器比如Vite的5173端口和后端8080端口不是同一个源浏览器会拦截跨域请求。解决方式推荐在后端配置全局跨域不用在前端配代理因为部署到正式环境后前端和后端通常在同一个域名下前端代理在生产环境反而不生效。后端的跨域配置可以写一个配置类实现WebMvcConfigurer接口重写addCorsMappings方法允许的源设置为http://localhost:5173或*允许的方法设置为GET、POST、PUT、DELETE、OPTIONS。这里有一个细节点如果引入了Spring Security跨域配置和Security的Filter顺序也要处理好否则请求会先被Security拦截跨域头未设置导致前端报CORS错误。开发调试时建议开启后端接口的日志打印把每个请求的URL、参数、耗时打出来定位问题会非常方便。Spring Boot的application.yml里把MyBatis的日志级别配置为DEBUG就能在控制台看到完整的SQL语句和参数这一步强烈建议做。6. 数据库设计细节与常见问题排查6.1 事务失效的经典场景在实际开发中事务是非常容易用错的地方。最常见的失效场景是this调用一个类里的方法A没有加Transactional方法A内部直接调用了同类的Transactional方法B由于Spring事务是通过代理对象拦截的直接调用内部方法时不会经过代理导致方法B上的事务注解根本没生效。解决方案是注入自身的代理对象或者把方法B放到另一个Service类里通过注入的Bean来调用。另一个容易踩的坑是Transactional没有指定rollbackFor。Spring默认只在遇到RuntimeException时回滚事务如果业务代码里抛出一个自定义的Exception继承自Exception但没有继承RuntimeException事务就不会回滚。所以统一写成Transactional(rollbackFor Exception.class)是稳妥做法。还有一个和事务绑定很紧的问题是连接池耗尽。如果在Service的方法里写了大循环每个循环都调用一次Mapper查询且这些查询都发生在同一个事务中连接就会被长时间占用并发上来时就有可能导致事务等待、连接池耗尽。选课系统虽然是毕业设计规模但如果选课列表页用了分页一般不会有问题。这里提醒一下接口设计时尽量避免在事务里做耗时的外部调用比如远程HTTP请求、文件IO等。6.2 SQL优化与性能排查学生选课系统数据量不大正常使用不会出现性能问题但答辩时老师可能会问“如果数据量大你的系统怎么优化”这个问题可以从几个层面回答第一表结构层面合理的主键和索引。课程表的course_id、选课记录表的student_id和course_id都需要建索引联合索引uk_student_course已经覆盖了选课查询的场景。第二查询层面避免SELECT *只查询所需字段列表接口用分页限制数据量。第三可以考虑在课程列表页加Redis缓存选课高峰期把课程列表缓存到Redis减少数据库访问频率。不过考虑到这是毕设不用真的做Redis整合在论文里写清楚“优化方案”即可。6.3 数据库连接配置与字符集问题application.yml中的数据库连接配置是必修课。url地址建议加上characterEncodingUTF-8和useSSLfalse参数。字符集不统一会导致中文乱码这是特别容易被新手忽视的问题。创建数据库时也要指定utf8mb4编码而不是默认的latin1。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/course_selection?characterEncodingUTF-8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 hikari: maximum-pool-size: 10 minimum-idle: 5MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver如果用的是5.x的驱动类名是com.mysql.jdbc.Driver这里不要写错。密码如果包含特殊字符记得在配置里对特殊字符做转义。6.4 常见异常速查表我把做这个系统时最容易遇到的几类报错和解决方案整理成了一张速查表遇到类似问题可以直接对着查异常现象可能原因解决方案启动报Access denied for user数据库账号密码错误或权限不足检查账号密码授予对应库的权限启动报Unable to find column实体类字段与表字段映射不一致检查MyBatis Plus的驼峰命名开启配置启动报Port 8080 was already in use端口被占用修改server.port或结束占用进程登录报401 UNAUTHORIZEDToken缺失、过期或密钥不匹配检查拦截器是否放行login接口检查Token过期时间跨域报No Access-Control-Allow-Origin跨域配置未生效或顺序不对检查CorsFilter的配置类是否被Spring扫描选课报乱码数据库字符集问题统一库、表、连接URL字符集为utf8mb4接口返回500且日志有BadSqlGrammarExceptionSQL编写错误开启MyBatis SQL日志检查对应的XML或注解SQL前端页面白屏路由配置错误或依赖未安装完整检查路由路径、npm install是否成功6.5 一个真实的Bug排查案例我在帮学生调试时遇到过这样一个问题学生端“我的课程”列表接口时好时坏有时候能查出数据有时候查出空列表。看起来像灵异事件最后定位到是分页参数的问题。列表接口写得是SELECT * FROM ... WHERE student_id ?但前端分页组件传了current和size两个参数而后端分页逻辑依赖这两个参数当前端切换页码时数据库根据页码和每页大小计算出偏移量当current变大时offset可能会超过实际数据量返回空列表。解决方案是合理处理分页如果查询的是某个学生的选课记录数据量通常不大可以先不分页等数据量大到一定量级再做前端分页。这个案例说明有些看似突然出现的问题其实是边界条件没处理好。调试时我一般建议按照“前端Network看请求和响应→后端日志看SQL和异常→数据库手动执行SQL验证”的顺序去定位问题不要一上来就怀疑框架有Bug。绝大多数网上说的“框架Bug”最后都会发现是自己配置或者使用时的问题。7. 项目部署与文档撰写7.1 本地打包与部署流程毕业设计到最后一步就是打包部署。后端打包用Maven的package命令会生成一个jar包直接在命令行用java -jar启动。这里有一个走弯路的地方Spring Boot默认打包方式是不包含依赖的用spring-boot-maven-plugin打包时需要在plugin配置里指定mainClass有时候IDEA的打包按钮点下去会发现jar包运行时报no main manifest attribute就是因为plugin配置有问题。前端的部署相对来说复杂一些。开发模式下前端是一个开发服务器生产环境需要把Vue项目build成静态文件。执行npm run build后会生成一个dist目录里面是打包好的HTML、JS、CSS文件。部署方式有两种选择一种是把dist目录放在Nginx的静态资源目录里Nginx负责托管前端页面同时配置反向代理把/api开头的请求转发到后端Java进程另一种是把dist目录直接复制到Spring Boot的src/main/resources/static目录下然后重新打包成jar包这样前端页面和后端接口就同源了不需要处理跨域。我推荐第二种方案简单粗暴适合毕设演示。但要注意放进去之前把前端请求的baseURL改成相对路径别写死成http://localhost:8080/api。7.2 演示时的注意事项毕业设计演示的时候最怕的就是现场翻车。根据我陪学生模拟答辩的经验有三个坑必须提前踩第一运行环境要提前一天检查数据库服务是否启动、Redis是否启动如果用的话、后端jar包能不能正常启动这些都要从头到尾跑一遍不要等到答辩当天早上才发现MySQL起不来。第二测试数据要提前准备好演示用的账号和密码写在一张纸上三种角色的账号各准备一个登录时不要录密码录错。第三准备一个“故障预案”万一选课按钮点了没反应要能迅速切换到一个备用页面或者通过手动调接口的方式展示增加的数据记录。7.3 毕业论文结构建议毕业设计论文的框架基本是固定的给出一个参考的章节安排摘要中英文、绪论研究背景、国内外现状、研究内容、相关技术介绍Spring Boot、Vue、MySQL、JWT等、需求分析功能性需求、非功能性需求、用例图、系统设计架构设计、功能模块设计、数据库设计、系统实现每个模块的实现界面截图和核心代码讲解、系统测试功能测试、性能测试、测试用例表、总结与展望。论文写作方面给一个实用建议代码不要大段大段地贴要挑重点、贴关键的几行并用文字解释这段代码的作用。界面截图要尽量规范把窗口调整到合适的大小再截图避免出现任务栏、桌面图标等无关信息。测试部分要写具体的测试用例和预期结果比如“输入正确的用户名密码点击登录系统跳转到首页”把实际结果写出来。这样的论文内容充实格式也符合要求。8. 答辩高频考点与面试延伸8.1 评委老师大概率会问什么答辩的时候老师不会照着你的论文从头到尾读一般会针对几个核心点追问。我把高频问题列一下大家可以提前准备项目用了什么技术栈、为什么选这个技术栈数据库表有哪几张表之间的关联关系是什么权限控制是怎么实现的选课并发是怎么处理的JWT的过期时间怎么设置的、Token被窃取了怎么办密码是怎么加密的为什么不用MD5项目里的事务是怎么控制的分页是怎么做的如果用户量大了怎么办。这些问题不一定全部都会问到但每一个都能在本文里找到答案提前准备充分的回答答辩基本就稳了。8.2 从毕设项目到面试谈资顺带说一句这个项目的技术点完全可以延伸到面试场景。Spring Boot方面可以深入讲讲自动配置原理、启动流程、事务传播机制Vue方面可以聊聊响应式原理、虚拟DOM、组件通信方式数据库方面可以聊聊索引失效场景、事务隔离级别、MVCC。如果能把选课并发控制那颗“原子更新”的解法讲清楚面试官会认为你是真的写过代码而不是只会背八股文。8.3 项目的扩展方向如果时间充裕可以适当做一些扩展让系统的完整度和技术亮点更突出引入Redis缓存课程列表和Token黑名单引入RabbitMQ做选课异步通知比如选课成功后发邮件或站内信引入Spring Security替换手写拦截器加入数据统计模块用ECharts展示各学院选课人数等。扩展功能的难度并不大但在论文里作为“提升优化”章节出现会让整个项目更有竞争力。要注意的是扩展功能不要贪多一个到两个就够用了重点是能讲清楚原理能演示出效果。做得过多反而容易在演示时暴露出Bug。最后分享一点我个人的指导经验做毕业设计最大的收获不是那个分数而是把一个想法变成一个能跑的系统这个完整过程。很多同学平时上课是知识点一块一块学的Spring Boot学了个Controller、Vue学了个组件到这个项目里要把它们串起来的时候才会发现自己哪块是真正理解了、哪块只是“看着眼熟”。如果你正在做这个选题遇到问题先自己查、自己调实在卡住了再找人问这个独立解决问题的过程比任何代码本身都重要。希望这篇文章能帮你少踩一些坑顺利度过毕设这道关。