news 2026/10/4 5:09:54

Java学生选课管理系统:从数据库设计到Spring Boot部署的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java学生选课管理系统:从数据库设计到Spring Boot部署的完整实战

简介:这份资源面向高校计算机相关专业学生与Java Web初学者,提供一套可直接运行的学生选课管理系统完整项目,帮助读者理解从需求分析到部署上线的全流程开发思路。压缩包共5个文件,包含2个zip源码与数据库脚本、2个mp4部署演示视频、1个sql文件,整体约208.74MB,源码、建库脚本与操作录屏相互配套,便于对照学习。系统采用Java语言,结合Spring Boot、MySQL与前端页面实现登录注册、课程管理、选课退课、成绩管理与权限控制等核心模块,并涉及并发选课冲突处理、数据库规范化设计等实践要点。部署视频讲解JDK、Tomcat与数据库环境配置及项目发布过程,可辅助读者完成本地运行与调试。目前已有152人学习下载,适合作为课程设计、毕业设计参考或Java Web入门练手项目,帮助读者掌握三层架构与数据库交互的实际应用。

1. 从一份「学生选课管理系统」压缩包说起:Java 课设怎么做出能跑、能讲、能交付的完整项目

很多 Java 初学者拿到「基于 Java 的学生选课管理系统设计与实现」这个题目时,第一反应是去搜现成源代码,下载一个压缩包,解压、导入 IDE、跑起来,然后交差。但真正做过一轮的人都知道,能跑起来和能讲清楚之间隔着一条鸿沟:数据库表为什么这样设计、选课冲突怎么判断、并发抢课怎么处理、部署视频里那些环境配置到底踩了哪些坑,这些才是答辩和面试里被追问的地方。这个标题背后其实是一套完整的 Java Web 工程链路——从需求拆解、数据库建模、后端接口设计,到前端页面交互、本地部署、打包交付。它适合正在做课程设计的学生、准备 Java 开发岗面试的求职者,以及想用一个完整项目把 SSM 或 Spring Boot 串起来的自学者。接下来我会按实际做项目的顺序,把选课管理系统的设计思路、建表脚本、核心接口、部署步骤和常见翻车点逐一拆开讲,让你拿到任何一个类似的源代码包都能看懂、改得动、讲得出。

2. 选课管理系统的数据库怎么建:从 ER 图到 6 张核心表的建表脚本

2.1 先想清楚业务实体,再动手写 CREATE TABLE

学生选课管理系统的业务实体并不复杂,但关系容易理乱。核心实体有四个:学生(student)、教师(teacher)、课程(course)、选课记录(course_selection)。围绕这四个实体,还需要两个辅助表:用户登录表(sys_user)和班级表(class_info)。很多人在这一步直接抄网上的建表语句,结果字段类型不对、外键关系缺失,后面写查询时到处报错。

我一般会先在纸上画一遍关系:一个班级有多个学生,一个教师可以开多门课,一门课可以被多个学生选,选课记录就是学生和课程之间的多对多桥接表。选课记录表里必须有一个状态字段,用来标记「已选」「已退」「已修完」,否则后面统计学分时没法区分。课程表里要有容量字段(capacity)和已选人数字段(selected_count),这两个字段是判断课程是否满员的关键。

下面是我在实际项目中反复用的一套建表脚本,基于 MySQL 8.0,字符集用 utf8mb4,引擎 InnoDB:

