SpringCloud天机学堂:我的课表(三)

SpringCloud天机学堂:我的课表(三)


1、添加课程到课表

首先,用户支付完成后,需要将购买的课程加入课表:

img

而支付成功后,交易服务会基于MQ通知的方式,通知学习服务来执行加入课表的动作。因此,我们要实现的第一个接口就是:

支付或报名课程后,监听到MQ通知,将课程加入课表。

trade-serviceOrderController中,有一个报名免费课程的接口:

@ApiOperation("免费课立刻报名接口")
@PostMapping("/freeCourse/{courseId}")
public PlaceOrderResultVO enrolledFreeCourse(
    @ApiParam("免费课程id") @PathVariable("courseId") Long courseId) {
    return orderService.enrolledFreeCourse(courseId);
}

可以看到这里调用了OrderServiceenrolledFreeCourse()方法:

@Override
@Transactional
public PlaceOrderResultVO enrolledFreeCourse(Long courseId) {
    Long userId = UserContext.getUser();
    // 1.查询课程信息
    List<Long> cIds = CollUtils.singletonList(courseId);
    List<CourseSimpleInfoDTO> courseInfos = getOnShelfCourse(cIds);
    if (CollUtils.isEmpty(courseInfos)) {
        // 课程不存在
        throw new BizIllegalException(TradeErrorInfo.COURSE_NOT_EXISTS);
    }
    CourseSimpleInfoDTO courseInfo = courseInfos.get(0);
    if(!courseInfo.getFree()){
        // 非免费课程,直接报错
        throw new BizIllegalException(TradeErrorInfo.COURSE_NOT_FREE);
    }
    // 2.创建订单
    Order order = new Order();
    // 2.1.基本信息
    order.setUserId(userId);
    order.setTotalAmount(0);
    order.setDiscountAmount(0);
    order.setRealAmount(0);
    order.setStatus(OrderStatus.ENROLLED.getValue());
    order.setFinishTime(LocalDateTime.now());
    order.setMessage(OrderStatus.ENROLLED.getProgressName());
    // 2.2.订单id
    Long orderId = IdWorker.getId(order);
    order.setId(orderId);

    // 3.订单详情
    OrderDetail detail = packageOrderDetail(courseInfo, order);

    // 4.写入数据库
    saveOrderAndDetails(order, CollUtils.singletonList(detail));

    // 5.发送MQ消息,通知报名成功
    rabbitMqHelper.send(
            MqConstants.Exchange.ORDER_EXCHANGE,
            MqConstants.Key.ORDER_PAY_KEY,
            OrderBasicDTO.builder().orderId(orderId).userId(userId).courseIds(cIds).build());
    // 6.返回vo
    return PlaceOrderResultVO.builder()
            .orderId(orderId)
            .payAmount(0)
            .status(order.getStatus())
            .build();
}

其中,通知报名成功的逻辑是这部分:

img

由此,我们可以得知发送消息的Exchange、RoutingKey,以及消息体。消息体的格式是OrderBasicDTO,包含四个字段:

  • orderId:订单id
  • userId:下单的用户id
  • courseIds:购买的课程id集合
  • finishTime:支付完成时间

因此,在学习服务,我们需要编写的消息监听接口规范如下:

接口说明当用户购买/报名课程后,交易服务(trade-service)会通过MQ消息通知其它微服务。学习服务(learning-service)需要监听该通知,将用户报名的课程加入我的课表中。
请求方式MQ异步通知:exchange :MqConstants.Exchange.ORDER_EXCHANGEroutingKey :MqConstants.Key.ORDER_PAY_KEY
请求路径
请求参数格式{ "orderId": "1578558664933920770", // 订单id "userId": "2", // 用户id "courseIds": [ "1549025085494521857" // 购买的课程id集合 ], "finishTime": "2023-02-21" // 支付完成时间 }
返回值格式

我们在tj-learning服务中定义一个MQ的监听器:

img

代码如下:

@Slf4j
@Component
@RequiredArgsConstructor
public class LessonChangeListener {

    private final ILearningLessonService 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 = 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());
    }
}

