SpringCloud天机学堂:学习计划与进度(四)

SpringCloud天机学堂:学习计划与进度(四)


文章目录

1、业务接口统计

按照用户的学习顺序,依次有下面几个接口:

  • 创建学习计划
  • 查询学习记录
  • 提交学习记录
  • 查询我的计划

创建学习计划

在个人中心的我的课表列表中,没有学习计划的课程都会有一个创建学习计划的按钮,在原型图就能看到:

创建学习计划,本质就是让用户设定自己每周的学习频率:

而学习频率我们在设计learning_lesson表的时候已经有两个字段来表示了:

SQL 复制代码
CREATE TABLE `learning_lesson`  (
  `id` bigint NOT NULL COMMENT '主键',
  `user_id` bigint NOT NULL COMMENT '学员id',
  `course_id` bigint NOT NULL COMMENT '课程id',
  `status` tinyint NULL DEFAULT 0 COMMENT '课程状态,0-未学习,1-学习中,2-已学完,3-已失效',
  `week_freq` tinyint NULL DEFAULT NULL COMMENT '每周学习频率,每周3天,每天2节,则频率为6',
  `plan_status` tinyint NOT NULL DEFAULT 0 COMMENT '学习计划状态,0-没有计划,1-计划进行中',
  `learned_sections` int NOT NULL DEFAULT 0 COMMENT '已学习小节数量',
  `latest_section_id` bigint NULL DEFAULT NULL COMMENT '最近一次学习的小节id',
  `latest_learn_time` datetime NULL DEFAULT NULL COMMENT '最近一次学习的时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `expire_time` datetime NOT NULL COMMENT '过期时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `idx_user_id`(`user_id`, `course_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '学生课程表' ROW_FORMAT = Dynamic;

当我们创建学习计划时,就是更新learning_lesson表,写入week_freq并更新学习计划状态plan_status字段为1(表示计划进行中)即可。因此请求参数就是课程的id、每周学习频率。

再按照Restful风格,最终接口如下:

查询学习记录

用户创建完计划自然要开始学习课程,在用户学习视频的页面,首先要展示课程的一些基础信息。例如课程信息、章节目录以及每个小节的学习进度:

其中,课程、章节、目录信息等数据都在课程微服务,而学习进度肯定是在学习微服务。课程信息是必备的,而学习进度却不一定存在

因此,查询这个接口的请求肯定是请求到课程微服务,查询课程、章节信息,再由课程微服务向学习微服务查询学习进度,合并后一起返回给前端即可。

所以,学习中心要提供一个查询章节学习进度的Feign接口,事实上这个接口已经在tj-api模块的LearningClient中定义好了:

Java 复制代码
/**
 * 查询当前用户指定课程的学习进度
 * @param courseId 课程id
 * @return 课表信息、学习记录及进度信息
 */
@GetMapping("/learning-records/course/{courseId}")
LearningLessonDTO queryLearningRecordByCourse(@PathVariable("courseId") Long courseId);

对应的DTO也都在tj-api模块定义好了,因此整个接口规范如下:

查询我的学习计划

在个人中心的我的课程页面,会展示用户的学习计划及本周的学习进度,原型如图:

需要注意的是这个查询其实是一个分页查询,因为页面最多展示10行,而学员同时在学的课程可能会超过10个,这个时候就会分页展示,当然这个分页可能是滚动分页,所以没有进度条。另外,查询的是我的学习计划,隐含的查询条件就是当前登录用户,这个无需传递,通过请求头即可获得。

因此查询参数只需要分页参数即可。

查询结果中有很多对于已经学习的小节数量的统计,因此将来我们一定要保存用户对于每一个课程的学习记录,哪些小节已经学习了,哪些已经学完了。只有这样才能统计出学习进度。

查询的结果如页面所示,分上下两部分。:

总的统计信息:

  • 本周已完成总章节数:需要对学习记录做统计
  • 课程总计划学习数量:累加课程的总计划学习频率即可
  • 本周学习积分:积分暂不实现

正在学习的N个课程信息的集合,其中每个课程包含下列字段:

  • 该课程本周学了几节:统计学习记录
  • 计划学习频率:在learning_lesson表中有对应字段
  • 该课程总共学了几节:在learning_lesson表中有对应字段
  • 课程总章节数:查询课程微服务
  • 该课程最近一次学习时间:在learning_lesson表中有对应字段

综上,查询学习计划进度的接口信息如下:

2、实现接口
2.1、查询学习记录

首先在tj-learning模块下的com.tianji.learning.controller.LearningRecordController下定义接口:

Java 复制代码
@RestController
@RequestMapping("/learning-records")
@Api(tags = "学习记录的相关接口")
@RequiredArgsConstructor
public class LearningRecordController {

    private final ILearningRecordService recordService;

    @ApiOperation("查询指定课程的学习记录")
    @GetMapping("/course/{courseId}")
    public LearningLessonDTO queryLearningRecordByCourse(
            @ApiParam(value = "课程id", example = "2") @PathVariable("courseId") Long courseId){
        return recordService.queryLearningRecordByCourse(courseId);
    }
}

然后在com.tianji.learning.service.ILearningRecordService中定义方法:

Java 复制代码
public interface ILearningRecordService extends IService<LearningRecord> {

    LearningLessonDTO queryLearningRecordByCourse(Long courseId);
}

最后在com.tianji.learning.service.impl.LearningRecordServiceImpl中定义实现类:

Java 复制代码
@Service
@RequiredArgsConstructor
public class LearningRecordServiceImpl extends ServiceImpl<LearningRecordMapper, LearningRecord> implements ILearningRecordService {

    private final ILearningLessonService lessonService;

    @Override
    public LearningLessonDTO queryLearningRecordByCourse(Long courseId) {
        // 1.获取登录用户
        Long userId = UserContext.getUser();
        // 2.查询课表
        LearningLesson lesson = lessonService.queryByUserAndCourseId(userId, courseId);
        // 3.查询学习记录
        // select * from xx where lesson_id = #{lessonId}
        List<LearningRecord> records = lambdaQuery()
                            .eq(LearningRecord::getLessonId, lesson.getId()).list();
        // 4.封装结果
        LearningLessonDTO dto = new LearningLessonDTO();
        dto.setId(lesson.getId());
        dto.setLatestSectionId(lesson.getLatestSectionId());
        dto.setRecords(BeanUtils.copyList(records, LearningRecordDTO.class));
        return dto;
    }
}

其中查询课表的时候,需要调用ILessonService中的queryByUserAndCourseId()方法,该方法代码如下:

Java 复制代码
@Override
public LearningLesson queryByUserAndCourseId(Long userId, Long courseId) {
    return getOne(buildUserIdAndCourseIdWrapper(userId, courseId));
}

private LambdaQueryWrapper<LearningLesson> buildUserIdAndCourseIdWrapper(Long userId, Long courseId) {
    LambdaQueryWrapper<LearningLesson> queryWrapper = new QueryWrapper<LearningLesson>()
            .lambda()
            .eq(LearningLesson::getUserId, userId)
            .eq(LearningLesson::getCourseId, courseId);
    return queryWrapper;
}
2.2、提交学习记录

首先在tj-learning模块下的com.tianji.learning.controller.LearningRecordController下定义接口:

Java 复制代码
@RestController
@RequestMapping("/learning-records")
@Api(tags = "学习记录的相关接口")
@RequiredArgsConstructor
public class LearningRecordController {

    private final ILearningRecordService recordService;

    @ApiOperation("查询指定课程的学习记录")
    @GetMapping("/course/{courseId}")
    public LearningLessonDTO queryLearningRecordByCourse(
            @ApiParam(value = "课程id", example = "2") @PathVariable("courseId") Long courseId){
        return recordService.queryLearningRecordByCourse(courseId);
    }

    @ApiOperation("提交学习记录")
    @PostMapping
    public void addLearningRecord(@RequestBody LearningRecordFormDTO formDTO){
        recordService.addLearningRecord(formDTO);
    }
}

然后在com.tianji.learning.service.ILearningRecordService中定义方法:

Java 复制代码
public interface ILearningRecordService extends IService<LearningRecord> {

    LearningLessonDTO queryLearningRecordByCourse(Long courseId);

    void addLearningRecord(LearningRecordFormDTO formDTO);
}

最后在com.tianji.learning.service.impl.LearningRecordServiceImpl中定义实现类:

Java 复制代码
@Service
@RequiredArgsConstructor
public class LearningRecordServiceImpl extends ServiceImpl<LearningRecordMapper, LearningRecord> implements ILearningRecordService {

    private final ILearningLessonService lessonService;

    private final CourseClient courseClient;

    // 。。。略

    @Override
    @Transactional
    public void addLearningRecord(LearningRecordFormDTO recordDTO) {
        // 1.获取登录用户
        Long userId = UserContext.getUser();
        // 2.处理学习记录
        boolean finished = false;
        if (recordDTO.getSectionType() == SectionType.VIDEO) {
            // 2.1.处理视频
            finished = handleVideoRecord(userId, recordDTO);
        }else{
            // 2.2.处理考试
            finished = handleExamRecord(userId, recordDTO);
        }

        // 3.处理课表数据
        handleLearningLessonsChanges(recordDTO, finished);
    }

    private void handleLearningLessonsChanges(LearningRecordFormDTO recordDTO, boolean finished) {
        // 1.查询课表
        LearningLesson lesson = lessonService.getById(recordDTO.getLessonId());
        if (lesson == null) {
            throw new BizIllegalException("课程不存在,无法更新数据!");
        }
        // 2.判断是否有新的完成小节
        boolean allLearned = false;
        if(finished){
            // 3.如果有新完成的小节,则需要查询课程数据
            CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
            if (cInfo == null) {
                throw new BizIllegalException("课程不存在,无法更新数据!");
            }
            // 4.比较课程是否全部学完:已学习小节 >= 课程总小节
            allLearned = lesson.getLearnedSections() + 1 >= cInfo.getSectionNum();     
        }
        // 5.更新课表
        lessonService.lambdaUpdate()
                .set(lesson.getLearnedSections() == 0, LearningLesson::getStatus, LessonStatus.LEARNING.getValue())
                .set(allLearned, LearningLesson::getStatus, LessonStatus.FINISHED.getValue())
                .set(!finished, LearningLesson::getLatestSectionId, recordDTO.getSectionId())
                .set(!finished, LearningLesson::getLatestLearnTime, recordDTO.getCommitTime())
                .setSql(finished, "learned_sections = learned_sections + 1")
                .eq(LearningLesson::getId, lesson.getId())
                .update();
    }

    private boolean handleVideoRecord(Long userId, LearningRecordFormDTO recordDTO) {
        // 1.查询旧的学习记录
        LearningRecord old = queryOldRecord(recordDTO.getLessonId(), recordDTO.getSectionId());
        // 2.判断是否存在
        if (old == null) {
            // 3.不存在,则新增
            // 3.1.转换PO
            LearningRecord record = BeanUtils.copyBean(recordDTO, LearningRecord.class);
            // 3.2.填充数据
            record.setUserId(userId);
            // 3.3.写入数据库
            boolean success = save(record);
            if (!success) {
                throw new DbException("新增学习记录失败!");
            }
            return false;
        }
        // 4.存在,则更新
        // 4.1.判断是否是第一次完成
        boolean finished = !old.getFinished() && recordDTO.getMoment() * 2 >= recordDTO.getDuration();
        // 4.2.更新数据
        boolean success = lambdaUpdate()
                .set(LearningRecord::getMoment, recordDTO.getMoment())
                .set(finished, LearningRecord::getFinished, true)
                .set(finished, LearningRecord::getFinishTime, recordDTO.getCommitTime())
                .eq(LearningRecord::getId, old.getId())
                .update();
        if(!success){
            throw new DbException("更新学习记录失败!");
        }
        return finished ;
    }

    private LearningRecord queryOldRecord(Long lessonId, Long sectionId) {
        return lambdaQuery()
                .eq(LearningRecord::getLessonId, lessonId)
                .eq(LearningRecord::getSectionId, sectionId)
                .one();
    }

    private boolean handleExamRecord(Long userId, LearningRecordFormDTO recordDTO) {
        // 1.转换DTO为PO
        LearningRecord record = BeanUtils.copyBean(recordDTO, LearningRecord.class);
        // 2.填充数据
        record.setUserId(userId);
        record.setFinished(true);
        record.setFinishTime(recordDTO.getCommitTime());
        // 3.写入数据库
        boolean success = save(record);
        if (!success) {
            throw new DbException("新增考试记录失败!");
        }
        return true;
    }
}
2.3、创建学习计划

首先,在com.tianji.learning.controller.LearningLessonController中添加一个接口:

Java 复制代码
@RestController
@RequestMapping("/lessons")
@Api(tags = "我的课表相关接口")
@RequiredArgsConstructor
public class LearningLessonController {

    private final ILearningLessonService lessonService;

    // 略。。。

    @ApiOperation("创建学习计划")
    @PostMapping("/plans")
    public void createLearningPlans(@Valid @RequestBody LearningPlanDTO planDTO){
        lessonService.createLearningPlan(planDTO.getCourseId(), planDTO.getFreq());
    }
}

然后,在com.tianji.learning.service.ILearningLessonService中定义service方法:

Java 复制代码
public interface ILearningLessonService extends IService<LearningLesson> {
    // ... 略

    void createLearningPlan(Long courseId, Integer freq);
}

最后,在com.tianji.learning.service.impl.LearningLessonServiceImpl中实现方法:

Java 复制代码
@Override
public void createLearningPlan(Long courseId, Integer freq) {
    // 1.获取当前登录的用户
    Long userId = UserContext.getUser();
    // 2.查询课表中的指定课程有关的数据
    LearningLesson lesson = queryByUserAndCourseId(userId, courseId);
    AssertUtils.isNotNull(lesson, "课程信息不存在!");
    // 3.修改数据
    LearningLesson l = new LearningLesson();
    l.setId(lesson.getId());
    l.setWeekFreq(freq);
    l.setLearnedSections(0); 	// 避免提交时NPE错误
    if(lesson.getPlanStatus() == PlanStatus.NO_PLAN) {
        l.setPlanStatus(PlanStatus.PLAN_RUNNING);
    }
    updateById(l);
}

// ... 略
2.4、查询学习计划进度

首先在tj-learning模块的com.tianji.learning.controller.LearningLessonController中定义controller接口:

Java 复制代码
@ApiOperation("查询我的学习计划")
@GetMapping("/plans")
public LearningPlanPageVO queryMyPlans(PageQuery query){
    return lessonService.queryMyPlans(query);
}

然后在com.tianji.learning.service.ILearningLessonService中定义service方法:

Java 复制代码
LearningPlanPageVO queryMyPlans(PageQuery query);

最后在com.tianji.learning.service.impl.LearningLessonServiceImpl中实现该方法:

版本1:物理分页,分别统计

Java 复制代码
@Override
public LearningPlanPageVO queryMyPlans(PageQuery query) {
    LearningPlanPageVO result = new LearningPlanPageVO();
    // 1.获取当前登录用户
    Long userId = UserContext.getUser();
    // 2.获取本周起始时间
    LocalDate now = LocalDate.now();
    LocalDateTime begin = DateUtils.getWeekBeginTime(now);
    LocalDateTime end = DateUtils.getWeekEndTime(now);
    // 3.查询总的统计数据
    // 3.1.本周总的已学习小节数量
    Integer weekFinished = recordMapper.selectCount(new LambdaQueryWrapper<LearningRecord>()
            .eq(LearningRecord::getUserId, userId)
            .eq(LearningRecord::getFinished, true)
            .gt(LearningRecord::getFinishTime, begin)
            .lt(LearningRecord::getFinishTime, end)
    );
    result.setWeekFinished(weekFinished);
    // 3.2.本周总的计划学习小节数量
    Integer weekTotalPlan = getBaseMapper().queryTotalPlan(userId);
    result.setWeekTotalPlan(weekTotalPlan);
    // TODO 3.3.本周学习积分

    // 4.查询分页数据
    // 4.1.分页查询课表信息以及学习计划信息
    Page<LearningLesson> p = lambdaQuery()
            .eq(LearningLesson::getUserId, userId)
            .eq(LearningLesson::getPlanStatus, PlanStatus.PLAN_RUNNING)
            .in(LearningLesson::getStatus, LessonStatus.NOT_BEGIN, LessonStatus.LEARNING)
            .page(query.toMpPage("latest_learn_time", false));
    List<LearningLesson> records = p.getRecords();
    if (CollUtils.isEmpty(records)) {
        return result.emptyPage(p);
    }
    // 4.2.查询课表对应的课程信息
    Map<Long, CourseSimpleInfoDTO> cMap = queryCourseSimpleInfoList(records);
    // 4.3.统计每一个课程本周已学习小节数量
    List<IdAndNumDTO> list = recordMapper.countLearnedSections(userId, begin, end);
    Map<Long, Integer> countMap = IdAndNumDTO.toMap(list);
    // 4.4.组装数据VO
    List<LearningPlanVO> voList = new ArrayList<>(records.size());
    for (LearningLesson r : records) {
        // 4.4.1.拷贝基础属性到vo
        LearningPlanVO vo = BeanUtils.copyBean(r, LearningPlanVO.class);
        // 4.4.2.填充课程详细信息
        CourseSimpleInfoDTO cInfo = cMap.get(r.getCourseId());
        if (cInfo != null) {
            vo.setCourseName(cInfo.getName());
            vo.setSections(cInfo.getSectionNum());
        }
        // 4.4.3.每个课程的本周已学习小节数量
        vo.setWeekLearnedSections(countMap.getOrDefault(r.getId(), 0));
        voList.add(vo);
    }
    return result.pageInfo(p.getTotal(), p.getPages(), voList);
}


private Map<Long, CourseSimpleInfoDTO> queryCourseSimpleInfoList(List<LearningLesson> records) {
    // 3.1.获取课程id
    Set<Long> cIds = records.stream().map(LearningLesson::getCourseId).collect(Collectors.toSet());
    // 3.2.查询课程信息
    List<CourseSimpleInfoDTO> cInfoList = courseClient.getSimpleInfoList(cIds);
    if (CollUtils.isEmpty(cInfoList)) {
        // 课程不存在,无法添加
        throw new BadRequestException("课程信息不存在!");
    }
    // 3.3.把课程集合处理成Map,key是courseId,值是course本身
    Map<Long, CourseSimpleInfoDTO> cMap = cInfoList.stream()
            .collect(Collectors.toMap(CourseSimpleInfoDTO::getId, c -> c));
    return cMap;
}
相关推荐
Komorebi.py1 分钟前
【Linux】-学习笔记04
linux·笔记·学习
乌啼霜满天24916 分钟前
Spring 与 Spring MVC 与 Spring Boot三者之间的区别与联系
java·spring boot·spring·mvc
Grey_fantasy31 分钟前
高级编程之结构化代码
java·spring boot·spring cloud
Elaine20239139 分钟前
零碎04 MybatisPlus自定义模版生成代码
java·spring·mybatis
weiabc1 小时前
学习electron
javascript·学习·electron
HackKong2 小时前
小白怎样入门网络安全?
网络·学习·安全·web安全·网络安全·黑客
Bald Baby2 小时前
JWT的使用
java·笔记·学习·servlet
心怀梦想的咸鱼2 小时前
UE5 第一人称射击项目学习(四)
学习·ue5
AI完全体3 小时前
【AI日记】24.11.22 学习谷歌数据分析初级课程-第2/3课
学习·数据分析
.生产的驴3 小时前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构