-- 创建数据库 CREATE DATABASE IF NOT EXISTS student_course_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE student_course_system; -- 1. 班级表 CREATE TABLE class_info ( class_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '班级ID', class_name VARCHAR(50) NOT NULL COMMENT '班级名称', major VARCHAR(50) NOT NULL COMMENT '所属专业', grade YEAR NOT NULL COMMENT '入学年份' ) ENGINE=InnoDB COMMENT='班级信息表'; -- 2. 学生表 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT '学号', student_name VARCHAR(30) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT 'M' COMMENT '性别 M/F', class_id INT NOT NULL COMMENT '所属班级', phone VARCHAR(15) COMMENT '联系电话', FOREIGN KEY (class_id) REFERENCES class_info(class_id) ) ENGINE=InnoDB COMMENT='学生信息表'; -- 3. 教师表 CREATE TABLE teacher ( teacher_id VARCHAR(20) PRIMARY KEY COMMENT '工号', teacher_name VARCHAR(30) NOT NULL COMMENT '姓名', title VARCHAR(20) COMMENT '职称', department VARCHAR(50) COMMENT '所属院系' ) ENGINE=InnoDB COMMENT='教师信息表'; -- 4. 课程表 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY COMMENT '课程编号', course_name VARCHAR(60) NOT NULL COMMENT '课程名称', credit DECIMAL(3,1) NOT NULL COMMENT '学分', capacity INT NOT NULL DEFAULT 50 COMMENT '容量上限', selected_count INT NOT NULL DEFAULT 0 COMMENT '已选人数', teacher_id VARCHAR(20) COMMENT '授课教师', semester VARCHAR(20) COMMENT '开课学期', FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) ENGINE=InnoDB COMMENT='课程信息表'; -- 5. 选课记录表 CREATE TABLE course_selection ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(20) NOT NULL COMMENT '学号', course_id VARCHAR(20) NOT NULL COMMENT '课程编号', select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', status TINYINT DEFAULT 1 COMMENT '1已选 0已退 2已修完', score DECIMAL(5,1) COMMENT '成绩', UNIQUE KEY uk_student_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINE=InnoDB COMMENT='选课记录表'; -- 6. 系统用户表 CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码哈希', role TINYINT NOT NULL COMMENT '1学生 2教师 3管理员', ref_id VARCHAR(20) COMMENT '关联学号或工号' ) ENGINE=InnoDB COMMENT='系统用户表';

这段脚本里有几个设计决策值得展开说。选课记录表的UNIQUE KEY uk_student_course是防止同一个学生重复选同一门课的第一道防线,数据库层面直接拦住,比在 Java 代码里查一遍再插入要可靠得多。status字段用 TINYINT 而不是布尔类型,是因为后面可能扩展出「旁听」「重修」等状态。selected_count冗余在课程表里,是为了避免每次查课程列表都去 count 选课记录表,选课高峰期这个优化很关键。

2.2 初始化数据与索引优化

建完表之后要插入测试数据,否则前端页面全是空的,没法验证功能。初始化数据至少需要:3 个班级、10 个学生、5 个教师、8 门课程、若干选课记录。插入时注意selected_count要和选课记录保持一致,不然后面做容量校验时数据对不上。

-- 初始化班级 INSERT INTO class_info (class_name, major, grade) VALUES ('计科2101', '计算机科学与技术', 2021), ('软工2102', '软件工程', 2021), ('网安2103', '网络空间安全', 2021); -- 初始化教师 INSERT INTO teacher (teacher_id, teacher_name, title, department) VALUES ('T001', '张明', '教授', '计算机学院'), ('T002', '李华', '副教授', '计算机学院'), ('T003', '王芳', '讲师', '软件学院'); -- 初始化课程 INSERT INTO course (course_id, course_name, credit, capacity, teacher_id, semester) VALUES ('C001', 'Java程序设计', 3.0, 60, 'T001', '2024-2025-1'), ('C002', '数据结构', 4.0, 50, 'T002', '2024-2025-1'), ('C003', '数据库原理', 3.0, 55, 'T001', '2024-2025-1'), ('C004', '操作系统', 4.0, 50, 'T003', '2024-2025-1');

索引方面,选课记录表的student_id和course_id上除了外键自带的索引,还应该根据查询模式补索引。如果经常按学生查已选课程,student_id上的索引就够了;如果经常按课程统计选课名单,course_id上需要单独索引。我一般会加一个联合索引idx_student_status (student_id, status),因为查「某学生当前已选课程」是最高频的操作。

提示:建表时字段类型要和 Java 实体类严格对应。DECIMAL(3,1)对应BigDecimal,不要用double,否则学分计算会出现精度问题。DATETIME对应LocalDateTime,JDBC 驱动从 8.0 开始已经原生支持。

3. 后端接口怎么设计:Spring Boot + MyBatis 实现选课、退课与冲突检测

3.1 项目分层与核心依赖配置

后端我一般用 Spring Boot 2.7 + MyBatis 的组合,原因是配置量少、社区资料多、课设场景完全够用。项目结构按 Controller → Service → Mapper → Entity 四层划分,每层职责明确。pom.xml里核心依赖包括:spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok。如果前端是独立页面,再加一个spring-boot-starter-thymeleaf或者直接放静态资源。

