news 2026/9/14 12:00:33

Java学生选课管理系统源码实战:Spring Boot并发选课与事务控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java学生选课管理系统源码实战:Spring Boot并发选课与事务控制

简介:这套基于Java的学生选课管理系统设计与实现资源,专注高校选课管理场景,面向需要完成课程设计、毕业设计或系统学习Java Web项目开发的师生与开发者,覆盖了课程发布、学生选课/退课、成绩管理、权限控制等典型教学业务。资源共5个文件,含源代码压缩包、MySQL数据库脚本与两段部署演示视频,整体大小208.74MB:源代码展示Spring Boot、Maven和经典三层架构的项目组织方式,sql脚本提供课程表、学生表、选课记录等核心数据表结构及初始数据,部署视频则逐步演示JDK、Tomcat、数据库等环境配置和项目发布过程。目前已有152人学习,读者可获得可直接运行的全量代码、数据库初始化脚本和操作录屏,既能快速部署体验完整选课流程,也能参考其需求分析、数据库设计、并发选课冲突处理与角色权限划分等实现思路,方便二次开发、课程报告或毕业设计参考。整体结构清晰,开发流程完整,对理解Java Web实际项目很有价值。

1. 学生选课管理系统源码拿回来后,先想清楚并发这件事

打开下载包里那份基于 Java 的学生选课管理系统源代码时,先别急着导入 IDE。这个资源包含002_code/SelectCource-master.zip001_video里的两个部署视频和sc.sql数据库脚本,整体是一个可以完整跑通的 Java Web 课程设计项目。选课系统在高校场景里太常见了,但大多数实现逻辑只停留在“前端调接口、后端写个 insert”的程度,一到选课高峰就出现超选、重复选课、成绩错乱。真正值得拆解的是它在数据库约束、事务边界、权限控制上的处理方式。适合两类人:一是想弄懂 Java 项目里 DAO、Service、Controller 怎么协同的初学者,二是在做数据库课程设计、需要从建表到部署全流程能复现的学生。

2. 三层架构与数据库表设计:从 E-R 图到 sc.sql

2.1 表现层、业务逻辑层、数据访问层怎么分工

这个系统的代码组织方式很清晰:表现层由 Controller 和视图文件组成,负责接收请求参数、校验基本格式、返回页面或 JSON;业务逻辑层放在 Service 接口和实现类里,课程发布、选课资格判断、退课规则、成绩录入这些教学规则都集中在这一层,不直接拼 SQL;数据访问层由 MyBatis 的 Mapper 接口和 XML 映射文件承担,只做最原子的增删改查。

三层架构的核心价值是让替换成本变低。比如把数据库从 MySQL 换成 PostgreSQL,只需要改动 Mapper XML 里的 SQL 方言和驱动依赖,Controller 里的参数结构、Service 里的业务规则完全不用动。同时,事务边界放在 Service 层而不是 DAO 层,才能保证一次选课动作里多个 SQL 要么全部成功,要么全部回滚。这个学生选课管理系统在SelectCource-master里对包名的划分很明显,你可以直接看到controllerservicedaoentity这些目录,照着包结构就能反推出整个请求的调用链。

2.2 课程表、学生表、选课表的主外键设计

sc.sql是整份资源里最先应该打开的文件。它对应的数据库名和表结构直接决定后面的业务代码怎么写。我拆开这份 SQL 后整理出最核心的三张表:学生表(student)、课程表(course)、选课记录表(sc)。其中选课表名称sc既指代 Student 与 Course 的关系,也与文件名sc.sql保持一致。

表名关键字段约束与索引作用
studentid, student_no, password, namestudent_no 唯一保存学生基本信息,学号作为登录凭证
courseid, course_no, name, teacher, capacity, selected_countcourse_no 唯一保存课程容量与当前已选人数
scid, student_id, course_id, create_timeUNIQUE(student_id, course_id),外键关联 student 和 course记录每一次成功选课的动作

注意课程表里的selected_count,它不是冗余字段,而是后面控制并发选课的关键抓手。每次选课成功后该字段加 1,退课后减 1。如果把已选人数实时从scCOUNT(*)统计,在并发情况下性能会明显下降,而且不好做“先到先得”的容量判断。用capacityselected_count两个字段配合更新的思路,后面会在第 3 章详细展开。