订单中与课表有关的字段就是userId、courseId,因此这里要传递的就是这两个参数。

注意,这里添加课程的核心逻辑是在ILearningLessonService中实现的,首先是接口声明:

/**
 * <p>
 * 学生课程表 服务类
 * </p>
 */
public interface ILearningLessonService extends IService<LearningLesson> {
    void addUserLessons(Long userId, List<Long> courseIds);
}

然后是对应的实现类:

@Service
public class LearningLessonServiceImpl extends ServiceImpl<LearningLessonMapper, LearningLesson> implements ILearningLessonService {

    @Override
    public void addUserLessons(Long userId, List<Long> courseIds) {
        // TODO 添加课程信息到用户课程表
    }
}

添加课表的流程分析

接下来,我们来分析一下添加课表逻辑的业务流程。首先来对比一下请求参数和数据库字段:

参数:

  • Long userId
  • List courseIds

数据表:

img

一个userId和一个courseId是learning_lesson表中的一条数据。而订单中一个用户可能购买多个课程。因此请求参数中的courseId集合就需要逐个处理,将来会有多条课表数据。

另外,可以发现参数中只有userId和courseId,表中的其它字段都需要我们想办法来组织:

  • status:课程状态,可以默认为0,代表未学习
  • week_freq:学习计划频率,可以为空,代表没有设置学习计划
  • plan_status:学习计划状态,默认为0,代表没有设置学习计划
  • learned_sections:已学习小节数,默认0,代表没有学习
  • latest_section_id:最近学习小节id,可以为空,代表最近没有学习任何小节
  • latest_learn_time:最近学习时间,可以为空,代表最近没有学习
  • create_time:创建时间,也就是当前时间
  • expire_time:过期时间,这个要结合课程来计算。每个课程都有自己的有效期(valid_duration),因此过期时间就是create_time加上课程的有效期
  • update_time:更新时间,默认当前时间,有数据库实时更新,不用管

可见在整张表中,需要我们在新增时处理的字段就剩下过期时间expire_time了。而要知道这个就必须根据courseId查询课程的信息,找到其中的课程有效期(valid_duration)。课程表结构如图:

img

因此,我们要做的事情就是根据courseId集合查询课程信息,然后分别计算每个课程的有效期,组织多个LearingLesson的数据,形成集合。最终批量新增到数据库即可。

流程如图:

image-20240308140628998

那么问题来了,我们该如何根据课程id查询课程信息呢?

获取课程信息

课程(course)的信息是由课程服务(course-service)来维护的,目前已经开发完成并部署到了虚拟机的开发环境中。

我们现在需要查询课程信息,自然需要调用课程服务暴露的Feign接口。如果没有这样的接口,则需要联系维护该服务的同事,协商开发相关接口。

在咱们的项目中,课程微服务已经暴露了一些接口。我们有三种方式可以查看已经开放的接口:

  • 与开发的同事交流沟通
  • 通过网关中的Swagger文档来查看
  • 直接查看课程服务的源码

首先,我们来看一下swagger文档:

img

不过这种方式查看到的接口数量非常多,有很多是给前端用的。不一定有对应的Feign接口。

要查看Feign接口,需要到tj-api中查看:

img

检索其中的API,可以发现一个这样的接口:

img

根据id批量查询课程的基本信息,而在课程基本信息(CourseSimpleInfoDTO)中,就有有效期信息:

img

实现添加课程到课表

现在,我们正式实现LearningLessonServiceImpl中的addUserLessons方法:

@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.查询课程有效期
        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);
    }
}
2、分页查询课表

在加入课表以后,用户就可以在个人中心查看到这些课程:

img

因此,这里就需要第二个接口:

分页查询我的课表

当然,在这个页面大家还能看到跟学习计划有关的按钮,不过本节课我们暂时不讨论学习计划的相关功能实现。

另外,当课程学完后,可以选择删除课程:

img

所以,还要有删除课程的接口:

删除指定课程

