天机学堂-day2(我的课表)

目录


文章目录


前言

本文章使用的是《天机学堂》开源的资料,并从创建虚拟机开始部署《天机学堂项目》,避免还要下载资料中的20GB虚拟机 ,只需要下载镜像以及其他基础资料即可,请大家放心食用

注意:若是还不可以启动项目的可以先看上一篇:天机学堂-自定义部署详细流程(部署篇:初始化项目、启动)

上一篇:《天机学堂------day1(修改bug)》


一、小TIP

1、启动服务

在启动的时候大家可以不需要在虚拟机中将所有服务启动,只需要启动:

  1. tj-auth:登录时所会用到的服务
  2. tj-gateway:这个接口很明显就是网关服务,但是大家可能会有疑惑为什么他不可以在本地启动,我测试过当他在本地启动时,会有一些关于跨域的问题所以建议大家在虚拟机中启动
  3. tj-exam:这个接口是播放视频的服务,当然现在还用不到大家也可以不去启动

其余的接口暂时都可以在本地启动会方便很多

二、我的课表api接口

编号 接口简述 请求方式 请求路径
1 支付或报名课程后,立刻加入课表 MQ通知
2 分页查询我的课表 GET /lessons/page
3 查询我最近正在学习的课程 GET /lessons/now
4 根据id查询指定课程的学习状态 GET /lessons/{courseId}
5 删除课表中的某课程 DELETE /lessons/{courseId}
6 退款后,立刻移除课表中的课程 MQ通知
7 校验指定课程是否是课表中的有效课程(Feign接口) GET /lessons/{courseId}/valid
8 统计课程学习人数(Feign接口) GET /lessons/{courseId}/count

1、支付或报名课程后,立刻加入课表

①、调用链

此接口是使用MQ所请求接口,这里我们可以先去查看这个接口的调用链是怎么样的?

  1. 首先登录到后台网站中随便点击课程,同时在网络中找到接口http://api.tianji.com/ts/orders/freeCourse/1549025085494521857
  2. 顺着网址可以找到对应的微服务接口:
  3. 找到这个接口的逻辑层部分,并简单预览查找有关发送MQ消息的逻辑代码:
②、编写接口

好了,此时便知道了这个接口的产生,也知道了MQ发送消息时所带来的数据、key、虚拟机

  1. 找到课表服务,在mq文件中找到文件编写代码:
java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class LessonChangeListener {

    private final LearningLessonService lessonService;

    /**
     * 监听订单支付或课程报名的信息
     *
     * @param order 订单信息
     */
    @RabbitListener(bindings = @QueueBinding(
            /*绑定队列*/
            value = @Queue(value = "learning.lesson.pay.queue", durable = "true"),
            /*绑定交换机*/
            exchange = @Exchange(name = MqConstants.Exchange.ORDER_EXCHANGE,
                    type = ExchangeTypes.TOPIC),
            /*绑定key*/
            key = MqConstants.Key.ORDER_PAY_KEY
    ))
    public void listenLessonPay(OrderBasicDTO order) {
        //1、健壮性处理
        if (order == null || order.getUserId() == null || CollUtils.isEmpty(order.getCourseIds())) {
            //数据误,无需处理
            log.error("接收到MQ消息有误,订单数据为空");
            return;
        }
        //2、添加课程
        log.debug("监听到用户{}的订单{},需要添加课程{}到课表中", order.getUserId(),
                order.getOrderId(), order.getCourseIds());
        lessonService.addUserLessons(order.getUserId(), order.getCourseIds());
    }
}
  1. 服务层代码:
