天机学堂笔记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
    @Transactional
    public 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和courseId
            lesson.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 {

    @Override
    public 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;
        }
    }

    @Override
    public 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, 5
    Page<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转为VO
    for (LearningLesson r : records) {
        // 4.2.拷贝基础属性到vo
        LearningLessonVO vo = BeanUtils.copyBean(r, LearningLessonVO.class);
        // 4.3.获取课程信息,填充到vo
        CourseSimpleInfoDTO 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.获取课程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;
}

作业

检查课程是否有效

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) {
        // 获取当前登录用户的userId
        Long userId = UserContext.getUser();
        // 校验用户课表中是否有该课程
        LearningLesson learningLesson = this.lambdaQuery().eq(LearningLesson::getUserId, userId)
                .eq(LearningLesson::getCourseId, courseId).one();
        // 用户课表中没有该课程
        if (learningLesson == null) {
            // throw new BizIllegalException("该课程不在用户课表中");
            return null;
        }
        // 封装数据到vo
        LearningLessonVO 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只有一个,所以可直接使用下标0
        CataSimpleInfoDTO catalogueInfo = catalogueInfoList.get(0);
        // 将po数据封装到vo
        LearningLessonVO 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());
        // 返回封装的vo
        return 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和课表id
        LearningLesson 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 分页参数
     */
    @Override
    public 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
        // 查询课表相关的课程信息并封装到Map
        Map<Long, CourseSimpleInfoDTO> simpleInfoDTOMap = getLongCourseSimpleInfoDTOMap(learningLessonPage);
        // 封装到VO
        return 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);
        // 封装到planVO
        learningPlanVO.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.方案思考​
思考题:思考一下目前提交学习记录功能可能存在哪些问题?有哪些可以改进的方向?​

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

lobechat搭建本地知识库

本文中&#xff0c;我们提供了完全基于开源自建服务的 Docker Compose 配置&#xff0c;你可以直接使用这份配置文件来启动 LobeChat 数据库版本&#xff0c;也可以对之进行修改以适应你的需求。 我们默认使用 MinIO 作为本地 S3 对象存储服务&#xff0c;使用 Casdoor 作为本…

沸点 | 聚焦嬴图Cloud V2.1:具备水平可扩展性+深度计算的云原生嬴图动力站!

近日&#xff0c;嬴图正式推出嬴图Cloud V2.1&#xff0c;此次发布专注于提供无与伦比的用户体验&#xff0c;包括具有水平可扩展性的嬴图Powerhouse的一键部署、具有灵活定制功能的管理控制台、VPC / 专用链接等&#xff0c;旨在满足用户不断变化需求的各项前沿功能&#xff0…

Linux---shell脚本练习

要求&#xff1a; 1、shell 脚本写出检测 /tmp/size.log 文件如果存在显示它的内容&#xff0c;不存在则创建一个文件将创建时间写入。 2、写一个 shel1 脚本,实现批量添加 20个用户,用户名为user01-20,密码为user 后面跟5个随机字符。 3、编写个shel 脚本将/usr/local 日录下…

LiveNVR监控流媒体Onvif/RTSP常见问题-二次开发接口jquery调用示例如何解决JS|axios调用接口时遇到的跨域问题

LiveNVR二次开发接口jquery调用示例如何解决JS|axios调用接口时遇到的跨域问题 1、接口调用示例2、JS调用遇到跨域解决示例3、axios请求接口遇到跨域问题3.1、post请求3.2、get请求 4、RTSP/HLS/FLV/RTMP拉流Onvif流媒体服务 1、接口调用示例 下面是完整的 jquery 调用示例 $.a…

Canvas简历编辑器-选中绘制与拖拽多选交互方案

Canvas简历编辑器-选中绘制与拖拽多选交互方案 在之前我们聊了聊如何基于Canvas与基本事件组合实现了轻量级DOM&#xff0c;并且在此基础上实现了如何进行管理事件以及多层级渲染的能力设计。那么此时我们就依然在轻量级DOM的基础上&#xff0c;关注于实现选中绘制与拖拽多选交…

服务器数据恢复—raid5故障导致上层ORACLE无法启动的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 一台服务器上的8块硬盘组建了一组raid5磁盘阵列。上层安装windows server操作系统&#xff0c;部署了oracle数据库。 raid5阵列中有2块硬盘的硬盘指示灯显示异常报警。服务器操作系统无法启动&#xff0c;ORACLE数据库也无法启动。 服…

LabVIEW光流算法的应用

该VI展示了如何使用NI Vision Development Module中的光流算法来计算图像序列中像素的运动矢量。通过该方法&#xff0c;可以实现目标跟踪、运动检测等功能&#xff0c;适用于视频处理、机器人视觉和监控领域。程序采用模块化设计&#xff0c;包含图像输入、算法处理、结果展示…

