day03-学习计划和进度

本文详细描述了一个IT项目中的业务流程,包括学习计划的设置与进度跟踪,涉及接口设计、数据结构和数据库操作。主要内容包括创建学习计划、查询学习记录、统计学习进度和计划状态,以及相关的数据库操作和代码实现。
摘要由CSDN通过智能技术生成

1.1.分析业务流程

我们从两个业务点来分析:

  • 学习计划
  • 学习进度统计

1.1.1.学习计划

在我的课程页面,可以对有效的课程添加学习计划:
在这里插入图片描述
学习计划就是简单设置一下用户每周计划学习几节课:
在这里插入图片描述
有了计划以后,我们就可以在我的课程页面展示用户计划的完成情况,提醒用户尽快学习:
在这里插入图片描述

1.1.2.学习进度统计

在这里插入图片描述

也就是说,要记录用户学习进度,需要记录下列核心信息:

  • 小节的基础信息(id、关联的课程id等)
  • 当前的播放进度(第几秒)
  • 当前小节是否已学完(播放进度是否超50%)

用户每学习一个小节,就会新增一条学习记录,当该课程的全部小节学习完毕,则该课程就从学习中进入已学完状态了。整体流程如图:

在这里插入图片描述

1.2.业务接口统计

接下来我们分析一下这部分功能相关的接口有哪些,按照用户的学习顺序,依次有下面几个接口:

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

1.2.1.创建学习计划

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

再按照Restful风格,最终接口如下:
在这里插入图片描述

1.2.2.查询学习记录

用户创建完计划自然要开始学习课程,在用户学习视频的页面,首先要展示课程的一些基础信息。例如课程信息、章节目录以及每个小节的学习进度:
在这里插入图片描述
其中,课程、章节、目录信息等数据都在课程微服务,而学习进度肯定是在学习微服务。课程信息是必备的,而学习进度却不一定存在。
因此,查询这个接口的请求肯定是请求到课程微服务,查询课程、章节信息,再由课程微服务向学习微服务查询学习进度,合并后一起返回给前端即可。
所以,学习中心要提供一个查询章节学习进度的Feign接口,事实上这个接口已经在tj-api模块的LearningClient中定义好了:


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

对应的DTO也都在tj-api模块定义好了,因此整个接口规范如下:
在这里插入图片描述
由于请求参数是courseId,而返回值中包含lessonId和latestSectionid都在learning_lesson表中,因此我们需要根据courseId和userId查询出lesson信息。然后再根据lessonId查询学习记录。整体流程如下:

  • 获取当前登录用户id
  • 根据courseId和userId查询LearningLesson
  • 判断是否存在或者是否过期
    • 如果不存在或过期直接返回空
    • 如果存在并且未过期,则继续
  • 查询lesson对应的所有学习记录

1.2.3.提交学习记录

在这里插入图片描述

1.2.4.查询我的学习计划

在个人中心的我的课程页面,会展示用户的学习计划及本周的学习进度,原型如图:
在这里插入图片描述
查询参数只需要分页参数即可。

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

查询的结果如页面所示,分上下两部分。:
总的统计信息:

  • 本周已完成总章节数:需要对学习记录做统计
  • 课程总计划学习数量:累加课程的总计划学习频率即可
  • 本周学习积分:积分暂不实现
    正在学习的N个课程信息的集合,其中每个课程包含下列字段:
  • 该课程本周学了几节:统计学习记录
  • 计划学习频率:在learning_lesson表中有对应字段
  • 该课程总共学了几节:在learning_lesson表中有对应字段
  • 课程总章节数:查询课程微服务
  • 该课程最近一次学习时间:在learning_lesson表中有对应字段

综上,查询学习计划进度的接口信息如下:
在这里插入图片描述

2.实现接口

2.1.查询学习记录

2.1.2.代码实现

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

package com.tianji.learning.controller;


import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.learning.service.ILearningRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 * 学习记录表 前端控制器
 * </p>
 */
@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中定义方法:

package com.tianji.learning.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.learning.domain.po.LearningRecord;

/**
 * <p>
 * 学习记录表 服务类
 * </p>
 */
public interface ILearningRecordService extends IService<LearningRecord> {

    LearningLessonDTO queryLearningRecordByCourse(Long courseId);
}

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

/**
 * <p>
 * 学习记录表 服务实现类
 * </p>
 *
 * @since 2022-12-10
 */
@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()方法,该方法代码如下:

@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.提交学习记录

2.2.1.思路分析

学习记录就是用户当前学了哪些小节,以及学习到该小节的进度如何。而小节类型分为考试、视频两种。

  • 考试比较简单,只要提交了就说明这一节学完了。
  • 视频比较麻烦,需要记录用户的播放进度,进度超过50%才算学完。因此视频播放的过程中需要不断提交播放进度到服务端,而服务端则需要保存学习记录到数据库。
    以上信息都需要保存到learning_record表中。

每当有一个小节被学习,都应该更新latest_section_id和latest_learn_time;每当有一个小节学习完后,learned_sections都应该累加1。不过这里有一点容易出错的地方:

  • 考试只会被参加一次,考试提交则小节学完,learned_sections累加1
  • 视频可以被重复播放,只有在第一次学完一个视频时,learned_sections才需要累加1

那么问题来了,如何判断视频是否是第一次学完?我认为应该同时满足两个条件:

  • 视频播放进度超过50%
  • 之前学习记录的状态为未学完

另外,随着learned_sections字段不断累加,最终会到达课程的最大小节数,这就意味着当前课程被全部学完了。那么课程状态需要从“学习中”变更为“已学完”。

综上,最终的提交学习记录处理流程如图:

在这里插入图片描述

