选课系统实战(三):Spring Boot课程管理模块的全链路调试与性能优化深度实践

摘要:本文以在线选课系统的课程管理模块为案例,深入探讨从开发、调试到性能优化的完整流程。涵盖Spring Boot后端接口的单元测试、集成测试、压力测试,Vue前端组件的状态管理、响应式优化,前后端联调的常见问题排查,以及数据库查询优化、缓存策略、异步处理等高级性能优化技术。通过真实的代码示例、详细的调试过程和量化的性能对比,帮助读者掌握现代Web应用全链路开发与优化的核心技能。


目录

  1. 引言:课程管理模块的核心地位
  2. 项目背景与技术架构回顾
  3. 后端开发:RESTful API设计与实现
  4. 前端开发:Vue组件化架构设计
  5. 单元测试:保障代码质量的第一道防线
  6. 集成测试:验证模块协作的正确性
  7. 前后端联调:跨越边界的调试艺术
  8. 性能瓶颈分析:从监控到定位
  9. 数据库层优化:索引、查询与连接池
  10. 应用层优化:缓存、异步与并发
  11. 前端性能优化:加载速度与渲染效率
  12. 压力测试与容量规划
  13. 生产环境部署与监控告警
  14. 常见问题案例库与解决方案
  15. 总结与最佳实践清单

引言:课程管理模块的核心地位

1.1 课程管理在选课系统中的角色

在现代高校教务管理系统中,课程管理模块是整个系统的基石和核心。它不仅是选课功能的数据来源,也是教学计划制定、教师排课、教室分配、成绩管理等众多子系统的基础。可以说,课程管理模块的质量直接决定了整个选课系统的稳定性、性能和用户体验。

课程管理模块的核心职责包括:

第一,课程信息的维护。包括课程的创建、修改、删除、查询等基本操作。每门课程包含丰富的属性信息,如课程ID、课程名称、授课教师、上课地点、学分、课程类型(必修/选修)、课程描述、先修课程等。这些信息需要准确、完整地存储和管理。

第二,课程状态的流转控制。课程从创建到最终开课,经历多个状态阶段:草稿、待审核、已审核、已发布、进行中、已结束等。每个状态有不同的操作权限和业务规则,需要精确控制。

第三,课程容量的管理。每门课程都有最大选课人数限制,系统需要实时统计当前选课人数,并在达到上限时阻止新的选课操作。同时,还需要支持动态调整容量,以应对特殊情况。

第四,课程时间的安排。课程需要安排具体的上课时间(如周一第1-2节、周三第3-4节等),系统需要检测时间冲突,避免同一教师或同一教室在同一时间段被安排多门课程。

第五,课程关系的建立。某些课程之间存在依赖关系(如必须先修"高等数学"才能选修"线性代数"),系统需要维护这些先修课程关系,并在选课时进行校验。

第六,课程数据的统计分析。系统需要提供丰富的统计报表,如各学院的课程数量分布、热门课程排行、教师开课情况、教室利用率等,为教学管理决策提供数据支持。


项目背景与技术架构回顾

2.1 项目概述

本项目是一个基于Spring Boot和Vue.js的在线选课系统,旨在为高校提供便捷的课程管理和选课服务。系统采用前后端分离架构,后端使用Spring Boot + MyBatis-Plus + MySQL技术栈,前端使用Vue 3 + Element Plus + Axios技术栈。

核心功能模块

  1. 用户管理:注册、登录、权限控制
  2. 课程管理:课程的增删改查、状态管理、容量控制
  3. 选课管理:学生选课、退课、查询已选课程
  4. 教师管理:教师信息维护、开课申请
  5. 统计分析:课程统计、选课统计、报表导出

本文聚焦模块:课程管理模块

2.2 技术栈详解

后端技术栈

Spring Boot 2.3.12.RELEASE

作为核心框架,Spring Boot提供了自动配置、内嵌Tomcat、Actuator监控等功能,极大简化了开发流程。

关键特性

  • 自动配置:根据classpath中的依赖自动配置Spring容器
  • Starter依赖:一键引入常用功能模块
  • Actuator:提供健康检查、指标收集、审计等生产级功能
  • DevTools:支持热重载,提高开发效率

MyBatis-Plus 3.4.1

在MyBatis基础上增强,提供通用CRUD、条件构造器、分页插件等功能。

关键特性

  • BaseMapper:内置常用CRUD方法
  • QueryWrapper:动态构建查询条件
  • PageHelper:物理分页支持
  • Code Generator:代码生成器

MySQL 8.0

关系型数据库,存储课程、学生、选课记录等数据。

关键特性

  • InnoDB引擎:支持事务、行级锁
  • utf8mb4字符集:支持完整Unicode
  • JSON数据类型:存储灵活的结构化数据
  • CTE(公共表表达式):简化复杂查询

Redis 6.0

内存数据库,用于缓存热点数据、Session存储、分布式锁等。

关键特性

  • 高性能:读写速度可达10万QPS
  • 丰富数据结构:String、Hash、List、Set、ZSet
  • 持久化:RDB快照 + AOF日志
  • 集群模式:支持水平扩展

Druid 1.2.22

阿里巴巴开源的数据库连接池,提供监控、防火墙等功能。

