黑马天机学堂课程模块

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 的三级分类课程数量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值