news 2026/9/8 12:30:11

SpringBoot学生选课管理系统毕设实战:选课约束与数据库设计详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot学生选课管理系统毕设实战:选课约束与数据库设计详解

毕业设计里,基于SpringBoot的学生选课管理系统是一个出现频率很高的选题。它看起来只是管理课程和学生选课,实际上要处理角色权限、课程资源约束、选课冲突、成绩回填等多个环节,做完之后几乎可以把 JavaWeb 阶段的知识点全部串起来。这篇博客按照一个完整毕设项目的推进顺序展开,从业务拆分、表结构设计、后端接口实现、前端页面联调,到打包部署和常见故障排查,全部走一遍。无论你是准备用这个题目写毕设,还是想复现一个 SpringBoot 管理系统加深理解,都可以按文章步骤跑出一套可演示、可答辩、可扩展的系统。

1. 先理解学生选课管理系统要解决什么问题

1.1 高校选课的核心业务流程

选课管理系统并不是一个单纯的“课程增删改查”项目。它的核心业务是:在一个学期内,学生从开设的课程列表中选择自己要修的课程,系统要保证这门课没有选满、学生没有重复选择、课程时间没有和其他已选课程冲突。选课结束后,教师还需要看到选课名单,录入成绩,学生才能查到最终成绩。

这个流程涉及三类角色:

角色主要功能
学生浏览课程、选课、退课、查看已选课程、查看成绩
教师维护课程信息、查看选课名单、录入成绩
管理员管理学生账号、教师账号、课程信息、开课学期

很多毕设项目会把管理员功能做得很大,比如部门管理、菜单管理、操作日志统计等。作为毕设系统,这些功能可以做,但真正决定项目质量的是“选课约束”是否严谨。也就是说,学生选课时,系统能不能真的防止重复选、超容量选、同一时间冲突选,而不是只在页面上提示一个空泛的错误。

1.2 四个不能回避的业务边界

实际做这个系统时,会反复遇到这几个边界条件,它们是后端代码设计的主线。

第一,一个学期内只能操作当前学期的课程。课程必须带有学期字段,学生选课记录也要绑定学期,否则上一届的历史数据会混进来。

第二,选课前必须校验是否已经选过同一门课。这里的判断不能只靠代码里的集合判断,还要靠数据库唯一索引兜底,防止并发请求同时写入。

第三,选课时必须判断课程容量。课程有容量上限,已选人数不能超过容量。比如课程容量是 60,第 61 个学生再来选课时必须被拒绝。

第四,同一天同一时间段不能安排两门课。判断时间冲突需要拿到学生所有已选课程,再和当前课程的时间段做交叉比对。

这四条如果全部落地,项目就已经超过很多只做增删改查的毕设。答辩时被问到“系统怎么保证选课数据正确”,可以直接从这四个约束入手展开。

1.3 为什么选 Spring Boot 作为毕设基础框架

选课管理系统的功能集中在业务逻辑和数据操作,适合用 Spring Boot 这种“约定优先于配置”的框架快速搭建。

Spring Boot 自带内嵌的 Tomcat,写完代码后一个java -jar就能启动,不需要单独安装和配置外部服务器,这对计算机设计演示很有帮助。Spring Boot 的自动配置机制能减少大量 XML 配置,数据库连接、事务管理、Web 请求处理都可以在application.yml里集中维护。

更重要的是 Spring Boot 与 MyBatis、MyBatis-Plus、Thymeleaf、Vue 等常见技术的配合非常成熟。遇到问题搜索时,可以找到大量资料,适合第一次完整做项目的学生阅读和排查。选课管理系统的数据量不大、业务链路清晰,不需要微服务和高性能并发方案,Spring Boot 单体架构已经足够。

2. 环境准备与技术栈选型

2.1 版本约定要一致,否则后面全是坑

版本选择是毕设项目最容易被忽略的地方。很多同学把别人的源码拉到本地,一启动就报各种错误,原因往往是本机 JDK 版本和项目的 Spring Boot 版本不匹配。

