作为一名经历过毕业设计"洗礼"的过来人,我深知选课系统这类课题的技术难点和实现痛点。这次分享的基于SpringBoot的大学生选课系统,是我指导过多个学弟学妹成功实现的成熟方案。不同于市面上简单的CRUD示例,这个系统完整涵盖了高校选课的真实业务场景,包括最让人头疼的选课冲突检测、高并发处理等核心问题。
系统采用现在企业主流的SpringBoot+Vue前后端分离架构,后端使用Spring Security实现RBAC权限控制,前端采用Vue+ElementUI构建响应式界面。数据库设计上特别针对选课业务的特殊性做了优化,比如采用冗余字段减少多表关联查询,这对提升系统性能非常关键。
SpringBoot 2.7.x作为基础框架是经过深思熟虑的选择。相比传统的SSM框架,它的自动配置特性让开发效率提升至少30%。我特别推荐使用SpringBoot的这几个特性:
数据库访问层采用Spring Data JPA + QueryDSL组合。JPA简化了基础CRUD操作,而QueryDSL则完美解决了复杂查询的可读性问题。比如选课冲突检测的SQL,用QueryDSL可以写成这样:
java复制BooleanExpression conflictCondition = qCourseSchedule.semester.eq(semester)
.and(qCourseSchedule.weekDay.eq(weekDay))
.and(qCourseSchedule.startTime.loe(newEndTime))
.and(qCourseSchedule.endTime.goe(newStartTime));
Vue 3 + Element Plus的组合提供了极佳的开发体验。特别值得一提的是:
前端工程化方面,我强烈建议配置好这些:
这是系统的核心难点,我们采用时间区间重叠算法来实现。关键点在于:
具体实现逻辑:
java复制public boolean checkScheduleConflict(Long studentId, Long courseId) {
// 获取学生已选课程时间
List<CourseSchedule> selectedSchedules = scheduleRepository
.findByStudentIdAndSemester(studentId, currentSemester);
// 获取待选课程时间
CourseSchedule newSchedule = scheduleRepository
.findByCourseId(courseId)
.orElseThrow(() -> new BusinessException("课程安排不存在"));
// 检查时间冲突
return selectedSchedules.stream()
.anyMatch(selected -> isTimeOverlap(selected, newSchedule));
}
private boolean isTimeOverlap(CourseSchedule s1, CourseSchedule s2) {
return s1.getWeekDay() == s2.getWeekDay()
&& s1.getStartTime() < s2.getEndTime()
&& s1.getEndTime() > s2.getStartTime();
}
选课系统最怕的就是"秒杀"场景。我们采用多级缓存的策略:
关键代码示例:
java复制public boolean selectCourse(Long studentId, Long courseId) {
// 获取分布式锁
String lockKey = "course:lock:" + courseId;
try {
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后再试");
}
// 检查课程余量
Integer remaining = (Integer) redisTemplate.opsForValue()
.get("course:remaining:" + courseId);
if (remaining == null || remaining <= 0) {
throw new BusinessException("课程已满");
}
// 扣减库存
redisTemplate.opsForValue()
.decrement("course:remaining:" + courseId);
// 数据库操作
return courseService.doSelectCourse(studentId, courseId);
} finally {
redisTemplate.delete(lockKey);
}
}
课程表的设计特别考虑了查询效率:
sql复制CREATE TABLE `course` (
`id` bigint NOT NULL AUTO_INCREMENT,
`course_code` varchar(32) NOT NULL COMMENT '课程编号',
`name` varchar(100) NOT NULL COMMENT '课程名称',
`credit` decimal(3,1) NOT NULL COMMENT '学分',
`max_students` int NOT NULL COMMENT '最大选课人数',
`remaining` int NOT NULL COMMENT '剩余名额',
`teacher_id` bigint NOT NULL COMMENT '授课教师',
`semester` varchar(20) NOT NULL COMMENT '学期',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_course_code_semester` (`course_code`,`semester`),
KEY `idx_teacher_semester` (`teacher_id`,`semester`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
采用RBAC模型实现精细化的权限管理:
安全配置示例:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/teacher/**").hasRole("TEACHER")
.antMatchers("/api/student/**").hasRole("STUDENT")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
使用JMeter模拟高并发选课场景,重点关注:
测试结果示例:
| 并发用户数 | 平均响应时间(ms) | 错误率 | 吞吐量(TPS) |
|---|---|---|---|
| 100 | 235 | 0% | 420 |
| 500 | 398 | 0% | 1250 |
| 1000 | 812 | 0.2% | 1230 |
推荐使用Docker Compose进行容器化部署:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
volumes:
- ./mysql/data:/var/lib/mysql
redis:
image: redis:6.2
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
一个特别容易踩的坑是选课冲突检测的时间比较。最初我们直接用LocalTime比较,结果发现跨天课程会有问题。后来改为将时间转换为分钟数再比较,才彻底解决了这个问题。