1. 项目概述
这个基于Spring Boot和Vue.js的学生选课系统是一个典型的现代化Web应用,采用了前后端分离的架构设计。作为一名有多年全栈开发经验的工程师,我认为这种架构非常适合高校选课这类并发量高、交互复杂的场景。
系统主要解决了传统选课过程中的几个痛点:选课高峰期服务器崩溃、界面操作不流畅、数据统计困难等。通过前后端分离的设计,前端可以专注于用户体验,后端则保证数据处理的稳定性和安全性。我在实际开发中发现,这种架构特别适合需要快速响应和频繁交互的教育管理系统。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型解析
2.1 后端框架:Spring Boot
Spring Boot是我在后端开发中的首选框架,特别是在需要快速开发和部署的项目中。在这个选课系统中,我们利用了Spring Boot的几个关键特性:
-
自动配置:通过简单的依赖引入就能集成数据库连接、Web MVC等功能,大大减少了XML配置。例如,只需添加
spring-boot-starter-data-jpa依赖,系统就自动配置了JPA相关组件。 -
内嵌服务器:Tomcat作为内嵌服务器,使得应用可以打包成单个可执行的JAR文件。这简化了部署流程,特别适合学校IT部门的技术水平。
-
生产就绪特性:Actuator端点提供了健康检查、指标监控等功能。在实际部署中,我们特别关注了
/health和/metrics端点,用于监控系统在高并发选课期间的状态。
提示:Spring Boot的版本选择很重要。我们使用的是2.7.x版本,这个版本长期支持(LTS),比最新的3.x版本有更丰富的社区资源。
2.2 前端框架:Vue.js
Vue.js的渐进式特性使其成为教育管理系统前端的理想选择:
-
组件化开发:我们将选课界面拆分为多个可复用的组件,如课程卡片组件、筛选器组件等。这种设计提高了代码的可维护性。
-
状态管理:使用Vuex管理全局状态,特别是用户登录状态和选课车数据。在实际开发中,我们发现将选课车数据存储在Vuex中能提供更好的跨组件同步。
-
路由管理:Vue Router实现了单页面应用的导航,配合路由守卫实现了权限控制。例如,学生角色无法访问管理员的路由。
javascript复制// 典型的路由守卫实现
router.beforeEach((to, from, next) => {
const isAuthenticated = store.getters.isAuthenticated
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else {
next()
}
})
2.3 数据库:MySQL
MySQL作为关系型数据库,在这个项目中展现了几个优势:
-
事务支持:选课过程需要保证数据一致性。我们使用
@Transactional注解确保选课操作的原子性。 -
性能优化:针对高并发的选课场景,我们为课程表的剩余名额字段添加了索引,并使用了乐观锁机制:
java复制@Transactional
public boolean selectCourse(Long courseId, Long studentId) {
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new RuntimeException("课程不存在"));
if (course.getRemainingSeats() <= 0) {
return false;
}
int updated = courseRepository.reduceRemainingSeats(courseId, course.getVersion());
if (updated == 0) {
throw new OptimisticLockingFailureException("选课冲突,请重试");
}
// 创建选课记录
SelectionRecord record = new SelectionRecord(studentId, courseId);
selectionRecordRepository.save(record);
return true;
}
- 数据安全:通过定期备份和binlog日志,确保选课数据不会丢失。我们在生产环境配置了每日全量备份+每小时增量备份的策略。
3. 系统架构设计
3.1 整体架构
系统采用经典的三层架构,但针对选课场景做了特殊优化:
-
表现层:Vue.js前端,通过Axios与后端通信。我们特别设计了节流机制,防止用户频繁点击选课按钮造成服务器压力。
-
业务逻辑层:Spring Boot应用,包含控制器、服务和仓库。控制器负责接收HTTP请求,服务类包含核心业务逻辑,仓库处理数据持久化。
-
数据访问层:MySQL数据库,配合Redis缓存热门课程数据,减轻数据库压力。
3.2 关键业务流程
3.2.1 选课流程
- 学生浏览课程列表(带分页和筛选)
- 点击"选课"按钮
- 系统检查冲突(时间冲突、先修课程等)
- 扣减课程剩余名额
- 创建选课记录
- 更新学生课表
注意:在高并发场景下,步骤3-5需要放在一个事务中,并使用乐观锁防止超选。
3.2.2 退课流程
- 学生查看已选课程列表
- 点击"退课"按钮
- 系统检查退课截止时间
- 增加课程剩余名额
- 删除选课记录
- 更新学生课表
3.3 数据库设计
核心表结构设计如下:
courses表:
sql复制CREATE TABLE `courses` (
`id` bigint NOT NULL AUTO_INCREMENT,
`course_name` varchar(100) NOT NULL,
`teacher_id` bigint NOT NULL,
`credit` int NOT NULL,
`schedule` varchar(255) NOT NULL COMMENT '上课时间,如"周一1-2节"',
`location` varchar(50) NOT NULL,
`max_students` int NOT NULL,
`remaining_seats` int NOT NULL,
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
KEY `idx_teacher` (`teacher_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
students表:
sql复制CREATE TABLE `students` (
`id` bigint NOT NULL AUTO_INCREMENT,
`student_no` varchar(20) NOT NULL,
`name` varchar(50) NOT NULL,
`major` varchar(50) NOT NULL,
`grade` varchar(10) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_no` (`student_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
selection_records表:
sql复制CREATE TABLE `selection_records` (
`id` bigint NOT NULL AUTO_INCREMENT,
`student_id` bigint NOT NULL,
`course_id` bigint NOT NULL,
`selection_time` datetime NOT NULL,
`status` tinyint NOT NULL DEFAULT '1' COMMENT '1-有效,0-已退课',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_course` (`student_id`,`course_id`),
KEY `idx_course` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4. 核心功能实现
4.1 用户认证与授权
系统采用JWT进行身份认证,前端将token存储在localStorage中,每次请求通过Authorization头发送。
后端安全配置:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/student/**").hasRole("STUDENT")
.antMatchers("/api/teacher/**").hasRole("TEACHER")
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
4.2 选课接口实现
核心选课服务实现:
java复制@Service
public class CourseSelectionServiceImpl implements CourseSelectionService {
@Autowired
private CourseRepository courseRepository;
@Autowired
private SelectionRecordRepository selectionRecordRepository;
@Autowired
private StudentScheduleService scheduleService;
@Transactional(rollbackFor = Exception.class)
@Override
public SelectionResult selectCourse(Long studentId, Long courseId) {
// 1. 检查课程是否存在且有余量
Course course = courseRepository.findByIdForUpdate(courseId)
.orElseThrow(() -> new CourseNotFoundException("课程不存在"));
if (course.getRemainingSeats() <= 0) {
return SelectionResult.fail("课程已满");
}
// 2. 检查时间冲突
if (scheduleService.hasScheduleConflict(studentId, course.getSchedule())) {
return SelectionResult.fail("时间冲突");
}
// 3. 检查先修课程
if (!course.getPrerequisites().isEmpty()) {
List<Long> completed = selectionRecordRepository
.findCompletedCourses(studentId);
if (!completed.containsAll(course.getPrerequisites())) {
return SelectionResult.fail("未满足先修课程要求");
}
}
// 4. 扣减余量
course.setRemainingSeats(course.getRemainingSeats() - 1);
courseRepository.save(course);
// 5. 创建选课记录
SelectionRecord record = new SelectionRecord();
record.setStudentId(studentId);
record.setCourseId(courseId);
record.setSelectionTime(LocalDateTime.now());
selectionRecordRepository.save(record);
// 6. 更新学生课表缓存
scheduleService.updateStudentSchedule(studentId);
return SelectionResult.success("选课成功");
}
}
4.3 高并发优化
针对选课高峰期的并发问题,我们实现了多级优化:
- 前端限流:选课按钮点击后禁用3秒,防止重复提交
- 接口限流:使用Guava RateLimiter限制接口调用频率
java复制private final RateLimiter rateLimiter = RateLimiter.create(100); // 每秒100个请求
@PostMapping("/select")
public ResponseEntity<?> selectCourse(@RequestParam Long courseId) {
if (!rateLimiter.tryAcquire()) {
throw new TooManyRequestsException("操作太频繁,请稍后再试");
}
// 处理选课逻辑
}
- 数据库优化:使用SELECT FOR UPDATE悲观锁确保数据一致性
- 缓存预热:在选课开始前,将热门课程数据加载到Redis
5. 测试与部署
5.1 测试策略
我们采用了分层测试策略:
- 单元测试:使用JUnit+Mockito测试服务层逻辑
- 集成测试:使用Testcontainers测试数据库交互
- API测试:使用Postman+Newman进行接口测试
- 压力测试:使用JMeter模拟高并发选课场景
示例测试用例:
java复制@Test
public void testSelectCourseSuccess() {
// 准备测试数据
Long studentId = 1L;
Long courseId = 101L;
Course course = new Course();
course.setId(courseId);
course.setRemainingSeats(10);
when(courseRepository.findByIdForUpdate(courseId))
.thenReturn(Optional.of(course));
when(scheduleService.hasScheduleConflict(studentId, any()))
.thenReturn(false);
// 调用测试方法
SelectionResult result = selectionService.selectCourse(studentId, courseId);
// 验证结果
assertTrue(result.isSuccess());
assertEquals(9, course.getRemainingSeats());
verify(selectionRecordRepository).save(any());
}
5.2 部署方案
系统采用Docker容器化部署,使用docker-compose编排服务:
yaml复制version: '3.8'
services:
backend:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://mysql:3306/course_selection
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=course_selection
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
mysql_data:
6. 常见问题与解决方案
6.1 选课冲突处理
问题现象:多个学生同时选同一门课时,可能出现超选情况。
解决方案:
- 使用乐观锁机制,通过version字段检测并发修改
- 数据库层面设置剩余名额检查约束
sql复制ALTER TABLE courses ADD CONSTRAINT chk_remaining_seats CHECK (remaining_seats >= 0);
- 前端显示剩余名额时添加缓冲值(如显示比实际少5个)
6.2 性能瓶颈
问题现象:选课高峰期系统响应变慢。
优化措施:
- 添加Redis缓存层,缓存课程基本信息
- 使用消息队列异步处理非核心逻辑(如发送选课通知邮件)
- 对数据库进行读写分离,查询操作走从库
6.3 数据一致性问题
问题现象:系统显示选课成功,但实际未成功。
解决方案:
- 实现分布式事务或最终一致性方案
- 添加定时任务核对选课记录与课程余量
- 提供选课结果查询接口,允许学生确认选课状态
7. 项目扩展方向
在实际使用中,我们可以进一步扩展系统功能:
- 智能推荐:基于学生专业、已修课程和成绩,推荐适合的课程
- 选课策略:实现"抢课模式"和"志愿模式"两种选课机制
- 移动端适配:开发微信小程序或APP,方便学生随时选课
- 数据分析:统计课程热度、教师评分等数据,辅助教学管理
我在开发过程中发现,系统的可扩展性非常重要。采用清晰的模块划分和接口设计,可以使后续功能添加更加容易。例如,我们将选课核心逻辑抽象为CourseSelectionStrategy接口,不同的选课模式只需实现这个接口即可:
java复制public interface CourseSelectionStrategy {
SelectionResult selectCourse(Long studentId, Long courseId);
SelectionResult dropCourse(Long studentId, Long courseId);
}
@Service
@Primary
public class FirstComeFirstServeStrategy implements CourseSelectionStrategy {
// 先到先得策略实现
}
@Service
public class LotteryBasedStrategy implements CourseSelectionStrategy {
// 抽签策略实现
}
这种设计使得系统可以在配置文件中切换选课策略,甚至可以根据课程类型自动选择不同的策略。
