简介:这是一套面向计算机相关专业学生的Java毕业设计完整项目,以学生选课系统为实战案例,适合正在准备毕业设计或课程设计、需要参考前后端分离开发流程的学习者。项目采用SpringBoot+MyBatis后端与Vue前端组合,MySQL存储数据,覆盖管理员管理专业、教师、学生、课程,教师审核选课与录入成绩,学生选课、查询选课及成绩等完整业务闭环。压缩包共126个文件,约3.83MB,以97个Java源码为核心,辅以bat启动脚本、js与css前端资源、xml与yml配置、sql初始化脚本,以及docx开题报告、doc说明文档和ppt展示文件,另含Maven构建相关文件,结构清晰便于按模块查阅。已有61人学习下载。读者可据此获得一套可直接运行调试的选课系统源码、数据库脚本与配套文档,理解前后端分离架构、接口设计与成绩管理逻辑,为毕业设计撰写与答辩提供完整参考。
1. 从一份学生选课系统源码说起:SpringBoot+Vue+MySQL 到底能跑出什么
很多同学拿到「学生选课系统源码(SpringBoot+Vue+MySQL+说明文档+LW)」这类压缩包时,第一反应是解压、找 SQL、改数据库密码、npm run dev,然后卡在跨域或者 401 上。这套技术栈之所以成为 Java 毕业设计里最稳的组合,是因为它把「后端接口 + 前端页面 + 数据持久化」三件事拆得足够清楚:SpringBoot 负责 REST 接口和业务事务,Vue 负责选课列表、已选课程、退课按钮这些交互,MySQL 存学生、课程、选课记录三张核心表。它适合两类人:一是需要在一到两周内把系统跑起来、能演示、能写论文的应届生;二是想拿一个真实 CRUD 项目练手 SpringBoot 分层和 Vue 路由的初学者。这一章先把「这个系统由什么组成、数据怎么流」讲透,后面再动手。
选课系统的业务闭环其实很短:学生登录 → 浏览课程列表 → 选课(写入选课记录)→ 查看已选 → 退课(删除记录)。管理员则多一条线:维护课程、设置容量、查看选课统计。真正容易翻车的地方不在页面,而在「选课」这个动作背后的并发和约束——同一门课容量 50,第 51 个人点选课时后端必须拦住。所以这套源码的价值不只是「能跑」,而是它把事务、唯一索引、容量校验这些点放在了一个你能看懂的小场景里。下面按「先理解结构,再动手跑通,最后处理坑」的顺序展开。
2. 拆解源码结构:SpringBoot 分层与 Vue 页面怎么对应
2.1 后端目录:controller / service / mapper 各管什么
拿到源码后先别急着改配置,先看后端包结构。典型的 SpringBoot 选课系统会按controller、service、service.impl、mapper、entity、common分层。controller只做参数接收和返回封装,service写业务规则(比如选课前查容量、查是否已选),mapper对应 MyBatis 或 MyBatis-Plus 的数据库操作,entity是和学生、课程、选课记录一一映射的 Java 类。
// CourseController.java 片段:只负责接收请求,业务交给 service @RestController @RequestMapping("/api/course") public class CourseController { @Autowired private CourseService courseService; // 学生选课入口,studentId 从登录态里取,不信任前端传值 @PostMapping("/select") public Result select(@RequestParam Long courseId, HttpServletRequest request) { Long studentId = (Long) request.getSession().getAttribute("userId"); courseService.selectCourse(studentId, courseId); return Result.success(); } }这段代码的关键点是:studentId不从请求参数拿,而是从 session 或 token 里取。很多同学调试时图省事直接前端传studentId,结果演示时被老师一问「能不能改别人的选课」就露馅。Result是统一返回体,一般包含code、msg、data三个字段,前端根据code判断成功失败。
2.2 前端目录:views / api / router 的职责边界
Vue 侧通常分views(页面)、api(请求封装)、router(路由)、store(状态,可选)。选课相关页面一般有CourseList.vue、MyCourse.vue、Login.vue。api目录里用 axios 封装后端地址,方便统一改 baseURL 和加拦截器。
// api/course.js:统一封装选课相关请求 import request from '@/utils/request' // 获取可选课程列表,带分页参数 export function listCourses(params) { return request({ url: '/api/course/list', method: 'get', params }) } // 选课,courseId 由页面传入 export function selectCourse(courseId) { return request({ url: '/api/course/select', method: 'post', params: { courseId } }) }request.js里一般会配baseURL和响应拦截器。如果后端返回code !== 200,拦截器统一弹提示,页面就不用每个请求都写错误处理。这里要注意:前端params传参对应后端@RequestParam,data传参对应@RequestBody,两者混用是 400 错误的常见来源。
2.3 数据库三张核心表与字段设计
选课系统的表不用多,但字段要能支撑业务。常见三张表如下:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| student | id, username, password, name, class_name | 学生信息,password 建议存加密值 |
| course | id, name, teacher, credit, capacity, selected_count | 课程信息,capacity 为容量上限 |
| course_selection | id, student_id, course_id, create_time | 选课记录,student_id+course_id 建唯一索引 |
course_selection上的唯一索引uk_student_course (student_id, course_id)是防重复选课的第一道防线。course表的selected_count是冗余字段,用来快速判断是否满员,但它必须和选课记录在同一个事务里更新,否则会出现「记录写了但计数没加」的不一致。
3. 本地跑通:MySQL 建库、SpringBoot 启动、Vue 联调
3.1 MySQL 建库与导入:字符集和时区两个必改项
先在 MySQL 里建库,字符集用utf8mb4,否则学生姓名里的生僻字会变问号。导入源码里的.sql文件时,注意文件里可能写死了CREATE DATABASE名字,先打开看一眼再执行。
-- 建库,字符集和排序规则显式指定,避免中文乱码 CREATE DATABASE course_selection DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_selection; -- 选课记录表:唯一索引防止同一学生重复选同一门课 CREATE TABLE course_selection ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;执行完用SHOW TABLES;确认表都建好了。如果导入时报Unknown collation: utf8mb4_0900_ai_ci,说明 SQL 文件来自 MySQL 8,而本机是 5.7,把排序规则改成utf8mb4_general_ci即可。时区问题表现为插入时间差 8 小时,在连接串里加serverTimezone=Asia/Shanghai。
3.2 SpringBoot 配置:数据源、端口、MyBatis 映射
application.yml里重点改三处:数据库连接、服务端口、MyBatis 的 mapper 路径。连接串要带时区和字符集参数。
server: port: 8080 # 后端端口,前端代理要对应 spring: datasource: url: jdbc:mysql://localhost:3306/course_selection?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml # XML 映射文件位置 configuration: map-underscore-to-camel-case: true # 下划线字段自动映射驼峰属性map-underscore-to-camel-case打开后,数据库的student_id会自动映射到 Java 的studentId,省掉大量resultMap。如果启动报Failed to configure a DataSource,多半是 yml 缩进错了或者密码含特殊字符没加引号。启动成功后访问http://localhost:8080/api/course/list看是否返回 JSON,这一步能排除掉大部分后端问题。
3.3 Vue 启动与跨域:proxy 配置比改后端更省事
前端先npm install,再npm run serve。如果请求后端报 CORS 错误,不要急着在后端加@CrossOrigin,在vue.config.js里配代理更干净。
// vue.config.js:开发环境把 /api 代理到后端,避免跨域 module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true // 如果后端接口本身带 /api 前缀,这里不要 rewrite } } } }配好代理后,前端请求写/api/course/list,浏览器实际发给8081,由 devServer 转发到8080,同源问题消失。注意changeOrigin: true要加,否则某些后端会因 Host 不匹配拒绝。联调顺序建议:先单独测后端接口(用浏览器或 Postman),再启动前端,这样出问题能快速定位是哪一层。
4. 选课核心逻辑:事务、容量校验与并发处理
4.1 选课接口的完整业务链路
选课不是简单 insert 一条记录。正确顺序是:查课程是否存在 → 查是否已选(唯一索引兜底)→ 查是否满员 → 插入选课记录 → 更新课程已选人数。这五步里,后两步必须在一个事务里,否则并发下会出现超卖。
@Service public class CourseServiceImpl implements CourseService { @Autowired private CourseMapper courseMapper; @Autowired private SelectionMapper selectionMapper; @Override @Transactional(rollbackFor = Exception.class) // 任何异常都回滚 public void selectCourse(Long studentId, Long courseId) { Course course = courseMapper.selectById(courseId); if (course == null) { throw new BizException("课程不存在"); } // 已选人数达到容量,直接拒绝 if (course.getSelectedCount() >= course.getCapacity()) { throw new BizException("课程已满"); } // 唯一索引会拦住重复选课,这里先查一次给出友好提示 if (selectionMapper.exists(studentId, courseId)) { throw new BizException("你已选过这门课"); } selectionMapper.insert(studentId, courseId); // 带条件的更新,防止并发下计数错误 int rows = courseMapper.increaseSelected(courseId); if (rows == 0) { throw new BizException("选课失败,请重试"); } } }@Transactional(rollbackFor = Exception.class)里的rollbackFor不能省,默认只回滚运行时异常,业务里抛的自定义异常如果是受检异常就不会回滚。increaseSelected对应的 SQL 要带条件,见下一节。
4.2 用带条件的 UPDATE 防超卖
单纯UPDATE course SET selected_count = selected_count + 1 WHERE id = ?在并发下仍可能超卖,因为容量判断和更新是分开的。更稳的做法是把容量条件写进 UPDATE。
-- 只有当前已选人数小于容量时才加一,返回影响行数 UPDATE course SET selected_count = selected_count + 1 WHERE id = #{courseId} AND selected_count < capacity;这条 SQL 返回的影响行数是 0 就说明被别人抢先占满了,service 里据此抛异常回滚。这样即使两个请求同时进来,数据库行锁也会保证只有一个能更新成功。这是选课系统里最值得写进论文的一个技术点,比堆一堆页面更有说服力。
4.3 退课与容量回退
退课逻辑是选课的反向:删选课记录 → 已选人数减一,同样放一个事务里。删除时要用student_id + course_id双条件,防止删到别人的记录。
@Transactional(rollbackFor = Exception.class) public void dropCourse(Long studentId, Long courseId) { int rows = selectionMapper.deleteByStudentAndCourse(studentId, courseId); if (rows == 0) { throw new BizException("你未选这门课"); } courseMapper.decreaseSelected(courseId); // selected_count - 1 }decreaseSelected的 SQL 要加selected_count > 0条件,避免减成负数。退课接口同样要校验登录态,不能只靠前端传的 studentId。
5. 避坑与排查:那些让演示当场翻车的细节
5.1 现象:登录后刷新页面就退出登录
原因:登录态存在 Vuex 或组件 data 里,刷新后内存清空。解决:把 token 或用户信息存localStorage,路由守卫里从localStorage读取判断。后端如果用 session,要确认前端请求带上了 cookie,axios 需设withCredentials: true,同时后端 CORS 要允许凭证。
5.2 现象:选课成功但已选人数没变
原因:插入选课记录和更新计数没放在同一事务,或者更新 SQL 没执行。解决:确认 service 方法上有@Transactional,并检查increaseSelected的影响行数是否被忽略。血泪经验是:先看数据库里course_selection有没有记录,再看course.selected_count,两个都对不上就是事务没生效。
5.3 现象:中文课程名在页面显示成乱码
原因:数据库、连接串、前端三处字符集不一致。解决:数据库用utf8mb4,连接串加characterEncoding=utf8,前端 HTML 的meta声明charset="utf-8"。三处缺一处都可能乱码,排查时从数据库SELECT出来看是不是已经乱了,能快速定位是哪一层。
5.4 现象:npm install 报 node-sass 编译失败
原因:node-sass 和本机 Node 版本不匹配,这是 Vue2 老项目的经典坑。解决:换用sass(dart-sass),或者用 nvm 把 Node 降到 14/16。如果项目里写死了 node-sass,改package.json换成sass并同步改vue.config.js里的implementation。
5.5 现象:接口返回 401 但登录明明成功了
原因:前端请求没带 token,或后端拦截器路径配错把登录接口也拦了。解决:检查 axios 请求拦截器有没有统一加Authorization头,检查后端拦截器的excludePathPatterns是否放行了/api/login。这类问题用浏览器 Network 面板看请求头最快。
6. 进阶技巧:把这份源码改造成能写进简历的项目
跑通只是起点,真正让这份源码有价值的是你能在它上面加东西。第一个建议是加「选课时间窗口」:在course表加start_time、end_time,service 里判断当前时间是否在窗口内,这能体现你对业务约束的理解。第二个建议是把选课记录做成带状态的(正常/已退),而不是物理删除,这样能支持「退课记录查询」,也更贴近真实系统。
// 选课时间窗口校验,放在容量校验之前 LocalDateTime now = LocalDateTime.now(); if (now.isBefore(course.getStartTime()) || now.isAfter(course.getEndTime())) { throw new BizException("不在选课时间内"); }第三个建议是加一个简单的选课统计接口,用GROUP BY返回每门课的已选人数和剩余名额,前端用表格展示。这个接口能让你在答辩时讲清楚「聚合查询 + 索引优化」——给course_selection.course_id建索引,统计会快很多。
-- 选课统计:每门课已选人数与剩余名额 SELECT c.id, c.name, c.capacity, COUNT(s.id) AS selected, c.capacity - COUNT(s.id) AS remain FROM course c LEFT JOIN course_selection s ON s.course_id = c.id GROUP BY c.id, c.name, c.capacity;验证改造是否成功,别只看页面。用EXPLAIN看统计 SQL 有没有走索引,用两个浏览器同时点选课看会不会超卖,用SELECT * FROM course_selection确认没有重复记录。我自己的习惯是每改一个业务点,先在数据库层面验证数据对不对,再看接口返回,最后才看页面——页面会骗人,数据不会。这套流程走下来,这份源码就不只是毕业设计,而是你能讲清楚每一个技术决策的项目。希望帮到你。
本文还有配套的精品资源,点击获取