2.3 用 sc.sql 初始化数据库:表结构、索引与示例数据

打开 MySQL 客户端,直接执行sc.sql就能得到完整初始库。下面是摘出的核心建表语句:

CREATE DATABASE IF NOT EXISTS course_select DEFAULT CHARSET utf8mb4; USE course_select; CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, name VARCHAR(50) NOT NULL ) ENGINE=InnoDB; CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, teacher VARCHAR(50) NOT NULL, capacity INT NOT NULL DEFAULT 60, selected_count INT NOT NULL DEFAULT 0 ) ENGINE=InnoDB; CREATE TABLE sc ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, course_id INT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINE=InnoDB;

这段 SQL 里有三个关键点。第一,所有表都用 InnoDB 引擎,因为 InnoDB 支持外键和事务,后面的@Transactional回滚、行级锁都依赖这个引擎。第二,sc表上的唯一约束uk_stu_course是防止重复选课的底线,即使 Java 代码里忘记判重,数据库也会直接抛 DuplicateKeyException。第三,外键约束保证不存在无主学生的选课记录,这是数据完整性的一部分。

执行完建表后再往course表插入几门课程数据,比如course_noCS101、容量为 2 的《Java 程序设计》,方便后续测试并发选课。注意CREATE DATABASE语句里必须指定utf8mb4,否则中文课程名存储后可能出现乱码,这也是部署视频里常见的一个环境问题。

3. Spring Boot 后端:选课事务与权限控制实现

3.1 登录注册与密码加密存储

这个系统的登录注册模块没有把密码明文存进student.password,而是使用 BCrypt 哈希。原因是很多课程设计项目直接存明文,一旦数据库泄露,所有学生账号全部暴露。BCrypt 会自动生成随机盐,同一个密码每次加密结果都不同,即使两张表同密码也无法反推。

@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 注册时 student.setPassword(passwordEncoder.encode(rawPassword)); // 登录时校验 if (passwordEncoder.matches(rawPassword, student.getPassword())) { session.setAttribute("loginStudent", student); }

BCryptPasswordEncoder是 Spring Security 提供的实现,抽出来单独声明为 Bean,这样注册和登录用的都是同一个加密策略。passwordEncoder.encode负责加盐和哈希,passwordEncoder.matches会从密文中取出盐并重新计算比较。注意password字段长度要至少 60 位,sc.sql里设置的是VARCHAR(64),正好够放 BCrypt 的输出。

3.2 课程管理接口与参数校验

教师端课程发布是标准的三层调用:Controller 接收表单数据,Service 做业务处理,Mapper 写入数据库。这里用参数校验解决“课程名称为空”“容量为负数”这类低级问题。

@PostMapping("/course/add") public Result addCourse(@RequestBody @Valid CourseForm form) { courseService.addCourse(form); return Result.ok(); } public class CourseForm { @NotBlank(message = "课程名称不能为空") private String name; @NotNull @Min(value = 1, message = "课程容量至少为1") private Integer capacity; @NotBlank(message = "授课教师不能为空") private String teacher; }

@Valid触发 CourseForm 里的校验注解,校验失败时 Spring 会抛出MethodArgumentNotValidException,统一异常处理器把它转成 JSON 返回给前端。这里把capacity限制为整数且最小值为 1,可以防止后续并发更新时出现容量为 0 的边界问题。实际开发中我还会给course_no加格式校验,比如限定为字母加数字的正则,避免课程编号出现空格或特殊符号。

3.3 选课、退课的事务边界与并发处理

选课接口是整个系统最值得看的代码。如果先查selected_count,再判断是否小于capacity,最后再执行insert,两个事务并发时会同时通过判断,导致超选。正确做法是把“判断容量”和“更新已选人数”合并到一条 UPDATE 语句里,利用 InnoDB 的行锁串行化。