下面是一套比较稳妥的版本组合,本次示例代码也按这个组合设计:

组件推荐版本说明
JDK8 或 17Spring Boot 2.7.x 用 JDK 8,Spring Boot 3.x 用 JDK 17
Maven3.6+用于依赖管理和打包
Spring Boot2.7.x兼容性强,资料最多,适合毕设
MySQL8.08.0 需要关注时区配置
MyBatis-Plus3.5.x减少基础 SQL 编写,降低工作量
前端方案Thymeleaf 或 Vue 3快速完成选 Thymeleaf,想展示前后端分离选 Vue

注意:如果本机已经安装了 JDK 17,建议直接选择 Spring Boot 3.x。反过来,如果本机是 JDK 8,却使用了 Spring Boot 3.x,编译时会出现UnsupportedClassVersionError,因为 Spring Boot 3.x 的最低要求是 Java 17。开始编码之前,先使用java -versionmvn -v确认本机环境。

2.2 Maven 工程结构规划

推荐按下面这种结构组织代码,分层清晰,答辩时也容易讲:

course-management-system ├── src/main/java/com/example/course │ ├── CourseManagementApplication.java │ ├── config │ │ ├── InterceptorConfig.java │ │ └── WebMvcConfig.java │ ├── controller │ │ ├── LoginController.java │ │ ├── StudentCourseController.java │ │ ├── TeacherCourseController.java │ │ └── AdminController.java │ ├── entity │ │ ├── User.java │ │ ├── Course.java │ │ └── StudentCourse.java │ ├── mapper │ │ ├── UserMapper.java │ │ ├── CourseMapper.java │ │ └── StudentCourseMapper.java │ ├── service │ │ ├── UserService.java │ │ ├── CourseService.java │ │ └── StudentCourseService.java │ ├── interceptor │ │ └── LoginInterceptor.java │ └── vo │ ├── Result.java │ ├── LoginRequest.java │ └── CourseSelectForm.java └── src/main/resources ├── application.yml ├── mapper │ ├── UserMapper.xml │ ├── CourseMapper.xml │ └── StudentCourseMapper.xml └── templates ├── login.html ├── student │ ├── courseList.html │ └── selectedList.html └── teacher ├── courseManage.html └── scoreInput.html

业务层级按照Controller -> Service -> Mapper组织,实体类放在entity包,接口返回数据封装在vo包。这样每个类的职责具体,答辩时可以直接说清楚每一层的作用。

2.3 引入核心依赖

pom.xml中加入以下依赖:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>

这里把 MyBatis-Plus 放在基础 SQL 编写的位置,可以省去大量重复的insertselectById代码,让关注点集中在选课规则上。如果希望练习手写 SQL,也可以把依赖换成mybatis-spring-boot-starter,两种方式对毕设都够用。

2.4 配置文件注意时区和驼峰映射

application.yml是项目能否连接数据库的关键,下面是一份完整配置:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/course_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 thymeleaf: cache: false mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto

serverTimezone=Asia/Shanghai用于解决 MySQL 8.0 的时区报错。map-underscore-to-camel-case: true让数据库的student_id字段自动映射到实体的studentId属性,避免每次手写繁琐的 resultMap。log-impl会把执行的 SQL 打印到控制台,选课逻辑出错时可以直接看到 SQL 语句,这在调试阶段非常有用。

3. 数据库设计要支撑业务约束

3.1 核心表结构总览

选课管理系统的数据库至少包含三张核心表。用户表保存学生、教师、管理员账号,课程表保存课程的容量、时间、学分等基本信息,选课记录表保存学生和课程之间的绑定关系以及成绩。

表名用途
user用户基本信息,区分角色
course课程基本信息、容量、时间、学期
student_course学生选课记录、成绩

如果需要更完整地模拟高校场景,可以增加student表、teacher表和semester表。毕设阶段为了控制复杂度,可以先合并为上面的三表结构,答辩时再说明扩展思路。

