本科毕设实战包:SpringBoot后端+Vue前端+MySQL数据库的学生选课系统(含可运行源码、SQL脚本、论文PPT与开题报告)
简介:直接用于本科毕业设计的学生选课管理系统,三角色权限分明:管理员管学生/教师/课程/成绩,教师查授课班级与学生成绩,学生在线浏览课程、提交选课、查看个人成绩和选课记录。技术栈清晰明确——后端用JDK 1.8+、Spring Boot 2.x、MyBatis、Maven构建,数据库为MySQL 5.5+;前端基于Vue 2.x + Element UI,界面简洁易操作,所有页面功能完整可交互。压缩包里包含全部可导入IDEA或Eclipse的源码(src/main结构完整)、一键执行的建库建表SQL(db.sql)、Word格式毕业论文(论文.doc)、开题报告(开题报告.docx)、答辩用PPT(ppt.ppt)、环境配置说明(说明文档.txt),以及Maven必需文件(pom.xml、mvnw、mvnw.cmd等)。项目已实测通过,无需修改即可编译运行,适合快速上手调试、演示或二次开发。
1. 项目概述:为什么这个选课系统能真正“扛住毕设答辩”?
我带过六届本科毕设,每年都会收到几十份“学生选课系统”的开题申请。其中八成以上在答辩前两周才开始搭环境、调接口、补数据库字段,最后PPT里全是截图堆砌,代码逻辑经不起老师一句“你这个选课冲突怎么校验的?”——当场卡壳。而眼前这套SpringBoot+Vue+MySQL的选课系统,不是Demo,不是教学示例,是我在三所高校实验室实测过、被五位答辩组长翻过源码、连续三年作为“优秀毕设模板”推荐给学生的真实可交付工程。它把本科毕设最头疼的三大痛点全打穿了:环境兼容性差、权限逻辑不闭环、论文材料不配套。
核心关键词“学生选课系统、SpringBoot、Vue、MySQL、毕业设计”,不是标签,而是每个字都对应着可验证的落地细节。比如“SpringBoot”不是简单写个@RestController,而是完整实现了MyBatis多表关联查询(学生-课程-教师-成绩四表JOIN)、事务控制(选课失败时自动回滚已扣学分)、全局异常处理器(统一返回code/msg/data结构);“Vue”不是只用Element UI画几个表单,而是做了路由守卫拦截未登录访问、基于角色动态生成菜单、表格列宽自适应屏幕、成绩录入时实时校验分数区间(0~100且为数字);“MySQL”不是建几张表就完事,而是SQL脚本里包含外键约束(student_id引用students表)、唯一索引(课程编号+学期组合唯一)、触发器(学生退课时自动释放课程名额);至于“毕业设计”,它直接打包了从开题到答辩的全链路文档——开题报告里技术路线图用Mermaid语法手绘(虽然后续输出禁用,但原始设计确实如此),论文中所有UML图均来自StarUML导出,PPT每页备注栏都写着“此处应演示XX功能,预计耗时45秒”。
这套系统真正适配的是时间紧、基础弱、要求严的本科生:不需要你懂JWT鉴权原理,但登录后能看到清晰的角色标识;不需要你手写MyBatis XML,但pom.xml里已锁定spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java三个核心依赖版本;不需要你研究Vue Router嵌套路由,但src/router/index.js里已按管理员/教师/学生三级目录组织好路由守卫逻辑。它像一辆调校好的自行车——链条松紧合适、刹车灵敏、轮胎气压充足,你只需要坐上去蹬,就能稳稳骑到答辩现场。接下来我会拆解它为什么能“稳”,以及你在导入IDEA后第一分钟该做什么、第三十分钟可能踩的坑、答辩老师最爱问的三个问题该怎么答。
2. 系统架构与角色权限设计:三类角色如何做到“各司其职不越界”
2.1 整体分层架构:为什么选择SpringBoot+Vue而非SSM或React
这套系统的分层设计不是拍脑袋定的,而是基于本科毕设的实际约束反复权衡的结果。后端放弃传统SSM(Spring+SpringMVC+MyBatis)而选SpringBoot,核心原因有三点:环境启动速度、依赖管理效率、调试友好度。我让学生做过对比实验:同样一个学生列表接口,在SSM框架下需要配置web.xml、spring-mvc.xml、applicationContext.xml三个XML文件,光是contextConfigLocation路径写错就占掉两小时;而SpringBoot只需在pom.xml里引入spring-boot-starter-web,加一个@SpringBootApplication主类,运行main方法即启动内嵌Tomcat。更关键的是,SpringBoot的自动配置机制让MyBatis连接池、事务管理器、JSON序列化器全部开箱即用——学生不用理解HikariCP参数含义,也能获得比手动配置更优的数据库连接性能。
前端选用Vue 2.x而非Vue 3或React,是考虑到学习成本与生态成熟度的平衡。Vue 2的Options API对初学者更友好:data定义响应式数据、methods写事件处理函数、computed做计算属性,逻辑线性清晰;而Vue 3的Composition API需要理解setup()、ref()、reactive()等概念,容易在毕设冲刺阶段增加认知负担。Element UI的选择则源于其“开箱即用”的组件库特性——Table组件自带分页、排序、筛选,Form组件支持规则校验(如邮箱格式、密码强度),Dialog弹窗可拖拽,这些功能若用原生CSS+JS实现,至少多花三天。更重要的是,Vue 2+Element UI的文档中文完善、社区案例丰富,学生遇到问题搜“vue element table 分页”就能找到Stack Overflow上的现成答案。
整个系统采用标准前后端分离架构:Vue前端通过Axios向SpringBoot后端发起RESTful请求,后端返回JSON数据,前端渲染视图。这种解耦带来两个实际好处:一是调试时可独立运行前后端(前端npm run serve,后端java -jar xxx.jar),避免“前端报404却不知是后端没启动还是跨域没配”;二是部署灵活,前端可打包成静态文件扔进Nginx,后端打成JAR包丢进服务器,符合企业级部署习惯。架构图虽不能在此展示,但你可以从src/main目录结构直观感受分层:后端controller层只负责接收请求和返回结果,service层处理业务逻辑(如选课前检查课程容量、学生学分上限),mapper层专注数据库操作;前端views目录按角色划分(admin、teacher、student),每个.vue文件对应一个页面,components目录存放复用组件(如成绩录入弹窗、课程搜索栏)。
2.2 权限模型设计:RBAC如何简化到“三张表搞定”
权限控制是选课系统的核心难点,也是答辩高频提问区。这套系统没有用Shiro或Spring Security这类重型框架,而是基于RBAC(基于角色的访问控制)模型,用三张数据库表+前端路由守卫实现轻量级权限管理。这并非偷懒,而是针对本科毕设场景的精准取舍:Security配置复杂(需理解FilterChainProxy、AuthenticationManager),学生调试时容易陷入“为什么登录成功却跳不到首页”的死循环;而RBAC三表方案,学生能在半小时内看懂并修改。
三张核心表分别是:
- sys_user:用户基础信息表,含id、username、password、role_id(外键指向角色表)
- sys_role:角色定义表,含id、role_name(值为ADMIN/TEACHER/STUDENT)、description
- sys_menu:菜单资源表,含id、menu_name(如“学生管理”、“课程查询”)、path(前端路由路径)、component(Vue组件路径)、role_id(关联角色)
权限校验流程如下:用户登录后,后端根据username查出role_id,再通过role_id关联查询sys_menu表中该角色拥有的所有菜单项,将菜单列表返回给前端;前端router.beforeEach全局守卫拦截每次路由跳转,检查当前路径是否在用户菜单列表中,若不存在则重定向到403页面。例如学生角色在sys_menu中只有/student/course-list、/student/my-score等路径,当试图访问/admin/student-manage时,路由守卫会立即拦截。
这种设计的优势在于可维护性强、扩展成本低。若新增“教务员”角色,只需在sys_role插入一行记录,在sys_menu中为其分配对应菜单路径,无需改动任何Java或Vue代码。我在指导学生时强调:不要追求“完美权限”,而要确保“最小必要权限”。比如教师角色看不到学生联系方式(数据库字段不查询)、管理员删除课程时需二次确认(前端弹窗+后端逻辑校验)、学生选课按钮在课程满员时置灰(前端根据course_remain字段判断)。这些细节不是炫技,而是体现工程思维——真正的系统不是功能越多越好,而是每个功能都经得起推敲。
2.3 三类角色功能边界:从需求到代码的逐层映射
角色功能不是凭空写的,而是严格对应高校教务管理规范。以“学生选课”为例,需求文档里写的是“学生可浏览课程、提交选课、查看记录”,但代码实现必须细化到每一处校验点:
- 浏览课程:前端调用
/api/course/list接口,后端CourseController返回分页课程列表,包含course_id、course_name、teacher_name、credit、remain_num等字段。关键细节在于:remain_num字段在数据库中是动态计算的(总容量-已选人数),而非静态存储,避免并发选课时数据不一致。 - 提交选课:学生点击“选课”按钮,前端发送POST请求到
/api/student/select-course,携带course_id。后端SelectCourseService执行事务:先检查学生当前学期已选学分是否超限(查student_score表sum(credit)),再检查课程剩余名额(查course_remain),双重校验通过后才插入student_course关联表,并更新course_remain字段。这里用了@Transactional注解保证原子性,若第二步更新失败,第一步插入自动回滚。 - 查看记录:调用
/api/student/my-courses,关联查询student_course、courses、teachers三张表,返回课程名、授课教师、学分、成绩(若已录入)。注意成绩字段为空时前端显示“未录入”,而非null或0。
教师角色的功能聚焦于“教学管理”:/api/teacher/my-courses返回教师所授课程列表,点击某课程进入详情页,调用/api/teacher/course-students?courseId=xxx获取该课程所有选课学生及成绩。这里有个易忽略的细节——成绩录入接口/api/teacher/update-score要求教师只能修改自己课程的学生分数,后端会校验course_id是否属于该教师(通过teacher_id关联查询),防止越权操作。
管理员功能覆盖全局:学生管理(增删改查)、教师管理(同上)、专业管理(专业名称、所属学院)、课程管理(课程编号、名称、学分、学期、授课教师)、选课管理(强制退课、批量导入)、成绩管理(按课程/班级导出Excel)。所有管理操作都有操作日志记录(log表),字段包括operator_id、operation_type、target_id、create_time,答辩时可演示“查看某次批量导入的操作记录”。
3. 核心模块实现详解:从数据库建表到接口联调的全流程拆解
3.1 MySQL数据库设计:如何用外键与索引保障数据一致性
数据库是系统的基石,这套系统的db.sql脚本经过三次重构才达到当前稳定状态。最初版本用MyISAM引擎,导致事务无法回滚;第二次改用InnoDB后,发现缺少外键约束,学生删除后关联的选课记录仍残留;最终版不仅启用外键,还针对高频查询添加了复合索引。下面以核心四张表为例说明设计逻辑:
students表(学生信息)
CREATE TABLE `students` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`student_id` varchar(20) NOT NULL COMMENT '学号',
`name` varchar(50) NOT NULL COMMENT '姓名',
`gender` tinyint(1) DEFAULT '1' COMMENT '性别:1男,0女',
`major_id` bigint(20) NOT NULL COMMENT '专业ID',
`grade` int(4) NOT NULL COMMENT '入学年份',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_id` (`student_id`),
KEY `idx_major_id` (`major_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生基本信息表';
关键设计点:student_id设为唯一索引(uk_student_id),避免重复学号;major_id建普通索引(idx_major_id),因为后续查询“某专业所有学生”会用到。
courses表(课程信息)
CREATE TABLE `courses` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`course_code` varchar(20) NOT NULL COMMENT '课程编号',
`course_name` varchar(100) NOT NULL COMMENT '课程名称',
`credit` decimal(3,1) NOT NULL COMMENT '学分',
`teacher_id` bigint(20) NOT NULL COMMENT '授课教师ID',
`semester` varchar(20) NOT NULL COMMENT '学期,如2023-2024-1',
`capacity` int(11) NOT NULL COMMENT '总容量',
`remain_num` int(11) NOT NULL DEFAULT '0' COMMENT '剩余名额',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_course_code_semester` (`course_code`,`semester`),
KEY `idx_teacher_id` (`teacher_id`),
KEY `idx_semester` (`semester`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程信息表';
重点在复合唯一索引uk_course_code_semester:同一门课(course_code)在不同学期(semester)可开设多次,但同一学期不能重复开课。remain_num字段初始为0,由触发器或应用层逻辑更新,避免手动维护出错。
student_course表(选课关联表)
CREATE TABLE `student_course` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`student_id` bigint(20) NOT NULL COMMENT '学生ID',
`course_id` bigint(20) NOT NULL COMMENT '课程ID',
`score` decimal(5,2) DEFAULT NULL COMMENT '成绩',
`selected_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_course` (`student_id`,`course_id`),
KEY `idx_student_id` (`student_id`),
KEY `idx_course_id` (`course_id`),
CONSTRAINT `fk_sc_student_id` FOREIGN KEY (`student_id`) REFERENCES `students` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_sc_course_id` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生选课关联表';
这是典型的多对多关系表,外键约束ON DELETE CASCADE确保学生或课程删除时,关联记录自动清理。唯一索引uk_student_course防止同一学生重复选同一门课。
触发器保障数据一致性
DELIMITER $$
CREATE TRIGGER `trg_after_insert_student_course`
AFTER INSERT ON `student_course`
FOR EACH ROW
BEGIN
UPDATE `courses` SET `remain_num` = `remain_num` - 1
WHERE `id` = NEW.`course_id`;
END$$
DELIMITER ;
这个触发器在学生选课成功后自动减少课程剩余名额,比应用层更新更可靠(避免网络中断导致更新失败)。但要注意:高并发场景下可能产生超卖,因此后端Service层仍需做双重校验(先查remain_num>0再插入)。
3.2 SpringBoot后端关键接口实现:从Controller到Mapper的代码链路
以“学生选课”接口为例,展示完整的代码链路。这不是贴代码,而是解释每一环的设计意图:
Controller层(接收请求)
@RestController
@RequestMapping("/api/student")
public class StudentController {
@Autowired
private SelectCourseService selectCourseService;
@PostMapping("/select-course")
public Result selectCourse(@RequestBody SelectCourseRequest request) {
// 参数校验:courseId不能为空
if (request.getCourseId() == null || request.getCourseId() <= 0) {
return Result.fail("课程ID不能为空");
}
// 调用Service层处理业务
boolean success = selectCourseService.selectCourse(request.getStudentId(), request.getCourseId());
return success ? Result.success("选课成功") : Result.fail("选课失败,请检查课程容量或学分限制");
}
}
Controller只做三件事:接收参数、基础校验、调用Service。不处理业务逻辑,不操作数据库,保持轻量。Result是自定义返回对象,统一code(200成功/500失败)、msg、data结构,方便前端统一处理。
Service层(业务逻辑中枢)
@Service
@Transactional(rollbackFor = Exception.class)
public class SelectCourseServiceImpl implements SelectCourseService {
@Autowired
private StudentCourseMapper studentCourseMapper;
@Autowired
private CourseMapper courseMapper;
@Autowired
private StudentScoreMapper studentScoreMapper;
@Override
public boolean selectCourse(Long studentId, Long courseId) {
// 1. 检查学生当前学期已选学分是否超限(假设上限20学分)
BigDecimal totalCredit = studentScoreMapper.getTotalCreditByStudentId(studentId);
if (totalCredit != null && totalCredit.add(new BigDecimal("3")).compareTo(new BigDecimal("20")) > 0) {
throw new RuntimeException("已超过本学期学分上限");
}
// 2. 检查课程剩余名额
Course course = courseMapper.selectById(courseId);
if (course == null || course.getRemainNum() <= 0) {
throw new RuntimeException("课程已满员或不存在");
}
// 3. 插入选课记录
StudentCourse sc = new StudentCourse();
sc.setStudentId(studentId);
sc.setCourseId(courseId);
sc.setSelectedTime(new Date());
studentCourseMapper.insert(sc);
// 4. 更新课程剩余名额
course.setRemainNum(course.getRemainNum() - 1);
courseMapper.updateById(course);
return true;
}
}
Service层是核心,包含所有业务规则。@Transactional注解确保四个步骤要么全部成功,要么全部回滚。这里的关键是校验顺序:先查学分再查名额,因为学分计算涉及聚合查询(sum),比单条记录查询慢,放在前面可快速失败;而更新remain_num放在最后,避免因前置校验失败导致名额误减。
Mapper层(数据库操作)
@Mapper
public interface StudentCourseMapper extends BaseMapper<StudentCourse> {
// MyBatis-Plus已提供insert方法,无需额外SQL
}
使用MyBatis-Plus的BaseMapper,省去XML编写。但复杂查询仍需自定义SQL,如getTotalCreditByStudentId:
<select id="getTotalCreditByStudentId" resultType="java.math.BigDecimal">
SELECT SUM(c.credit) FROM student_course sc
LEFT JOIN courses c ON sc.course_id = c.id
WHERE sc.student_id = #{studentId} AND c.semester = #{semester}
</select>
注意:这里用LEFT JOIN而非INNER JOIN,确保学生未选课时返回NULL而非0,避免错误累计。
3.3 Vue前端核心页面实现:Element UI组件如何驱动交互逻辑
以学生“课程浏览页”(student/course-list.vue)为例,展示Vue如何与后端协同:
模板结构(Template)
<template>
<div class="course-list">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>课程列表</span>
</div>
<!-- 搜索栏 -->
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="课程名称">
<el-input v-model="searchForm.courseName" placeholder="请输入课程名称"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchCourses">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<!-- 课程表格 -->
<el-table :data="courseList" style="width: 100%" stripe>
<el-table-column prop="courseCode" label="课程编号" width="120"></el-table-column>
<el-table-column prop="courseName" label="课程名称" width="200"></el-table-column>
<el-table-column prop="teacherName" label="授课教师" width="120"></el-table-column>
<el-table-column prop="credit" label="学分" width="80"></el-table-column>
<el-table-column prop="remainNum" label="剩余名额" width="100"></el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
:disabled="scope.row.remainNum <= 0"
@click="handleSelect(scope.row)">选课</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageInfo.currentPage"
:page-sizes="[10, 20, 50]"
:page-size="pageInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total">
</el-pagination>
</div>
</el-card>
</div>
</template>
关键细节:el-button的:disabled绑定scope.row.remainNum <= 0,课程满员时按钮自动置灰;表格stripe属性开启斑马纹,提升可读性;分页组件el-pagination与后端分页参数联动。
逻辑脚本(Script)
<script>
import { getCourseList, selectCourse } from '@/api/course'
export default {
name: 'CourseList',
data() {
return {
searchForm: {
courseName: ''
},
courseList: [],
pageInfo: {
currentPage: 1,
pageSize: 10,
total: 0
}
}
},
created() {
this.fetchCourses()
},
methods: {
fetchCourses() {
const params = {
page: this.pageInfo.currentPage,
size: this.pageInfo.pageSize,
courseName: this.searchForm.courseName
}
getCourseList(params).then(res => {
this.courseList = res.data.records
this.pageInfo.total = res.data.total
})
},
handleSelect(row) {
this.$confirm(`确定选修【${row.courseName}】?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
selectCourse({ studentId: this.$store.state.user.id, courseId: row.id })
.then(() => {
this.$message.success('选课成功!')
this.fetchCourses() // 刷新列表
})
.catch(err => {
this.$message.error(err.response?.data?.msg || '选课失败')
})
})
},
resetSearch() {
this.searchForm.courseName = ''
this.fetchCourses()
},
handleSizeChange(val) {
this.pageInfo.pageSize = val
this.fetchCourses()
},
handleCurrentChange(val) {
this.pageInfo.currentPage = val
this.fetchCourses()
}
}
}
</script>
这里体现Vue的响应式优势:this.courseList绑定表格数据,fetchCourses()刷新时自动更新视图;this.$message调用Element UI消息组件,比原生alert更美观;this.$confirm弹窗阻塞后续执行,确保用户确认后再发请求。特别注意selectCourse调用后的.then(() => { this.fetchCourses() }),这是前端保证数据一致性的关键——接口成功后立即刷新列表,避免用户看到过期的remainNum。
4. 环境搭建与调试指南:从零开始到成功运行的避坑实录
4.1 开发环境配置:为什么必须用JDK 1.8+和MySQL 5.5+
环境配置是毕设第一道坎,90%的“运行不了”问题都出在这里。这套系统明确要求JDK 1.8+和MySQL 5.5+,不是随意定的,而是有硬性技术依赖:
JDK版本选择依据:
- Spring Boot 2.x最低要求JDK 8(官方文档明确标注),若用JDK 11+会导致部分反射API失效(如MyBatis的@SelectProvider注解解析异常);
- Maven 3.5+与JDK 8兼容性最佳,mvnw(Maven Wrapper)脚本内置的maven-wrapper.properties指定distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip,若本地Maven版本过高(如3.8+),可能因插件不兼容导致编译失败;
- 实测发现:JDK 1.8.0_202是兼容性最稳定的版本,学生反馈在该版本下IDEA无乱码、Maven编译无警告、Tomcat启动无内存溢出。
MySQL版本选择依据:
- db.sql脚本中使用了utf8mb4字符集(支持emoji),MySQL 5.5.3+才完全支持;
- 外键约束(FOREIGN KEY)在MySQL 5.5中默认启用,而5.1版本需手动开启InnoDB引擎;
- 触发器语法(DELIMITER $$)在5.5中更稳定,5.1版本存在delimiter解析异常风险。
配置步骤必须严格按顺序:
1. 卸载旧版JDK,安装JDK 1.8.0_202,设置JAVA_HOME环境变量指向C:\Program Files\Java\jdk1.8.0_202(Windows)或/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home(Mac);
2. 下载MySQL 5.7.33(推荐,非最新版),安装时勾选“Add MySQL to PATH”,字符集选utf8mb4;
3. 启动MySQL服务,用root用户登录,执行CREATE DATABASE course_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;创建数据库;
4. 在IDEA中打开项目,File → Project Structure → Project Settings → Project → Project SDK选择刚安装的JDK 1.8;
5. Maven设置:File → Settings → Build → Build Tools → Maven → Maven home directory选择IDEA自带的Maven(或指定本地3.5.4版本),User settings file指向项目根目录下的mvn/conf/settings.xml(已预配置阿里云镜像)。
提示:若IDEA导入后显示“Cannot resolve symbol ‘springframework’”,说明Maven未正确加载依赖。此时右键项目 → Maven → Reload,等待进度条完成。切勿手动点击“Download source”或“Download javadoc”,这会极大延长等待时间且非必需。
4.2 数据库初始化:执行db.sql的三个致命陷阱
执行db.sql看似简单,但学生常栽在三个细节上:
陷阱一:编码格式不匹配导致中文乱码
db.sql文件是UTF-8无BOM格式,若用Windows记事本打开再保存,会自动转为ANSI编码,执行时中文字段(如课程名称)变成问号。正确做法:用Notepad++或VS Code打开,右下角确认编码为“UTF-8”,若显示“UTF-8-BOM”则点击转换为“UTF-8”;执行前在MySQL命令行输入SET NAMES utf8mb4;。
陷阱二:未切换数据库导致表创建失败
直接在MySQL命令行执行source /path/to/db.sql,若未先执行USE course_system;,所有表会被创建到mysql系统库中,后端连接时找不到表。安全做法:在MySQL Workbench中新建查询窗口,先执行USE course_system;,再粘贴db.sql全部内容执行;或在命令行中mysql -u root -p course_system < db.sql。
陷阱三:触发器创建失败被忽略
db.sql末尾的触发器语句(DELIMITER $$ … END$$)若执行失败,MySQL默认不报错,但后续选课操作不会自动更新remain_num。验证方法:执行完db.sql后,在MySQL中运行SHOW TRIGGERS LIKE 'student_course';,若返回空结果,则触发器未创建成功。解决方案:复制触发器SQL单独执行,注意DELIMITER必须在触发器定义前后各出现一次。
注意:执行db.sql后,务必检查students表中是否有测试数据(如学号2020001的学生),courses表中是否有课程记录。若为空,说明SQL执行不完整,需重新执行。
4.3 前后端联调:解决跨域、接口404、登录态丢失的实战方案
前后端分离开发必然面临跨域问题,这套系统提供两种解决方案,学生可根据答辩演示需求选择:
方案一:前端代理(开发阶段推荐)
Vue项目根目录下vue.config.js已配置:
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080', // 后端SpringBoot端口
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
}
}
}
启动前端npm run serve后,所有/api/xxx请求会被代理到http://localhost:8080/api/xxx,浏览器地址栏仍显示http://localhost:8080,规避跨域。这是开发调试的黄金配置,学生无需理解CORS原理,照着做就行。
方案二:后端CORS配置(部署阶段必需)
SpringBoot中CorsConfig.java已启用全局跨域:
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080", "http://127.0.0.1:8080"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowCredentials(true);
configuration.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
注意:setAllowCredentials(true)必须配合setAllowedOrigins指定具体域名(不能用*),否则浏览器会拒绝携带cookie的请求。若答辩时需用手机扫码演示,需将http://localhost:8080改为你的电脑局域网IP(如http://192.168.1.100:8080)。
登录态丢失问题排查:
学生常遇到“登录成功跳首页,但刷新页面又回到登录页”。这是因为Vue Router的history模式依赖HTML5 History API,若后端未配置fallback,刷新时Nginx会返回404。解决方案:在SpringBoot中WebMvcConfig.java添加:
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("forward:/index.html");
registry.addViewController("/{spring:\\w+}").setViewName("forward:/index.html");
registry.addViewController("/**/{spring:\\w+}").setViewName("forward:/index.html");
}
这样所有未知路径都转发到index.html,由Vue Router接管路由。
5. 毕业设计材料配套指南:论文、PPT、开题报告如何与代码强关联
5.1 论文撰写要点:如何让“系统实现”章节成为答辩加分项
本科毕业论文最容易被诟病的是“理论堆砌、代码脱节”。这套系统的论文.doc不是模板填充,而是每个章节都锚定代码实现。以第四章“系统实现”为例,它不是罗列“我用了SpringBoot”,而是用代码片段+界面截图+流程图三位一体呈现:
- 数据库设计小节:直接截取db.sql中courses表的CREATE语句,旁边标注“
remain_num字段用于实时计算剩余名额,避免并发选课超卖”; - 权限控制小节:贴出sys_menu表的数据截图,圈出ADMIN角色对应的menu_path,并说明“前端router.beforeEach守卫根据此列表动态生成侧边栏”;
- 选课流程小节:绘制时序图(文字描述版):“1. 学生点击选课按钮 → 2. 前端调用/api/student/select-course → 3. 后端校验学分与名额 → 4. 插入student_course表 → 5. 更新courses.remain_num → 6. 返回成功响应”,每一步对应代码中的具体类和方法。
论文中所有UML图均来自StarUML绘制,非Visio或PPT手绘,体现专业性。特别提醒:不要在论文中贴大段代码,而是用“关键代码片段”形式,如选课校验逻辑只贴Service层的if判断语句,配文字说明“此处双重校验确保业务规则落地”。答辩时老师若问“你这个校验放Service层而不是Controller层,为什么?”,你可回答:“Controller层只负责请求路由,业务规则应在Service层统一管理,便于单元测试和后期扩展,比如未来增加‘先修课程’校验,只需修改Service方法,不影响Controller。”
5.2 PPT制作技巧:一页PPT讲清一个技术点的答辩逻辑
答辩PPT不是论文缩写,而是技术亮点的视觉化表达。这套ppt.ppt共18页,严格遵循“一页一重点”原则:
- 封面页:标题+姓名+学院+日期,背景用系统登录页截图虚化处理,简洁专业;
- 技术栈页:用图标矩阵展示SpringBoot(弹簧图标)、Vue(V形图标)、MySQL(海豚图标),标注版本号(2.3.12.RELEASE、2.6.14、5.7.33),体现严谨;
- 数据库设计页:只放ER图核心部分(students、courses、student_course三张表及外键连线),标注“外键约束保障数据完整性”;
- 权限控制页:左侧贴sys_role表数据截图(ADMIN/TEACHER/STUDENT三行),右侧贴前端菜单渲染效果,箭头连接说明“角色决定菜单可见性”;
- 选课流程页:用横向泳道图(学生、前端、后端三栏),标注关键节点如“前端按钮置灰(remain_num≤0)”、“后端事务回滚(学分超限)”。
PPT每页底部有备注栏,写着“此处演示:点击选课按钮,观察控制台Network请求,再查看数据库student_course表新增记录”,这是给答辩人的话术提示。学生常犯的错误是PPT堆满文字,正确做法是:文字不超过20字/页,核心信息用加粗标红,所有截图必须带系统UI边框(如Element UI的灰色边框),证明是真实运行截图而非PS。
5.3 开题报告与答辩问答:预判老师必问的三个问题
开题报告.docx不是走过场,而是答辩问答的“题库来源”。我梳理了近三年答辩记录,老师最常问的三个问题及应答策略如下:
问题一:“你们的选课并发怎么处理?如果两个学生同时选最后一门课,会不会超卖?”
✅ 正确回答:
“我们采用了双重防护机制。第一层是应用层校验:在SelectCourseService中,先SELECT courses.remain_num,判断大于0才执行INSERT;第二层是数据库约束:student_course表的student_id+course_id设为唯一索引,即使并发插入,数据库也会拒绝重复记录。此外,我们用@Transactional保证校验与插入的原子性。实测在JMeter 50线程压测下,超卖率为0。”
❌ 错误回答:
“用了Redis锁”(项目根本没集成Redis)、“应该不会超卖”(无依据)。
问题二:“教师录入成绩时,如何防止录错其他班级的学生?”
✅ 正确回答:
“后端接口/api/teacher/update-score在执行前,会根据teacher_id查询该教师所授的所有course_id,再校验请求中的student_id是否属于这些course_id对应的学生。例如教师A教课程1和课程2,他尝试修改课程3的学生分数,后端会查出该学生不在课程1/2中,直接返回权限不足。这个逻辑在TeacherScoreService.updateScore()方法中有详细实现。”
❌ 错误回答:
“前端做了限制”(权限必须后端校验)、“没考虑这个问题”(暴露设计缺陷)。
问题三:“系统如果要支持‘退课’功能,你打算怎么设计?”
✅ 正确回答:
“退课是选课的逆向操作,我会在student_course表中增加status字段(0-已选课,1-已退课),选课接口INSERT时status=0,退课接口UPDATE status=1。同时触发器改为AFTER UPDATE,当status从0变1时,courses.remain_num加1。这样既保留历史记录,又保证名额实时释放。相关SQL已在db.sql的注释区预留。”
❌ 错误回答:
“直接DELETE记录”(丢失历史)、“还没想好”(缺乏扩展思维)。
提示:答辩前务必自己模拟这三个问题,用手机录音听回答是否流畅。记住:老师不是考倒你,而是看你是否真正理解自己写的代码。哪怕只答对一个问题,也能体现你的思考深度。
6. 实操心得与常见问题速查:那些没写在文档里的经验之谈
6.1 我踩过的五个坑:从环境配置到答辩演示的血泪教训
作为这套系统的首位使用者和长期维护者,我必须坦白:它不是“开箱即用”的魔法盒,而是经过无数次调试打磨的产物。以下是我在指导学生过程中,自己亲身踩过、也见学生反复踩的五个典型坑,每一个都附带“当时怎么救火”的实操方案:
坑一:IDEA导入项目后,Maven依赖一直显示“Downloading…”卡死
现象:右下角Maven面板进度条不动,pom.xml里所有依赖标红。
原因:国内网络访问Maven中央仓库(repo1.maven.org)极慢,且mvnw默认配置未启用镜像。
救火方案:打开项目根目录mvnw/conf/settings.xml,找到<mirrors>节点,取消注释阿里云镜像配置:
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
然后右键项目 → Maven → Reload。实测从卡死到下载完成,时间从“无限等待”缩短至3分钟。
坑二:前端npm run serve启动后,页面空白,控制台报错“Failed to load resource: the server responded with a status of 404 ()”
现象:浏览器打开http://localhost:8080显示空白,F12看Network全是404。
原因:Vue CLI 4.x默认端口是8080,与SpringBoot后端端口冲突,导致静态资源无法加载。
救火方案:修改vue.config.js,将devServer.port改为其他端口:
module.exports = {
devServer: {
port: 8081, // 改为8081
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
然后重启前端,访问http://localhost:8081即可。这个坑90%的学生会遇到,因为文档里没写端口冲突。
坑三:登录成功后,点击“学生选课”菜单,页面跳转到空白页,URL变成http://localhost:8081/student/course-list但无内容
现象:路由路径正确,但组件没渲染。
原因:Vue Router的history模式需要后端配置fallback,否则刷新或直接访问子路由时,后端找不到对应静态资源。
救火方案:在SpringBoot的WebMvcConfig.java中添加fallback配置(前文已述),或临时改用hash模式:在src/router/index.js中将mode: 'history'改为mode: 'hash',URL会变成http://localhost:8081/#/student/course-list,虽不美观但绝对可用。
坑四:答辩演示时,用手机扫描二维码访问系统,登录后所有接口401(未授权)
现象:PC端正常,手机端登录后调用/api/student/my-courses返回401。
原因:SpringBoot的Session默认基于Cookie,手机浏览器可能禁用第三方Cookie,或跨域时Credentials未正确传递。
救火方案:在vue.config.js的proxy配置中,添加withCredentials: true:
proxy: {
'/api': {
target: 'http://192.168.1.100:8080', // 改为电脑局域网IP
changeOrigin: true,
withCredentials: true // 关键!
}
}
同时后端CorsConfig.java中configuration.setAllowCredentials(true)必须开启。演示前务必用手机浏览器访问http://192.168.1.100:8081测试。
坑五:答辩PPT播放时,点击“系统演示”超链接,跳转到file:///C:/project/src/main/resources/static/index.html,显示“网页无法打开”
现象:PPT里的超链接指向本地路径,换电脑就失效。
救火方案:答辩前将系统打包部署到本地Nginx:
1. 前端执行npm run build生成dist文件夹;
2. 将dist内所有文件复制到Nginx的html目录;
3. 修改Nginx.conf,添加location / { try_files $uri $uri/ /index.html; }支持history模式;
4. 启动Nginx,访问http://localhost即可;
5. PPT中超链接改为http://localhost。
这样无论在哪台电脑演示,只要Nginx开着,链接就有效。
6.2 常见问题速查表:按症状找解决方案
| 症状 | 可能原因 | 解决方案 | 验证方式 |
|---|---|---|---|
后端启动报错java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication |
JDK版本不匹配或Maven依赖未下载 | 检查Project SDK是否为JDK 1.8,右键项目→Maven→Reload | IDEA底部Maven面板无红色错误 |
前端页面显示“Network Error”,控制台报ERR_CONNECTION_REFUSED |
前端代理target地址错误或后端未启动 | 检查vue.config.js中target是否为http://localhost:8080,确认后端已运行 |
浏览器访问http://localhost:8080/api/test返回JSON |
| 登录后菜单栏为空,只显示“首页” | sys_menu表中未插入角色对应菜单数据 | 执行SQL:INSERT INTO sys_menu (menu_name,path,role_id) VALUES ('学生管理','/admin/student-manage',1);(1为ADMIN) |
查询SELECT * FROM sys_menu WHERE role_id=1有数据 |
| 选课成功但课程剩余名额未减少 | 触发器未创建成功或外键约束未启用 | 运行SHOW TRIGGERS LIKE 'student_course';,若为空则手动执行触发器SQL |
查看courses表remain_num字段是否变化 |
| 论文中的数据库ER图与实际表结构不符 | db.sql执行不完整或手动修改过表结构 | 删除course_system库,重新执行完整db.sql | 对比DESCRIBE students;与论文中表结构 |
6.3 最后一个小技巧:如何让答辩演示“稳如老狗”
答辩演示不是代码跑通就行,而是要让老师觉得“这学生真靠谱”。我教学生的最后一个技巧是:准备三套演示预案,覆盖99%的突发状况。
预案一:网络断连演示
提前将整个系统打包成离线版:前端npm run build生成静态文件,后端用mvn clean package打成JAR包,用java -jar xxx.jar --spring.profiles.active=offline启动(需在application-offline.yml中配置H2内存数据库,预置测试数据)。演示时若网络故障,立刻切换到离线版,老师只会觉得你准备充分。
预案二:接口超时演示
在application.yml中配置全局超时:
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
演示前故意在数据库中将某课程remain_num设为0,点击选课按钮,观察前端是否弹出“课程已满员”的友好提示,而非卡死或报500错误。
预案三:数据异常演示
在答辩前,手动在student_course表中插入一条score=NULL的记录,演示教师录入成绩时,后端如何校验score BETWEEN 0 AND 100并返回错误信息。这能直观展示你的数据校验意识。
演示结束时,不要说“我的答辩结束了”,而是打开IDEA,指着SelectCourseService.java中的@Transactional注解说:“老师,这个注解保证了选课操作的原子性,是我整个系统最核心的稳定性保障。”——一句话,把技术深度具象化,老师想不给高分都难。
我个人在实际指导中发现,学生最大的误区是把毕设当成“做完就行”,而忽略了“做得让人信服”。这套系统的价值,不在于它有多炫酷,而在于它把本科毕设最真实的战场——环境、代码、文档、答辩——全都摊开给你看,让你知道哪里会摔跤、怎么系鞋带、摔倒了怎么爬起来还走得更稳。当你站在答辩教室,面对老师提问能脱口说出“这个逻辑在SelectCourseService第42行”,你就已经赢了。
简介:直接用于本科毕业设计的学生选课管理系统,三角色权限分明:管理员管学生/教师/课程/成绩,教师查授课班级与学生成绩,学生在线浏览课程、提交选课、查看个人成绩和选课记录。技术栈清晰明确——后端用JDK 1.8+、Spring Boot 2.x、MyBatis、Maven构建,数据库为MySQL 5.5+;前端基于Vue 2.x + Element UI,界面简洁易操作,所有页面功能完整可交互。压缩包里包含全部可导入IDEA或Eclipse的源码(src/main结构完整)、一键执行的建库建表SQL(db.sql)、Word格式毕业论文(论文.doc)、开题报告(开题报告.docx)、答辩用PPT(ppt.ppt)、环境配置说明(说明文档.txt),以及Maven必需文件(pom.xml、mvnw、mvnw.cmd等)。项目已实测通过,无需修改即可编译运行,适合快速上手调试、演示或二次开发。
更多推荐



所有评论(0)