@Transactional(rollbackFor = Exception.class) public boolean selectCourse(Long studentId, Long courseId) { // 第一步:写入选课记录,唯一索引负责阻挡重复选课 try { scMapper.insert(studentId, courseId); } catch (DuplicateKeyException e) { return false; } // 第二步:带条件更新,返回0表示课程已满,抛错回滚 int updated = courseMapper.increaseSelectedCountWithCheck(courseId); if (updated == 0) { throw new ServiceException("课程已满"); } return true; }

scMapper.insert会先插入一条选课记录,如果事务 A 和事务 B 同时选同一门课但不同学生,两个 insert 都能成功;接着两个事务对course表的同一行执行带条件的更新,第二个执行的 UPDATE 会阻塞等待第一个提交或回滚。第一个完成后selected_count加 1,如果容量只剩 1,第二个事务的UPDATE ... WHERE selected_count < capacity就匹配不到行,返回 0,然后抛出异常触发@Transactional回滚——之前插入的选课记录也被撤销。这样超选的漏洞就被数据库锁封住了。

对应的 Mapper SQL 如下:

UPDATE course SET selected_count = selected_count + 1 WHERE id = #{courseId} AND selected_count < capacity;

退课逻辑反过来执行:先删除sc表记录,再让selected_count减 1。删除记录数返回 0 说明根本没有选过这门课,直接返回失败。

并发场景解决机制
两个学生同时选最后一门课UPDATE 带条件更新,行锁保证只有一个成功
同一个学生重复选同一门课sc 表唯一索引直接抛 DuplicateKeyException
选课后服务崩溃Service 层事务注解,未提交记录自动回滚

3.4 基于拦截器的角色权限控制

系统里有教师和学生两种角色,权限控制不能只在前端藏按钮,后端每个请求都要校验。这里用 Spring MVC 的HandlerInterceptor实现,不做角色分离的权限框架,更适合课程设计体量。

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); String role = (String) session.getAttribute("role"); if (role == null) { response.sendRedirect("/login"); return false; } if (request.getRequestURI().startsWith("/teacher") && !"TEACHER".equals(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } if (request.getRequestURI().startsWith("/student") && !"STUDENT".equals(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } return true; } }

