course
模块架构
核心功能分析
1 课程分类
• 支持三级分类管理,限制分类层级不超过三级。
• 提供工具类(CategoryDataWrapper)处理分类的树形结构。
Category 表(或对应 PO 类)设计如下:
public class Category { private Long id; // 分类ID private String name; // 分类名称 private Long parentId; // 父分类ID private 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; // 目录ID private String name; // 名称 private Long courseId; // 所属课程ID private Integer type; // 类型(章、节、练习) private Long parentCatalogueId;// 父目录ID private Long mediaId; // 媒资ID(视频或其他媒体) private Long videoId; // 视频ID private 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; // 课程ID private 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; // 主键ID private Long courseId; // 课程ID private Long cataId; // 目录ID private 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 的三级分类课程数量。