天机学堂笔记1

@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.方案思考​
思考题:思考一下目前提交学习记录功能可能存在哪些问题?有哪些可以改进的方向?​

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/67007.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NLP中常见的分词算法(BPE、WordPiece、Unigram、SentencePiece)

文章目录 一、基本概念二、传统分词方法2.1 古典分词方法2.2 拆分为单个字符 三、基于子词的分词方法&#xff08;Subword Tokenization&#xff09;3.1 主要思想3.2 主流的 Subword 算法3.3 Subword 与 传统分词方法的比较 四、Byte Pair Encoding (BPE)4.1 主要思想4.2 算法过…

搭建Hadoop分布式集群

软件和操作系统版本 Hadoop框架是采用Java语言编写&#xff0c;需要java环境&#xff08;jvm&#xff09; JDK版本&#xff1a;JDK8版本 &#xff0c;本次使用的是 Java: jdk-8u431-linux-x64.tar.gz Hadoop: hadoop-3.3.6.tar.gz 三台Linux虚拟节点: CentOS-7-x86_64-DVD-2…

分享一下使用高版本(3.10.x)sonar-maven-plugin 进行构建时需要注意的两个问题

SonarScanner用来执行源代码分析。这个独立的程序在CI/CD主机上运行&#xff0c;并将分析结果发送到SonarQube服务器&#xff0c;由其计算分析结果&#xff0c;计算质量门并生成报告。我们可以通过命令行和maven构建两种方式来执行SonarScanner的源码分析。关于SonarScanner更多…

C语言---函数和数组实践:扫雷游戏

函数和数组实践&#xff1a;扫雷游戏 在这次的实践项目中&#xff0c;需要编写一个可以在在控制台运行的经典的扫雷游戏。 一、游戏要求 游戏有菜单&#xff0c;可以通过菜单实现继续玩或者退出游戏游戏要求棋盘9*9&#xff0c;雷&#xff08;10个&#xff09;要求随机布置可…

数据结构与算法之二叉树: LeetCode 701. 二叉搜索树中的插入操作 (Ts版)

二叉搜索树中的插入操作 https://leetcode.cn/problems/insert-into-a-binary-search-tree/description/ 描述 给定二叉搜索树&#xff08;BST&#xff09;的根节点 root 和要插入树中的值 value &#xff0c;将值插入二叉搜索树返回插入后二叉搜索树的根节点。 输入数据 保…

数据集-目标检测系列- 石榴 检测数据集 pomegranate >> DataBall

数据集-目标检测系列- 石榴 检测数据集 pomegranate >> DataBall DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;持续增加中。 需要更多数据资源和技术解决方案&#xff0c;知识星球&#xff1a; “DataBall - X 数据球(fre…

项目实战——使用python脚本完成指定OTA或者其他功能的自动化断电上电测试

前言 在嵌入式设备的OTA场景测试和其他断电上电测试过程中&#xff0c;有的场景发生在夜晚或者随时可能发生&#xff0c;这个时候不可能24h人工盯着&#xff0c;需要自动化抓取串口日志处罚断电上电操作。 下面的python脚本可以实现自动抓取串口指定关键词&#xff0c;然后触发…

深入详解DICOM医学影像定位线相关知识:理解定位线的概念、定位线的作用以及定位线显示和计算原理

DICOM医学影像中的定位线(Localization Line) 在医学影像学中,DICOM是用于存储和交换医学影像的标准格式。定位线(Localization Line)在医学影像的显示和分析中起着重要作用,它帮助医生和医学专业人员在影像中精确地标定重要的解剖结构、区域或特征,辅助进行定位、治疗计…

《自动驾驶与机器人中的SLAM技术》ch8:基于 IESKF 的紧耦合 LIO 系统

目录 基于 IESKF 的紧耦合 LIO 系统 1 IESKF 的状态变量和运动过程 1.1 对名义状态变量的预测 1.2 对误差状态变量的预测及对协方差矩阵的递推 2 观测方程中的迭代过程 3 高维观测中的等效处理 4 NDT 和 卡尔曼滤波的联系 5 紧耦合 LIO 系统的主要流程 5.1 IMU 静止初始化 …

HTML实战课堂之简单的拜年程序

