SpringCloud天机学堂:学习计划与进度(四)
文章目录
- SpringCloud天机学堂:学习计划与进度(四)
- 1、业务接口统计
- 2、实现接口
- 2.1、查询学习记录
- 2.2、提交学习记录
- 2.3、创建学习计划
- 2.4、查询学习计划进度
1、业务接口统计
按照用户的学习顺序,依次有下面几个接口:
- 创建学习计划
- 查询学习记录
- 提交学习记录
- 查询我的计划
创建学习计划
在个人中心的我的课表列表中,没有学习计划的课程都会有一个创建学习计划的按钮,在原型图就能看到:
创建学习计划,本质就是让用户设定自己每周的学习频率:
而学习频率我们在设计learning_lesson表的时候已经有两个字段来表示了:
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中定义好了:
/**
* 查询当前用户指定课程的学习进度
* @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
下定义接口:
@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
中定义方法:
public interface ILearningRecordService extends IService<LearningRecord> {
LearningLessonDTO queryLearningRecordByCourse(Long courseId);
}
最后在com.tianji.learning.service.impl.LearningRecordServiceImpl中定义实现类:
@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、提交学习记录
首先在tj-learning
模块下的com.tianji.learning.controller.LearningRecordController
下定义接口:
@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
中定义方法:
public interface ILearningRecordService extends IService<LearningRecord> {
LearningLessonDTO queryLearningRecordByCourse(Long courseId);
void addLearningRecord(LearningRecordFormDTO formDTO);
}
最后在com.tianji.learning.service.impl.LearningRecordServiceImpl
中定义实现类:
@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
中添加一个接口:
@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方法:
public interface ILearningLessonService extends IService<LearningLesson> {
// ... 略
void createLearningPlan(Long courseId, Integer freq);
}
最后,在com.tianji.learning.service.impl.LearningLessonServiceImpl
中实现方法:
@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
接口:
@ApiOperation("查询我的学习计划")
@GetMapping("/plans")
public LearningPlanPageVO queryMyPlans(PageQuery query){
return lessonService.queryMyPlans(query);
}
然后在com.tianji.learning.service.ILearningLessonService
中定义service方法:
LearningPlanPageVO queryMyPlans(PageQuery query);
最后在com.tianji.learning.service.impl.LearningLessonServiceImpl
中实现该方法:
版本1:物理分页,分别统计
@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;
}