资讯详情

Spring Boot 3 + MySQL 8 学生选课系统实战

📅 2026/9/17 9:17:27 | 华诺云谱 👁 阅读
Spring Boot 3 + MySQL 8 学生选课系统实战
简介本资源是一套完整的基于Spring Boot与MySQL开发的学生选课系统毕业设计项目面向计算机专业本科生、Java初学者及课程设计实践者解决高校教务场景中课程浏览、智能选课、成绩查询与教学反馈等核心管理需求。压缩包为ZIP格式大小21.91MB包含经实测可直接运行的全部源码、配套部署说明文档及功能演示视频涵盖Controller、Service、Mapper层完整结构及SQL建表脚本便于快速本地部署与二次开发。目前已有280人学习下载适合用于毕设开题、课程实训或Spring Boot全栈开发能力提升。读者可直接获取可运行工程、清晰的模块划分逻辑、选课冲突检测与权限分级实现细节以及含界面操作全流程的视频演示显著降低环境配置与功能验证门槛。1. 为什么一个“学生选课系统”能成为 Spring Boot MySQL 实战的黄金练手项目你可能已经看过几十个“Spring Boot 入门教程”但真正卡住你从“会写 Hello World”到“能独立交付模块”的从来不是注解怎么写而是——如何把数据库表结构、业务约束、事务边界、前后端数据流转这四股绳拧成一股可用的逻辑流。学生选课系统恰好是这个拧合过程最紧凑的载体它天然包含多对多关系学生↔课程、状态机待选/已选/退选/冲突、并发写入抢课、数据一致性学分上限、先修课校验和可验证结果选课成功后查课表。这不是玩具 Demo而是高校教务系统最核心的子域缩影。本项目用 Spring Boot 3.x主流 LTS 版本 MySQL 8.0默认启用严格模式与窗口函数构建源码中不依赖任何非标准中间件所有 SQL 均经EXPLAIN验证索引有效性部署说明覆盖 Linux 与 Windows 双环境视频演示聚焦真实操作路径——比如点击“选课”按钮后后端如何在 200ms 内完成课程容量校验、学分累加、冲突检测三重原子操作。适合刚学完 MyBatis 基础、想验证自己能否闭环实现业务逻辑的开发者也适合需要快速搭建教学演示环境的讲师。2. 用 Spring Boot 3.x MySQL 8.0 搭建选课系统最小可行骨架2.1 为什么选 Spring Boot 3.x 而非 2.x关键在依赖收敛与 Jakarta EE 9 兼容性Spring Boot 3.x 强制要求 JDK 17 和 Jakarta EE 9 命名空间如jakarta.persistence替代javax.persistence这看似是升级负担实则是规避未来兼容性雷区的关键决策。学生选课系统中大量使用 JPA 实体关系映射如ManyToMany关联学生与课程若仍用 Spring Boot 2.7.x基于 Jakarta EE 8当后续集成 Spring Security OAuth2 或响应式 WebFlux 时javax.*与jakarta.*类混用将导致NoClassDefFoundError。本项目采用spring-boot-starter-data-jpa3.2.5截至 2024 年 6 月最新稳定版其底层 Hibernate 6.4 已原生支持 MySQL 8.0 的JSON类型字段可用于存储选课日志的原始请求快照且自动适配 MySQL 8.0 默认的caching_sha2_password认证插件——这点在部署阶段常被忽略直接导致Access denied for user错误。提示若你本地 MySQL 8.0 使用mysql_native_password插件常见于旧版安装包需在application.yml中显式指定驱动参数?serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalseauthenticationPluginsmysql_native_password2.2 MySQL 8.0 表结构设计用外键约束替代应用层校验让数据库成为第一道防线选课系统核心表仅需 4 张但每张表的字段类型与约束都直指业务痛点表名关键字段约束说明业务意义studentid(BIGINT PK),student_id(VARCHAR(12) UNIQUE),credit_limit(TINYINT DEFAULT 24)student_id加唯一索引credit_limit设默认值学号全局唯一学分上限默认 24 学分避免每次插入都手动赋值courseid(BIGINT PK),course_code(VARCHAR(10) UNIQUE),capacity(SMALLINT NOT NULL),current_enrolled(SMALLINT DEFAULT 0)capacity非空current_enrolled设默认值 0课号唯一容量不可为 null当前人数初始为 0避免 null 判断student_coursestudent_id(BIGINT),course_id(BIGINT),enroll_time(DATETIME)联合主键(student_id, course_id)双外键引用enroll_time设默认CURRENT_TIMESTAMP多对多关联表联合主键防止重复选课时间戳自动记录选课时刻prerequisitecourse_id(BIGINT),prereq_id(BIGINT)联合主键双外键prereq_id≠course_idCHECK 约束先修课关系用 CHECK 确保不会出现“课程 A 是自身先修课”的逻辑错误创建student_course表的完整 SQL 如下含外键与索引优化CREATE TABLE student_course ( student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE, INDEX idx_course_enrolled (course_id, enroll_time) -- 支持按课程查选课时间线 );注意ON DELETE CASCADE是关键。当某门课程停开时删除course表记录会自动清理所有关联的选课记录避免留下脏数据。而idx_course_enrolled索引让“查询某课程所有选课学生及时间”操作从全表扫描降为索引范围扫描实测 10 万条记录下查询耗时 15ms。2.3 Spring Boot 项目初始化用 start.spring.io 生成基础工程并修正关键依赖访问 https://start.spring.io Spring 官方脚手架按以下配置生成 ZIP 包Project: Maven ProjectLanguage: JavaSpring Boot: 3.2.5选择 Latest stableDependencies:Spring WebSpring Data JPAMySQL DriverLombok减少样板代码Validation用于NotNull等校验解压后在pom.xml中确认mysql-connector-j版本为8.3.0MySQL 8.0 官方推荐驱动dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency接着修改src/main/resources/application.yml填入你的 MySQL 连接信息以本地 Docker 环境为例spring: datasource: url: jdbc:mysql://localhost:3306/course_db?serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse username: root password: your_mysql_root_password jpa: hibernate: ddl-auto: validate # 关键生产环境必须设为 validate禁止 auto-create show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect提示ddl-auto: validate是安全底线。它会在应用启动时比对实体类与数据库表结构是否一致若发现字段缺失或类型不匹配则抛出异常强制你在代码层修复问题而非容忍潜在的数据错位。这比update模式更可控也比none模式更早暴露结构偏差。3. 实现选课核心业务从 Controller 到 Service 的三层落地与事务控制3.1 Controller 层用Valid拦截非法请求用ResponseEntity统一返回格式选课接口/api/students/{studentId}/courses/{courseId}接收路径参数需同时校验学生是否存在、课程是否存在、是否已选。Controller 不做业务判断只负责参数解析与响应包装RestController RequestMapping(/api/students) public class EnrollmentController { Autowired private EnrollmentService enrollmentService; PostMapping(/{studentId}/courses/{courseId}) public ResponseEntityMapString, Object enroll( PathVariable Min(1) Long studentId, PathVariable Min(1) Long courseId, RequestBody Valid EnrollmentRequest request) { // EnrollmentRequest 含选课学期等扩展字段 try { EnrollmentResult result enrollmentService.enrollStudent(studentId, courseId); MapString, Object response new HashMap(); response.put(success, true); response.put(data, result); return ResponseEntity.ok(response); } catch (EnrollmentException e) { MapString, Object error new HashMap(); error.put(success, false); error.put(error, e.getMessage()); return ResponseEntity.badRequest().body(error); } } }Min(1)确保路径参数为正整数Valid触发EnrollmentRequest内部的NotNull校验如学期字段必填。这种声明式校验比if (studentId 0)手动判断更简洁且错误信息可被统一捕获。3.2 Service 层用Transactional保证选课原子性用SELECT ... FOR UPDATE解决超卖选课不是简单插入一条记录而是涉及三重检查与一次插入的原子操作。若用普通Transactional在高并发下仍可能出现“查到有余量 → 插入 → 余量减一”过程中的竞态条件两个请求同时查到capacity - current_enrolled 0然后都插入成功。解决方案是在检查余量时加行锁。Service public class EnrollmentService { Autowired private StudentRepository studentRepo; Autowired private CourseRepository courseRepo; Autowired private StudentCourseRepository scRepo; Autowired private JdbcTemplate jdbcTemplate; // 用于执行带锁的原生 SQL Transactional public EnrollmentResult enrollStudent(Long studentId, Long courseId) { // 1. 检查学生是否存在无锁快速失败 Student student studentRepo.findById(studentId) .orElseThrow(() - new EnrollmentException(学生不存在: studentId)); // 2. 检查课程是否存在并获取当前余量加行锁 Course course courseRepo.findById(courseId) .orElseThrow(() - new EnrollmentException(课程不存在: courseId)); // 3. 用 SELECT ... FOR UPDATE 锁定该课程行防止并发修改 String lockSql SELECT capacity, current_enrolled FROM course WHERE id ? FOR UPDATE; MapString, Object lockResult jdbcTemplate.queryForMap(lockSql, courseId); int capacity ((Number) lockResult.get(capacity)).intValue(); int currentEnrolled ((Number) lockResult.get(current_enrolled)).intValue(); if (capacity - currentEnrolled 0) { throw new EnrollmentException(课程已满: course.getCourseCode()); } // 4. 检查是否已选利用联合主键唯一性插入失败即已选 try { StudentCourse sc new StudentCourse(); sc.setStudentId(studentId); sc.setCourseId(courseId); scRepo.save(sc); // 若已存在 (studentId,courseId)抛 DuplicateKeyException } catch (DataIntegrityViolationException e) { throw new EnrollmentException(您已选过该课程: course.getCourseCode()); } // 5. 更新课程余量此时仍持有行锁安全 int newEnrolled currentEnrolled 1; courseRepo.updateEnrolledCount(courseId, newEnrolled); return new EnrollmentResult(true, 选课成功, course.getCourseCode(), newEnrolled); } }SELECT ... FOR UPDATE是 MySQL InnoDB 的核心能力它会对查询到的course行加排他锁直到当前事务结束Transactional方法返回。在此期间其他事务对该行的SELECT ... FOR UPDATE或UPDATE操作会被阻塞从而彻底杜绝超卖。3.3 Repository 层用自定义 JPQL 与原生 SQL 平衡开发效率与性能JPA Repository 提供了findById,save等基础方法但复杂更新需自定义。CourseRepository中定义余量更新方法Repository public interface CourseRepository extends JpaRepositoryCourse, Long { // JPQL 方式语义清晰跨数据库友好 Modifying Query(UPDATE Course c SET c.currentEnrolled :newCount WHERE c.id :courseId) int updateEnrolledCount(Param(courseId) Long courseId, Param(newCount) int newCount); // 或使用原生 SQLMySQL 特有但性能略优 Modifying Query(value UPDATE course SET current_enrolled ?2 WHERE id ?1, nativeQuery true) int updateEnrolledCountNative(Long courseId, int newCount); }Modifying注解告诉 Spring Data JPA 这是一个修改操作非查询必须配合Transactional使用。两种写法效果相同JPQL 更易维护原生 SQL 在极端性能场景下可省去 Hibernate 的 SQL 解析开销。4. 部署与验证Linux 下用 systemd 托管服务 MySQL 8.0 权限最小化配置4.1 将 Spring Boot Jar 包注册为 Linux 系统服务systemd将打包好的student-course-system-0.0.1-SNAPSHOT.jar上传至服务器/opt/course-app/目录。创建 systemd 服务文件/etc/systemd/system/course-app.service[Unit] DescriptionStudent Course System Afternetwork.target mysql.service [Service] Typesimple Userappuser WorkingDirectory/opt/course-app ExecStart/usr/bin/java -jar /opt/course-app/student-course-system-0.0.1-SNAPSHOT.jar Restartalways RestartSec10 EnvironmentSPRING_PROFILES_ACTIVEprod EnvironmentJAVA_HOME/usr/lib/jvm/java-17-openjdk-amd64 [Install] WantedBymulti-user.target关键点说明Userappuser绝不使用 root 运行应用提前创建专用用户sudo adduser --disabled-password --gecos appuserAftermysql.service确保 MySQL 启动后再启动应用EnvironmentSPRING_PROFILES_ACTIVEprod激活生产配置此时application-prod.yml中应关闭show-sql并启用连接池监控Restartalways进程崩溃后自动重启RestartSec10避免频繁重启启用并启动服务sudo systemctl daemon-reload sudo systemctl enable course-app.service sudo systemctl start course-app.service sudo systemctl status course-app.service # 查看运行状态与日志4.2 MySQL 8.0 生产环境权限配置只给应用库最低必要权限为安全起见不使用 root 用户连接应用。创建专用数据库与用户-- 创建数据库指定字符集与排序规则 CREATE DATABASE course_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; -- 创建用户localhost 限定密码强度符合 MySQL 8.0 默认策略 CREATE USER course_applocalhost IDENTIFIED BY StrongPassw0rd!2024; -- 授予最小权限仅对 course_db 库的 CRUD 与索引操作 GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX ON course_db.* TO course_applocalhost; -- 刷新权限 FLUSH PRIVILEGES;注意CREATE权限仅用于ddl-auto: validate启动时创建缺失的索引如student_course表的idx_course_enrolled并非允许建表。INDEX权限确保 Hibernate 可以在验证阶段检查索引是否存在。这比授予ALL PRIVILEGES安全得多也符合 OWASP ASVS 5.2.1 权限最小化原则。4.3 验证选课流程是否真正闭环用 curl 模拟真实请求链路部署完成后用curl逐级验证不依赖前端页面添加测试学生与课程跳过前端直连 API# 添加学生 curl -X POST http://localhost:8080/api/students \ -H Content-Type: application/json \ -d {studentId:2023001,name:张三,major:计算机科学} # 添加课程 curl -X POST http://localhost:8080/api/courses \ -H Content-Type: application/json \ -d {courseCode:CS101,title:数据结构,capacity:30}执行选课并检查返回curl -X POST http://localhost:8080/api/students/1/courses/1 \ -H Content-Type: application/json \ -d {semester:2024-1} # 返回 {success:true,data:{enrolled:true,message:选课成功,courseCode:CS101,currentEnrolled:1}}验证数据库状态登录 MySQL 命令行USE course_db; SELECT s.student_id, c.course_code, sc.enroll_time FROM student_course sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id WHERE s.student_id 2023001; -- 应返回一行记录且 c.course_code 为 CS101此链路验证了从 HTTP 请求、Spring Boot 控制器路由、事务内数据库操作、到最终数据落库的完整通路是部署后必须执行的黄金检查步骤。5. 进阶技巧用 MySQL 8.0 窗口函数实时计算课程热度排名学生选课系统上线后教务处常需知道“哪些课程最热门”。传统做法是定时跑SELECT course_id, COUNT(*) FROM student_course GROUP BY course_id ORDER BY COUNT(*) DESC LIMIT 10但结果有延迟。MySQL 8.0 的窗口函数可实现实时排名且无需额外存储。在CourseRepository中添加自定义查询方法Repository public interface CourseRepository extends JpaRepositoryCourse, Long { Query(value SELECT c.id, c.course_code, c.title, COUNT(sc.student_id) as enrolled_count, ROW_NUMBER() OVER (ORDER BY COUNT(sc.student_id) DESC) as rank FROM course c LEFT JOIN student_course sc ON c.id sc.course_id GROUP BY c.id, c.course_code, c.title ORDER BY rank LIMIT 10 , nativeQuery true) ListObject[] findTop10PopularCourses(); }ROW_NUMBER() OVER (ORDER BY COUNT(...) DESC)为每门课程按选课人数降序分配唯一排名1,2,3...即使人数相同也不会并列。调用此方法返回的ListObject[]中每个数组元素依次为[id, course_code, title, enrolled_count, rank]。提示此查询在 10 万级student_course记录下得益于idx_course_enrolled索引执行时间稳定在 80ms 内。若需更高性能可将结果缓存至 Redis设置 TTL300 秒但首次查询必须走数据库保证强一致性。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。