拦截器把role存到 Session,所有/teacher/**/student/**路径都要经过这个过滤器。没有登录的直接重定向到登录页;角色不匹配的返回 403。登录时在 Service 里从student表查出用户,再根据是否属于教师表判断角色并写入 Session。这个方法比在每个 Controller 里重复写 if 判断要干净得多,后续增加管理员角色,只要在拦截器里加一段路径与角色映射即可。

4. 从源代码到可运行:JDK、Tomcat、MySQL 部署实录

4.1 项目结构解析与 Maven 依赖

解开002_code/SelectCource-master.zip后,可以看到标准的 Maven 项目布局。pom.xml是入口,然后才是src/main/javasrc/main/resourcessrc/main/webapp。如果你是直接用 IDEA 打开,最好选择“Open as Maven Project”,让它自动下载依赖。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

这里的mybatis-spring-boot-starter版本要跟 Spring Boot 2.7.x 匹配。如果直接拿来跑,建议先看一眼pom.xml里有没有引入spring-boot-starter-tomcat和打 war 包的配置。部署视频里给出的命令是mvn clean package,执行完会生成target/SelectCource.war,这个 war 包就是要丢进 Tomcat 的东西。

4.2 本地环境配置与数据库连接

数据库连接信息集中在src/main/resources/application.properties里。拿到源码后第一件事就是改数据库账号密码。

spring.datasource.url=jdbc:mysql://localhost:3306/course_select?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver mybatis.mapper-locations=classpath:mapper/*.xml mybatis.type-aliases-package=com.example.selectcourse.entity

serverTimezone=Asia/Shanghai必须配置,MySQL 8.x 默认时区不是中国时区,不配会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognizeduseUnicode=true&characterEncoding=utf8保证中文进出数据库都不会乱码。如果你本地 MySQL 是 5.7,驱动类可以换成com.mysql.jdbc.Driver,但 MySQL 8 环境建议保持com.mysql.cj.jdbc.Driver

4.3 部署到 Tomcat 的步骤与常见报错

整条部署链路是按下面这几步来的:

  1. 用 IDEA 打开项目,等待 Maven 依赖加载完。
  2. 修改application.properties里的数据库连接。
  3. 执行mvn clean package -DskipTests打成 war 包。
  4. target/SelectCource.war复制到 Tomcat 的webapps目录。
  5. 启动 Tomcat,访问http://localhost:8080/SelectCource/
现象原因处理方式
访问报 404war 包部署名跟访问路径不一致查看webapps下解压的目录名,按实际名称访问
启动报 ClassNotFoundExceptionJDK 版本与 Spring Boot 版本不匹配使用 JDK 8 或 11,不要用 JDK 17+ 跑 2.7 以下项目
Access denied for user数据库密码错误重新核对 application.properties
中文乱码数据库连接未指定 utf8连接 URL 加characterEncoding=utf8

部署视频01-学生选课管理系统.mp402-学生选课管理系统.mp4里演示的是把整个项目打成 war 丢到 Tomcat,这个过程比 IDEA 里直接运行更能看清楚容器的生命周期。我一般会先用java -jar跑一遍确认代码没问题,再丢 Tomcat,避免环境变量干扰。

4.4 部署视频中容易忽略的三处配置

视频里鼠标点得很快,有三次操作很容易被忽略,但少了任何一个都会导致部署失败。

第一是 Tomcat 的启动脚本需要JAVA_HOME环境变量。Windows 下如果没有先配置 JAVA_HOME,点击startup.bat会闪退。第二是数据库执行sc.sql时要确认当前选中的是刚建好的course_select库,否则表建到别的库下面,业务代码连接时看不到表。第三是 war 包名如果包含中文或空格,Tomcat 解压后访问路径会带着编码转义,建议把 war 包重命名为英文,比如select-course.war

这些细节在部署视频里往往一闪而过,只有在报错回头看时才注意到。建议每改一个配置,就tail -f logs/catalina.out看一次日志。

5. 验证系统健康度的四个指标与一条排错命令

系统跑起来后,别急着说完成。用下面四个指标快速判断它能不能扛住真实选课场景:重复选课率是否为 0,已选人数是否等于选课记录数,退课后人数是否正确回退,并发压测下有没有超选。第四个指标可以这样验证:先用sc.sql建一门容量为 2 的课程,然后用命令同时发 10 个选课请求。

for i in $(seq 1 10); do curl -s -b cookies_$i.txt -c cookies_$i.txt \ -X POST http://localhost:8080/SelectCource/student/select?courseId=1 & done wait

理论上这个过程只有 2 个请求会成功。验证的关键是最后一步 SQL:查询课程表里的已选人数,和选课表里的实际记录数对比。

SELECT selected_count FROM course WHERE id = 1; SELECT COUNT(*) FROM sc WHERE course_id = 1;

如果两个数字一致且等于容量,说明并发控制生效;如果COUNT(*)大于selected_count,说明事务没有正常回滚,需要检查@Transactional是否只加在 Service 实现类方法上,而不是 private 方法上。还有一条排错命令,当怀疑死锁时,在 MySQL 里执行SHOW ENGINE INNODB STATUS;,查看LATEST DETECTED DEADLOCK段,里面会打印出正在竞争的两条 SQL,据此调整更新顺序。最后把课程容量临时改成 1,重新多开几个窗口并发选课,观察第二个请求是否直接失败,这是最省时间的验收方式。

本文还有配套的精品资源,点击获取

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

安卓与嵌入式低功耗开发全栈解析:从PMIC寄存器到PowerHAL契约

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

作者头像 李华
网站建设 2026/9/14 11:56:59

UART通信实操指南:从电平波形到寄存器配置

1. 这不是“讲义”&#xff0c;而是一份UART通信的实操手记你打开开发板手册&#xff0c;第一页就写着“支持UART通信”&#xff1b;调试时串口助手一闪而过几行乱码&#xff1b;Linux下dmesg | grep tty突然冒出个ttyUSB0却连不上&#xff1b;用FT232R芯片焊好电路&#xff0c…

作者头像 李华
网站建设 2026/9/14 11:54:39

AI编曲5大技巧:从清唱到专业级音乐制作

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

作者头像 李华