java 复制代码
/**
* @author chyb
* @description 针对表【learning_lesson(学生课表)】的数据库操作Service实现
* @createDate 2025-11-12 10:59:30
  */
  @SuppressWarnings("ALL")
  @Service
  /**
* 自动生成
* 所有 final 字段和所有带有 @NonNull 注解的非初始化的字段
* public UserService(CourseClient courseClient) {
*         this.CourseClient = courseClient;
* }
* 的构造函数
  */
  @RequiredArgsConstructor
  @Slf4j
  public class LearningLessonServiceImpl extends ServiceImpl<LearningLessonMapper, LearningLesson>
  implements LearningLessonService {
  /*
  Spring进行依赖注入:
  Spring发现构造函数参数 CourseClient
  在容器中查找对应的Feign代理Bean
  注入到UserService中
  */
  private final CourseClient courseClient;
  private final CatalogueClient catalogueClient;
  //    添加课程
  @Override
  /*因为是批量添加,最好加上事务处理*/
  @Transactional
  public void addUserLessons(Long userId, List<Long> courseIds) {
  //1.查询课程有效期
  List<CourseSimpleInfoDTO> simpleInfoList = courseClient.getSimpleInfoList(courseIds);
  if (CollUtils.isEmpty(simpleInfoList)) {
  log.error("课程信息不存在,无法添加到课表");
  return;
  }
  //2.循环便利,处理LearningLesson数据
  ArrayList<LearningLesson> lessons = new ArrayList<>(simpleInfoList.size());
  for (CourseSimpleInfoDTO infoDTO : simpleInfoList) {
  LearningLesson learningLesson = new LearningLesson();
  //2.1获取过期时间
  Integer validDuration = infoDTO.getValidDuration();
  /*因为有效期可能是永久所以避免空指针异常,做一个判断*/
  if (validDuration != null && validDuration > 0) {
  LocalDateTime now = LocalDateTime.now();
  learningLesson.setCreateTime(now);
  learningLesson.setExpireTime(now.plusMonths(validDuration));
  }

           //2.2填充其他字段
           learningLesson.setUserId(userId);
           learningLesson.setCourseId(infoDTO.getId());
           //添加到记录列表
           lessons.add(learningLesson);
       }

       //3.批量处理
       this.saveBatch(lessons);
  }
}

2、分页查询我的课表

① 调用链

  1. 点击后台网站的学习中心,并在网络中可以查找到 http://api.tianji.com/ls/lessons/page网址
  2. 顺着网址可以知道这个接口需要在我的课表服务中创建接口

② 接口编写

逻辑层代码:

java 复制代码
    /**
     * 分页
     *
     * @param query
     * @return
     */
    @Override
    public PageDTO<LearningLessonVO> queryMyLessons(PageQuery query) {
        //获取当前用户
        Long user = UserContext.getUser();
        // 2.分页查询
        /*select * from where user_id = ${userid} order by latest_learn_time limit 0, 5*/
        Page<LearningLesson> page = lambdaQuery().eq(LearningLesson::getUserId, user)
                .page(query.toMpPage("latest_learn_time", false));
        List<LearningLesson> records = page.getRecords();
        /*如果为空直接返回*/
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        // 3.查询课程信息
        Map<Long, CourseSimpleInfoDTO> dtoMap = queryCourseSimpleInfoList(records);
        // 4.封装VO返回
        List<LearningLessonVO> vos = new ArrayList<>(records.size());
        for (LearningLesson record : records) {
            LearningLessonVO vo = BeanUtils.copyBean(record, LearningLessonVO.class);
            CourseSimpleInfoDTO infoDTO = dtoMap.get(record.getCourseId());

            vo.setCourseName(infoDTO.getName());
            vo.setCourseCoverUrl(infoDTO.getCoverUrl());
            vo.setSections(infoDTO.getSectionNum());
            vos.add(vo);
        }

        return PageDTO.of(page, vos);
    }
     /**
     * 查询课程基础信息并转化为Map
     * @param records
     * @return
     */
    private Map<Long, CourseSimpleInfoDTO> queryCourseSimpleInfoList(List<LearningLesson> records) {
        // 3.1.获取课程id
        Set<Long> collect = records.stream().map(c -> c.getCourseId()).collect(Collectors.toSet());
        // 3.2.查询课程信息
        List<CourseSimpleInfoDTO> cInfoList = courseClient.getSimpleInfoList(collect);
        if (CollUtils.isEmpty(cInfoList)) {
            // 课程不存在,无法添加
            throw new BadRequestException("课程信息不存在!");
        }
        // 3.3.把课程集合处理成Map,key是courseId,值是course本身
        Map<Long, CourseSimpleInfoDTO> collect1 =
                cInfoList
                        .stream()
                        .collect(Collectors.toMap(CourseSimpleInfoDTO::getId, c -> c));
        return collect1;
    }

③ 效果展现

3、查询我最近正在学习的课程

① 调用链

这里就不做过多阐述了,由这个调用接口便知道要在课表服务中创建接口:

② 接口编写

服务层方法:

