1. 项目概述
这个基于SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0技术栈的学生选课系统,是一个典型的Java Web全栈项目。我在实际开发中发现,这类系统虽然业务逻辑不复杂,但完整实现前后端分离架构、处理好数据一致性、保证系统性能等方面都有不少值得注意的技术细节。
系统主要包含学生端和管理端两个模块:学生可以进行课程查询、选课退课、课表查看等操作;管理员则负责课程管理、学生管理、教师管理等后台功能。采用前后端分离架构,后端提供RESTful API,前端通过axios调用接口,整体符合现代Web应用开发规范。
2. 技术栈选型解析
2.1 后端技术组合
SpringBoot2作为基础框架,我选择2.7.x稳定版本而非最新的3.x系列,主要考虑因素是:
- 企业生产环境对JDK17的接受度还不够高
- 2.x版本有更丰富的社区支持
- 与MyBatis-Plus等组件的兼容性更成熟
MyBatis-Plus 3.5.x版本提供了强大的单表CRUD操作支持,通过Lambda表达式可以写出更优雅的查询条件:
// 查询计算机学院开设的选修课 LambdaQueryWrapper<Course> query = new LambdaQueryWrapper<>(); query.eq(Course::getDepartment, "计算机学院") .eq(Course::getType, "选修") .orderByAsc(Course::getCourseId); List<Course> courses = courseMapper.selectList(query);2.2 前端技术方案
Vue3的组合式API相比Options API更适合复杂交互场景。在选课页面开发时,我特别利用了这些特性:
- 使用ref和reactive管理组件状态
- 用computed属性实现选课学分限制的实时计算
- 通过watchEffect监听选课列表变化
// 选课逻辑示例 const selectedCourses = ref([]) const totalCredits = computed(() => { return selectedCourses.value.reduce((sum, course) => sum + course.credits, 0) }) watchEffect(() => { if(totalCredits.value > 30) { showCreditLimitWarning() } })2.3 数据库设计要点
MySQL8.0提供了窗口函数、CTE等高级特性,但在选课系统中最关键的还是合理的表结构设计:
CREATE TABLE `student_course` ( `id` bigint NOT NULL AUTO_INCREMENT, `student_id` varchar(20) NOT NULL COMMENT '学号', `course_id` varchar(20) NOT NULL COMMENT '课程编号', `select_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `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 COLLATE=utf8mb4_0900_ai_ci;特别注意:
- 建立联合唯一索引防止重复选课
- 添加课程ID索引优化查询性能
- 使用utf8mb4字符集支持完整Unicode
3. 核心功能实现细节
3.1 选课业务逻辑
选课不是简单的insert操作,需要处理多种约束条件:
- 课程剩余名额检查
- 学生已选学分统计
- 时间冲突检测
- 先修课程要求验证
我采用事务+乐观锁的方案保证数据一致性:
@Transactional public Result selectCourse(Long studentId, Long courseId) { // 1. 检查课程可选性 Course course = courseMapper.selectByIdWithLock(courseId); if(course.getSelected() >= course.getCapacity()) { return Result.fail("课程已满"); } // 2. 检查学生已选学分 Integer selectedCredits = scMapper.sumSelectedCredits(studentId); if(selectedCredits + course.getCredits() > MAX_CREDITS) { return Result.fail("超过学分限制"); } // 3. 创建选课记录 StudentCourse sc = new StudentCourse(); sc.setStudentId(studentId); sc.setCourseId(courseId); scMapper.insert(sc); // 4. 更新课程已选人数 course.setSelected(course.getSelected() + 1); courseMapper.updateById(course); return Result.success(); }3.2 高并发场景处理
选课系统经常面临开学季的高并发压力,我通过以下措施提升系统性能:
- Redis缓存课程余量信息
// 课程余量缓存示例 public Integer getCourseRemain(Long courseId) { String key = "course:remain:" + courseId; Integer remain = redisTemplate.opsForValue().get(key); if(remain == null) { remain = courseMapper.selectRemain(courseId); redisTemplate.opsForValue().set(key, remain, 5, TimeUnit.MINUTES); } return remain; }- 使用分布式锁防止超卖
public boolean selectCourseWithLock(Long studentId, Long courseId) { String lockKey = "lock:course:" + courseId; try { // 尝试获取分布式锁 Boolean locked = redisTemplate.opsForValue().setIfAbsent( lockKey, "1", 10, TimeUnit.SECONDS); if(Boolean.TRUE.equals(locked)) { return doSelectCourse(studentId, courseId); } return false; } finally { redisTemplate.delete(lockKey); } }- 数据库连接池优化配置
spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 18000004. 前后端交互设计
4.1 API接口规范
采用RESTful风格设计API,部分关键接口示例:
| 功能 | 方法 | 路径 | 参数 |
|---|---|---|---|
| 查询可选课程 | GET | /api/courses/available | page,size,department |
| 学生选课 | POST | /api/selection | {studentId,courseId} |
| 退课 | DELETE | /api/selection/{id} | - |
| 查询课表 | GET | /api/schedule/{studentId} | - |
4.2 跨域与安全配置
SpringSecurity配置示例:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/admin/**").hasRole("ADMIN") .antMatchers("/api/**").authenticated() .anyRequest().permitAll() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }Axios请求拦截器示例:
service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = `Bearer ${token}` } return config }, error => { return Promise.reject(error) })5. 部署与监控方案
5.1 多环境配置
使用SpringBoot的profile特性管理不同环境配置:
# application-dev.yml server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_selection_dev username: dev_user password: dev123 # application-prod.yml server: port: 80 spring: datasource: url: jdbc:mysql://prod-db:3306/course_selection username: ${DB_USER} password: ${DB_PASSWORD}5.2 监控与日志
集成Prometheus监控:
@Configuration public class MetricsConfig { @Bean MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() { return registry -> registry.config().commonTags( "application", "course-selection-system" ); } }日志收集方案:
- 使用Logback输出JSON格式日志
- Filebeat收集日志发送到ELK
- 关键业务操作记录审计日志
<!-- logback-spring.xml --> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"/> </appender>6. 常见问题与优化建议
6.1 性能优化经验
- 课程列表分页查询优化:
-- 避免使用count(*)查询总数 SELECT SQL_CALC_FOUND_ROWS * FROM course WHERE status = 1 ORDER BY create_time DESC LIMIT 0, 20; SELECT FOUND_ROWS() AS total;- N+1查询问题解决:
# mybatis-plus配置 mybatis-plus: global-config: db-config: select-strategy: not_empty6.2 事务处理陷阱
- 事务方法自调用失效问题:
// 错误示例 public void processSelection(Long studentId, List<Long> courseIds) { courseIds.forEach(courseId -> { this.selectCourse(studentId, courseId); // 事务不会生效 }); } // 正确做法 @Transactional public void batchSelect(Long studentId, List<Long> courseIds) { for(Long courseId : courseIds) { selectCourseInternal(studentId, courseId); } }- 事务超时设置:
@Transactional(timeout = 30) public void complexOperation() { // 长时间运行的操作 }6.3 前端性能提升
- 课程列表虚拟滚动:
<template> <RecycleScroller class="scroller" :items="courses" :item-size="72" key-field="id" v-slot="{ item }" > <CourseItem :course="item" /> </RecycleScroller> </template>- API请求防抖:
import { debounce } from 'lodash-es'; const searchCourses = debounce(async (keyword) => { const res = await api.searchCourses(keyword); courses.value = res.data; }, 500);7. 项目文档要点
完善的文档应该包含:
- 接口文档(Swagger或YAPI)
@Operation(summary = "选课接口") @PostMapping("/selection") public Result selectCourse( @Parameter(description = "学生ID") @RequestParam Long studentId, @Parameter(description = "课程ID") @RequestParam Long courseId) { // 实现逻辑 }数据库设计文档(包含ER图)
部署手册(Docker Compose示例)
version: '3' services: app: image: course-selection:1.0 ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=prod depends_on: - redis - mysql mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: course_selection压力测试报告(JMeter测试计划)
代码规范检查清单(SonarQube配置)
在实际开发中,我发现这些技术决策和实现细节对系统的稳定性、性能和可维护性都有显著影响。特别是选课业务中的并发控制和事务管理,需要特别注意各种边界条件的处理。