Redis十大数据类型详解

Redis&#xff08;一&#xff09; 十大数据类型 redis字符串&#xff08;String&#xff09; string是redis最基本的类型&#xff0c;一个key对应一个value string类型是二进制安全的&#xff0c;意思是redis的string可以包含任何数据。例如说是jpg图片或者序列化对象 一个re…

Navicat Premium 16.0.90 for Mac 安装与free使用

步骤 0.下载 通过网盘分享的文件&#xff1a;Navicat Premium 16.0.90 链接: https://pan.baidu.com/s/12O22rXa9MiBPKKTGMELNIg 提取码: yyds 1.打开下好的 dmg 文件 (这个界面不要关闭&#xff09; 2.将Navicat Premium 拖动至 Applications 这时出现 点击取消。 3.点开…

基于Springboot + vue实现的购物推荐网站

&#x1f942;(❁◡❁)您的点赞&#x1f44d;➕评论&#x1f4dd;➕收藏⭐是作者创作的最大动力&#x1f91e; &#x1f496;&#x1f4d5;&#x1f389;&#x1f525; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;欢迎留言讨论 &#x1f525;&#x1f525;&…

【大数据】机器学习-----最开始的引路

以下是关于机器学习的一些基本信息&#xff0c;包括基本术语、假设空间、归纳偏好、发展历程、应用现状和代码示例&#xff1a; 一、基本术语 样本&#xff08;Sample&#xff09;&#xff1a; 也称为实例&#xff08;Instance&#xff09;或数据点&#xff08;Data Point&…

【WPS】【WORDEXCEL】【VB】实现微软WORD自动更正的效果

1. 代码规范方面 添加 Option Explicit&#xff1a;强制要求显式声明所有变量&#xff0c;这样可以避免因变量名拼写错误等情况而出现难以排查的逻辑错误&#xff0c;提高代码的健壮性。使用 On Error GoTo 进行错误处理&#xff1a;通过设置错误处理机制&#xff0c;当代码执行…

No one knows regex better than me

No one knows regex better than me 代码分析&#xff0c;传了两个参数zero,first&#xff0c;然后$second对两个所传的参数进行了拼接 好比&#xff1a;?zero1&first2 传入后就是: 12 然后对$second进行了正则匹配&#xff0c;匹配所传入的参数是否包含字符串Yeedo|wa…

Docker 安装开源的IT资产管理系统Snipe-IT

一、安装 1、创建docker-compose.yaml version: 3services:snipeit:container_name: snipeitimage: snipe/snipe-it:v6.1.2restart: alwaysports:- "8000:80"volumes:- ./logs:/var/www/html/storage/logsdepends_on:- mysqlenv_file:- .env.dockernetworks:- snip…

【RDMA】 ZTR(Zero Touch RoCE)技术(无需配置PFC和ECN)

目录 什么是Zero Touch RoCE&#xff08;ZTR&#xff09; 硬件和软件需求 使用方式 实现机制 ZTR-RTTCC 的工作原理 ZTR -RTTCC性能 官方文档 什么是Zero Touch RoCE&#xff08;ZTR&#xff09; Zero Touch RoCE&#xff08;ZTR&#xff09;技术是NVIDIA开发的一种创新…

【python】OpenCV—Local Translation Warps

文章目录 1、功能描述2、原理分析3、代码实现4、效果展示5、完整代码6、参考 1、功能描述 利用液化效果实现瘦脸美颜 交互式的液化效果原理来自 Gustafsson A. Interactive image warping[D]. , 1993. 2、原理分析 上面描述很清晰了&#xff0c;鼠标初始在 C&#xff0c;也即…

大疆上云API基于源码部署

文章目录 大疆上云API基于源码部署注意事项1、学习官网2、环境准备注意事项3、注册成为DJI开发者4、下载前后端运行所需要的包/依赖前端依赖下载后端所需要的Maven依赖包 用到的软件可以在这里下载5、MySQL数据库安装安装MySQL启动MySQL服务在IDEA中配置MySQL的连接信息 6、Red…

AI学习路线图-邱锡鹏-神经网络与深度学习

1 需求 神经网络与深度学习 2 接口 3 示例 4 参考资料

行业案例:高德服务单元化方案和架构实践

目录 为什么要做单元化 高德单元化的特点 高德单元化实践 服务单元化架构 就近接入实现方案 路由表设计 路由计算 服务端数据驱动的单元化场景 总结 系列阅读 为什么要做单元化 单机房资源瓶颈 随着业务体量和服务用户群体的增长,单机房或同城双机房无法支持服…

【计算机网络】lab7 TCP协议

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;计算机网络_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 实验目的…