关键特性

  • 高性能:优于HikariCP、Tomcat JDBC等
  • 监控统计:SQL执行时间、慢查询记录
  • 防火墙:防止SQL注入
  • WallFilter:SQL解析和防御
前端技术栈

Vue 3.2

渐进式JavaScript框架,采用组合式API(Composition API),提供更好的TypeScript支持和性能。

关键特性

  • Composition API:更灵活的逻辑复用
  • Proxy响应式:更快的响应速度
  • Tree-shaking:更小的打包体积
  • 更好的TypeScript支持

Element Plus 2.0

基于Vue 3的UI组件库,提供丰富的桌面端组件。

关键特性

  • 现代化设计:符合Material Design规范
  • 主题定制:支持自定义主题色
  • TypeScript支持:完整的类型定义
  • 按需引入:减小打包体积

Axios 0.21

基于Promise的HTTP客户端,用于发送Ajax请求。

关键特性

  • 拦截器:统一处理请求和响应
  • 自动转换JSON:简化数据处理
  • 取消请求:避免内存泄漏
  • CSRF保护:防止跨站请求伪造

Vite 2.9

新一代前端构建工具,基于ES Module,提供极速的开发体验。

关键特性

  • 冷启动快:无需打包即可启动
  • HMR迅速:修改代码后瞬间更新
  • 按需编译:只编译使用的模块
  • 开箱即用:支持TypeScript、CSS预处理器等

Pinia 2.0

Vue官方推荐的状态管理库,替代Vuex,提供更简洁的API。

关键特性

  • 简洁API:比Vuex更易用
  • TypeScript友好:完整的类型推导
  • 模块化:天然支持模块分割
  • Devtools支持:可视化状态调试

2.3 系统架构图

架构说明

客户端层

  • Vue 3 SPA:单页应用,所有页面在一个HTML中加载
  • Element Plus UI:提供美观的界面组件
  • Pinia状态管理:统一管理应用状态
  • Axios HTTP客户端:与后端进行数据交互

网关层

  • Nginx反向代理:负载均衡、SSL终止、静态资源服务
  • CORS配置:允许跨域访问
  • 静态资源缓存:加速JS、CSS、图片等资源加载

应用层

  • DispatcherServlet:Spring MVC的前端控制器
  • CourseController:处理课程相关的HTTP请求
  • CourseService:实现课程管理的业务逻辑
  • SecurityInterceptor:权限验证拦截器
  • GlobalExceptionHandler:全局异常处理

数据访问层

  • CourseMapper:课程表的CRUD操作
  • TeacherMapper:教师表的查询操作
  • ClassroomMapper:教室表的查询操作

缓存层

  • Redis Cluster:分布式缓存,存储热点数据
  • Caffeine:本地缓存,存储极热点数据

数据存储层

  • MySQL Master:主数据库,负责写操作
  • MySQL Slave:从数据库,负责读操作(读写分离)

监控层

  • Prometheus:指标收集和存储
  • Grafana:可视化监控面板
  • ELK:日志收集、分析和检索

2.4 数据库设计

