@FeignClient(contextId = "course", value = "course-service")
public interface CourseClient {/*** 根据老师id列表获取老师出题数据和讲课数据* @param teacherIds 老师id列表* @return 老师id和老师对应的出题数和教课数*/@GetMapping("/course/infoByTeacherIds")List<SubNumAndCourseNumDTO> infoByTeacherIds(@RequestParam("teacherIds") Iterable<Long> teacherIds);/*** 根据小节id获取小节对应的mediaId和课程id** @param sectionId 小节id* @return 小节对应的mediaId和课程id*/@GetMapping("/course/section/{id}")SectionInfoDTO sectionInfo(@PathVariable("id") Long sectionId);/*** 根据媒资Id列表查询媒资被引用的次数** @param mediaIds 媒资id列表* @return 媒资id和媒资被引用的次数的列表*/@GetMapping("/course/media/useInfo")List<MediaQuoteDTO> mediaUserInfo(@RequestParam("mediaIds") Iterable<Long> mediaIds);/*** 根据课程id查询索引库需要的数据** @param id 课程id* @return 索引库需要的数据*/@GetMapping("/course/{id}/searchInfo")CourseSearchDTO getSearchInfo(@PathVariable("id") Long id);/*** 根据课程id集合查询课程简单信息* @param ids id集合* @return 课程简单信息的列表*/@GetMapping("/courses/simpleInfo/list")List<CourseSimpleInfoDTO> getSimpleInfoList(@RequestParam("ids") Iterable<Long> ids);/*** 根据课程id,获取课程、目录、教师信息* @param id 课程id* @return 课程信息、目录信息、教师信息*/@GetMapping("/course/{id}")CourseFullInfoDTO getCourseInfoById(@PathVariable("id") Long id,@RequestParam(value = "withCatalogue", required = false) boolean withCatalogue,@RequestParam(value = "withTeachers", required = false) boolean withTeachers);
}
day02-我的课表
支付或报名课程后,监听到MQ通知,将课程加入课表。
除此以外,如果用户退款,也应该删除课表中的课程,这里同样是通过MQ通知来实现:
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 DEFAULT '0' COMMENT '课程状态,0-未学习,1-学习中,2-已学完,3-已失效',week_freq tinyint 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 DEFAULT NULL COMMENT '最近一次学习的小节id',latest_learn_time datetime 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),UNIQUE KEY idx_user_id (user_id,course_id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='学生课表';
package com.tianji.learning.mq;import cn.hutool.core.collection.CollUtil;
import com.tianji.api.dto.trade.OrderBasicDTO;
import com.tianji.common.constants.MqConstants;
import com.tianji.learning.service.ILearningLessonService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;@Component
@Slf4j
@RequiredArgsConstructor
public class LessonChangeListener {private final ILearningLessonService lessonService;/**** MQ消息发送相关代码:* rabbitMqHelper.send(* MqConstants.Exchange.ORDER_EXCHANGE, // Exchange* MqConstants.Key.ORDER_PAY_KEY, // Key* OrderBasicDTO.builder()* .orderId(orderId)* .userId(userId)* .courseIds(cIds)* .finishTime(order.getFinishTime())* .build()* );** @param dto 接受的参数类型为OrderBasicDTO*/@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "learning.lesson.pay.queue", durable = "true"),exchange = @Exchange(value = MqConstants.Exchange.ORDER_EXCHANGE, type = ExchangeTypes.TOPIC),key = MqConstants.Key.ORDER_PAY_KEY))public void onMsg(OrderBasicDTO dto) {log.info("LessonChangeListener接收消息,用户{},添加课程{}", dto.getUserId(), dto.getCourseIds());// 校验if (dto.getUserId() == null|| dto.getOrderId() == null|| CollUtil.isEmpty(dto.getCourseIds())) {// 这里是接受MQ消息,中断即可,若抛异常,则自动重试return;}// 保存课程到课表lessonService.addUserLesson(dto.getUserId(),dto.getCourseIds());}/*** 当用户退款成功时,取消相应课程*/@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "learning.lesson.refund.queue ",durable = "true"),exchange = @Exchange(value = MqConstants.Exchange.ORDER_EXCHANGE,type = ExchangeTypes.TOPIC ),key = MqConstants.Key.ORDER_REFUND_KEY))public void receiveMsg(OrderBasicDTO dto){log.info("LessonChangeListener接收消息,用户{},取消课程{}", dto.getUserId(), dto.getCourseIds());// 校验if (dto.getUserId() == null|| dto.getOrderId() == null|| CollUtil.isEmpty(dto.getCourseIds())) {// 这里是接受MQ消息,中断即可,若抛异常,则会开启重试return;}// 从课表中删除课程lessonService.deleteLessionById(dto.getUserId(),dto.getCourseIds());}
}
添加课程
package com.tianji.learning.service.impl;@SuppressWarnings("ALL")
@Service
@RequiredArgsConstructor
@Slf4j
public class LearningLessonServiceImpl extends ServiceImpl<LearningLessonMapper, LearningLesson> implements ILearningLessonService {private final CourseClient courseClient;@Override@Transactionalpublic void addUserLessons(Long userId, List<Long> courseIds) {// 1.查询课程有效期 通过Feign远程调用课程服务,得到课程信息List<CourseSimpleInfoDTO> cInfoList = courseClient.getSimpleInfoList(courseIds);if (CollUtils.isEmpty(cInfoList)) {// 课程不存在,无法添加log.error("课程信息不存在,无法添加到课表");return;}// 2.循环遍历,处理LearningLesson数据List<LearningLesson> list = new ArrayList<>(cInfoList.size());for (CourseSimpleInfoDTO cInfo : cInfoList) {LearningLesson lesson = new LearningLesson();// 2.1.获取过期时间Integer validDuration = cInfo.getValidDuration();if (validDuration != null && validDuration > 0) {LocalDateTime now = LocalDateTime.now();lesson.setCreateTime(now);lesson.setExpireTime(now.plusMonths(validDuration));}// 2.2.填充userId和courseIdlesson.setUserId(userId);lesson.setCourseId(cInfo.getId());list.add(lesson);}// 3.批量新增saveBatch(list);}
}
MQ
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在加入课表以后,用户就可以在个人中心查看到这些课程:
因此,这里就需要第二个接口
分页查询个人课程
用户信息存到ThreadLocal
jwt:头部+载体+签名
网关判断权限
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取请求request信息ServerHttpRequest request = exchange.getRequest();String method = request.getMethodValue();String path = request.getPath().toString();String antPath = method + ":" + path;// 2.判断请求路径是否在默认不拦截的路径中if(isExcludePath(antPath)){// 直接放行return chain.filter(exchange);}// 3.尝试获取用户信息List<String> authHeaders = exchange.getRequest().getHeaders().get(AUTHORIZATION_HEADER);String token = authHeaders == null ? "" : authHeaders.get(0);R<LoginUserDTO> r = authUtil.parseToken(token);// 4.如果用户是登录状态,尝试更新请求头,传递用户信息if(r.success()){exchange.mutate().request(builder -> builder.header(USER_HEADER, r.getData().getUserId().toString()))//验证通过后将请求头中的"authorization"改成"user_info".build();}// 5.校验权限authUtil.checkAuth(antPath, r);// 6.放行return chain.filter(exchange);
}private boolean isExcludePath(String antPath) {for (String pathPattern : authProperties.getExcludePath()) {if(antPathMatcher.match(pathPattern, antPath)){return true;}}return false;
}
拦截器
authsdk.resource.interceptors下:
public class UserInfoInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.尝试获取头信息中的用户信息String authorization = request.getHeader(JwtConstants.USER_HEADER);// 2.判断是否为空(非法用户 也让访问但是threadlocal中不保存)if (authorization == null) {return true;}// 3.转为用户id并保存try {Long userId = Long.valueOf(authorization);UserContext.setUser(userId);//保存到线程池return true;} catch (NumberFormatException e) {log.error("用户身份信息格式不正确,{}, 原因:{}", authorization, e.getMessage());return true;}}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 清理用户信息UserContext.removeUser();}
}
package com.tianji.common.utils;public class UserContext {private static final ThreadLocal<Long> TL = new ThreadLocal<>();/*** 保存用户信息*/public static void setUser(Long userId){TL.set(userId);}/*** 获取用户*/public static Long getUser(){return TL.get();}/*** 移除用户信息*/public static void removeUser(){TL.remove();}
}
分页查询我的课程
@Override
public PageDTO<LearningLessonVO> queryMyLessons(PageQuery query) {// 1.获取当前登录用户Long userId = UserContext.getUser();// 2.分页查询// select * from learning_lesson where user_id = #{userId} order by latest_learn_time limit 0, 5Page<LearningLesson> page = lambdaQuery().eq(LearningLesson::getUserId, userId) // where user_id = #{userId}.page(query.toMpPage("latest_learn_time", false));List<LearningLesson> records = page.getRecords();if (CollUtils.isEmpty(records)) {return PageDTO.empty(page);}// 3.查询课程信息Map<Long, CourseSimpleInfoDTO> cMap = queryCourseSimpleInfoList(records);// 4.封装VO返回List<LearningLessonVO> list = new ArrayList<>(records.size());// 4.1.循环遍历,把LearningLesson转为VOfor (LearningLesson r : records) {// 4.2.拷贝基础属性到voLearningLessonVO vo = BeanUtils.copyBean(r, LearningLessonVO.class);// 4.3.获取课程信息,填充到voCourseSimpleInfoDTO cInfo = cMap.get(r.getCourseId());vo.setCourseName(cInfo.getName());vo.setCourseCoverUrl(cInfo.getCoverUrl());vo.setSections(cInfo.getSectionNum());list.add(vo);}return PageDTO.of(page, list);
}private Map<Long, CourseSimpleInfoDTO> queryCourseSimpleInfoList(List<LearningLesson> records) {// 3.1.获取课程idSet<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;
}
作业
检查课程是否有效
public Long isLessonValid(Long courseId) {Long userId = UserContext.getUser();// 获取当前登录用户的userId// 校验用户课表中是否有该课程LearningLesson learningLesson = this.lambdaQuery().eq(LearningLesson::getUserId, userId).eq(LearningLesson::getCourseId, courseId).one();// 用户课表中没有该课程if (learningLesson == null) {// throw new BizIllegalException("该课程不在用户课表中");return null;}// 校验课程状态是否有效,即是否已过期,根据过期时间字段是否大于当前时间进行判断LocalDateTime expireTime = learningLesson.getExpireTime();// 当前时间晚于过期时间,已过期if (expireTime != null && LocalDateTime.now().isAfter(expireTime)) {// throw new BizIllegalException("该课程已过期");return null;}return learningLesson.getId();}
根据id查询指定课程的学习状态
- 对于已经购买的课程:展示为马上学习,并且显示学习的进度、有效期
- 对于未购买的课程:展示为立刻购买或加入购物车
public LearningLessonVO getLessonInfo(Long courseId) {// 获取当前登录用户的userIdLong userId = UserContext.getUser();// 校验用户课表中是否有该课程LearningLesson learningLesson = this.lambdaQuery().eq(LearningLesson::getUserId, userId).eq(LearningLesson::getCourseId, courseId).one();// 用户课表中没有该课程if (learningLesson == null) {// throw new BizIllegalException("该课程不在用户课表中");return null;}// 封装数据到voLearningLessonVO learningLessonVO = LearningLessonVO.builder().id(learningLesson.getId()).courseId(learningLesson.getCourseId()).status(learningLesson.getStatus()).learnedSections(learningLesson.getLearnedSections()).createTime(learningLesson.getCreateTime()).expireTime(learningLesson.getExpireTime()).planStatus(learningLesson.getPlanStatus()).build();return learningLessonVO;}
public LearningLessonVO now() {// 获取当前登录用户Long userId = UserContext.getUser();if (userId == null) {throw new BizIllegalException("用户未登录");}// 查询当前用户最近学习课表,降序排序取第一条, status为1表示学习中LearningLesson learningLesson = this.lambdaQuery().eq(LearningLesson::getUserId, userId).eq(LearningLesson::getStatus, 1).orderByDesc(LearningLesson::getLatestLearnTime).last("limit 1 ").one();if (learningLesson == null) {return null;}// 查询当前用户报名的课程数Integer courseAmount = this.lambdaQuery().eq(LearningLesson::getUserId, userId).count();// feign远程调用查询相关课程的课程名、封面url等CourseFullInfoDTO courseInfo = courseClient.getCourseInfoById(learningLesson.getCourseId(), false, false);if (Objects.isNull(courseInfo)) {throw new BizIllegalException("课程不存在");}// feign远程调用查询相关小节的小节名称,小节编号List<CataSimpleInfoDTO> catalogueInfoList = catalogueClient.batchQueryCatalogue(List.of(learningLesson.getLatestSectionId()));if (CollUtil.isEmpty(catalogueInfoList)) {throw new BizIllegalException("最新学习小节不存在");}// 传参的小节id只有一个,所以可直接使用下标0CataSimpleInfoDTO catalogueInfo = catalogueInfoList.get(0);// 将po数据封装到voLearningLessonVO learningLessonVO = new LearningLessonVO();BeanUtil.copyProperties(learningLesson, learningLessonVO);learningLessonVO.setCourseAmount(courseAmount); // 课程数量learningLessonVO.setCourseName(courseInfo.getName()); // 最近学习课程名称learningLessonVO.setCourseCoverUrl(courseInfo.getCoverUrl()); // 最近学习课程封面learningLessonVO.setSections(courseInfo.getSectionNum()); // 最近学习课程的章节数// 最近学习的小节id和小节名称learningLessonVO.setLatestSectionName(catalogueInfo.getName());learningLessonVO.setLatestSectionIndex(catalogueInfo.getCIndex());// 返回封装的voreturn learningLessonVO;}
DAY3
学习计划和进度
保存用户播放到哪里:
learn_lesson有latest_section_id bigint DEFAULT NULL COMMENT ‘最近一次学习的小节id’,
CREATE TABLE IF NOT EXISTS `learning_record` (`id` bigint NOT NULL COMMENT '学习记录的id',`lesson_id` bigint NOT NULL COMMENT '对应课表的id',`section_id` bigint NOT NULL COMMENT '对应小节的id',`user_id` bigint NOT NULL COMMENT '用户id',`moment` int DEFAULT '0' COMMENT '视频的当前观看时间点,单位秒',`finished` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否完成学习,默认false',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '第一次观看时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间(最近一次观看时间)',PRIMARY KEY (`id`) USING BTREE,KEY `idx_update_time` (`update_time`) USING BTREE,KEY `idx_user_id` (`user_id`) USING BTREE,KEY `idx_lesson_id` (`lesson_id`,`section_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学习记录表';
DTO:接收前端参数或者返回时候 用的
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;
}
想循环引用的话,不用注入service,可以注入它的下一层Mapper
查询学习记录
public LearningLessonDTO queryLearningRecordByCourse(Long courseId) {Long userId = UserContext.getUser();// 根据用户userId和课程courseId获取最近学习的小节id和课表idLearningLesson learningLesson = learningLessonService.lambdaQuery().eq(LearningLesson::getCourseId, courseId).eq(LearningLesson::getUserId, userId).one();if (Objects.isNull(learningLesson)) {throw new BizIllegalException("该课程未加入课表");}// 根据课表id获取学习记录List<LearningRecord> learningRecordList = this.lambdaQuery().eq(LearningRecord::getLessonId, learningLesson.getId()).list();// copyToList有判空校验,不再赘余List<LearningRecordDTO> learningRecordDTOList = BeanUtil.copyToList(learningRecordList, LearningRecordDTO.class);LearningLessonDTO learningLessonDTO = new LearningLessonDTO();learningLessonDTO.setId(learningLesson.getId());learningLessonDTO.setLatestSectionId(learningLesson.getLatestSectionId());learningLessonDTO.setRecords(learningRecordDTOList);return learningLessonDTO;}
提交学习记录
保存用户播放到哪里,续播
参数校验:validator
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
续播如何精确到分钟秒的? 有上一次学习时间
.set(!finished, LearningLesson::getLatestLearnTime, recordDTO.getCommitTime())
创建学习计划
首先,要做到切换设备后还能续播,用户的播放进度必须保存在服务端,而不是客户端。
其次,用户突然断开或者切换设备,续播的时间误差不能超过30秒,那播放进度的记录频率就需要比较高。我们会在前端每隔15秒就发起一次心跳请求,提交最新的播放进度,记录到服务端。这样用户下一次续播时直接读取服务端的播放进度,就可以将时间误差控制在15秒左右。
分页查询我的学习计划
/*** 分页查询我的学习计划** @param pageQuery 分页参数*/@Overridepublic LearningPlanPageVO queryMyPlans(PageQuery pageQuery) {Long userId = UserContext.getUser();if (Objects.isNull(userId)) {throw new BizIllegalException("用户未登录");}// 查询用户正在进行的课表Page<LearningLesson> learningLessonPage = lambdaQuery().eq(LearningLesson::getUserId, userId).eq(LearningLesson::getPlanStatus, PlanStatus.PLAN_RUNNING).in(LearningLesson::getStatus, LessonStatus.NOT_BEGIN, LessonStatus.LEARNING).page(pageQuery.toMpPage("latest_learn_time", false));// 判断用户是否有正在上的课if (CollUtil.isEmpty(learningLessonPage.getRecords())) {// 返回空数据LearningPlanPageVO emptyVO = new LearningPlanPageVO();emptyVO.setTotal(0L);emptyVO.setPages(0L);emptyVO.setList(Collections.emptyList());return emptyVO;}// TODO 实现本周学习积分,暂未实现,默认0// 查询课表相关的课程信息并封装到MapMap<Long, CourseSimpleInfoDTO> simpleInfoDTOMap = getLongCourseSimpleInfoDTOMap(learningLessonPage);// 封装到VOreturn getPlanPageVO(learningLessonPage, simpleInfoDTOMap);}
/*** 数据封装*/
private LearningPlanPageVO getPlanPageVO(Page<LearningLesson> learningLessonPage, Map<Long, CourseSimpleInfoDTO> simpleInfoDTOMap) {// 遍历课表List<LearningPlanVO> learningPlanVOList = learningLessonPage.getRecords().stream().map(learningLesson -> {// 从课程map中取出相应的课程信息CourseSimpleInfoDTO courseSimpleInfoDTO = simpleInfoDTOMap.get(learningLesson.getCourseId());//LearningPlanVO learningPlanVO = BeanUtils.copyBean(learningLesson, LearningPlanVO.class);LearningPlanVO learningPlanVO = LearningPlanVO.builder().courseId(learningLesson.getCourseId()) // 课程id.weekFreq(learningLesson.getWeekFreq()) // 本周计划学习数量.learnedSections(learningLesson.getLearnedSections()) // 已学习小节数量.latestLearnTime(learningLesson.getLatestLearnTime()) // 最近一次学习时间.build();if (courseSimpleInfoDTO != null) {// 赋值课程名和总小节数量属性learningPlanVO.setCourseName(courseSimpleInfoDTO.getName()); // 课程名learningPlanVO.setSections(courseSimpleInfoDTO.getSectionNum()); // 课程总小节数量}// 查询该课程本周已学完的小节数LocalDate now = LocalDate.now();LocalDateTime weekBeginTime = DateUtils.getWeekBeginTime(now);LocalDateTime weekEndTime = DateUtils.getWeekEndTime(now);// 避免循环依赖,用Mapper不用service// 查询该课程本周已学习的小节数量:即查一周内该用户该课程有多少条学习记录Integer weekLearnedSections = learningRecordMapper.getWeekLearnedSections(learningLesson.getId(),weekBeginTime,weekEndTime);// 封装到planVOlearningPlanVO.setWeekLearnedSections(weekLearnedSections);return learningPlanVO;}).collect(Collectors.toList());// 累加计算本周计划完成的小节数和已学完的小节数量Integer weekFinished = 0; // 本周已学完小节数量Integer weekTotalPlan = 0; // 本周计划学习小节数量for (LearningPlanVO learningPlanVO : learningPlanVOList) {weekFinished += learningPlanVO.getWeekLearnedSections();weekTotalPlan += learningPlanVO.getWeekFreq();}LearningPlanPageVO planPageVO = LearningPlanPageVO.builder().weekFinished(weekFinished).weekTotalPlan(weekTotalPlan)// TODO 学习积分暂为0.weekPoints(0).build();return planPageVO.pageInfo(learningLessonPage.getTotal(), learningLessonPage.getPages(), learningPlanVOList);
}/*** 查询课表相关的课程信息并封装到Map*/
private Map<Long, CourseSimpleInfoDTO> getLongCourseSimpleInfoDTOMap(Page<LearningLesson> learningLessonPage) {List<Long> courseIds = learningLessonPage.getRecords().stream().map(LearningLesson::getCourseId).collect(Collectors.toList());List<CourseSimpleInfoDTO> simpleInfoList = courseClient.getSimpleInfoList(courseIds);// 校验课表相关的课程信息if (CollUtil.isEmpty(simpleInfoList)) {throw new BizIllegalException("未查询到课表中相关课程");}// 封装为map,方便后面取出,空间换时间Map<Long, CourseSimpleInfoDTO> simpleInfoDTOMap = simpleInfoList.stream().collect(Collectors.toMap(CourseSimpleInfoDTO::getId, courseSimpleInfoDTO -> courseSimpleInfoDTO));return simpleInfoDTOMap;
}
基础知识
使用 Builder 模式可以让对象的创建更加清晰和灵活,避免了传统构造函数参数过多时的复杂性和可读性问题。在代码中使用 .builder() 方法是一种简洁的方式来创建对象,同时可以提高代码的可维护性和可扩展性。
LearningPlanPageVO planPageVO = LearningPlanPageVO.builder().weekFinished(weekFinished).weekTotalPlan(weekTotalPlan).weekPoints(0).build();
// LearningPlanVO learningPlanVO = BeanUtils.copyBean(learningLesson, LearningPlanVO.class);
LearningPlanVO learningPlanVO = LearningPlanVO.builder().courseId(learningLesson.getCourseId()) // 课程id.weekFreq(learningLesson.getWeekFreq()) // 本周计划学习数量.learnedSections(learningLesson.getLearnedSections()) // 已学习小节数量.latestLearnTime(learningLesson.getLatestLearnTime()) // 最近一次学习时间.build();
3.1.课程过期
编写一个SpringTask定时任务,定期检查learning_lesson表中的课程是否过期,如果过期则将课程状态修改为已过期。
启动类上加@EnableScheduling // 开启定时任务
task上加@Scheduled(cron = “0 0 3 1 * ?”) //秒分时日月周年
3.2.方案思考
思考题:思考一下目前提交学习记录功能可能存在哪些问题?有哪些可以改进的方向?