3.2 建表 SQL

下面是一份可以直接执行的建表 SQL:

CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARACTER SET utf8mb4; USE course_db; CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, `real_name` VARCHAR(50) DEFAULT NULL, `role` VARCHAR(20) NOT NULL COMMENT 'STUDENT/TEACHER/ADMIN', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `course` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `course_name` VARCHAR(100) NOT NULL, `course_code` VARCHAR(50) NOT NULL, `teacher_id` BIGINT DEFAULT NULL, `credit` DECIMAL(3, 1) DEFAULT 0, `capacity` INT NOT NULL DEFAULT 60, `selected_num` INT NOT NULL DEFAULT 0, `start_week` INT NOT NULL DEFAULT 1, `end_week` INT NOT NULL DEFAULT 18, `day_of_week` INT NOT NULL COMMENT '1-7 表示周一至周日', `start_section` INT NOT NULL DEFAULT 1 COMMENT '开始节次', `end_section` INT NOT NULL DEFAULT 2 COMMENT '结束节次', `classroom` VARCHAR(100) DEFAULT NULL, `term` VARCHAR(30) NOT NULL COMMENT '例如 2025-2026-1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `student_course` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `student_id` BIGINT NOT NULL, `course_id` BIGINT NOT NULL, `select_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `score` DECIMAL(5, 2) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_course` (`student_id`, `course_id`), KEY `idx_course_id` (`course_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

初始化一些演示账号:

INSERT INTO `user` (`username`, `password`, `real_name`, `role`) VALUES ('admin', '123456', '系统管理员', 'ADMIN'), ('teacher01', '123456', '王老师', 'TEACHER'), ('student01', '123456', '张三', 'STUDENT'), ('student02', '123456', '李四', 'STUDENT');

3.3 字段设计里的三个关键决策

第一个关键决策是选课记录表必须加唯一索引。只靠 Service 层代码判断“是否已选”在单用户操作时有效,但两个学生同时请求,或者同一个学生快速点击两次选课按钮时,代码判断可能同时通过,唯一索引uk_student_course会在数据库层拦住重复数据。这是选课系统防止脏数据最重要的兜底手段。

第二个关键决策是课程表设计了selected_num冗余字段。它保存当前已选人数,目的是在选课操作时执行UPDATE course SET selected_num = selected_num + 1 WHERE id = ? AND selected_num < capacity这种原子更新,而不是先查人数再更新人数。这样即使并发选课,数据库也能通过行锁保证不会超过容量。

第三个关键决策是时间字段拆分成day_of_weekstart_sectionend_section,而不是直接存一个字符串。拆分之后,判断两门课是否冲突的逻辑更容易实现。比如课程 A 是周一 1-2 节,课程 B 是周一 2-3 节,两条记录可以按“星期相同,且节次区间有交集”来判断冲突。

4. 后端核心实现:把约束写进代码

4.1 登录认证和角色权限拦截

先实现一个简单的登录接口。登录成功后把用户信息放进 Session,后续请求通过拦截器校验。

@RestController @RequestMapping("/api/auth") public class LoginController { @Resource private UserService userService; @PostMapping("/login") public Result login(@RequestBody LoginRequest request, HttpSession session) { User user = userService.login(request.getUsername(), request.getPassword()); if (user == null) { return Result.error("用户名或密码错误"); } session.setAttribute("loginUser", user); return Result.success(user); } @PostMapping("/logout") public Result logout(HttpSession session) { session.invalidate(); return Result.success(null); } }

拦截器用来统一校验是否登录,以及访问路径是否匹配角色:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}"); return false; } String uri = request.getRequestURI(); if (uri.startsWith("/api/teacher") && !"TEACHER".equals(user.getRole()) && !"ADMIN".equals(user.getRole())) { response.getWriter().write("{\"code\":403,\"message\":\"无权限\"}"); return false; } return true; } }

在配置类里注册拦截器路径:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login"); } }