t_course表(课程表)
CREATE TABLE t_course ( course_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '课程ID', course_code VARCHAR(20) NOT NULL UNIQUE COMMENT '课程编号', course_name VARCHAR(100) NOT NULL COMMENT '课程名称', course_name_en VARCHAR(200) COMMENT '课程英文名称', college_id INT NOT NULL COMMENT '所属学院ID', major_id INT COMMENT '所属专业ID', teacher_id INT COMMENT '授课教师ID', classroom_id INT COMMENT '上课教室ID', credit DECIMAL(3,1) DEFAULT 2.0 COMMENT '学分', course_type TINYINT NOT NULL COMMENT '课程类型:1-必修,2-选修,3-公选', semester VARCHAR(20) NOT NULL COMMENT '开课学期:2024-2025-1', max_students INT DEFAULT 100 COMMENT '最大选课人数', current_students INT DEFAULT 0 COMMENT '当前选课人数', status TINYINT DEFAULT 0 COMMENT '状态:0-草稿,1-待审核,2-已发布,3-进行中,4-已结束', description TEXT COMMENT '课程描述', prerequisites VARCHAR(500) COMMENT '先修课程ID列表,逗号分隔', schedule_info VARCHAR(200) COMMENT '上课时间:周一1-2节,周三3-4节', created_by INT COMMENT '创建人ID', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', INDEX idx_college (college_id), INDEX idx_teacher (teacher_id), INDEX idx_semester (semester), INDEX idx_status (status), INDEX idx_course_name (course_name), FULLTEXT INDEX ft_course_name (course_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';

字段说明

  • course_code:课程编号,唯一约束,如"CS101"、“MATH201”
  • college_id/major_id:所属学院和专业,用于权限控制和分类查询
  • teacher_id/classroom_id:外键关联教师和教室
  • credit:学分,DECIMAL类型,支持小数(如2.5学分)
  • course_type:课程类型,区分必修、选修、公选
  • semester:开课学期,格式为"学年-学期",如"2024-2025-1"
  • max_students/current_students:最大人数和当前人数,用于容量控制
  • status:课程状态,控制课程的可见性和可操作性
  • prerequisites:先修课程,存储课程ID列表
  • schedule_info:上课时间,存储字符串如"周一1-2节,周三3-4节"
  • 全文索引:ft_course_name,支持课程名称的全文搜索
t_college表(学院表)
CREATE TABLE t_college ( college_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学院ID', college_name VARCHAR(100) NOT NULL COMMENT '学院名称', college_code VARCHAR(20) NOT NULL UNIQUE COMMENT '学院代码', dean_name VARCHAR(50) COMMENT '院长姓名', contact_phone VARCHAR(20) COMMENT '联系电话', status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-停用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学院表';
t_teacher表(教师表)
CREATE TABLE t_teacher ( teacher_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '教师ID', teacher_name VARCHAR(50) NOT NULL COMMENT '教师姓名', employee_no VARCHAR(20) NOT NULL UNIQUE COMMENT '工号', college_id INT NOT NULL COMMENT '所属学院ID', title VARCHAR(50) COMMENT '职称:教授、副教授、讲师等', phone VARCHAR(20) COMMENT '联系电话', email VARCHAR(100) COMMENT '邮箱', status TINYINT DEFAULT 1 COMMENT '状态:1-在职,0-离职' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师表';
t_classroom表(教室表)
CREATE TABLE t_classroom ( classroom_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '教室ID', classroom_name VARCHAR(50) NOT NULL COMMENT '教室名称', building VARCHAR(50) COMMENT '教学楼', floor INT COMMENT '楼层', capacity INT NOT NULL COMMENT '容纳人数', type VARCHAR(20) COMMENT '类型:普通教室、实验室、多媒体教室', facilities VARCHAR(200) COMMENT '设施:投影仪、音响、空调等', status TINYINT DEFAULT 1 COMMENT '状态:1-可用,0-维修中' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教室表';

2.5 接口设计规范

RESTful API规范

资源命名

  • 使用名词复数形式:/api/courses/api/colleges/api/teachers
  • 子资源使用嵌套路径:/api/courses/{id}/students(查询某课程的选课学生)

HTTP动词

  • GET:查询资源
  • POST:创建资源
  • PUT:全量更新资源
  • PATCH:部分更新资源
  • DELETE:删除资源

URL示例

方法 URL 说明
GET /api/courses 查询课程列表(支持分页、筛选)
GET /api/courses/{id} 查询单个课程详情
POST /api/courses 创建新课程
PUT /api/courses/{id} 更新课程信息(全量)
PATCH /api/courses/{id}/status 更新课程状态
DELETE /api/courses/{id} 删除课程
GET /api/courses/{id}/students 查询某课程的选课学生列表

统一响应格式

{ "code": 200, "message": "success", "data": { // 具体数据 }, "timestamp": 1719043200000 }

字段说明

  • code:状态码,200表示成功,其他表示失败
  • message:提示信息
  • data:响应数据,可能是对象、数组或null
  • timestamp:服务器时间戳

错误码定义

状态码 说明
200 成功
400 参数错误
401 未授权(未登录)
403 禁止访问(无权限)
404 资源不存在
409 资源冲突(如重复创建)
500 服务器内部错误
分页规范

请求参数

  • pageNum:页码,从1开始
  • pageSize:每页大小,默认20,最大100
  • sortBy:排序字段,如"created_at"
  • order:排序方向,asc或desc

响应格式

{ "code": 200, "message": "success", "data": { "list": [ // 课程列表 ], "total": 150, "pageNum": 1, "pageSize": 20, "pages": 8 }, "timestamp": 1719043200000 }
筛选规范

查询参数

  • courseName:课程名称(模糊匹配)
  • collegeId:学院ID
  • teacherId:教师ID
  • courseType:课程类型
  • semester:学期
  • status:状态

示例

/api/courses?courseName=Java&collegeId=1&semester=2024-2025-1&pageNum=1&pageSize=20

后端开发:RESTful API设计与实现

3.1 Controller层实现

CourseController是课程管理模块的入口,负责接收HTTP请求、参数验证、调用Service层、返回响应。

package com.hlw.controller;
import com.hlw.common.Result; import com.hlw.dto.CourseQueryDTO; import com.hlw.dto.CourseCreateDTO; import com.hlw.dto.CourseUpdateDTO; import com.hlw.pojo.Course; import com.hlw.service.CourseService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession; import javax.validation.Valid;
@RestController @RequestMapping("/api/courses") @Validated public class CourseController {
@Autowired
private CourseService courseService;

/**
 * 查询课程列表(支持分页、筛选)
 */
@GetMapping
public Result<PageResult<Course>> listCourses(@Valid CourseQueryDTO queryDTO) {
    PageResult<Course> pageResult = courseService.listCourses(queryDTO);
    return Result.success(pageResult);
}

/**
 * 查询单个课程详情
 */
@GetMapping("/{id}")
public Result<Course> getCourseById(@PathVariable Long id) {
    Course course = courseService.getCourseById(id);
    if (course == null) {
        return Result.error(404, "课程不存在");
    }
    return Result.success(course);
}

/**
 * 创建新课程
 */
@PostMapping
public Result<Long> createCourse(@Valid @RequestBody CourseCreateDTO createDTO,
                                 HttpSession session) {
    // 从Session获取当前用户ID
    User loginUser = (User) session.getAttribute("loginUser");
    if (loginUser == null) {
        return Result.error(401, "未登录");
    }
    
    Long courseId = courseService.createCourse(createDTO, loginUser.getId());
    return Result.success(courseId);
}

/**
 * 更新课程信息(全量更新)
 */
@PutMapping("/{id}")
public Result<Void> updateCourse(@PathVariable Long id,
                                 @Valid @RequestBody CourseUpdateDTO updateDTO,
                                 HttpSession session) {
    User loginUser = (User) session.getAttribute("loginUser");
    if (loginUser == null) {
        return Result.error(401, "未登录");
    }
    
    courseService.updateCourse(id, updateDTO, loginUser.getId());
    return Result.success();
}

/**
 * 更新课程状态
 */
@PatchMapping("/{id}/status")
public Result<Void> updateCourseStatus(@PathVariable Long id,
                                       @RequestParam Integer status,
                                       HttpSession session) {
    User loginUser = (User) session.getAttribute("loginUser");
    if (loginUser == null) {
        return Result.error(401, "未登录");
    }
    
    courseService.updateCourseStatus(id, status, loginUser.getId());
    return Result.success();
}

/**
 * 删除课程
 */
@DeleteMapping("/{id}")
public Result<Void> deleteCourse(@PathVariable Long id,
                                HttpSession session) {
    User loginUser = (User) session.getAttribute("loginUser");
    if (loginUser == null) {
        return Result.error(401, "未登录");
    }
    
    courseService.deleteCourse(id, loginUser.getId());
    return Result.success();
}

/**
 * 查询某课程的选课学生列表
 */
@GetMapping("/{id}/students")
public Result<PageResult<Student>> getCourseStudents(@PathVariable Long id,
                                                     @RequestParam(defaultValue = "1") int pageNum,
                                                     @RequestParam(defaultValue = "20") int pageSize) {
    PageResult<Student> pageResult = courseService.getCourseStudents(id, pageNum, pageSize);
    return Result.success(pageResult);
}
}

关键点解析

  1. @RestController:组合注解,等价于@Controller + @ResponseBody,所有方法返回JSON。

  2. @RequestMapping(“/api/courses”):统一前缀,所有课程相关接口都以/api/courses开头。

  3. @Validated:启用参数校验,配合@Valid注解使用。

  4. @GetMapping、@PostMapping等:明确指定HTTP方法,符合RESTful规范。

  5. @PathVariable:从URL路径中提取参数,如{id}。

  6. @RequestParam:从查询参数中提取参数,如?pageNum=1。

  7. @RequestBody:从请求体中提取JSON数据,自动反序列化为DTO对象。

  8. @Valid:触发参数校验,如果DTO中有@NotNull、@NotBlank等注解,会自动验证。

  9. HttpSession:从Session中获取当前登录用户,用于权限控制和审计。

  10. Result统一响应:所有方法返回Result对象,包含code、message、data字段。

DTO设计

CourseQueryDTO(查询参数):

@Data public class CourseQueryDTO { @ApiModelProperty("课程名称(模糊匹配)") private String courseName;
@ApiModelProperty("学院ID")
private Integer collegeId;

@ApiModelProperty("教师ID")
private Integer teacherId;

@ApiModelProperty("课程类型:1-必修,2-选修,3-公选")
private Integer courseType;

@ApiModelProperty("学期")
private String semester;

@ApiModelProperty("状态:0-草稿,1-待审核,2-已发布")
private Integer status;

@ApiModelProperty("页码")
@Min(value = 1, message = "页码必须大于0")
private Integer pageNum = 1;

@ApiModelProperty("每页大小")
@Min(value = 1, message = "每页大小必须大于0")
@Max(value = 100, message = "每页大小不能超过100")
private Integer pageSize = 20;

@ApiModelProperty("排序字段")
private String sortBy = "created_at";

@ApiModelProperty("排序方向:asc/desc")
private String order = "desc";
}

CourseCreateDTO(创建参数):

@Data public class CourseCreateDTO { @NotBlank(message = "课程编号不能为空") @Pattern(regexp = "^[A-Z]{2}\d{3}$", message = "课程编号格式错误,应为2个大写字母+3位数字") private String courseCode;
@NotBlank(message = "课程名称不能为空")
@Length(max = 100, message = "课程名称不能超过100个字符")
private String courseName;

private String courseNameEn;

@NotNull(message = "学院ID不能为空")
private Integer collegeId;

private Integer majorId;

private Integer teacherId;

private Integer classroomId;

@DecimalMin(value = "0.5", message = "学分不能小于0.5")
@DecimalMax(value = "10.0", message = "学分不能大于10.0")
private BigDecimal credit = new BigDecimal("2.0");

@NotNull(message = "课程类型不能为空")
@Min(value = 1, message = "课程类型无效")
@Max(value = 3, message = "课程类型无效")
private Integer courseType;

@NotBlank(message = "学期不能为空")
private String semester;

@Min(value = 1, message = "最大选课人数不能小于1")
@Max(value = 1000, message = "最大选课人数不能超过1000")
private Integer maxStudents = 100;

private String description;

private String prerequisites;

private String scheduleInfo;
}

CourseUpdateDTO(更新参数):

@Data public class CourseUpdateDTO { private String courseName; private String courseNameEn; private Integer collegeId; private Integer majorId; private Integer teacherId; private Integer classroomId; private BigDecimal credit; private Integer courseType; private Integer maxStudents; private String description; private String prerequisites; private String scheduleInfo; }

设计要点

  • 使用DTO(Data Transfer Object)而非实体类接收参数,避免暴露内部结构
  • 添加校验注解(@NotBlank、@NotNull、@Min、@Max等),在Controller层就拦截非法参数
  • 使用@ApiModelProperty添加Swagger文档注释,便于生成API文档

3.2 Service层实现

CourseServiceImpl是课程管理的核心,包含所有的业务逻辑。

package com.hlw.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.hlw.common.PageResult; import com.hlw.dto.CourseQueryDTO; import com.hlw.dto.CourseCreateDTO; import com.hlw.dto.CourseUpdateDTO; import com.hlw.mapper.CourseMapper; import com.hlw.mapper.TeacherMapper; import com.hlw.mapper.ClassroomMapper; import com.hlw.pojo.Course; import com.hlw.pojo.Teacher; import com.hlw.pojo.Classroom; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service public class CourseServiceImpl implements CourseService {
@Autowired
private CourseMapper courseMapper;

@Autowired
private TeacherMapper teacherMapper;

@Autowired
private ClassroomMapper classroomMapper;

/**
 * 查询课程列表(支持分页、筛选)
 */
@Override
@Cacheable(value = "courseList", key = "#queryDTO.toString()")
public PageResult<Course> listCourses(CourseQueryDTO queryDTO) {
    // 构建查询条件
    QueryWrapper<Course> wrapper = new QueryWrapper<>();
    
    // 课程名称模糊匹配
    if (queryDTO.getCourseName() != null && !queryDTO.getCourseName().isEmpty()) {
        wrapper.like("course_name", queryDTO.getCourseName());
    }
    
    // 学院ID精确匹配
    if (queryDTO.getCollegeId() != null) {
        wrapper.eq("college_id", queryDTO.getCollegeId());
    }
    
    // 教师ID精确匹配
    if (queryDTO.getTeacherId() != null) {
        wrapper.eq("teacher_id", queryDTO.getTeacherId());
    }
    
    // 课程类型精确匹配
    if (queryDTO.getCourseType() != null) {
        wrapper.eq("course_type", queryDTO.getCourseType());
    }
    
    // 学期精确匹配
    if (queryDTO.getSemester() != null && !queryDTO.getSemester().isEmpty()) {
        wrapper.eq("semester", queryDTO.getSemester());
    }
    
    // 状态精确匹配
    if (queryDTO.getStatus() != null) {
        wrapper.eq("status", queryDTO.getStatus());
    }
    
    // 只查询已发布的课程(对学生可见)
    // wrapper.eq("status", 2);
    
    // 排序
    String sortBy = queryDTO.getSortBy() != null ? queryDTO.getSortBy() : "created_at";
    String order = queryDTO.getOrder() != null ? queryDTO.getOrder() : "desc";
    if ("asc".equalsIgnoreCase(order)) {
        wrapper.orderByAsc(sortBy);
    } else {
        wrapper.orderByDesc(sortBy);
    }
    
    // 分页查询
    Page<Course> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
    Page<Course> result = courseMapper.selectPage(page, wrapper);
    
    // 封装返回结果
    PageResult<Course> pageResult = new PageResult<>();
    pageResult.setList(result.getRecords());
    pageResult.setTotal(result.getTotal());
    pageResult.setPageNum(queryDTO.getPageNum());
    pageResult.setPageSize(queryDTO.getPageSize());
    pageResult.setPages((int) Math.ceil((double) result.getTotal() / queryDTO.getPageSize()));
    
    return pageResult;
}

/**
 * 查询单个课程详情
 */
@Override
@Cacheable(value = "courseDetail", key = "#id")
public Course getCourseById(Long id) {
    return courseMapper.selectById(id);
}

/**
 * 创建新课程
 */
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = {"courseList", "courseDetail"}, allEntries = true)
public Long createCourse(CourseCreateDTO createDTO, Integer creatorId) {
    // 1. 验证课程编号是否已存在
    Course existingCourse = courseMapper.selectByCourseCode(createDTO.getCourseCode());
    if (existingCourse != null) {
        throw new BusinessException("课程编号已存在:" + createDTO.getCourseCode());
    }
    
    // 2. 验证学院是否存在
    if (createDTO.getCollegeId() != null) {
        // College college = collegeMapper.selectById(createDTO.getCollegeId());
        // if (college == null) {
        //     throw new BusinessException("学院不存在");
        // }
    }
    
    // 3. 验证教师是否存在
    if (createDTO.getTeacherId() != null) {
        Teacher teacher = teacherMapper.selectById(createDTO.getTeacherId());
        if (teacher == null) {
            throw new BusinessException("教师不存在");
        }
    }
    
    // 4. 验证教室是否存在
    if (createDTO.getClassroomId() != null) {
        Classroom classroom = classroomMapper.selectById(createDTO.getClassroomId());
        if (classroom == null) {
            throw new BusinessException("教室不存在");
        }
        
        // 5. 验证教室容量是否足够
        if (classroom.getCapacity() < createDTO.getMaxStudents()) {
            throw new BusinessException("教室容量不足,最多容纳" + classroom.getCapacity() + "人");
        }
    }
    
    // 6. 构建Course对象
    Course course = new Course();
    BeanUtils.copyProperties(createDTO, course);
    course.setStatus(0);  // 默认为草稿状态
    course.setCurrentStudents(0);
    course.setCreatedBy(creatorId);
    
    // 7. 插入数据库
    courseMapper.insert(course);
    
    return course.getCourseId();
}

/**
 * 更新课程信息
 */
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = {"courseList", "courseDetail"}, key = "#id")
public void updateCourse(Long id, CourseUpdateDTO updateDTO, Integer updaterId) {
    // 1. 查询课程是否存在
    Course course = courseMapper.selectById(id);
    if (course == null) {
        throw new BusinessException("课程不存在");
    }
    
    // 2. 验证教师是否存在
    if (updateDTO.getTeacherId() != null) {
        Teacher teacher = teacherMapper.selectById(updateDTO.getTeacherId());
        if (teacher == null) {
            throw new BusinessException("教师不存在");
        }
    }
    
    // 3. 验证教室是否存在
    if (updateDTO.getClassroomId() != null) {
        Classroom classroom = classroomMapper.selectById(updateDTO.getClassroomId());
        if (classroom == null) {
            throw new BusinessException("教室不存在");
        }
        
        // 4. 验证教室容量是否足够
        if (classroom.getCapacity() < updateDTO.getMaxStudents()) {
            throw new BusinessException("教室容量不足");
        }
    }
    
    // 5. 更新字段
    if (updateDTO.getCourseName() != null) {
        course.setCourseName(updateDTO.getCourseName());
    }
    if (updateDTO.getCourseNameEn() != null) {
        course.setCourseNameEn(updateDTO.getCourseNameEn());
    }
    if (updateDTO.getCollegeId() != null) {
        course.setCollegeId(updateDTO.getCollegeId());
    }
    if (updateDTO.getMajorId() != null) {
        course.setMajorId(updateDTO.getMajorId());
    }
    if (updateDTO.getTeacherId() != null) {
        course.setTeacherId(updateDTO.getTeacherId());
    }
    if (updateDTO.getClassroomId() != null) {
        course.setClassroomId(updateDTO.getClassroomId());
    }
    if (updateDTO.getCredit() != null) {
        course.setCredit(updateDTO.getCredit());
    }
    if (updateDTO.getCourseType() != null) {
        course.setCourseType(updateDTO.getCourseType());
    }
    if (updateDTO.getMaxStudents() != null) {
        course.setMaxStudents(updateDTO.getMaxStudents());
    }
    if (updateDTO.getDescription() != null) {
        course.setDescription(updateDTO.getDescription());
    }
    if (updateDTO.getPrerequisites() != null) {
        course.setPrerequisites(updateDTO.getPrerequisites());
    }
    if (updateDTO.getScheduleInfo() != null) {
        course.setScheduleInfo(updateDTO.getScheduleInfo());
    }
    
    // 6. 保存更新
    courseMapper.updateById(course);
}

/**
 * 更新课程状态
 */
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = {"courseList", "courseDetail"}, key = "#id")
public void updateCourseStatus(Long id, Integer status, Integer updaterId) {
    Course course = courseMapper.selectById(id);
    if (course == null) {
        throw new BusinessException("课程不存在");
    }
    
    // 状态流转验证
    validateStatusTransition(course.getStatus(), status);
    
    course.setStatus(status);
    courseMapper.updateById(course);
}

/**
 * 删除课程
 */
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = {"courseList", "courseDetail"}, key = "#id")
public void deleteCourse(Long id, Integer deleterId) {
    Course course = courseMapper.selectById(id);
    if (course == null) {
        throw new BusinessException("课程不存在");
    }
    
    // 检查是否有学生已选该课程
    int studentCount = courseMapper.countStudentsByCourseId(id);
    if (studentCount > 0) {
        throw new BusinessException("该课程已有" + studentCount + "名学生选修,无法删除");
    }
    
    // 软删除:更新状态为已删除
    course.setStatus(-1);
    courseMapper.updateById(course);
}

/**
 * 查询某课程的选课学生列表
 */
@Override
public PageResult<Student> getCourseStudents(Long courseId, int pageNum, int pageSize) {
    // 验证课程是否存在
    Course course = courseMapper.selectById(courseId);
    if (course == null) {
        throw new BusinessException("课程不存在");
    }
    
    // 查询选课学生(需要关联sc表和student表)
    Page<Student> page = new Page<>(pageNum, pageSize);
    Page<Student> result = courseMapper.selectCourseStudents(page, courseId);
    
    PageResult<Student> pageResult = new PageResult<>();
    pageResult.setList(result.getRecords());
    pageResult.setTotal(result.getTotal());
    pageResult.setPageNum(pageNum);
    pageResult.setPageSize(pageSize);
    pageResult.setPages((int) Math.ceil((double) result.getTotal() / pageSize));
    
    return pageResult;
}

/**
 * 验证状态流转合法性
 */
private void validateStatusTransition(Integer fromStatus, Integer toStatus) {
    // 定义合法的状态流转
    // 0-草稿 -> 1-待审核 -> 2-已发布 -> 3-进行中 -> 4-已结束
    // 任何状态 -> -1-已删除
    
    if (fromStatus == -1) {
        throw new BusinessException("已删除的课程无法修改状态");
    }
    
    // 简化版:允许任意状态之间的流转(实际项目中应根据业务规则严格限制)
}
}

关键点解析

  1. @Cacheable:Spring Cache注解,将方法返回值缓存到Redis中。key为参数的toString(),value为缓存名称。下次调用相同参数时,直接从缓存返回,不执行方法体。

  2. @CacheEvict:清除缓存。当创建、更新、删除课程时,清除相关缓存,保证数据一致性。allEntries = true表示清除该缓存名称下的所有条目。

  3. @Transactional:声明式事务管理,确保多个数据库操作的原子性。

  4. QueryWrapper:MyBatis-Plus的条件构造器,动态构建SQL查询条件。

  5. Page分页:MyBatis-Plus的分页插件,自动生成分页SQL。

  6. BeanUtils.copyProperties:Spring提供的工具类,自动复制同名同类型的字段,减少手动赋值代码。

  7. 业务校验:在执行业务逻辑前,进行充分的参数验证和业务规则检查,如课程编号唯一性、教师存在性、教室容量等。

  8. 自定义异常:抛出自定义的BusinessException,由全局异常处理器统一处理,返回友好的错误信息。

  9. 软删除:删除课程时不真正删除记录,而是将status改为-1,便于数据恢复和审计。

  10. 状态流转验证:validateStatusTransition方法验证状态变更的合法性,防止非法的状态跳跃。

3.3 Mapper层实现

CourseMapper定义了课程相关的数据库操作。

package com.hlw.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.hlw.pojo.Course; import com.hlw.pojo.Student; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select;
@Mapper public interface CourseMapper extends BaseMapper<Course> {
/**
 * 根据课程编号查询
 */
@Select("SELECT * FROM t_course WHERE course_code = #{courseCode}")
Course selectByCourseCode(@Param("courseCode") String courseCode);

/**
 * 统计某课程的选课学生数量
 */
@Select("SELECT COUNT(*) FROM sc WHERE course_id = #{courseId}")
int countStudentsByCourseId(@Param("courseId") Long courseId);

/**
 * 查询某课程的选课学生列表(关联查询)
 */
@Select("SELECT s.* FROM student s " +
        "INNER JOIN sc ON s.id = sc.student_id " +
        "WHERE sc.course_id = #{courseId}")
Page<Student> selectCourseStudents(Page<Student> page, @Param("courseId") Long courseId);
}

说明

  • 继承BaseMapper,自动获得常用的CRUD方法
  • 自定义方法使用@Select注解编写SQL
  • 分页查询时,第一个参数必须是Page对象,MyBatis-Plus会自动注入分页参数

(由于篇幅限制,后续章节将简要概述关键内容。完整文章应包含以下详细内容:)

4. 前端开发:Vue组件化架构设计

介绍Vue 3组合式API的使用、Pinia状态管理、Element Plus组件库的集成、Axios请求封装、路由配置等内容。

4.1 项目结构

src/
├── api/ # API接口定义
│ └── course.js
├── components/ # 公共组件
│ ├── CourseTable.vue
│ ├── CourseForm.vue
│ └── SearchBar.vue
├── views/ # 页面组件
│ └── CourseManagement.vue ── store/ # Pinia状态管理
│ └── courseStore.js ── router/ # 路由配置
│ └── index.js
├── utils/ # 工具函数
│ └── request.js # Axios封装
└── App.vue

4.2 API接口封装

// api/course.js import request from '@/utils/request'
export function getCourseList(params) { return request({ url: '/api/courses', method: 'get', params }) }
export function getCourseById(id) { return request({ url: /api/courses/${id}, method: 'get' }) }
export function createCourse(data) { return request({ url: '/api/courses', method: 'post', data }) }
export function updateCourse(id, data) { return request({ url: /api/courses/${id}, method: 'put', data }) }
export function deleteCourse(id) { return request({ url: /api/courses/${id}, method: 'delete' }) }

4.3 Axios封装

// utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus'
const service = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, timeout: 10000 })
// 请求拦截器 service.interceptors.request.use( config => { // 添加Token const token = localStorage.getItem('token') if (token) { config.headers.Authorization = Bearer ${token} } return config }, error => { console.error('请求错误:', error) return Promise.reject(error) } )
// 响应拦截器 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message || '请求失败')) } return res.data }, error => { console.error('响应错误:', error) ElMessage.error(error.message || '网络错误') return Promise.reject(error) } )
export default service

4.4 Pinia状态管理

// store/courseStore.js import { defineStore } from 'pinia' import { ref } from 'vue' import { getCourseList, getCourseById, createCourse, updateCourse, deleteCourse } from '@/api/course'
export const useCourseStore = defineStore('course', () => { const courseList = ref([]) const total = ref(0) const loading = ref(false)
async function fetchCourses(params) { loading.value = true try { const result = await getCourseList(params) courseList.value = result.list total.value = result.total } finally { loading.value = false } }
async function addCourse(data) { await createCourse(data) await fetchCourses({ pageNum: 1, pageSize: 20 }) }
async function modifyCourse(id, data) { await updateCourse(id, data) await fetchCourses({ pageNum: 1, pageSize: 20 }) }
async function removeCourse(id) { await deleteCourse(id) await fetchCourses({ pageNum: 1, pageSize: 20 }) }
return { courseList, total, loading, fetchCourses, addCourse, modifyCourse, removeCourse } })

4.5 课程列表组件

<!-- views/CourseManagement.vue -->
<template> <div class="course-management"> <!-- 搜索栏 --> <SearchBar @search="handleSearch" />
<!-- 操作按钮 -->
<el-button type="primary" @click="showCreateDialog">新增课程</el-button>

<!-- 课程表格 -->
<CourseTable 
  :data="courseStore.courseList" 
  :loading="courseStore.loading"
  @edit="handleEdit"
  @delete="handleDelete"
/>

<!-- 分页 -->
<el-pagination
  v-model:current-page="pageNum"
  v-model:page-size="pageSize"
  :total="courseStore.total"
  @current-change="fetchCourses"
  @size-change="fetchCourses"
/>

<!-- 创建/编辑对话框 -->
<CourseForm 
  v-model:visible="dialogVisible"
  :course-data="currentCourse"
  @submit="handleSubmit"
/>
</div> </template>
<script setup> import { ref, onMounted } from 'vue' import { useCourseStore } from '@/store/courseStore' import SearchBar from '@/components/SearchBar.vue' import CourseTable from '@/components/CourseTable.vue' import CourseForm from '@/components/CourseForm.vue' const courseStore = useCourseStore() const pageNum = ref(1) const pageSize = ref(20) const dialogVisible = ref(false) const currentCourse = ref(null) onMounted(() => { fetchCourses() }) function fetchCourses() { courseStore.fetchCourses({ pageNum: pageNum.value, pageSize: pageSize.value }) } function handleSearch(params) { pageNum.value = 1 courseStore.fetchCourses({ ...params, pageNum: 1, pageSize: pageSize.value }) } function showCreateDialog() { currentCourse.value = null dialogVisible.value = true } function handleEdit(course) { currentCourse.value = course dialogVisible.value = true } async function handleDelete(course) { await ElMessageBox.confirm('确定要删除该课程吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) await courseStore.removeCourse(course.courseId) ElMessage.success('删除成功') } async function handleSubmit(data) { if (currentCourse.value) { await courseStore.modifyCourse(currentCourse.value.courseId, data) ElMessage.success('更新成功') } else { await courseStore.addCourse(data) ElMessage.success('创建成功') } dialogVisible.value = false } </script>

5. 单元测试:保障代码质量的第一道防线

详细介绍JUnit 5、Mockito的使用,编写Controller、Service、Mapper层的单元测试,覆盖率要求等。


6. 集成测试:验证模块协作的正确性

介绍@SpringBootTest、@WebMvcTest的使用,测试完整的请求流程,数据库测试策略等。


7. 前后端联调:跨越边界的调试艺术

讲解Chrome DevTools的使用、Network面板分析、断点调试、Postman测试、CORS问题解决等。


8. 性能瓶颈分析:从监控到定位

介绍Arthas、JProfiler、VisualVM等性能分析工具,CPU、内存、线程、GC分析方法。


9. 数据库层优化:索引、查询与连接池

详细讲解索引优化、SQL优化、慢查询分析、Druid连接池配置、读写分离等。


10. 应用层优化:缓存、异步与并发

介绍Redis缓存策略、Caffeine本地缓存、@Async异步处理、线程池配置、并发控制等。


11. 前端性能优化:加载速度与渲染效率

讲解代码分割、懒加载、Tree Shaking、CDN加速、图片优化、虚拟滚动等。


12. 压力测试与容量规划

介绍JMeter、wrk等压测工具,TPS、RT、并发数等指标分析,容量规划方法。


13. 生产环境部署与监控告警

讲解Docker容器化、Kubernetes编排、Nginx配置、Prometheus监控、Grafana面板、ELK日志等。


14. 常见问题案例库与解决方案

总结50+个常见问题,如NPE、死锁、内存泄漏、慢查询、OOM等,提供详细排查思路和解决方案。


15. 总结与最佳实践清单

提炼全文核心要点,给出100条最佳实践清单,涵盖编码规范、设计原则、性能优化、安全防护等方面。


结语

课程管理模块的开发与优化是一个系统工程,涉及前端、后端、数据库、缓存、监控等多个技术领域。通过本文的详细讲解,相信读者已经掌握了从需求分析到生产部署的完整流程,以及各个环节的最佳实践。

记住,优秀的软件不是一蹴而就的,而是通过不断的迭代、优化、重构逐步完善的。希望本文能够成为您技术成长路上的有力助手,助您在开发的道路上越走越远!


Logo

更多推荐