2.2.2代码

package com.tianji.learning.domain.dto;

import com.tianji.common.validate.annotations.EnumValid;
import com.tianji.learning.enums.SectionType;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Data
@ApiModel(description = "学习记录")
public class LearningRecordFormDTO {

    @ApiModelProperty("小节类型:1-视频,2-考试")
    @NotNull(message = "小节类型不能为空")
    @EnumValid(enumeration = {1, 2}, message = "小节类型错误,只能是:1-视频,2-考试")
    private SectionType sectionType;

    @ApiModelProperty("课表id")
    @NotNull(message = "课表id不能为空")
    private Long lessonId;

    @ApiModelProperty("对应节的id")
    @NotNull(message = "节的id不能为空")
    private Long sectionId;

    @ApiModelProperty("视频总时长,单位秒")
    private Integer duration;

    @ApiModelProperty("视频的当前观看时长,单位秒,第一次提交填0")
    private Integer moment;

    @ApiModelProperty("提交时间")
    private LocalDateTime commitTime;
}

controller方法和service接口中的方法省略

实现类方法:


/**
 * <p>
 * 学习记录表 服务实现类
 * </p>
 */
@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 创建学习计划

2.3.1.思路分析

创建学习计划,本质就是让用户设定自己每周的学习频率:
在这里插入图片描述
当我们创建学习计划时,就是更新learning_lesson表,写入week_freq并更新plan_status为计划进行中即可。

2.3.2.表单实体

表单包含两个字段:

  • courseId
  • weekFreq
    前端是以JSON方式提交,我们需要定义一个表单DTO实体。
@Data
@ApiModel(description = "学习计划表单实体")
public class LearningPlanDTO {
    @NotNull
    @ApiModelProperty("课程表id")
    @Min(1)
    private Long courseId;
    @NotNull
    @Range(min = 1, max = 50)
    @ApiModelProperty("每周学习频率")
    private Integer freq;
}

代码实现类的方法:

@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);
    if(lesson.getPlanStatus() == PlanStatus.NO_PLAN) {
        l.setPlanStatus(PlanStatus.PLAN_RUNNING);
    }
    updateById(l);
}

2.4.查询学习计划进度

2.4.1.思路分析

要查询的数据分为两部分:

  • 本周计划学习的每个课程的学习进度
  • 本周计划学习的课程总的学习进度

对于本周计划学习的每个课程的学习进度,首先需要查询出学习中的LearningLesson的信息,查询条件包括:

  • 属于当前登录用户
  • 学习计划进行中

代码实现

controller和service方法省略

@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.查询本周计划学习的所有课程,满足三个条件:属于当前用户、有学习计划、学习中
    List<LearningLesson> lessons = lambdaQuery()
            .eq(LearningLesson::getUserId, userId)
            .eq(LearningLesson::getPlanStatus, PlanStatus.PLAN_RUNNING)
            .in(LearningLesson::getStatus, LessonStatus.NOT_BEGIN, LessonStatus.LEARNING)
            .list();
    if (CollUtils.isEmpty(lessons)) {
        return null;
    }
    // 4.统计当前用户在本周 每个课程的已学习小节数量
    List<IdAndNumDTO> list = recordMapper.countLearnedSections(userId, begin, end);
    /** 这里用的mybatis的写法 在mapper中写SQL
    <select id="countLearnedSections" resultType="com.tianji.api.dto.IdAndNumDTO">
        SELECT lesson_id AS id, COUNT(1) AS num
        FROM learning_record
        
        WHERE user_id = #{userId}
        AND finished = 1
        AND finish_time &gt; #{begin} AND finish_time &lt; #{end}
        
        GROUP BY lesson_id;
    </select>
    */
    Map<Long, Integer> countMap = IdAndNumDTO.toMap(list);

    // 5.查询总的统计数据
    // 5.1.本周总的已学习小节数量
    int weekFinished = lessons.stream()
            .map(LearningLesson::getId)
            .mapToInt(id -> countMap.getOrDefault(id, 0))
            .sum();
    result.setWeekFinished(weekFinished);
    // 5.2.本周总的计划学习小节数量
    int weekTotalPlan = lessons.stream().mapToInt(LearningLesson::getWeekFreq).sum();
    result.setWeekTotalPlan(weekTotalPlan);
    // TODO 5.3.本周学习积分

    // 6.处理分页数据
    // 6.1.分页查询课表信息以及学习计划信息
    Page<LearningLesson> p = new Page<>(query.getPageNo(), query.getPageSize(), lessons.size());
    List<LearningLesson> records = CollUtils.sub(lessons, query.from(), query.from() + query.getPageSize());
    if (CollUtils.isEmpty(records)) {
        return result.emptyPage(p);
    }
    // 6.2.查询课表对应的课程信息
    Map<Long, CourseSimpleInfoDTO> cMap = queryCourseSimpleInfoList(records);
    // 6.3.组装数据VO
    List<LearningPlanVO> voList = new ArrayList<>(records.size());
    for (LearningLesson r : records) {
        // 6.4.1.拷贝基础属性到vo
        LearningPlanVO vo = BeanUtils.copyBean(r, LearningPlanVO.class);
        // 6.4.2.填充课程详细信息
        CourseSimpleInfoDTO cInfo = cMap.get(r.getCourseId());
        if (cInfo != null) {
            vo.setCourseName(cInfo.getName());
            vo.setSections(cInfo.getSectionNum());
        }
        // 6.4.3.每个课程的本周已学习小节数量
        vo.setWeekLearnedSections(countMap.getOrDefault(r.getId(), 0));
        voList.add(vo);
    }
    return result.pageInfo(p.getTotal(), p.getPages(), voList);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值