天机学堂笔记1-网关拦截器获取用户信息保存到ThreadLocal

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

在这里插入图片描述

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

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

相关文章

OpenAI推出首个AI Agent!日常事项自动化处理!

2025 年1月15日&#xff0c;OpenAI 正式宣布推出一项名为Tasks的测试版功能 。 该功能可以根据你的需求内容和时间实现自动化处理。比方说&#xff0c;你可以设置每天早晨 7 点获取天气预报&#xff0c;或定时提醒遛狗等日常事项。 看到这里&#xff0c;有没有一种熟悉的感觉&a…

关于Nvidia显卡在windows系统下存在部分软件屏闪/闪烁问题与解决方法

问题描述 部分软件GUI在使用时一直闪烁/闪屏&#xff0c;包括拖动侧栏与切换子页面时会留下残影。本人遇到发生该现象的桌面应用包括且不限于docker desktop, meta quest link, Vortex, Tabby Terminal等。 本人环境&#xff1a; 操作系统&#xff1a;windows11 显卡&#xf…

vue2配置跨域后请求的是本机

这个我来说明一下&#xff0c;因为我们公司的后端设置解决了跨域问题&#xff0c;所以我有很久没有看相关的内容了&#xff0c;然后昨天请求了需要跨域的接口&#xff0c;请求半天一直不对&#xff0c;浏览器显示的是本机地址&#xff0c;我以为是自己配置错了&#xff0c;后面…

[Qualcomm]Qualcomm MDM9607 SDK代码下载操作说明

登录Qualcomm CreatePoing Qualcomm CreatePointhttps://createpoint.qti.qua

ORB-SLAM2源码学习: Frame.cc: cv::Mat Frame::UnprojectStereo将某个特征点反投影到三维世界坐标系中

前言 这个函数是在跟踪线程中更新上一帧的函数中被调用。 1.函数声明 cv::Mat Frame::UnprojectStereo(const int &i) 2.函数定义 1.获取这个特征点的深度值。 const float z mvDepth[i];深度值由双目或 RGB-D 传感器获取。 在双目情况下&#xff0c;这个深度来自…

基于Piquasso的光量子计算机的模拟与编程

一、引言 在科技飞速发展的当下,量子计算作为前沿领域,正以前所未有的态势蓬勃崛起。它凭借独特的量子力学原理,为解决诸多经典计算难以攻克的复杂问题提供了全新路径。从优化物流配送网络,以实现资源高效调配,到药物分子结构的精准模拟,加速新药研发进程;从金融风险的…

本地部署Web-Check网站检测与分析利器并实现远程访问实时监测

文章目录 前言1.关于Web-Check2.功能特点3.安装Docker4.创建并启动Web-Check容器5.本地访问测试6.公网远程访问本地Web-Check7.内网穿透工具安装8.创建远程连接公网地址9.使用固定公网地址远程访问 前言 本文我们将详细介绍如何在Ubuntu系统上使用Docker部署Web-Check&#xf…

电子杂志制作平台哪个好

​作为一个热爱分享的人&#xff0c;我试过了好几个平台&#xff0c;终于找到了几款比较好用得电子杂志制作平台&#xff0c;都是操作界面很简洁&#xff0c;上手非常快的工具。 FLBOOK:这是一款在线制作H5电子画册软件&#xff0c;提供了各种类型的模板&#xff0c;可支持添加…

八 rk3568 android11 AP6256 蓝牙调试

一 经典蓝牙 经典蓝牙默认可以工作, 验证可以连接 蓝牙鼠标,键盘, 连接手机等等, 在 系统设置里打开蓝牙 ,扫描设备,配对连接即可。 注: 连接 ANDROID 手机的坑 1 手机连接之后空闲状态会断开 ,变成 配对的设备不是已连接,是正常,使用时又会自动 连接 2 手机传…

STM32-串口-UART-Asynchronous

一&#xff0c;发送数据 #include "stdio.h" uint8_t hello[]"Hello,blocking\r\n"; HAL_UART_Transmit(&huart1,hello,sizeof(hello),500); 二&#xff0c;MicroLIB-printf(" hello\r\n") #include "stdio.h" #ifdef __GNUC…

