实现查询指定课程学习记录接口
1.接口分析
由于要实现查询指定课程的学习记录,需要知道当前用户,首先就需要先从线程中取出用户ID,在通过观察到课程学习记录中的需要的字段,依次去数据库中查询。课程id可通过查询学习课程表去找到。而学习记录的列表可以从学习记录表去查询。随后将查询到的数据封装到DTO中返回即可。
2.接口实现
学习记录service实现接口代码如下:
/**
代码逻辑:首先获取用户id,根据用户id去查询课程信息,为后续的填充课程id字段,然后根据用户id查询
学习记录表,得到学习记录的信息,然后将其转化为DTO类型,最后天趣缺失的字段即可
**/
public LearningLessonDTO queryLearningRecordByCourse(Long courseId) {
//1.获取用户
Long user = UserContext.getUser();
//2.查询课表
LearningLesson lesson = lessonService.queryByUserIdAndCourseId(user,courseId);
if (lesson == null) {
return null;
}
//3.查询学习记录
List<LearningRecord> records = lambdaQuery().eq(LearningRecord::getUserId, user)
.list();
List<LearningRecordDTO> list = BeanUtils.copyList(records, LearningRecordDTO.class);
//4.组装数据
LearningLessonDTO lessonDTO = new LearningLessonDTO();
lessonDTO.setId(lesson.getId());
lessonDTO.setLatestSectionId(lesson.getLatestSectionId());
lessonDTO.setRecords(list);
return lessonDTO;
}
实现提交学习记录接口
1.接口分析
学习记录就是用户当前学了哪些小节,以及学习到该小节的进度如何。而小节类型分为考试、视频两种。对于考试,其用户完成了考试之后,直接提交就行了,而对于视频,则比较麻烦,需要记录用户当前的播放进度,进度超过50%才算学完。因此视频播放的过程中需要不断提交播放进度到服务端,而服务端则需要保存学习记录到数据库。而对于视频处理来说,还需要判断其是不是第一次提交,若是第一次提交,则需要将数据插入数据库中,若不是第一次提交,需要更新数据库。更新数据库还需要判断是不是第一次完成,若是第一次完成则需要将该小节设为已完成。
2.接口实现
service对应的接口实现代码分为四部分,一个是用来判断前端传入的提交类型是视频还是考试。另外两部分是对应的类型处理的方法,最后一部分是提交到数据库的方法,代码如下:
/**、
该部分是判断前端传入的是视频类型还是考试类型
然后选择对应的方法进行处理
**/
@Transactional
public void addLearningRecord(LearningRecordFormDTO dto) {
//1.获取用户
Long userId = UserContext.getUser();
//2.处理学习记录-一类是考试,一类是视频
boolean finished = false;
if (dto.getSectionType() == SectionType.VIDEO.getValue()) {
//2.1处理视频
finished = handleVideoRecord(userId,dto);
}else {
//2.2处理考试
finished = handleExamRecord(userId, dto);
}
//3.处理课表数据
handleLearningLessonsChanges(dto,finished);
}
/**
* 处理考试记录的方法
* @param userId 用户ID
* @param dto 学习记录表单数据传输对象
* @return 处理成功返回true
* @throws DbException 当新增考试记录失败时抛出数据库异常
*/
private boolean handleExamRecord(Long userId, LearningRecordFormDTO dto) {
//转换DTO为PO
LearningRecord record = BeanUtils.copyBean(dto, LearningRecord.class);
//填充数据
record.setUserId(userId);
record.setFinished(true);
record.setFinishTime(dto.getCommitTime());
//写入数据库
boolean success = save(record);
if(!success){
throw new DbException("新增考试记录失败");
}
return true;
}
/**
处理视频的方法
代码逻辑:首先,需要从数据库中查询,是否有旧的数据,也就是判断是不是第一次播放该视频,对该记录
做一个插入,若是没有旧的数据,则需要对该次播放进行新增到数据库中。若有旧数据,则需要更新数据库
中的旧数据。而在更新数据库之前,还需要判断该视频是不是已经播放了一半,也就是是否已经完成该小结
的学习,若是完成了,则需要将其的状态更新为已完成,已经将完成时间设置为提交时间。同时,不管完成
与否,都需要提交当前视频的播放进度,最后将其更新到数据库当中
**/
private void handleLearningLessonsChanges(LearningRecordFormDTO dto, boolean finished) {
//1.查询课表
LearningLesson lesson = lessonService.getById(dto.getLessonId());
if (lesson == null) {
throw new BizIllegalException("课表不存在,无法更新数据");
}
boolean allLearned = false;
//2.判断是否有新的完成小节
if(finished){
//有新的完成小节,则需要查询课程数据
CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
if (cInfo == null) {
throw new BizIllegalException("课程不存在,无法更新数据");
}
//比较课程是否全部学完,已学习小节>=课程总小节数
allLearned = lesson.getLearnedSections() + 1 >= cInfo.getSectionNum();
//更新课表
LocalDateTime time = LocalDateTime.now();
lessonService.lambdaUpdate().set(allLearned, LearningLesson::getStatus, LessonStatus.FINISHED)
.setSql(finished, "learned_sections = learned_sections + 1")
.set(!finished, LearningLesson::getLatestSectionId, dto.getSectionId())
.set(!finished, LearningLesson::getLatestLearnTime, dto.getCommitTime())
.set(lesson.getLearnedSections() == 0 ,LearningLesson::getStatus, LessonStatus.LEARNING.getValue())
.eq(LearningLesson::getId, lesson.getId())
.set(LearningLesson::getLatestLearnTime, time)
.update();
}
}
/**
代码逻辑:该代码是处理课程是否已完成,根据前面的处理视频和考试的方法传回的返回值,可以判断
该小结是否已经完成。然后在再这个代码块中,传入该参数,即可判断,是不是有新的小节已
完成,若是有,则需要将课程的已经学习的小节数量加一,同时还需要判断当前课程是否已经学完,将
该参数作为条件去更新数据库相关数据。
**/
private void handleLearningLessonsChanges(LearningRecordFormDTO dto, boolean finished) {
//1.查询课表
LearningLesson lesson = lessonService.getById(dto.getLessonId());
if (lesson == null) {
throw new BizIllegalException("课表不存在,无法更新数据");
}
boolean allLearned = false;
//2.判断是否有新的完成小节
if(finished){
//有新的完成小节,则需要查询课程数据
CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
if (cInfo == null) {
throw new BizIllegalException("课程不存在,无法更新数据");
}
//比较课程是否全部学完,已学习小节>=课程总小节数
allLearned = lesson.getLearnedSections() + 1 >= cInfo.getSectionNum();
//更新课表
lessonService.lambdaUpdate().set(allLearned, LearningLesson::getStatus, LessonStatus.FINISHED)
.setSql(finished, "learned_sections = learned_sections + 1")
.set(!finished, LearningLesson::getLatestSectionId, dto.getSectionId())
.set(!finished, LearningLesson::getLatestLearnTime, dto.getCommitTime())
.set(lesson.getLearnedSections() == 0 ,LearningLesson::getStatus, LessonStatus.LEARNING.getValue())
.eq(LearningLesson::getId, lesson.getId())
.update();
}
}
补充意外bug:
加载 jwt秘钥失败:
当遇到下述错误的时候,看看ip地址是不是发生了改变,要去网关的配置文件中改当前运行环境的ip。
INFO 8248 --- [hFetchJwkThread] c.t.a.gateway.util.JwtSignerHolder : 尝试加载auth服务地址
20:26:05.733-[LAPTOP-3QDJP4FH][sys] - INFO 8248 --- [hFetchJwkThread] c.t.a.gateway.util.JwtSignerHolder : 加载auth服务地址成功,http://192.168.33.205:8081/jwks
20:26:05.733-[LAPTOP-3QDJP4FH][sys] - INFO 8248 --- [hFetchJwkThread] c.t.a.gateway.util.JwtSignerHolder : 尝试加载jwk秘钥
20:26:26.751-[LAPTOP-3QDJP4FH][sys] -ERROR 8248 --- [hFetchJwkThread] c.t.a.gateway.util.JwtSignerHolder : 加载jwk秘钥失败,原因:ConnectException: Connection timed out: connect这个怎么解决
实现创建学习计划的接口
1.接口分析
由于创建学习计划和修改学习计划都可以算是更新学习计划,故可以将这两个接口视为一种接口,只需要对当前课程的学习计划进行更新即可。
2.接口实现
service实现的代码如下:
/**
代码逻辑:首先根据用户id查询当前课程是否存在,若是存在,则往当前课程去更新学习计划。注意
在更新学习计划之前还需要判断当前有没有学习计划,若没有学习计划还需修改学习计划的状态。
**/
@Transactional
public void createLearningPlans(Long courseId, Integer freq) {
//获取当前用户id
Long userId = UserContext.getUser();
//查询课表中的指定课程有关的数据
LearningLesson lesson = queryByUserIdAndCourseId(userId, courseId);
AssertUtils.isNotNull(lesson, "课程不存在");
//如果课程存在,则修改学习计划
LearningLesson l = new LearningLesson();
l.setId(lesson.getId());
l.setWeekFreq(freq);
if (lesson.getPlanStatus() == PlanStatus.NO_PLAN){
l.setPlanStatus(PlanStatus.PLAN_RUNNING);
}
//修改数据
updateById(l);
}
实现查询学习计划进度的接口
1.接口分析
要查询的数据分为两部分:本周计划学习的每个课程的学习进度;本周计划学习的课程总的学习进度。如下图所示。