一、目录&#xff1a; &#xfffc;&#xfffc; 一、目录&#xff1a; 二、祝福 三&#xff1a;代码讲解 &#xff08;1&#xff09;详细解释&#xff1a; 1.HTML部分 2. CSS部分 三、运行效果&#xff08;随机截图&#xff09;&#xff1a; 四、完整代码&#xff1a; 二、祝福…

vue 与 vue-json-viewer 实现 JSON 数据可视化

前言 接口的调试和测试是确保系统稳定性的重要步骤。为了让开发人员和测试人员能够直观地查看接口返回的 JSON 数据&#xff0c;使用合适的工具至关重要。vue-json-viewer 插件为 vue 开发者提供了一个简单而强大的解决方案。本文将详细介绍如何在 vue 项目中使用该插件&#x…

用Pygame Zero 画矩形(空心、实心、多个矩形、多层同心矩形、彩虹条矩形、条纹相间、随机颜色矩形、特殊效果、渐变效果)

用Pygame Zero 画矩形 &#xff08;空心、实心、多个矩形、多层同心矩形、彩虹条矩形、条纹相间、随机颜色矩形、特殊效果、渐变效果&#xff09; 本文目录&#xff1a; 零、时光宝盒 一、绘制空心矩形 二、绘制实心矩形 三、画多个静止矩形 四、绘制多层同心矩形 4.1、…

【Rust自学】11.9. 单元测试

喜欢的话别忘了点赞、收藏加关注哦&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 11.9.1. 测试的分类 Rust把测试分为两类&#xff0c;一个是单元测试&#xff0c;一个是集成测试。 单元测试比较小也比较专注&#xff…

[java基础-集合篇]优先队列PriorityQueue结构与源码解析

优先队列PriorityQueue 优先级队列表示为平衡二进制堆&#xff1a; queue[n] 的两个子级是 queue[2*n1] 和 queue[2*&#xff08;n1&#xff09;]。 注&#xff1a;左子节点index2*parentIndex1,右子节点index2*parentIndex2,源码中计算parent位置时就是这样反过来计算的 优…

回归预测 | MATLAB实RVM-Adaboost相关向量机集成学习多输入单输出回归预测

回归预测 | MATLAB实RVM-Adaboost相关向量机集成学习多输入单输出回归预测 目录 回归预测 | MATLAB实RVM-Adaboost相关向量机集成学习多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 RVM-Adaboost相关向量机集成学习多输入单输出回归预测是一种先进…

Linux(Centos7)安装Mysql/Redis/MinIO

安装Mysql 安装Redis 搜索Redis最先版本所在的在线安装yum库 查看以上两个组件是否是开机自启 安装MinIO 开源的对象存储服务&#xff0c;存储非结构化数据&#xff0c;兼容亚马逊S3协议。 minio --help #查询命令帮助minio --server --help #查询--server帮助minio serve…

MySQL批量修改数据表编码及字符集为utf8mb4

​​​​​​MySQL批量修改数据表编码及字符集为utf8mb4 utf8mb4编码是utf8编码的超集&#xff0c;兼容utf8&#xff0c;并且能存储4字节的表情字符。 采用utf8mb4编码的好处是&#xff1a;存储与获取数据的时候&#xff0c;不用再考虑表情字符的编码与解码问题。 更改数据库…

Edge浏览器网页设置深色模式/暗模式

文章目录 需求分析1. 浏览器中的设置——外观——深色。2. 在Edge浏览器的地址栏如下网址&#xff1a;edge://flags/&#xff0c;直接搜索Dark则有内容弹出&#xff0c;将Default更改为Enable即可设置成功。3. 成果 需求 长期对着电脑屏幕&#xff0c;白色实在太刺眼&#xff…

TypeScript Jest 单元测试 搭建

NPM TypeScript 项目搭建 创建目录 mkdir mockprojectcd mockproject初始化NPM项目 npm init -y安装TypeScript npm i -D typescript使用VSCode 打开项目 创建TS配置文件tsconfig.json {"compilerOptions": {"target": "es5","module&…

基于FPGA的多功能数字钟设计

基于FPGA的多功能数字钟设计 前言基础知识按键数码管 系统概述按键使用说明模块描述模块设计button_debouncebutton_controllerclock_controllerdigital_tube 整体资源用量测试视频 前言 本工程主要是数码管、按键、LED的应用开发。 注&#xff1a;本工程所有IP均使用源码开发…