course
模块架构
核心功能分析
1 课程分类
• 支持三级分类管理,限制分类层级不超过三级。
• 提供工具类(CategoryDataWrapper)处理分类的树形结构。
Category 表(或对应 PO 类)设计如下:
public class Category {private Long id; // 分类IDprivate String name; // 分类名称private Long parentId; // 父分类IDprivate Integer level; // 分类层级private Integer priority; // 排序优先级private Boolean status; // 状态(启用/禁用) }
• 层级计算:
• 如果无父分类(parentId = null),则为一级分类,level = 1。
• 有父分类时,子分类的层级为 父分类.level + 1。
@Override public void addCategory(CategoryAddDTO categoryAddDTO) {if (categoryAddDTO.getParentId() != null) {Category parentCategory = categoryMapper.selectById(categoryAddDTO.getParentId());if (parentCategory == null) {throw new BusinessException("父分类不存在");}if (parentCategory.getLevel() >= 3) {throw new BusinessException("三级分类下不能再创建子分类");}} Category category = new Category();category.setName(categoryAddDTO.getName());category.setParentId(categoryAddDTO.getParentId());category.setLevel(parentCategory == null ? 1 : parentCategory.getLevel() + 1);category.setStatus(true); // 默认启用categoryMapper.insert(category); }
• 查询分类树
查询分类时,返回完整的分类树结构:
@Override public List<CategoryVO> getCategoryTree() {List<Category> categories = categoryMapper.selectAll(); // 使用工具类将分类列表转换为树形结构List<CategoryVO> categoryTree = TreeDataUtils.buildTree(categories, new CategoryDataWrapper()); return categoryTree; }
2 目录管理
• 目录包括章和节,以及可能的练习题目(类型定义在CataType)。
• 草稿模式: 支持目录草稿管理,使用course_catalogue_draft表临时存储未发布的目录结构。
目录表:course_catalogue
public class CourseCatalogue {private Long id; // 目录IDprivate String name; // 名称private Long courseId; // 所属课程IDprivate Integer type; // 类型(章、节、练习)private Long parentCatalogueId;// 父目录IDprivate Long mediaId; // 媒资ID(视频或其他媒体)private Long videoId; // 视频IDprivate String videoName; // 视频名称private LocalDateTime livingStartTime; // 直播开始时间private LocalDateTime livingEndTime; // 直播结束时间private Boolean playBack; // 是否支持回放private Integer cIndex; // 排序索引private LocalDateTime createTime; // 创建时间private LocalDateTime updateTime; // 更新时间 }
CataType 定义了目录节点的类型:
public class CataType {public static final int CHAPTER = 1; // 章public static final int SECTION = 2; // 节public static final int PRACTICE = 3; // 练习或测试 }
草稿目录编辑完成后,将其发布到正式表中:
@Override public void publishDraft(Long courseId) {// 清空正式表中该课程的目录courseCatalogueMapper.deleteByCourseId(courseId); // 将草稿表中的目录迁移到正式表courseCatalogueDraftMapper.insertFromCourseCatalogue(courseId); // 删除草稿表中的数据courseCatalogueDraftMapper.deleteByCourseId(courseId); }
3 课程上下架
• 状态流转:
• 待上架 (1) → 已上架 (2) → 下架 (3) → 完结 (4)
• 定时任务通过CourseJobHandler实现课程的自动上下架。
课程状态存储在课程表中,通过 status 字段表示:
public class Course {private Long id; // 课程IDprivate String name; // 课程名称private Integer status; // 状态(待上架、已上架、下架、完结)private LocalDateTime publishTime;// 发布时间private LocalDateTime startTime; // 开始时间private LocalDateTime endTime; // 结束时间private Boolean deleted; // 是否删除private LocalDateTime createTime; // 创建时间private LocalDateTime updateTime; // 更新时间 }
在 CourseMapper 中定义查询方法:
@Select("SELECT * FROM course WHERE status = #{status} AND start_time <= #{time}") List<Course> getCoursesByStatusAndStartTime(@Param("status") Integer status, @Param("time") LocalDateTime time); @Select("SELECT * FROM course WHERE status = #{status} AND end_time <= #{time}") List<Course> getCoursesByStatusAndEndTime(@Param("status") Integer status, @Param("time") LocalDateTime time);
• 查询条件:
• 待上架课程:当前时间大于等于 start_time。
• 已上架课程:当前时间大于等于 end_time。
4 关联题目
• 提供对题目的关联管理,支持批量插入和删除。
• 工具类SubjectUtils用于处理题目选项。
• 课程-题目关联表
public class CourseCataSubject {private Long id; // 主键IDprivate Long courseId; // 课程IDprivate Long cataId; // 目录IDprivate Long subjectId; // 题目ID }
使用 MyBatis 的动态 SQL 构造批量插入语句:
@Insert("<script>INSERT INTO course_cata_subject (course_id, cata_id, subject_id) VALUES " +"<foreach collection='courseCataSubjects' item='ccs' separator=','>" +"(#{ccs.courseId}, #{ccs.cataId}, #{ccs.subjectId})" +"</foreach></script>") int batchInsert(@Param("courseCataSubjects") List<CourseCataSubject> courseCataSubjects);
• 工具类:SubjectUtils
• 动态处理题目选项(如单选、多选)。
• 封装选项:将选项集合写入题目对象。
• 解析选项:从题目对象中读取选项集合。
public class SubjectUtils { /*** 将选项列表设置到题目中* @param subject 题目* @param options 选项列表*/public static void setOptions(Subject subject, List<String> options) {if (CollUtils.isNotEmpty(options)) {for (int count = 0; count < options.size(); count++) {ReflectUtils.setFieldValue(subject, "option" + (count + 1), options.get(count));}}} /*** 从题目中获取选项列表* @param subject 题目* @return 选项列表*/public static List<String> getOptions(Subject subject) {List<String> options = new ArrayList<>();for (int count = 1; count <= 10; count++) {Object option = ReflectUtils.getFieldValue(subject, "option" + count);if (ObjectUtils.isEmpty(option) || StringUtils.isEmpty((String) option)) {return options;}options.add((String) option);}return options;} }
工具类实现细节
1. 设置选项
• 动态将 options 列表中的选项写入到 Subject 对象的 option1, option2 等字段中。
• 使用反射工具 ReflectUtils 动态设置字段值。
题目的选项数量可能是动态的,例如:
• 单选题可能有 4 个选项。
• 多选题可能有 6 个选项。
• 不同题目类型对选项数量的要求可能不同。
而 Subject 类可能预定义了固定数量的选项字段(如 option1, option2, …, option10),但具体使用多少字段取决于题目类型或数据。因此:
• 通过反射动态遍历字段,可以灵活处理任意数量的选项。
• 避免写死代码(如逐一手动访问 option1, option2 等)。
• 示例:
• 输入选项 ["A", "B", "C"]。
• 结果:subject.option1 = "A", subject.option2 = "B", subject.option3 = "C"。
2. 获取选项
• 通过反射获取 Subject 对象中的 option1, option2 等字段值。
• 将非空字段值依次加入选项列表。
• 示例:
• subject.option1 = "A", subject.option2 = "B", subject.option3 = null。
• 输出选项:["A", "B"]。
5 缓存机制
• 使用Redis存储分类和课程数量缓存,减少数据库查询压力。
• 定义了统一的缓存键规则(如REDIS_KEY_CATEGORY_THIRD_NUMBER)。
缓存用途
• 分类数据缓存:存储各分类及其子分类的课程数量。
• 课程数量缓存:记录课程的数量统计结果,用于快速查询。
在 RedisConstants 类中定义了统一的缓存键规则:
public class RedisConstants { // 一级二级分类拥有的三级分类的数量public static final String REDIS_KEY_CATEGORY_THIRD_NUMBER = "CATEGORY:THIRD_NUMBER"; // 内部嵌套类用于格式化动态键public static class Formatter {// 分类课程数量统计public static final String STATISTICS_COURSE_NUM_CATE = "COURSE:COURSE_NUM_CATEGORY";// 有课程的分类 ID 列表public static final String CATEGORY_ID_LIST_HAVE_COURSE = "COURSE:CATEGORY_ID_WITH_COURSE";} }
键命名规范
• 静态键:
• 如 CATEGORY:THIRD_NUMBER,用于存储固定层级的分类统计。
• 动态键:
• 如 COURSE:CATEGORY_ID_WITH_COURSE:{categoryId},通过参数动态生成完整的 Redis 键名,适用于按分类 ID 或其他标识存储的数据。
在 CourseCategoryServiceImpl 中实现存储分类课程数量的缓存逻辑:
@Override public void cacheCategoryCourseCount(Long categoryId, int courseCount) {String redisKey = RedisConstants.REDIS_KEY_CATEGORY_THIRD_NUMBER + ":" + categoryId;redisTemplate.opsForValue().set(redisKey, courseCount, Duration.ofHours(1));log.info("Cached course count [{}] for category [{}]", courseCount, categoryId); }
• Redis Key 构造:
• 静态键 + 动态参数组合生成完整的 Redis Key。
• 示例:CATEGORY:THIRD_NUMBER:101 表示分类 ID 为 101 的三级分类课程数量。