拦截器方案比引入 Spring Security 更容易理解,适合毕设项目的代码讲解。如果评委问为什么不直接用 Spring Security,可以回答:当前系统角色只有三种,业务重点是选课约束,使用拦截器可以把复杂度控制在合理范围,后续需要细粒度权限时再引入安全框架。

4.2 课程管理接口

课程管理主要包含分页查询课程、新增课程、修改课程信息。学生端只能查询,教师端可以维护课程。

@RestController @RequestMapping("/api/course") public class CourseController { @Resource private CourseService courseService; @GetMapping("/list") public Result listCourse( @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, HttpSession session) { User user = (User) session.getAttribute("loginUser"); Page<Course> result = courseService.getPage(keyword, page, size); return Result.success(result); } @PostMapping("/add") public Result addCourse(@RequestBody Course course) { courseService.saveCourse(course); return Result.success(null); } }

分页查询使用 MyBatis-Plus 的Page对象:

@Service public class CourseService { @Resource private CourseMapper courseMapper; public Page<Course> getPage(String keyword, int page, int size) { LambdaQueryWrapper<Course> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(keyword)) { wrapper.like(Course::getCourseName, keyword) .or().like(Course::getCourseCode, keyword); } Page<Course> p = new Page<>(page, size); return courseMapper.selectPage(p, wrapper); } }

4.3 核心:选课逻辑怎么保证不重复、不超选、不冲突

选课是整个系统技术含量最高的地方。先定义一个选课表单对象:

public class CourseSelectForm { private Long courseId; }

然后编写选课 Service 方法:

