资讯中心

Spring Boot选课系统:数据库设计、权限控制与并发事务实战

📅 2026/9/14 12:32:54
Spring Boot选课系统:数据库设计、权限控制与并发事务实战
简介这套Java毕业设计资料围绕基于Spring Boot开发的学生网上选课系统面向应届毕业生及需要快速搭建教务管理类项目的开发者适合作为毕设参考、二次开发起点或论文写作支撑。压缩包共417个文件、约21.09MB包含Java后端源码、Vue前端工程、SQL数据库脚本和doc格式毕业论文同时提供1-install.bat、2-run.bat、3-build.bat等一键构建运行脚本以及svg、png、css等静态资源目录划分清晰便于直接导入开发环境启动调试。目前已有167人学习下载。系统实现覆盖学生选课、课程管理、教师管理、成绩管理等核心模块并涉及数据加密、用户身份验证等安全设计能帮助读者理解Spring Boot与Vue整合的完整链路。读者既能获取一套可运行的前后端分离项目也能参考配套论文完成毕业设计撰写或在此基础上扩展更多教务管理功能。1. 基于Spring Boot的网上选课系统毕设答辩时最容易被追问的就是这三处学生选课系统是Java毕业设计里出现频率极高的一个题目Spring Boot MyBatis MySQL几乎成了默认组合。但正因为做的人太多答辩老师一眼就能看出你是自己写的还是从某个毕业设计源码包改的。常见被追问的点就三个并发选课怎么防超选、权限模型怎么设计的、事务和唯一约束到底起了什么作用。这三个问题正好覆盖了从数据库设计到Spring Boot核心机制的完整链路。这篇文章不会替你把源码包里的每一个文件过一遍而是按一个接手这类项目的一线工程师的思路把表结构、权限控制、选课事务这三个关键环节拆开讲透。无论你是准备用这个标题做毕业设计还是接手一个现成的Spring Boot选课项目做二次开发照着这几章的步骤和代码能直接跑通并应对大多数追问。2. 选课系统的数据库设计先定表再写接口联合唯一约束是第一条防线网上选的毕设源码包最大的问题不是代码跑不起来而是表结构经不起推敲。很多打包卖的项目把表建得很随意学生、教师、课程、选课记录四张表字段命名混乱没有唯一约束导致并发场景下数据直接脏掉。所以在看任何一个现成Spring Boot项目时我第一件事不读代码而是先打开 SQL 脚本文件看表。2.1 最少六张表从用户表拆到选课记录一个经过实践检验的选课系统表结构通常包含六张核心表加上可选的日志表。不要被网上那些动不动十几张表的所谓“完整版”吓到毕业设计这个体量六张表足够多了反而是负担。表名用途关键字段sys_user统一登录账号id, username, password, rolestudent学生扩展信息id, user_id, student_no, class_nameteacher教师扩展信息id, user_id, teacher_no, departmentcourse课程主表id, course_name, teacher_id, credit, capacity, selected_countcourse_selection选课记录id, course_id, student_id, select_timecourse_schedule上课时间地点id, course_id, weekday, period, classroom这里的核心设计思路是sys_user独立出来放通用账号信息所有角色共用一套登录逻辑。Spring Boot集成Spring Security时UserDetailsService只需要查这一张表而不需要分别查学生表和教师表角色用role字段区分即可。建表SQL里有一个点容易被忽视就是course_selection必须建联合唯一约束这也是选课系统数据安全的第一道防线。CREATE TABLE course_selection ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, student_id BIGINT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_course_student (course_id, student_id), KEY idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段SQL里UNIQUE KEY uk_course_student (course_id, student_id)是核心。它的作用是即使你的业务代码里忘了查重数据库层面也不会让同一个学生重复选择同一门课程。在面试和答辩时能主动说出这层设计比背几条Spring Boot注解有用得多。course表里的capacity和selected_count是一个经典的容量控制组合。selected_count的更新必须放在事务里并且要和选课记录的插入保持一致性这一点放到第4章单独展开。2.2 MyBatis-Plus实体映射逻辑删除和自动填充的坑大多数毕设源码包会用MyBatis-Plus而不是原生MyBatis原因很简单单表CRUD完全不需要手写XMLBaseMapper提供现成的insert、updateById、selectPage等方法。看源码包时先看实体类上的注解是否正确。Data TableName(course_selection) public class CourseSelection { TableId(type IdType.AUTO) private Long id; TableField(course_id) private Long courseId; TableField(student_id) private Long studentId; TableField(value select_time, fill FieldFill.INSERT) private LocalDateTime selectTime; }这里有两个点容易出问题。第一个是驼峰命名和下划线命名映射TableField(course_id)可以显式指定列名比全局开启map-underscore-to-camel-case更可控。第二个是FieldFill.INSERT自动填充配合MyBatis-Plus的MetaObjectHandler接口可以在insert时自动写入select_time不用在Service层手动set。在对源码包里的代码做替换或修改时建议保留这两条设计显式表的字段映射、自动填充的审计字段。它能让代码量减少不少答辩时讲“自动填充减少重复代码”也是一个容易说明白的亮点。3. Spring Security集成三种角色的认证与一套接口拦截规则很多毕设源码包里的权限控制做法就是一个拦截器判断session里有没有登录用户再判断role字符串。这种写法在小型系统里能跑但一旦涉及接口级别的细分控制代码就变得难以维护。Spring Boot项目里做权限应当基于Spring Security构建而不是自己写拦截器。3.1 基于内存还是数据库的UserDetailsService拿到一个Spring Boot项目先看它的SecurityConfig里是InMemoryUserDetailsManager还是数据库实现。正常的选课系统用户数据一定在数据库里你需要自定义一个UserDetailsService。Service public class UserDetailsServiceImpl implements UserDetailsService { private final SysUserMapper sysUserMapper; private final StudentMapper studentMapper; public UserDetailsServiceImpl(SysUserMapper sysUserMapper, StudentMapper studentMapper) { this.sysUserMapper sysUserMapper; this.studentMapper studentMapper; } Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser sysUserMapper.findByUsername(username); if (sysUser null) { throw new UsernameNotFoundException(用户不存在); } ListSimpleGrantedAuthority authorities new ArrayList(); authorities.add(new SimpleGrantedAuthority(ROLE_ sysUser.getRole())); String loginId sysUser.getId().toString(); if (STUDENT.equals(sysUser.getRole())) { Student student studentMapper.findByUserId(sysUser.getId()); if (student ! null) { loginId student.getId().toString(); } } return org.springframework.security.core.userdetails.User .withUsername(loginId) .password(sysUser.getPassword()) .authorities(authorities) .build(); } }这段代码的逻辑是先从sys_user表查出账号信息把role包装成ROLE_STUDENT或ROLE_TEACHER这样的权限标识然后把真正的业务主键比如student表的id塞进username字段。这样后续在Controller里拿Authentication.getPrincipal()时就能直接拿到业务id去查选课记录而不需要每次根据user_id去关联查询。3.2 SecurityFilterChain配置与PreAuthorize精确控制Spring Boot 3.x版本之后系统不再推荐继承WebSecurityConfigurerAdapter而是使用SecurityFilterChain的Bean方式。配置时把公开接口、认证接口和管理员接口分开处理。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/course/list).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .requestMatchers(/api/student/**).hasRole(STUDENT) .requestMatchers(/api/teacher/**).hasRole(TEACHER) .anyRequest().authenticated() ) .sessionManagement(session - session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ); return http.build(); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }接口层面的控制更细的粒度配合PreAuthorize注解使用。比如学生退课接口不能只验证角色还要验证这个选课记录是否属于当前登录的学生。RestController RequestMapping(/api/student) public class CourseSelectionController { DeleteMapping(/course/{selectionId}) PreAuthorize(hasRole(STUDENT)) public ResultVoid cancelSelection(PathVariable Long selectionId, Authentication authentication) { String studentId authentication.getName(); courseSelectionService.cancelSelection(selectionId, Long.valueOf(studentId)); return Result.success(); } }PreAuthorize里的SpEL表达式先做了角色校验然后Service层再根据studentId和selectionId做归属校验。这样即使有用户手工拼接口地址也无法操作别人的选课记录。这种“认证 角色 数据归属”的三层校验就是答辩时值得展开讲的权限设计思路。4. 选课事务唯一约束兜底悲观锁保证容量不超卖选课系统的核心难点不在CRUD而在选课那一刻的并发控制。几十个人同时抢同一门容量有限的课如果代码里没有锁或事务保护selected_count就可能会出现负数或者实际选课人数超过capacity这是典型的数据库并发问题也是投递岗位上经常被问到的业务场景。4.1 事务边界先锁课程行再插入选课记录代码质量控制的关键是让“校验容量-插入选课记录-更新已选人数”这三个动作处在同一个事务里并且保证多个事物不互相干扰。最常见的方案是使用悲观锁在查询课程表时加上FOR UPDATE。Service public class CourseSelectionService { private final CourseMapper courseMapper; private final CourseSelectionMapper courseSelectionMapper; private final CourseScheduleMapper courseScheduleMapper; Transactional(rollbackFor Exception.class) public void doSelect(Long courseId, Long studentId, Long scheduleId) { // 1. 悲观锁锁定课程行防止其他事务同时修改容量 Course course courseMapper.selectByIdForUpdate(courseId); if (course null) { throw new BizException(课程不存在); } if (course.getSelectedCount() course.getCapacity()) { throw new BizException(课程容量已满); } // 2. 检查是否已选过该课程数据库唯一约束兜底 Long count courseSelectionMapper.countByCourseAndStudent(courseId, studentId); if (count 0) { throw new BizException(请勿重复选课); } // 3. 插入选课记录 CourseSelection selection new CourseSelection(); selection.setCourseId(courseId); selection.setStudentId(studentId); selection.setScheduleId(scheduleId); courseSelectionMapper.insert(selection); // 4. 更新已选人数 courseMapper.increaseSelectedCount(courseId); } }对应的CourseMapper里需要手写一个带锁查询方法。这里要引入一个概念通过写上FOR UPDATEMySQL InnoDB引擎会对course_id对应的行加排他锁直到事务提交或回滚才释放其他事务的selectByIdForUpdate会被阻塞等待。Mapper public interface CourseMapper extends BaseMapperCourse { Select(SELECT * FROM course WHERE id #{id} FOR UPDATE) Course selectByIdForUpdate(Param(id) Long id); Update(UPDATE course SET selected_count selected_count 1 WHERE id #{id}) int increaseSelectedCount(Param(id) Long id); }这种做法的优点是正确性有绝对保证缺点是会阻塞并发连接。但在毕业设计这个体量下几十个并发请求完全能扛住是正确性和实现成本最均衡的方案。面试题里常见的“乐观锁 vs 悲观锁”讨论在这个场景里悲观锁比乐观锁更适合原因在于选课操作是很明显的写冲突场景用乐观锁会导致大量事务重试。4.2 数据库隔离级别与死锁的预判在把事务提交时需要注意嵌套事务的问题。如果一个事务里既调用courseSelectionMapper又调用courseMapper且所有公共查询都加FOR UPDATE遇到学生同时提交两门课的选课请求就可能出现互相持锁等待的死锁场景。避免死锁的通用策略是让所有事务按照相同的顺序加锁。比如选课业务里所有请求都先锁课程记录再操作选课表锁顺序一致就不会形成循环等待。MySQL默认的隔离级别是REPEATABLE_READ在这个级别下SELECT ... FOR UPDATE是当前读读取的是最新已提交数据。如果项目配置被改成READ_COMMITTED同样能保证正确性但对锁的理解要求更高。建议毕设项目不要改动默认隔离级别除非你真的清楚每一条SQL的加锁范围。5. 把项目跑起来并满足答辩验证打包命令、admin配置与并发模拟验证拿到网上这份源码包之后换到你自己电脑上跑通是第一步。这里整理三个最常见的坑以及一个能直接用于验证并发效果的技巧。首先是最常见的Spring Boot版本问题。网上源码包如果你检查到版本比较新当前环境JDK版本要匹配。本地环境特别要注意如果你的Spring Boot版本太高比如3.x在JDK 8上跑不起来。用java -version确认本机JDK版本再核对pom.xml里的spring-boot-starter-parent版本把这两者的匹配关系搞清楚。然后是application.yml的数据库连接配置重点检查MySQL驱动和时区设置spring: datasource: url: jdbc:mysql://localhost:3306/course_select?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: trueserverTimezoneAsia/Shanghai如果漏掉执行SQL时会报时区异常。log-impl配置为StdOutImpl后控制台会打印每条SQL和参数排错时非常方便答辩时可以现场展示SQL日志来佐证事务参数。密码字段建议全部使用BCrypt编码不要使用MD5因为Spring Security的默认PasswordEncoder就是BCrypt。打包部署的命令不需要额外工具直接使用Maven打包成可执行的jar包即可。在项目根目录执行mvn clean package -DskipTests之后启动时用java -jar target/course-selection-system.jar。如果服务器内存较小注意要带上-Xmx参数比如java -Xmx256m -jar否则Spring Boot默认堆内存可能把云主机拖垮。最后一个技巧是并发验证。写完选课接口后在论文里最好能附上并发测试数据。本地起一个简单的循环批量请求同一门课的选课接口验证最终selected_count是否等于实际选课成功的人数。for i in $(seq 1 50); do curl -s -X POST http://localhost:8080/api/student/course/1/select \ -H Authorization: Bearer TOKEN_STUDENT_$i \ -o /dev/null -w request $i: %{http_code}\n done观察返回码为200的请求总数再查看数据库里course_selection表的实际记录数这两个数字应该一致且不超过capacity。这一组数据截图放进论文能直接支撑“事务保证了数据一致性”这个论点。到这一步整个Spring Boot选课项目就从单纯“能跑”变成了“能讲清楚每一处设计和验证过程”的完整毕业设计。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案