SpringBoot学生选课管理系统毕设实战:选课约束与数据库设计详解
发布时间:2026/9/8 12:30:27来源:尧图网络
毕业设计里基于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 8Spring Boot 3.x 用 JDK 17Maven3.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 -version和mvn -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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency /dependencies这里把 MyBatis-Plus 放在基础 SQL 编写的位置可以省去大量重复的insert、selectById代码让关注点集中在选课规则上。如果希望练习手写 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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: autoserverTimezoneAsia/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下面是一份可以直接执行的建表 SQLCREATE 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;初始化一些演示账号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_week、start_section、end_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;charsetUTF-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); PageCourse 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 PageCourse getPage(String keyword, int page, int size) { LambdaQueryWrapperCourse wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Course::getCourseName, keyword) .or().like(Course::getCourseCode, keyword); } PageCourse 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. 时间冲突校验 ListCourse 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中对应的原子更新 SQLupdate idincreaseSelectedNum UPDATE course SET selected_num selected_num 1 WHERE id #{courseId} AND selected_num lt; capacity /updatedecreaseSelectedNum对应update iddecreaseSelectedNum 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) { ListStudentCourseVO 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:thhttp://www.thymeleaf.org head meta charsetUTF-8 title选课/title /head body h1课程列表/h1 table border1 tr th课程名称/th th教师/th th学分/th th容量/th th已选/th th上课时间/th th操作/th /tr tr th:eachcourse : ${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} methodpost input typehidden namecourseId th:value${course.id} button typesubmit选课/button /form /td /tr /table /body /htmlThymeleaf 的优点是部署简单、不需要跨域处理适合时间紧张的毕设项目。缺点是前后端耦合在一起遇到复杂交互时需要写较多 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 启动前检查清单在运行项目之前按下面的顺序逐项检查能避免大多数环境问题检查 MySQL 是否已经启动是否能通过mysql -u root -p登录。检查本机 JDK 版本java -version输出是否和 Spring Boot 版本匹配。检查 Maven 仓库是否能正常下载依赖mvn -v命令是否可用。检查application.yml中的数据库密码、用户名是否正确。检查 8080 端口是否被占用执行netstat -ano | findstr 8080Windows或lsof -i:8080Linux/macOS。启动成功后控制台会出现类似下面的内容Tomcat started on port(s): 8080 (http) Started CourseManagementApplication in 2.34 seconds6.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?page1size10选课接口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.jar6.4 学习环境与生产环境的关键差异毕设演示环境可以简化很多配置但如果是实际高校内部使用还需要补齐以下内容项目学习环境生产环境数据库密码写死可以接受使用环境变量或配置中心日志控制台输出文件输出 按天切割 日志监控上传图片本地目录对象存储或独立文件服务并发抢课单机测试需要缓存、队列、限流数据备份不涉及定期备份数据库主从前端部署本地运行Nginx 静态资源部署7. 毕设项目常见问题排查路径7.1 项目启动失败现象SpringApplication启动时报错程序直接退出。问题现象常见原因检查方式处理建议Port 8080 was already in use端口被占用查看启动日志中的Port already in use换端口或在application.yml中修改server.portUnsupportedClassVersionErrorJDK 版本和 Spring Boot 版本不匹配用java -version检查版本Spring Boot 2.x 配 JDK 8Spring Boot 3.x 配 JDK 17You arent using a compiler supported by lombokLombok 版本和 JDK 版本不兼容检查编译日志升级 Lombok 为最新稳定版或者更换 JDK 版本7.2 数据库连接失败现象项目能启动但访问任意接口时报数据库异常。问题现象常见原因检查方式处理建议Access denied for user rootlocalhost数据库密码错误用命令行登录 MySQL 验证修改application.yml中的密码Server returns invalid timezone未指定时区查看异常里的时区提示在 JDBC URL 增加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8.0 的认证方式问题检查连接 URL在 JDBC URL 后增加allowPublicKeyRetrievaltrueuseSSLfalse7.3 选课业务逻辑问题选课是业务核心最容易出问题的点包括数据重复、容量错误和时间判断不准确。问题现象常见原因检查方式处理建议重复选课成功只做了代码查询没有唯一索引直接执行 SQL 插入重复记录给student_course表加唯一索引选课人数超过容量先查询再更新没有用条件更新查看数据库的selected_num和capacity改成UPDATE ... WHERE selected_num capacity时间冲突判断不准直接用字符串比较时间段打印两门课的时间字段将星期、节次拆分为数字类型再比较区间交集选课失败后人数仍增加业务校验失败没有回滚查看selected_num是否比实际多失败时执行人数递减或使用TransactionTemplate包裹7.4 前端页面和静态资源问题现象浏览器访问页面时出现Whitelabel Error Page或静态资源加载不出来。问题现象常见原因检查方式处理建议出现 Whitelabel Error Page控制器返回了没有匹配的视图查看控制台 404 日志检查RestController和Controller是否用混修改 HTML 不生效Thymeleaf 缓存开启刷新页面仍是旧内容在配置中设置spring.thymeleaf.cachefalse静态资源 404静态资源路径错误检查浏览器请求 URL静态文件放在src/main/resources/static7.5 排查链路建议遇到问题不要盲目改代码按下面的顺序排查更高效看控制台完整日志找到第一处Caused by后面的堆栈通常是跟随异常。判断是编译期问题还是运行期问题。编译期问题先看 Maven 依赖和 JDK 版本。运行期问题先看数据库是否能连通再看 SQL 是否正确。使用 MyBatis 日志输出 SQL把 SQL 复制到数据库客户端里执行确认数据状态。业务逻辑问题优先在 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 三个层次的调用关系。选课链路讲到容量校验和时间冲突时停下来重点解释为什么使用条件更新和唯一索引这部分是整个项目的技术亮点。完成这些工作后这套选课管理系统不仅能通过答辩也能作为后续学习微服务、消息队列、安全框架的起步项目。做项目的价值在于理解约束如何从业务转化到代码再落到数据库这个过程培养的工程意识比功能齐全更值得体现。
网站建设高端定制企业官网