@Service public class StudentCourseService { @Resource private StudentCourseMapper studentCourseMapper; @Resource private CourseMapper courseMapper; @Autowired private TransactionTemplate transactionTemplate; public Result selectCourse(Long studentId, Long courseId) { Course course = courseMapper.selectById(courseId); if (course == null) { return Result.error("课程不存在"); } // 1. 防止重复选课 Long count = studentCourseMapper.countByStudentAndCourse(studentId, courseId); if (count > 0) { return Result.error("不能重复选择同一门课程"); } // 2. 容量校验 + 原子更新,防止超选 int updated = courseMapper.increaseSelectedNum(courseId); if (updated == 0) { return Result.error("课程已选满"); } // 3. 时间冲突校验 List<Course> selectedCourses = studentCourseMapper.findSelectedCourses(studentId); for (Course selected : selectedCourses) { if (isTimeConflict(course, selected)) { // 容量回滚,因为后续不会真正插入选课记录 courseMapper.decreaseSelectedNum(courseId); return Result.error("与课程[" + selected.getCourseName() + "]时间冲突"); } } StudentCourse record = new StudentCourse(); record.setStudentId(studentId); record.setCourseId(courseId); studentCourseMapper.insert(record); return Result.success(null); } private boolean isTimeConflict(Course newCourse, Course existCourse) { if (!newCourse.getDayOfWeek().equals(existCourse.getDayOfWeek())) { return false; } return newCourse.getStartSection() <= existCourse.getEndSection() && existCourse.getStartSection() <= newCourse.getEndSection(); } }

这里要特别注意事务边界。上面示例使用TransactionTemplate手动控制事务,避免多个操作之间出现问题。也可以直接在方法上使用@Transactional(rollbackFor = Exception.class),但这个方法的难点在于“时间冲突”属于业务校验失败,而不是抛出运行时异常,如果直接用@Transactional并且返回Result.error,事务不会回滚,已经执行成功的increaseSelectedNum就会把课程人数加错。

所以要在校验失败时执行courseMapper.decreaseSelectedNum(courseId),或者把整段逻辑拆成两次独立事务。更稳妥的写法是使用TransactionTemplate,把“扣减容量 + 插入选课记录”放在同一个事务中,任何一步抛异常都会回滚。

CourseMapper中对应的原子更新 SQL:

<update id="increaseSelectedNum"> UPDATE course SET selected_num = selected_num + 1 WHERE id = #{courseId} AND selected_num &lt; capacity </update>

decreaseSelectedNum对应:

<update id="decreaseSelectedNum"> UPDATE course SET selected_num = selected_num - 1 WHERE id = #{courseId} </update>

这种方案的好处是,数据库通过selected_num < capacity条件保证不会超选,即使两个学生同时执行该 SQL,也只有一个会更新成功。

4.4 退课、选课名单和成绩录入

退课逻辑相对简单,删除选课记录的同时把课程的selected_num减 1。

@Transactional(rollbackFor = Exception.class) public void cancelCourse(Long studentId, Long courseId) { studentCourseMapper.deleteByStudentAndCourse(studentId, courseId); courseMapper.decreaseSelectedNum(courseId); }

成绩录入由教师操作,直接更新student_course表:

@RestController @RequestMapping("/api/teacher") public class TeacherController { @Resource private StudentCourseService studentCourseService; @GetMapping("/course/{courseId}/students") public Result listSelectedStudents(@PathVariable Long courseId) { List<StudentCourseVO> list = studentCourseService.listByCourse(courseId); return Result.success(list); } @PostMapping("/score") public Result saveScore(@RequestBody ScoreRequest request) { studentCourseService.saveScore(request.getStudentCourseId(), request.getScore()); return Result.success(null); } }

5. 前端页面与联调方式

5.1 使用 Thymeleaf 快速完成后台页面

如果不想做前后端分离,可以用 Thymeleaf 直接渲染页面。选课页面的核心代码可以这样写:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>选课</title> </head> <body> <h1>课程列表</h1> <table border="1"> <tr> <th>课程名称</th> <th>教师</th> <th>学分</th> <th>容量</th> <th>已选</th> <th>上课时间</th> <th>操作</th> </tr> <tr th:each="course : ${page.records}"> <td th:text="${course.courseName}">课程名</td> <td th:text="${course.teacherName}">教师</td> <td th:text="${course.credit}">学分</td> <td th:text="${course.capacity}">容量</td> <td th:text="${course.selectedNum}">已选</td> <td th:text="${course.dayOfWeek + ' 第' + course.startSection + '-' + course.endSection + '节'}">时间</td> <td> <form th:action="@{/api/student/course/select}" method="post"> <input type="hidden" name="courseId" th:value="${course.id}"> <button type="submit">选课</button> </form> </td> </tr> </table> </body> </html>

Thymeleaf 的优点是部署简单、不需要跨域处理,适合时间紧张的毕设项目。缺点是前后端耦合在一起,遇到复杂交互时需要写较多 JavaScript。

5.2 使用 Vue 前后端分离时需要统一接口格式

如果选择 Vue 3 做前端,后端接口应该返回固定格式的 JSON,方便前端统一处理。可以定义一个Result类:

public class Result { private Integer code; private String message; private Object data; public static Result success(Object data) { Result r = new Result(); r.code = 200; r.message = "success"; r.data = data; return r; } public static Result error(String message) { Result r = new Result(); r.code = 500; r.message = message; return r; } }

前端使用 Axios 时,只需要拦截code字段:

axios.post('/api/student/course/select', { courseId: id }) .then(res => { if (res.data.code === 200) { alert('选课成功'); } else { alert(res.data.message); } });

前后端分离项目的地址要提前约定好。本地开发时 Vue 一般运行在 5173 端口,后端运行在 8080 端口,需要在 Vue 的vite.config.js里配置代理:

export default { server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

这样前端请求/api/xxx时会被代理转发到后端,不会出现跨域问题。

5.3 页面和接口的对应关系

页面接口功能
登录页POST /api/auth/login登录
学生选课页GET /api/course/list查询课程列表
学生选课页POST /api/student/course/select选课
学生已选页GET /api/student/course/selected查询已选课程
学生已选页POST /api/student/course/cancel退课
教师课程页GET /api/teacher/course/{courseId}/students查询选课名单
教师成绩页POST /api/teacher/score录入成绩
管理员用户页POST /api/admin/user/add新增用户

6. 启动、验证和打包发布

6.1 启动前检查清单

在运行项目之前,按下面的顺序逐项检查,能避免大多数环境问题:

  1. 检查 MySQL 是否已经启动,是否能通过mysql -u root -p登录。
  2. 检查本机 JDK 版本,java -version输出是否和 Spring Boot 版本匹配。
  3. 检查 Maven 仓库是否能正常下载依赖,mvn -v命令是否可用。
  4. 检查application.yml中的数据库密码、用户名是否正确。
  5. 检查 8080 端口是否被占用,执行netstat -ano | findstr 8080(Windows)或lsof -i:8080(Linux/macOS)。

启动成功后,控制台会出现类似下面的内容:

Tomcat started on port(s): 8080 (http) Started CourseManagementApplication in 2.34 seconds

6.2 用接口用例验证核心功能

启动项目后,建议用接口用例把核心链路验证一遍。下面以选课流程为例,给出关键接口调用。

登录接口:

curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"student01","password":"123456"}'

查询课程列表:

curl -X GET "http://localhost:8080/api/course/list?page=1&size=10"

选课接口:

curl -X POST http://localhost:8080/api/student/course/select \ -H "Content-Type: application/json" \ -d '{"courseId":1}'

重复选课验证:

curl -X POST http://localhost:8080/api/student/course/select \ -H "Content-Type: application/json" \ -d '{"courseId":1}'

第二次请求应该返回“不能重复选择同一门课程”。如果返回成功,说明唯一索引或查询逻辑没有生效。

还可以用以下表格整理验证用例,毕设测试报告中也可以直接引用:

验证项操作预期结果
登录成功输入正确账号密码返回用户信息
登录失败输入错误密码提示用户名或密码错误
选课成功学生选择未选课程选课记录插入成功
重复选课学生再次选择同一课程提示不能重复选择
超容量选课已选人数等于容量时继续选课提示课程已选满
时间冲突选课选择同一天同一节次的课程提示时间冲突
退课成功学生退掉已选课程记录删除且人数减一
成绩录入教师录入成绩学生可查询到成绩

6.3 Maven 打包和部署

验证完成之后,使用 Maven 打包:

mvn clean package -DskipTests

打包完成后,在target目录下生成course-management-system-0.0.1-SNAPSHOT.jar,使用命令启动:

java -jar target/course-management-system-0.0.1-SNAPSHOT.jar

6.4 学习环境与生产环境的关键差异

毕设演示环境可以简化很多配置,但如果是实际高校内部使用,还需要补齐以下内容:

项目学习环境生产环境
数据库密码写死可以接受使用环境变量或配置中心
日志控制台输出文件输出 + 按天切割 + 日志监控
上传图片本地目录对象存储或独立文件服务
并发抢课单机测试需要缓存、队列、限流
数据备份不涉及定期备份,数据库主从
前端部署本地运行Nginx 静态资源部署

7. 毕设项目常见问题排查路径

7.1 项目启动失败

现象:SpringApplication启动时报错,程序直接退出。

问题现象常见原因检查方式处理建议
Port 8080 was already in use端口被占用查看启动日志中的Port already in use换端口,或在application.yml中修改server.port
UnsupportedClassVersionErrorJDK 版本和 Spring Boot 版本不匹配java -version检查版本Spring Boot 2.x 配 JDK 8,Spring Boot 3.x 配 JDK 17
You aren't using a compiler supported by lombokLombok 版本和 JDK 版本不兼容检查编译日志升级 Lombok 为最新稳定版,或者更换 JDK 版本

7.2 数据库连接失败

现象:项目能启动,但访问任意接口时报数据库异常。

问题现象常见原因检查方式处理建议
Access denied for user 'root'@'localhost'数据库密码错误用命令行登录 MySQL 验证修改application.yml中的密码
Server returns invalid timezone未指定时区查看异常里的时区提示在 JDBC URL 增加serverTimezone=Asia/Shanghai
Public Key Retrieval is not allowedMySQL 8.0 的认证方式问题检查连接 URL在 JDBC URL 后增加allowPublicKeyRetrieval=true&useSSL=false

7.3 选课业务逻辑问题

选课是业务核心,最容易出问题的点包括数据重复、容量错误和时间判断不准确。

问题现象常见原因检查方式处理建议
重复选课成功只做了代码查询,没有唯一索引直接执行 SQL 插入重复记录student_course表加唯一索引
选课人数超过容量先查询再更新,没有用条件更新查看数据库的selected_numcapacity改成UPDATE ... WHERE selected_num < capacity
时间冲突判断不准直接用字符串比较时间段打印两门课的时间字段将星期、节次拆分为数字类型,再比较区间交集
选课失败后人数仍增加业务校验失败没有回滚查看selected_num是否比实际多失败时执行人数递减,或使用TransactionTemplate包裹

7.4 前端页面和静态资源问题

现象:浏览器访问页面时出现Whitelabel Error Page,或静态资源加载不出来。

问题现象常见原因检查方式处理建议
出现 Whitelabel Error Page控制器返回了没有匹配的视图查看控制台 404 日志检查@RestController@Controller是否用混
修改 HTML 不生效Thymeleaf 缓存开启刷新页面仍是旧内容在配置中设置spring.thymeleaf.cache=false
静态资源 404静态资源路径错误检查浏览器请求 URL静态文件放在src/main/resources/static

7.5 排查链路建议

遇到问题不要盲目改代码,按下面的顺序排查更高效:

