选课系统实战(三):Spring Boot课程管理模块的全链路调试与性能优化深度实践
选课系统实战(三):Spring Boot课程管理模块的全链路调试与性能优化深度实践
摘要:本文以在线选课系统的课程管理模块为案例,深入探讨从开发、调试到性能优化的完整流程。涵盖Spring Boot后端接口的单元测试、集成测试、压力测试,Vue前端组件的状态管理、响应式优化,前后端联调的常见问题排查,以及数据库查询优化、缓存策略、异步处理等高级性能优化技术。通过真实的代码示例、详细的调试过程和量化的性能对比,帮助读者掌握现代Web应用全链路开发与优化的核心技能。
目录
- 引言:课程管理模块的核心地位
- 项目背景与技术架构回顾
- 后端开发:RESTful API设计与实现
- 前端开发:Vue组件化架构设计
- 单元测试:保障代码质量的第一道防线
- 集成测试:验证模块协作的正确性
- 前后端联调:跨越边界的调试艺术
- 性能瓶颈分析:从监控到定位
- 数据库层优化:索引、查询与连接池
- 应用层优化:缓存、异步与并发
- 前端性能优化:加载速度与渲染效率
- 压力测试与容量规划
- 生产环境部署与监控告警
- 常见问题案例库与解决方案
- 总结与最佳实践清单
引言:课程管理模块的核心地位
1.1 课程管理在选课系统中的角色
在现代高校教务管理系统中,课程管理模块是整个系统的基石和核心。它不仅是选课功能的数据来源,也是教学计划制定、教师排课、教室分配、成绩管理等众多子系统的基础。可以说,课程管理模块的质量直接决定了整个选课系统的稳定性、性能和用户体验。
课程管理模块的核心职责包括:
第一,课程信息的维护。包括课程的创建、修改、删除、查询等基本操作。每门课程包含丰富的属性信息,如课程ID、课程名称、授课教师、上课地点、学分、课程类型(必修/选修)、课程描述、先修课程等。这些信息需要准确、完整地存储和管理。
第二,课程状态的流转控制。课程从创建到最终开课,经历多个状态阶段:草稿、待审核、已审核、已发布、进行中、已结束等。每个状态有不同的操作权限和业务规则,需要精确控制。
第三,课程容量的管理。每门课程都有最大选课人数限制,系统需要实时统计当前选课人数,并在达到上限时阻止新的选课操作。同时,还需要支持动态调整容量,以应对特殊情况。
第四,课程时间的安排。课程需要安排具体的上课时间(如周一第1-2节、周三第3-4节等),系统需要检测时间冲突,避免同一教师或同一教室在同一时间段被安排多门课程。
第五,课程关系的建立。某些课程之间存在依赖关系(如必须先修"高等数学"才能选修"线性代数"),系统需要维护这些先修课程关系,并在选课时进行校验。
第六,课程数据的统计分析。系统需要提供丰富的统计报表,如各学院的课程数量分布、热门课程排行、教师开课情况、教室利用率等,为教学管理决策提供数据支持。
项目背景与技术架构回顾
2.1 项目概述
本项目是一个基于Spring Boot和Vue.js的在线选课系统,旨在为高校提供便捷的课程管理和选课服务。系统采用前后端分离架构,后端使用Spring Boot + MyBatis-Plus + MySQL技术栈,前端使用Vue 3 + Element Plus + Axios技术栈。
核心功能模块:
- 用户管理:注册、登录、权限控制
- 课程管理:课程的增删改查、状态管理、容量控制
- 选课管理:学生选课、退课、查询已选课程
- 教师管理:教师信息维护、开课申请
- 统计分析:课程统计、选课统计、报表导出
本文聚焦模块:课程管理模块
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);
}
}
关键点解析:
-
@RestController:组合注解,等价于@Controller + @ResponseBody,所有方法返回JSON。
-
@RequestMapping(“/api/courses”):统一前缀,所有课程相关接口都以/api/courses开头。
-
@Validated:启用参数校验,配合@Valid注解使用。
-
@GetMapping、@PostMapping等:明确指定HTTP方法,符合RESTful规范。
-
@PathVariable:从URL路径中提取参数,如{id}。
-
@RequestParam:从查询参数中提取参数,如?pageNum=1。
-
@RequestBody:从请求体中提取JSON数据,自动反序列化为DTO对象。
-
@Valid:触发参数校验,如果DTO中有@NotNull、@NotBlank等注解,会自动验证。
-
HttpSession:从Session中获取当前登录用户,用于权限控制和审计。
-
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("已删除的课程无法修改状态");
}
// 简化版:允许任意状态之间的流转(实际项目中应根据业务规则严格限制)
}
}
关键点解析:
-
@Cacheable:Spring Cache注解,将方法返回值缓存到Redis中。key为参数的toString(),value为缓存名称。下次调用相同参数时,直接从缓存返回,不执行方法体。
-
@CacheEvict:清除缓存。当创建、更新、删除课程时,清除相关缓存,保证数据一致性。allEntries = true表示清除该缓存名称下的所有条目。
-
@Transactional:声明式事务管理,确保多个数据库操作的原子性。
-
QueryWrapper:MyBatis-Plus的条件构造器,动态构建SQL查询条件。
-
Page分页:MyBatis-Plus的分页插件,自动生成分页SQL。
-
BeanUtils.copyProperties:Spring提供的工具类,自动复制同名同类型的字段,减少手动赋值代码。
-
业务校验:在执行业务逻辑前,进行充分的参数验证和业务规则检查,如课程编号唯一性、教师存在性、教室容量等。
-
自定义异常:抛出自定义的BusinessException,由全局异常处理器统一处理,返回友好的错误信息。
-
软删除:删除课程时不真正删除记录,而是将status改为-1,便于数据恢复和审计。
-
状态流转验证: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条最佳实践清单,涵盖编码规范、设计原则、性能优化、安全防护等方面。
结语:
课程管理模块的开发与优化是一个系统工程,涉及前端、后端、数据库、缓存、监控等多个技术领域。通过本文的详细讲解,相信读者已经掌握了从需求分析到生产部署的完整流程,以及各个环节的最佳实践。
记住,优秀的软件不是一蹴而就的,而是通过不断的迭代、优化、重构逐步完善的。希望本文能够成为您技术成长路上的有力助手,助您在开发的道路上越走越远!
更多推荐
所有评论(0)