<!-- pom.xml 核心依赖片段 --> <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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

application.yml里配置数据源和 MyBatis 映射文件路径:

spring: datasource: url: jdbc:mysql://localhost:3306/student_course_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true

map-underscore-to-camel-case: true这一行很关键,它让数据库的student_name自动映射到 Java 的studentName,省掉大量手动 resultMap 配置。很多人不知道这个配置,写了一大堆<resultMap>结果还映射错。

3.2 选课接口的冲突检测与并发处理

选课接口是整个系统最核心也最容易出问题的地方。需要检测的冲突有三类:课程是否已满、是否重复选课、是否时间冲突(同一学期选了两门同一时段的课)。前两类可以在数据库层面用唯一约束和容量字段解决,第三类需要课程表里有时间段字段才能判断。

下面是一个典型的选课 Service 方法:

@Service @RequiredArgsConstructor public class CourseSelectionService { private final CourseMapper courseMapper; private final CourseSelectionMapper selectionMapper; @Transactional(rollbackFor = Exception.class) public String selectCourse(String studentId, String courseId) { // 1. 检查是否已经选过 int count = selectionMapper.countByStudentAndCourse(studentId, courseId); if (count > 0) { return "你已经选过这门课"; } // 2. 检查课程容量(带行锁,防止并发超选) Course course = courseMapper.selectForUpdate(courseId); if (course == null) { return "课程不存在"; } if (course.getSelectedCount() >= course.getCapacity()) { return "课程已满"; } // 3. 插入选课记录 CourseSelection record = new CourseSelection(); record.setStudentId(studentId); record.setCourseId(courseId); record.setStatus(1); selectionMapper.insert(record); // 4. 更新已选人数 courseMapper.incrementSelectedCount(courseId); return "选课成功"; } }

这段代码里有两个关键点。第一,@Transactional注解保证了插入记录和更新计数在同一个事务里,要么都成功要么都回滚。第二,selectForUpdate对应的 SQL 是SELECT * FROM course WHERE course_id = ? FOR UPDATE,它在数据库层面加了行锁,防止两个学生同时选最后一门课时都读到selected_count < capacity然后都插入成功。如果不加这个锁,选课高峰期一定会出现超选,这是血泪经验。

对应的 Mapper XML:

<select id="selectForUpdate" resultType="com.example.entity.Course"> SELECT * FROM course WHERE course_id = #{courseId} FOR UPDATE </select> <update id="incrementSelectedCount"> UPDATE course SET selected_count = selected_count + 1 WHERE course_id = #{courseId} AND selected_count &lt; capacity </update>

incrementSelectedCount的 WHERE 条件里带了selected_count < capacity,这是一道额外的保险。即使行锁因为某些原因没生效,这个条件也能保证不会把已选人数更新到超过容量。

3.3 退课与课程列表查询接口

退课的逻辑比选课简单,但要注意把status改成 0 而不是直接删除记录,这样后面统计选课历史时数据还在。同时要把selected_count减一。

@Transactional(rollbackFor = Exception.class) public String dropCourse(String studentId, String courseId) { int updated = selectionMapper.updateStatus(studentId, courseId, 0); if (updated == 0) { return "未找到选课记录"; } courseMapper.decrementSelectedCount(courseId); return "退课成功"; }

课程列表查询需要关联教师姓名,返回给前端展示。用 MyBatis 的关联查询或者直接写 JOIN 都可以:

<select id="listCoursesWithTeacher" resultType="com.example.vo.CourseVO"> SELECT c.course_id, c.course_name, c.credit, c.capacity, c.selected_count, t.teacher_name, c.semester FROM course c LEFT JOIN teacher t ON c.teacher_id = t.teacher_id ORDER BY c.course_id </select>

这里用LEFT JOIN而不是INNER JOIN,是因为有些课程可能还没分配教师,用 INNER JOIN 会导致这些课程查不出来。CourseVO是一个专门的视图对象,比直接返回Course实体多了一个teacherName字段。

注意:接口返回给前端的数据不要直接用实体类,尤其是学生表里的密码字段。用 VO 或者 DTO 做一层转换,避免敏感字段泄露。这是很多课设项目被扣分的地方。

4. 本地部署与打包交付:从数据库导入到可运行 JAR 的完整流程

4.1 环境准备与数据库导入

拿到一个源代码压缩包后,第一步不是急着导入 IDE,而是先把环境对齐。需要确认的版本:JDK 1.8 或 11(看项目用的 Spring Boot 版本)、Maven 3.6+、MySQL 5.7 或 8.0、IDE 用 IntelliJ IDEA 或 Eclipse。版本不匹配是部署失败的第一大原因,比如项目用 JDK 17 编译的,你本地只有 JDK 8,导入后满屏红线。

数据库导入的步骤:先启动 MySQL 服务,用命令行或 Navicat 等工具连接,执行建表脚本,再执行数据初始化脚本。如果压缩包里提供的是.sql文件,直接source导入:

# 命令行导入 SQL 文件 mysql -u root -p student_course_system < init.sql # 或者进入 MySQL 后执行 mysql> source /path/to/init.sql;

导入完成后用SHOW TABLES;确认六张表都在,再用SELECT COUNT(*) FROM course;确认数据插进去了。这一步看起来简单,但很多人导入时字符集不对,中文变成乱码,后面页面显示全是问号。解决办法是在导入前先执行SET NAMES utf8mb4;。

4.2 修改配置并启动项目

导入 IDE 后,第一件事是改application.yml里的数据库连接信息,把用户名密码改成自己本地的。然后检查 Maven 依赖是否全部下载成功,如果卡在某个依赖上,换阿里云镜像仓库:

<!-- settings.xml 中配置阿里云镜像 --> <mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>

启动方式有两种:在 IDE 里直接运行主类,或者用 Maven 打包成 JAR 后命令行运行。打包命令:

# 跳过测试打包 mvn clean package -DskipTests # 运行生成的 JAR java -jar target/student-course-system-1.0.0.jar

打包时跳过测试是常规操作,因为课设项目的测试用例往往不完整,带着跑容易失败导致打包中断。启动成功后控制台会输出 Tomcat 端口号,默认 8080,浏览器访问http://localhost:8080就能看到登录页。

4.3 部署视频里应该覆盖的关键步骤

如果压缩包里带了部署视频,重点看这几个环节:数据库导入时用的什么工具、配置文件改了哪几行、启动时控制台有没有报错、浏览器访问的完整 URL 是什么。自己录部署视频的话,按这个顺序走:环境版本展示 → 数据库导入 → 配置文件修改 → 项目启动 → 功能演示。视频不用太长,5 到 8 分钟足够,关键是每一步都要让看的人能跟着做。

提示:部署视频里不要只录成功路径,把常见的报错也录进去,比如端口被占用怎么改、数据库连不上怎么排查。这些才是看视频的人真正需要的。

5. 避坑与排查:选课系统开发中最容易翻车的 5 个地方

5.1 选课超员:并发下容量校验失效

现象:课程容量设了 50,但选课结束后发现选了 52 个人。原因:容量校验在 Java 代码里先查后插,两个请求同时查到selected_count = 49,都判断没满,然后都插入成功。解决:用数据库行锁SELECT ... FOR UPDATE或者乐观锁版本号,把校验和更新放在同一个事务里。上面 3.2 节的代码已经给了方案。

5.2 中文乱码:数据库、连接、页面三处字符集不一致

现象:课程名称在数据库里看是正常的,页面上显示成????。原因:数据库字符集是 utf8mb4,但 JDBC 连接串没加characterEncoding=utf8,或者 Tomcat 的server.xml里没配 URIEncoding。解决:三处统一——建库用 utf8mb4,连接串加useUnicode=true&characterEncoding=utf8,前端页面<meta charset="UTF-8">。

5.3 外键约束导致删数据失败

现象:想删一门已经有人选的课程,报Cannot delete or update a parent row。原因:选课记录表有外键指向课程表,直接删课程会违反约束。解决:要么先删选课记录再删课程,要么把外键改成ON DELETE CASCADE。我一般不建议级联删除,太危险,手动按顺序删更可控。

5.4 MyBatis 映射失败:字段名和属性名对不上

现象:查询返回的对象里某些字段是 null,但数据库里明明有值。原因:数据库用下划线命名(student_name),Java 用驼峰(studentName),没开自动映射。解决:在application.yml里加map-underscore-to-camel-case: true,或者手动写 resultMap。

5.5 打包后运行报错:配置文件没被打进去

现象:IDE 里跑得好好的,java -jar启动就报数据库连接失败。原因:application.yml放在了项目根目录而不是src/main/resources下,Maven 打包时没包含进去。解决:确认配置文件在resources目录下,打包后用jar tf target/xxx.jar | grep application检查文件是否在 JAR 里。

6. 让选课系统经得起追问:三个进阶改造和验证方法

把基础功能跑通只是及格线,想让这个项目在答辩或面试里成为加分项,需要做几个进阶改造。第一个是加 Redis 缓存课程列表。选课高峰期所有人都在刷课程列表,每次都查数据库压力很大。把课程列表缓存到 Redis,设置 5 分钟过期,选课或退课时主动删缓存。改造点很小,但能讲出「缓存穿透」「缓存一致性」这些面试常问的概念。

第二个是加接口限流。用 Spring Boot 的@RestControllerAdvice加一个简单的令牌桶,限制每个学生每秒只能发一次选课请求。防止有人写脚本抢课。代码量不大,但能体现你对系统稳定性的考虑。

第三个是加操作日志。每次选课、退课都往operation_log表里插一条记录,记录操作人、操作时间、操作类型。答辩时老师问「怎么追溯谁退了课」,你直接查日志表就行。

验证方法上,我习惯用 Postman 或 curl 做接口级测试,重点测三个场景:正常选课、重复选课、并发选课。并发测试可以用 JMeter 开 100 个线程同时请求选课接口,看最终selected_count是否等于capacity。如果超了,说明锁没生效,回去检查FOR UPDATE是不是在事务里执行的。

# 用 curl 快速验证选课接口 curl -X POST http://localhost:8080/api/selection/select \ -H "Content-Type: application/json" \ -d '{"studentId":"2021001","courseId":"C001"}'

最后说一个我自己的习惯:每次改完代码,先把数据库备份一份,再跑一遍完整流程。课设项目最容易出的问题不是代码写错,而是数据被改乱了导致后面演示时对不上。备份一条命令的事,但能省掉很多后悔药。希望帮到你。

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

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

插件加载失败?从entries did not activate到IAR与MusicFree排查实战

用了这么多年的各种软件和开发环境&#xff0c;我越来越觉得plugins这个词背后藏着的东西&#xff0c;比大多数教程里写的都要实在。它不是一个“高级功能开关”&#xff0c;而是整个软件生态能够活起来的关键机制。最近我连续碰到了两条有点吓人的报错&#xff0c;一条是faile…

作者头像 李华
网站建设 2026/10/4 5:08:13

OpenShell经典开始菜单替代工具:从安装到深度配置完全指南

如果你在Windows 10或者Windows 11上总觉得开始菜单用着别扭&#xff0c;尤其从Win7时代一路折腾过来的老用户&#xff0c;应该早就听过OpenShell这个名字。OpenShell的全称其实是Open-Shell&#xff0c;老玩家更习惯叫它Classic Shell的续作——它解决的问题很直接&#xff1a…

作者头像 李华
网站建设 2026/10/4 5:06:34

8款降AI工具实测:从检测原理到压个位数的完整方案

最近一个月至少二十几个人问我同一个问题&#xff1a;AI生成的内容&#xff0c;检测率怎么才能压到个位数&#xff1f;尤其笔灵AI这一波同款工具扎堆出现&#xff0c;各个都号称能把AI率降到80%以下&#xff0c;我干脆把手头能集齐的8款降AI工具全部实测了一遍。这篇就把实测数…

作者头像 李华
网站建设 2026/10/4 5:05:27

跨境电商转大模型:能算的别让模型猜

版权与内容来源声明 本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容&#xff0c;均在附表 A 中标注来源&#xff1b;引用官方原文保持原样&#xff0c;不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准&#xff0c;标注「待验证」的部…

作者头像 李华
网站建设 2026/10/4 5:03:24

甲状腺结节超声AI识别:从数据到部署的深度学习实践

简介&#xff1a;《基于深度学习的甲状腺结节自动识别方法在超声图像中的应用》&#xff08;PDF格式&#xff09;是一份医学影像与人工智能交叉领域的学术文献&#xff0c;面向超声医师、医学影像研究者及人工智能医疗算法工程师&#xff0c;提供了完整的甲状腺结节自动识别研究…

作者头像 李华
网站建设 2026/10/4 5:03:22

Matlab批量修改SWMM模型INP文件参数的完整指南

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

作者头像 李华