除此以外,如果用户退款,也应该删除课表中的课程,这里同样是通过MQ通知来实现:

退款后,监听到MQ通知,删除指定课程

修改之前的tj-learning中的LearningLessonServiceImplqueryMyLessons方法:

@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;
}
3、查询正在学习的课程
参数说明
请求方式GET
请求路径/lessons/now
请求参数无参,程序从登录凭证中获取当前用户
返回值字段名类型说明
courseIdString课程id
courseNameString课程名称
sectionsint课程总课时数
learnedSectionsint已学习课时数
createTimeLocalDateTime加入课表时间
expireTimeLocalDateTime过期时间
courseAmountlong课表中课程总数
latestSectionNameString最近一次学习的小节名称
latestSectionIndexint最近一次学习的小节序号

可以看到返回值结果与分页查询的课表VO基本类似,因此这里可以复用LearningLessonVO实体,但是需要添加几个字段:

  • courseAmount
  • latestSectionName
  • latestSectionIndex

查询章节信息

小节名称、序号信息都在课程微服务(course-service)中,因此可以通过课程微服务提供的接口来查询:

img

接口:

img

其中CataSimpleInfoDTO中就包含了章节信息:

@Data
public class CataSimpleInfoDTO {
    @ApiModelProperty("目录id")
    private Long id;
    @ApiModelProperty("目录名称")
    private String name;
    @ApiModelProperty("数字序号,不包含章序号")
    private Integer cIndex;
}

代码实现

首先是controller,tj-learning服务的LearningLessonController

@Api(tags = "我的课表相关接口")
@RestController
@RequestMapping("/lessons")
@RequiredArgsConstructor
public class LearningLessonController {
    
    private final ILearningLessonService lessonService;

    // 。。。略

    @GetMapping("/now")
    @ApiOperation("查询我正在学习的课程")
    public LearningLessonVO queryMyCurrentLesson() {
        return lessonService.queryMyCurrentLesson();
    }
}

需要注意的是,这里添加了Swagger相关注解,标记接口信息。

然后是service的接口,tj-learning服务的ILearningLessonService

LearningLessonVO queryMyCurrentLesson();

最后是实现类,tj-learning服务的LearningLessonServiceImpl

private final CatalogueClient catalogueClient;

@Override
public LearningLessonVO queryMyCurrentLesson() {
    // 1.获取当前登录的用户
    Long userId = UserContext.getUser();
    // 2.查询正在学习的课程 select * from xx where user_id = #{userId} AND status = 1 order by latest_learn_time limit 1
    LearningLesson lesson = lambdaQuery()
            .eq(LearningLesson::getUserId, userId)
            .eq(LearningLesson::getStatus, LessonStatus.LEARNING.getValue())
            .orderByDesc(LearningLesson::getLatestLearnTime)
            .last("limit 1")
            .one();
    if (lesson == null) {
        return null;
    }
    // 3.拷贝PO基础属性到VO
    LearningLessonVO vo = BeanUtils.copyBean(lesson, LearningLessonVO.class);
    // 4.查询课程信息
    CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
    if (cInfo == null) {
        throw new BadRequestException("课程不存在");
    }
    vo.setCourseName(cInfo.getName());
    vo.setCourseCoverUrl(cInfo.getCoverUrl());
    vo.setSections(cInfo.getSectionNum());
    // 5.统计课表中的课程数量 select count(1) from xxx where user_id = #{userId}
    Integer courseAmount = lambdaQuery()
            .eq(LearningLesson::getUserId, userId)
            .count();
    vo.setCourseAmount(courseAmount);
    // 6.查询小节信息
    List<CataSimpleInfoDTO> cataInfos =
            catalogueClient.batchQueryCatalogue(CollUtils.singletonList(lesson.getLatestSectionId()));
    if (!CollUtils.isEmpty(cataInfos)) {
        CataSimpleInfoDTO cataInfo = cataInfos.get(0);
        vo.setLatestSectionName(cataInfo.getName());
        vo.setLatestSectionIndex(cataInfo.getCIndex());
    }
    return vo;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小林学习编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值