开发流程
今天要实现的业务是学习计划和学习进度。
首先是需求分析统计需要的接口;
进一步对接口详细分析,得到接口“四个要素”;
基于需求和接口分析对数据库字段分析,创建一个新的表格,关于学习记录的表格;
创建分支开发,基于MP创建代码,引入枚举简化代码。
实现接口
提交学习记录
这个业务逻辑比较麻烦,需要分情况讨论;当课程小节为考试,考完就是完成学习了;当课程为视频,需要进行实时反馈,当视频第一次播放超过总时长的一半才算完成学习了;
总框架的提交学习记录的代码:
@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 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;
}
提交学习记录的代码:
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();
}
查询学习计划进度
这个业务也比较麻烦,返回的参数比较多。但是分页的数量比较少,所以可以不分页。
物理分页:
先调用相应的mapper统计数据,计划学习课程调用lessonmapper,已经学习的课程的调用recordsnapper;物理分页;对分页的数据获得课程详细信息;再次调用mapper获得每一个课程的已经看过的数据进行统计;然后组装返回数据;
@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);
}
不分页,stream流统计:
先找到有学习计划的课程,然后找到已经学习的课程,两次查询,然后基于查询结果统计数目,最后基于文件流进行分页,对分页的数据查找课程详细信息,组装数据返回结果;
@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<LearningRecord> learnedRecords = recordMapper.selectList(new QueryWrapper<LearningRecord>().lambda()
.eq(LearningRecord::getUserId, userId)
.eq(LearningRecord::getFinished, true)
.gt(LearningRecord::getFinishTime, begin)
.lt(LearningRecord::getFinishTime, end)
);
Map<Long, Long> countMap = learnedRecords.stream()
.collect(Collectors.groupingBy(LearningRecord::getLessonId, Collectors.counting()));
// 5.查询总的统计数据
// 5.1.本周总的已学习小节数量
int weekFinished = learnedRecords.size();
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;
}
// 6.2.查询课表对应的课程信息
Map<Long, CourseSimpleInfoDTO> cMap = queryCourseInfo(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(), 0L).intValue());
voList.add(vo);
}
return result.pageInfo(p.getTotal(), p.getPages(), voList);
}
不分页更简单,只需要调用一次mapper实现对已经学习的课程的数据库的访问。
课后作业
课程过期
思路:
在Spring框架中,可以使用@Scheduled
注解来创建定时任务。
首先,确保你的Spring Boot项目已经添加了Spring Boot的spring-boot-starter-data-jpa
依赖,以及数据库连接的依赖。
然后,你可以创建一个服务类,比如叫LessonService
,用于处理业务逻辑,比如检查课程是否过期和更新状态
(我感觉就是在Learning那个service里面编写一个方法,然后在定时调用这个方法)
(请求方式:update,请求路径:无,请求参数:无,写实现代码自己调用用户信息id,返回参数,无)
import org.springframework.stereotype.Service;
@Service
public class LearningLessonService {
// 假设有一个repository来访问数据库
private final LessonRepository lessonRepository;
public LessonService(LessonRepository lessonRepository) {
this.lessonRepository = lessonRepository;
}
// 检查并更新过期课程的方法
public void checkAndUpdateExpiredLessons() {
// 这里写检查逻辑,比如找出所有过期的课程
// 然后更新它们的状态
}
}
这里这个检查逻辑就是课表里面的课程的过期时间和当前时间进行比对,然后对课程的状态进行修改;
创建一个定时任务的类,比如叫LessonScheduler
,使用@Scheduled
注解来设置执行计划
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class LessonScheduler {
private final LearningLessonService lessonService;
public LearningLessonScheduler(LessonService lessonService) {
this.lessonService = lessonService;
}
// 使用cron表达式设置定时任务的执行计划
// 例如,每天凌晨1点执行
@Scheduled(cron = "0 0 1 * * ?")
public void scheduledCheckLessons() {
lessonService.checkAndUpdateExpiredLessons();
}
}
最后,确保你的Spring Boot应用类或者配置类上使用了@EnableScheduling
注解,以启用定时任务的支持。