java 复制代码
    /**
     * 查询当前用户正在学习的课程
     *
     * @return
     */
    @Override
    public LearningLessonVO nowLessons() {
        Long user = UserContext.getUser();
        /*查询最近学习的课程*/
        /*select * from where user_id = {userId} and status = 1 order by LatestLearnTime limit 1*/
        LearningLesson lesson = lambdaQuery()
                .eq(LearningLesson::getUserId, user)
                .eq(LearningLesson::getStatus, LessonStatus.LEARNING.getValue())
                .orderByDesc(LearningLesson::getLatestLearnTime)
                .last("limit 1")
                .one();
        /*若是为空直接返回*/
        if (lesson == null) {
            return null;
        }
        /*复制基本类型*/
        LearningLessonVO lessonVO = BeanUtils.copyBean(lesson, LearningLessonVO.class);
        // 4.查询课程信息
        CourseFullInfoDTO courseInfoById = courseClient
                .getCourseInfoById(lessonVO.getCourseId(), false, false);
        if (courseInfoById == null) {
            throw new BadRequestException("课程不存在");
        }
        lessonVO.setCourseName(courseInfoById.getName());
        lessonVO.setCourseCoverUrl(courseInfoById.getCoverUrl());
        lessonVO.setSections(courseInfoById.getSectionNum());
        // 5.统计课表中的课程数量 select count(1) from xxx where user_id = #{userId}
        Integer count = lambdaQuery().eq(LearningLesson::getUserId, user)
                .count();
        lessonVO.setCourseAmount(count);
        // 6.查询小节信息
        List<CataSimpleInfoDTO> cataSimpleInfoDTOS = catalogueClient
                .batchQueryCatalogue(CollUtils
                        .singletonList(lessonVO.getCourseId()));
        if (!cataSimpleInfoDTOS.isEmpty()) {
            CataSimpleInfoDTO cataSimpleInfoDTO = cataSimpleInfoDTOS.get(0);
            lessonVO.setLatestSectionIndex(cataSimpleInfoDTO.getCIndex());
            lessonVO.setLatestSectionName(cataSimpleInfoDTO.getName());
        }
        return lessonVO;
    }

③ 效果展示

4、根据id查询指定课程的学习状态

① 接口编写

  1. 创建接口/lessons/{courseId}
java 复制代码
    @ApiOperation("根据id查询指定课程的学习状态")
    @GetMapping("/{courseId}")
    public LearningLessonVO getStatusById(@PathVariable("courseId") Long courseId) {
        return lessonService.getStatusById(courseId);
    }
  1. 逻辑层代码:
java 复制代码
    /**
     * 查询课程状态
     *
     * @param courseId
     * @return
     */
    @Override
    public LearningLessonVO getStatusById(Long courseId) {
        Long user = UserContext.getUser();
        /*1、判断当前用户是否有购买此课程*/
        LearningLesson one = lambdaQuery()
                .eq(LearningLesson::getUserId, user)
                .eq(LearningLesson::getCourseId, courseId)
                .one();
        if (one == null) {
            log.debug("当前用户${}没有购买${}此课程:", user, courseId);
            return null;
        }
        /*2、若是购买了此课程便查询详细学习进度信息*/
        LearningLessonVO vo = BeanUtils.copyBean(one, LearningLessonVO.class);
        return vo;
    }

② 测试

在接口文档中先定义一个user-info = 2 请求头,值为用户id

找到接口进行调试发送:

5、删除课表中的某课程

① 接口编写

定义/lessons/{courseId}接口:

java 复制代码
   @ApiOperation("删除课表中的某课程")
   @DeleteMapping("/{courseId}")
   public void deleteByCourseId(@PathVariable("courseId") Long courseId) {
        lessonService.deleteCourse(courseId, UserContext.getUser());
    }

逻辑层

java 复制代码
    /**
     * 删除用户课表中的课程
     *
     * @param courseId
     * @param userId
     */
    @Override
    public void deleteCourse(Long courseId, Long userId) {
        //1.判断当前登录用户id是否为null
        //调用这个方法有两种情况:
        //                      用户直接删除已失效的课程 -> 在controller中调用,没有获取用户id,只传了null值
        //                      用户退款后触发课表自动删除 -> 在listener中调用,直接获取了OrderBasicDTO中的用户id
        //listenCourseRefund已有健壮性判断,这里目的是在直接删除已失效的课程时,获取用户id
        if (userId == null) {
            userId = UserContext.getUser();
        }

        //2、删除
//        remove(lambdaQuery().eq(LearningLesson::getUserId, userId)
//                .eq(LearningLesson::getCourseId, userId));
        lambdaUpdate()
                .eq(LearningLesson::getUserId, userId)
                .eq(LearningLesson::getCourseId, courseId)
                .remove();
    }