  1. 看控制台完整日志,找到第一处Caused by,后面的堆栈通常是跟随异常。
  2. 判断是编译期问题还是运行期问题。编译期问题先看 Maven 依赖和 JDK 版本。
  3. 运行期问题先看数据库是否能连通,再看 SQL 是否正确。
  4. 使用 MyBatis 日志输出 SQL,把 SQL 复制到数据库客户端里执行,确认数据状态。
  5. 业务逻辑问题优先在 Service 层加日志,输出入参、查询结果和每一步的校验结果。

8. 答辩准备与项目扩展方向

8.1 答辩中高频问题及应答思路

答辩功能演示只是一部分,评委更关心的是你有没有理解系统的设计。提前准备下面几个问题,会比临场发挥更从容。

问题一:系统如何防止学生重复选课?

回答思路:两层校验。业务层先查询student_course表是否已有记录,如果已有就返回错误。数据库层通过uk_student_course唯一索引兜底,即使并发请求同时到达,也会因为唯一约束而只插入一条记录。

问题二:课程容量满了怎么办?

回答思路:容量字段是capacity,当前已选人数是selected_num。选课操作执行UPDATE course SET selected_num = selected_num + 1 WHERE id = ? AND selected_num < capacity,数据库返回更新行数为 0 时表示课程已满,选课失败。这种条件更新能避免先查询再更新带来的超选问题。

问题三:为什么不用 Spring Security?

回答思路:本系统角色只有三种,权限规则比较简单,用拦截器可以满足需求且代码更少。如果需要实现细粒度权限、OAuth2 登录、动态菜单,可以在此基础上引入 Spring Security,当前拦截器方案不影响后续扩展。

问题四:如何判断两门课时间冲突?

回答思路:课程表保存了星期几和节次区间。选课时查询学生所有已选课程,对每门已选课程判断星期是否相同,如果相同再判断节次区间是否有交集,即新课程的start_section <= 已选课程的 end_section且 已选课程的start_section <= 新课程的 end_section

问题五:数据库为什么冗余selected_num字段?

回答思路:如果不冗余,查询选课人数需要每次统计student_course表,课程列表页会反复执行聚合查询。冗余字段把“已选人数”变成课程表的普通字段,选课时通过单条 UPDATE 完成校验和更新,性能更好。代价是需要保证删除选课记录时同步扣减人数。

8.2 可扩展方向

如果做完基础功能还想加深项目难度,可以从下面几个方向选一个:

扩展方向难度说明
Redis 缓存课程列表中等减少数据库查询压力,答辩时可以讲缓存一致性
RabbitMQ 异步处理选课请求较高缓解抢课瞬时压力,引入消息队列削峰
定时任务自动释放未交费名额中等使用 Spring 的@Scheduled定时扫描
Vue 3 前端重构中等展示前后端分离能力
JWT 无状态登录中等替代 Session,适合前后端分离
Flowable 工作流审批较高用于退课审批、加选审批等流程

新增扩展功能时,要注意保持原有接口兼容。后端接口全部返回统一Result,前端新增页面时只需要调用对应接口即可,不需要重构已有模块。

8.3 代码、文档和讲解如何配套

毕设项目不只是能跑就行,代码、文档和讲解要能互相呼应。

代码要做到小步提交,注释写清楚关键方法。比如选课方法中的容量校验、时间冲突判断,每段逻辑都要写注释,因为答辩时很可能直接讲这个方法。

文档报告建议包含需求分析、数据库设计、接口说明、测试用例、部署说明五个部分。数据库设计直接复制建表 SQL 和字段说明表,接口说明把Result返回格式和核心接口列出来。测试用例可以使用本文第 6.2 节的用例表。

代码讲解时,不要从头到尾逐行念代码。可以按“用户登录 -> 学生选课 -> 教师录成绩”三条业务链路讲,每条链路讲清 Controller、Service、Mapper 三个层次的调用关系。选课链路讲到容量校验和时间冲突时停下来,重点解释为什么使用条件更新和唯一索引,这部分是整个项目的技术亮点。

完成这些工作后,这套选课管理系统不仅能通过答辩,也能作为后续学习微服务、消息队列、安全框架的起步项目。做项目的价值在于理解约束如何从业务转化到代码,再落到数据库,这个过程培养的工程意识,比功能齐全更值得体现。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 12:29:40

自动化工具选型实战:氪金兽与凌绝代售深度对比评测

最近在帮团队做自动化工具选型&#xff0c;两个名字频繁出现在讨论中&#xff1a;一个是老牌方案“凌绝代售”&#xff0c;另一个是新兴工具“氪金兽”。名字听起来都挺有特色&#xff0c;但真正用起来差别有多大&#xff1f;我决定花一周时间&#xff0c;把两个工具从安装部署…

作者头像 李华
网站建设 2026/9/8 12:29:38

临床预测模型构建全流程:从数据准备到模型部署的13个关键步骤

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:28:08

opencode实战指南:从安装配置到Skills与Agent工作流

opencode我用了三个多月&#xff0c;从最初的命令行尝鲜&#xff0c;到后来把日常开发流程整个迁过来&#xff0c;算是把这条路趟得比较熟了。如果你最近在关注AI编程助手&#xff0c;大概率会频繁看到这个名字——它和Codex、Claude Code这类工具一样&#xff0c;都属于终端里…

作者头像 李华
网站建设 2026/9/8 12:24:16

GIS定义投影与投影操作区别:从原理到ArcGIS实战避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:22:03

视频号带货榜单拆解:三大主流打法与中小达人起号路径

1. 榜单观察&#xff1a;视频号带货生态正在经历一轮“结构性换血”每个月25号左右&#xff0c;我都有个固定动作&#xff1a;蹲友望数据的月度榜单更新。2026年1月的视频号带货达人榜出来后&#xff0c;我盯着前排看了一会儿&#xff0c;第一反应不是“谁上榜了”&#xff0c;…

作者头像 李华
网站建设 2026/9/8 12:21:51

现场智能:AI落地工业一线的关键路径与实战指南

逛完IOTE 2026物联网展&#xff0c;说实话我最大的感受不是展馆又大了多少、展商又多了多少家&#xff0c;而是整个行业终于开始认真回答一个问题&#xff1a;AI到底怎么真正落到一线工人的手里、巡检员的眼睛里、仓库叉车的路径里。过去几年大家都在拼大模型参数、拼榜单分数&…

作者头像 李华