WEB攻防-通用漏洞_XSS跨站_权限维持_捆绑钓鱼_浏览器漏洞

目录 XSS的分类 XSS跨站-后台植入Cookie&表单劫持 【例1】&#xff1a;利用beef或xss平台实时监控Cookie等凭据实现权限维持 【例2】&#xff1a;XSS-Flash钓鱼配合MSF捆绑上线 【例3】&#xff1a;XSS-浏览器网马配合MSF访问上线 XSS的分类 反射型&#xff08;非持久…

HTML文章翻页功能

效果展示&#xff1a; 效果原理&#xff1a; 1、引入CDN 2、绘制文章翻页样式&#xff0c;以及自动分段 3、获取窗口宽高&#xff0c;计算出当前文章总分段&#xff0c;并实现分页 4、完整代码 <!DOCTYPE html> <html><head><meta charset"utf-8&qu…

jenkins-node节点配置

一.简述&#xff1a; Jenkins有一个很强大的功能&#xff1a; 即&#xff1a;支持分布式构建(jenkins配置中叫节点(node),也被称为slave)。分布式构建通常是用来吸收额外的负载。通过动态添加额外的机器应对构建作业中的高峰期&#xff0c;或在特定操作系统或环境运行特定的构建…

网络分析仪测试S参数

S参数的测试 一&#xff1a;S参数的定义 S参数&#xff08;Scattering Parameters&#xff0c;散射参数&#xff09;是一个表征器件在射频信号激励下的电气行为的工具&#xff0c;它以输入信号、输出信号为元素的矩阵来表现DUT的“传输”和“散射”效应&#xff0c;输入、输出…

【数据结构高阶】B-树

目录 一、常见的搜索结构 二、B树 2.1 B树的概念 2.2 B树插入数据的分析 2.3 B树的性能分析 2.4 模拟实现B树 2.4.1 B树节点的定义 2.4.2 B树数据的查找 2.4.3 B树节点的数据插入 2.4.4 B树的遍历 2.4.5 模拟实现B树实现的完整代码 三、B树 3.1 B树的概念 3.2 B树…

Linux 服务器挖矿木马防护实战:快速切断、清理与加固20250114

Linux 服务器挖矿木马防护实战&#xff1a;快速切断、清理与加固 引言 挖矿木马作为一种常见的恶意软件&#xff0c;对服务器资源和安全构成严重威胁。据安全机构统计&#xff0c;2023 年全球约 45%的 Linux 服务器遭受过挖矿木马攻击&#xff0c;平均每台被感染服务器每月造…

015: 深度学习之正向传播和反向传播

本文为合集收录&#xff0c;欢迎查看合集/专栏链接进行全部合集的系统学习。 合集完整版请参考这里。 上一节介绍了训练和推理的概念&#xff0c;这一节接着训练和推理的概念讲一下&#xff0c;神经网络的正向传播和反向传播。 正反向传播 其实单看正向传播和反向传播这两个…

梁山派入门指南2——滴答定时器位带操作按键输入(包括GPIO中断)

梁山派入门指南2——滴答定时器&位带操作&按键输入 1. 滴答定时器1.1 滴答定时器简介1.2 相关寄存器1.3 固件库函数 2. 位带操作2.1 位带操作介绍2.2 位带操作的优势2.3 支持位带操作的内存地址2.4 位带别名区地址的计算方式2.5 位带操作使用示例 3 按键输入3.1 独立按…

安全类脚本:拒绝ssh暴力破解

要求如下&#xff1a; 一个小时内&#xff0c;连续密码错误4次。 Linux lastb 命令用于列出登入系统失败的用户相关信息。 实验过程如下&#xff1a; 1. 创建两个IP地址不同的干净环境&#xff0c;分别是&#xff1a;192.168.46.101 Rocky 2 和 192.168.46.120 openEuler 2. 2.…

HugeGraph集群部署

部署HugeGraph集群 最近工作中&#xff0c;需要部署一个HugeGraph的多副本集群&#xff0c;要求一个主节点&#xff0c;两个从节点。由于HugeGraph官网并没有完整的搭建集群实例&#xff0c;所以这里写一篇文章记录自己搭建集群的过程&#xff0c;供自己和大家学习和参考。 注…