② 测试

这里点击发送后可以去课表数据库中是否有此课表了:

6、退款后,立刻移除课表中的课程

① 接口编写

在mq文件中定义监听消息的方法:

java 复制代码
    /**
     * 监听课程退款或删除课程的消息
     *
     * @param dto 订单信息
     **/
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "learning.lesson.refund.queue", durable = "true"),
            exchange = @Exchange(name = MqConstants.Exchange.ORDER_EXCHANGE,
                    type = ExchangeTypes.TOPIC),
            key = MqConstants.Key.ORDER_REFUND_KEY
    ))
    public void listenCourseRefund(OrderBasicDTO dto) {
        //1、健壮性处理
        if (dto == null || dto.getUserId() == null || CollUtils.isEmpty(dto.getCourseIds())) {
            //数据误,无需处理
            log.error("接收到MQ消息有误,订单数据为空");
            return;
        }
        //2.调用service,删除课程
        log.debug("监听到用户{}的订单{}要退款,需要从课表中删除课程{}",
                dto.getUserId(), dto.getOrderId(), dto.getCourseIds());
        lessonService.deleteCourse(dto.getCourseIds().get(0), dto.getUserId());
    }

监听到消息后调用上面的删除课表即可

7、校验指定课程是否是课表中的有效课程(Feign接口)

① 接口编写

这里因为时Feign接口直接将接口从tj-api直接复制过来即可

java 复制代码
    @ApiOperation("校验指定课程是否是课表中的有效课程")
    @GetMapping("/lessons/{courseId}/valid")
    public Long isLessonValid(@PathVariable("courseId") Long courseId) {
        return lessonService.verifyCourseOfValid(courseId);
    }

逻辑层

java 复制代码
    /**
     * 校验指定课程是否是课表中的有效课程
     *
     * @param courseId
     * @return
     */
    @Override
    public Long verifyCourseOfValid(Long courseId) {
        Long user = UserContext.getUser();
        /*查询当前用户的课表中的此课程*/
        LearningLesson one =
                lambdaQuery().eq(LearningLesson::getUserId, user)
                        .eq(LearningLesson::getCourseId, courseId)
                        .one();//联合唯一索引,唯一
        /*判断是否过期*/
        LocalDateTime expireTime = one.getExpireTime();
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(expireTime)) {//"当前时间" 在 "过期时间" 之前便是没有过期
            return one.getId(); //返回当前这段课表的id
        }
        //这里我们可以直接返回空代表已过期
        return null;
    }

② 测试

返回为空则说明这个课表已过期

8、统计课程学习人数(Feign接口)

① 接口编写

这里因为逻辑比较简单我就直接进行查询了:

java 复制代码
    @ApiOperation("统计课程学习人数(Feign接口)")
    @GetMapping("/lessons/{courseId}/count")
    public Integer countLearningLessonByCourse(@PathVariable("courseId") Long courseId) {
        return lessonService.lambdaQuery()
                .eq(LearningLesson::getCourseId, courseId)
                .in(LearningLesson::getStatus,
                        LessonStatus.LEARNING.getValue(),
                        LessonStatus.NOT_BEGIN.getValue())
                .count();
    }

下一篇:《天机学堂-day3(学习计划和进度)》

相关推荐
汽车仪器仪表相关领域1 小时前
PSB-1:安全增压与空燃比双监控仪表 - 高性能引擎的 “双重安全卫士“
java·人工智能·功能测试·单元测试·汽车·可用性测试·安全性测试
c***21291 小时前
删除文件夹,被提示“需要来自 TrustedInstaller 的权限。。。”的解决方案
java
狂奔小菜鸡1 小时前
Day21 | 枚举(Enum)与常见应用场景
java·后端·java ee
q***01651 小时前
Spring 过滤器:OncePerRequestFilter 应用详解
java·后端·spring
z***94841 小时前
解决SpringBoot项目启动错误:找不到或无法加载主类
java·spring boot·后端
lichong9511 小时前
android 使用 java 编写网络连通性检查
android·java·前端
TracyCoder1231 小时前
Java后端Redis客户端选型指南
java·开发语言·redis
u***42071 小时前
Spring Data JDBC 详解
java·数据库·spring
sheji34161 小时前
【开题答辩全过程】以 基于Spring Boot的驾校预约练车系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端