因此查询需要查询本周已经学习的小节数量,计划学习的数量,本周学习积分(暂先不做)
以及需要分页的查询课程信息和每个课程对应的学习记录的信息。通过当前时间减去周几加1即可推断一周的开始时间,同理,通过8-当前是周几,则可判断当前日期还需要加几天才是周日,然后再去当前周日的最大时间即可。
2.接口实现
具体的service层代码实现如下:
/**
代码逻辑分析:首先获取当前用户的id和当前一周的起始时间和结束时间,然后通过这三个字段去查询学习记录表中的数据,将查询到的数量返回,则可查询到已经学习完成的小节数量,然后将该字段注入到返回的VO之中。同理去查询本周总的计划学习的数量,通过查询week_freq字段(需要判断有没有学习计划,课程是不是生效中),去求和该字段的数量即为一周总计划需要学习的,然后再查询第二步分页的信息,将分页的信息的记录取出。在根据前端的需要的字段,还需要返回对应的课程信息,以及已经学习的小节数量(此处不用mp去查询的原因是为了防止循环依赖),将上述字段填充到一个新的volist中,返回给前端所需要的分页数据即可
**/
@Override
@Transactional
public LearningPlanPageVO queryMyPlans(PageQuery pageQuery) {
LearningPlanPageVO result = new LearningPlanPageVO();
//1.获取当前用户id
Long userId = UserContext.getUser();
//2.获取本周的起始时间
LocalDate now = LocalDate.now();
LocalDateTime beginTime = DateUtils.getWeekBeginTime(now);
LocalDateTime endTime = DateUtils.getWeekEndTime(now);
//3.查询总的统计数据
//3.1本周已经学习的小节数量
Integer weekFinished = recordMapper.selectCount(new LambdaQueryWrapper<LearningRecord>()
.eq(LearningRecord::getUserId, userId)
.eq(LearningRecord::getFinished, true)
.gt(LearningRecord::getFinishTime, beginTime)
.lt(LearningRecord::getFinishTime, endTime));
result.setWeekFinished(weekFinished);
//3.2本周总的计划学习的小节数量
Integer weekTotalPlan = getBaseMapper().queryTotalPlan(userId);
result.setWeekTotalPlan(weekTotalPlan);
//3.3 TODO 本周学习积分
//4查询分页数据
//4.1 分页查询课表信息以及学习计划信息
Page<LearningLesson> page = lambdaQuery()
.eq(LearningLesson::getUserId, userId)
.eq(LearningLesson::getPlanStatus, PlanStatus.PLAN_RUNNING)
.in(LearningLesson::getStatus, LessonStatus.LEARNING, LessonStatus.NOT_BEGIN)
.page(pageQuery.toMpPage("latest_learn_time", false));
List<LearningLesson> records = page.getRecords();
if (CollUtils.isEmpty(records)){
return result.emptyPage(page);
}
//4.2 查询课表对应的课程信息
Map<Long, CourseSimpleInfoDTO> cMap = queryCourseSimpleInfo(records);
//4.3 统计每一个课程本周已经学习的小节数量
List<IdAndNumDTO> list = recordMapper.countLearnedSections(userId, beginTime, endTime);
Map<Long, Integer> countMap = IdAndNumDTO.toMap(list);
//4.4 封装数据
List< LearningPlanVO> voList = new ArrayList<>(records.size());
for (LearningLesson record : records) {
//1.直接拷贝属性
LearningPlanVO vo = BeanUtils.copyBean(record, LearningPlanVO.class);
//填充课程详细信息
CourseSimpleInfoDTO cInfo = cMap.get(record.getCourseId());
if (cInfo != null){
vo.setCourseName(cInfo.getName());
vo.setSections(cInfo.getSectionNum());
}
//每个课程本周已经学习的小节数量
vo.setWeekLearnedSections(countMap.getOrDefault(record.getId(),0));
voList.add(vo);
}
return result.pageInfo(page.getTotal(),page.getPages(),voList);
}
对应的mapper层如下:
/**
统计每一个课程已经学习的小节数量
**/
<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 > #{begin} and finish_time < #{end}
group by lesson_id
</select>
实现定时检查课程是否过期的接口
1.接口分析
首先要实现定时任务,需要去启动类上检查是否有定时任务的注解@EnableScheduling。如果通过创建定时任务类。在定时任务类中,首先要得到那些没有过期的课程表列表,只有这些才是有可能过期的。然后循环遍历该列表,如果过期了,重新设置状态,最后统一的交给mp去更新。
2.接口实现
以下是定时任务实现类:
/**
注意,需要将该类注入到容器当中,否则会不生效
**/
@Slf4j
@Component
@RequiredArgsConstructor
public class CheckCourseIsExpire {
private final ILearningLessonService service;
/**
* 检查课程状态的任务方法
* 该方法用于执行课程状态的检查操作,检查课程是否过期
*/
@Scheduled(cron = "0 * * * * ?") //一分钟执行一次,判断课程是否过期
private void CheckCourseStatusTask() {
LocalDateTime currentTime = LocalDateTime.now();
log.info("检查课程是否过期,当前时间为:{}", currentTime);
//首先要去学生课程表中取出数据,其里面就有过期时间
//取出当前状态且为未过期的数据
List<LearningLesson> learningLessonList = service.list(new LambdaQueryWrapper<LearningLesson>()
.ne(LearningLesson::getStatus, LessonStatus.EXPIRED));
//然后判断当前时间是否大于过期时间,如果大于则修改课程状态为已过期
for (LearningLesson list : learningLessonList) {
if (list.getExpireTime().isBefore(currentTime)) {
//设置状态为已过期
list.setStatus(LessonStatus.EXPIRED);
// service.updateById(list); 这里调用对性能影响大
}
}
service.updateBatchById(learningLessonList);
}
}
代码推送
可能会发生的情况:
将当前分支合并到master分支中会发生冲突,原因就是因为依赖代码生成器生成的Controller,service等等没有推送到本地仓库,使用git status可以发现有些文件没有添加,使用git add 即可添加文件到本地仓库,然后就可以将当前分支合并到master分支,后续使用master分支推送即可。
浙公网安备 33010602011771号