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

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


文章目录

      • SpringCloud天机学堂:我的课表(三)
        • 1、添加课程到课表
        • 2、分页查询课表
        • 3、查询正在学习的课程

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.订单idLong 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.返回voreturn 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 {@Overridepublic 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@Transactionalpublic 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和courseIdlesson.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, 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;
}
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 1LearningLesson 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基础属性到VOLearningLessonVO 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;
}

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

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

相关文章

JSON + AJAX + ThreadLocal + 文件上传下载

JSON数据交换 规则 JSON对象和字符串转换 <script type"text/javascript">var jsonPerson {"name": "jack","age": 20}console.log(jsonPerson);var strPerson JSON.stringify(jsonPerson);//对jsonPerson没有影响console.lo…

django如何更新数据库字段并与数据库保持同步?

关键步骤&#xff1a; 第一步&#xff1a; 执行&#xff1a;python manage.py makemigrations 你的项目名称第二步&#xff1a;它会提示你选1还是2&#xff0c;这里因为添加字段&#xff0c;所以选1第三步&#xff1a;出现>>>这个&#xff0c;直接输入这个第四步&am…

cnetos部署高可用以及七层负载均衡

文章目录 centos部署高可用以及七层负载均衡一、准备环境二、配置高可用第一台&#xff1a;第二台&#xff1a; 三、配置负载均衡第一台的七层负载均衡器为haproxy第二台的七层负载均衡器为nginx 四、配置后端服务器五、测试 centos部署高可用以及七层负载均衡 一、准备环境 …

密码学基础:搞懂Hash函数SHA1、SHA-2、SHA3(1)

目录 1.消息摘要(Hash) 2.SHA-1 3.SHA-2 4.小结 1.消息摘要(Hash) Hash函数是一种单向密码体制&#xff0c;把任意长度的输入经过变换得到一个固定长度的输出&#xff0c;同时它还具备单向性&#xff0c;只能从明文到密文&#xff0c;不能逆向&#xff0c;正是由于Hash函数…

数据结构----队列和栈

小编会一直更新数据结构相关方面的知识&#xff0c;使用的语言是Java&#xff0c;但是其中的逻辑和思路并不影响&#xff0c;如果感兴趣可以关注合集。 希望大家看完之后可以自己去手敲实现一遍&#xff0c;同时在最后我也列出一些基本和经典的题目&#xff0c;可以尝试做一下。…

C# winform 三层架构 增删改查 修改数据(修改篇)

ss一.留言 本专栏三层架构已经更新了 添加 登录 显示&#xff0c;还差修改以及删除&#xff0c;本篇更新修改&#xff0c;主要操作为点击修改某一条数据&#xff0c;然后跳转页面进行修改。 二.展示 我们先看DAL代码 /// <summary>/// 修改/// </summary>/// &l…

算法基础知识——11种距离度量

简介&#xff1a;个人学习分享&#xff0c;如有错误&#xff0c;欢迎批评指正。 前言&#xff1a;距离的作用 数据聚类&#xff1a;距离度量在聚类算法&#xff08;如K-means、层次聚类&#xff09;中用于衡量数据点之间的相似性或差异性&#xff0c;帮助确定聚类的中心和边界…

haproxy总结与实验

一、负载均衡 1.1 简述负载均衡 在高并发的业务场景下&#xff0c;解决单个节点压力过大&#xff0c;导致Web服务响应过慢&#xff0c;特别是严重的情况下导致服务瘫痪&#xff0c;无法正常提供服务的问题&#xff0c;而负载均衡的目的就是为了维护系统稳定可靠。负载均衡&…

Redis -LFU(Least Frequently Used,最少使用频率)缓存淘汰算法

在 Redis 的 LFU&#xff08;Least Frequently Used&#xff0c;最少使用频率&#xff09;缓存淘汰算法中&#xff0c;lru 字段被拆分成两部分&#xff1a;高 16 位存储 ldt&#xff08;Last Decrement Time&#xff09;&#xff0c;低 8 位存储 logc&#xff08;Logistic Coun…

Postgresql导入矢量数据

前期准备 工具&#xff1a;PgAdmin&#xff0c;postgis-bundle Postgres安装和postgis安装可以百度别的教程。 创建数据库添加扩展 如图&#xff0c;使用PgAdmin创建名为shp的数据库&#xff0c;并在扩展item中添加postgis扩展。 添加扩展方法可以用查询工具输入以下sql语句&…

LeetCode 热题 HOT 100 (024/100)【宇宙最简单版】

【哈希表】No. 0128 最长连续序列【中等】&#x1f449;力扣对应题目指路 希望对你有帮助呀&#xff01;&#xff01;&#x1f49c;&#x1f49c; 如有更好理解的思路&#xff0c;欢迎大家留言补充 ~ 一起加油叭 &#x1f4a6; 欢迎关注、订阅专栏 【力扣详解】谢谢你的支持&am…

ES6模块化简明笔记

1、什么是模块化 详见看上一篇笔记CommonJs模块化简明笔记 2、为什么需要模块化 详见看上一篇笔记CommonJs模块化简明笔记 3、导入和导出的概念 详见看上一篇笔记CommonJs模块化简明笔记 4、模块导出&#xff08;暴露&#xff09; 4.1 导出&#xff08;暴露&#xff09;方式1&…

C++ list的基本使用

目录 1.list简要介绍 2. list的构造 3. list中迭代器的使用 (1). 双向迭代器与随机访问迭代器使用区别 4.判空、获取元素个数 5. list头、尾元素的访问 6. 插入与删除操作 (1). 头插头删&#xff0c;尾插尾删 (2). 插入&#xff0c;删除与清空 (3). 交换 7. list容器迭代…

前端面试常考的HTML标签知识!!!

语义标签 标签名语义描述header网页头部网页的主要头部区域nav网页导航网页的导航链接区域footer网页底部网页的底部区域aside网页侧边栏网页的侧边栏区域section网页区块网页的独立区块 | article | 网页文章 | 网页的独立文章区域 | 字符实体 作用&#xff1a;在网页中显…

DataWhale夏令营——AIGC技术

一、任务流程 第一步——开通阿里云PAI-DSW试用 1.进入阿里云社区 阿里云社区&#xff1a;阿里云免费试用 - 阿里云 (aliyun.com) 2. 登录或者注册自己的阿里云账号&#xff1a; 3. 点击立即试用 领取成功之后关闭页面即可。 第二步——进入魔搭社区授权 魔搭社区&#…

HTML标记与文档结构

1.1 HTML标记基础 内容标记包括闭合标签和非闭合标签。 1.1.1 文本用闭合标签 闭合标签基本格式和属性&#xff1a; <标签名 属性1"属性值" >文本内容</标签名> 闭标签比开标签多一个斜杠。 1.1.2 引用内容够用自闭和标签 <标签名 属性1"…

Android逆向题解 攻防世界难度5- APK逆向

jeb打开apk 一眼就能看出来&#xff0c;没啥难度&#xff0c;这个难度还不如上一个难度4的题。 直接还原即可 public static void main(String[] args) throws NoSuchAlgorithmException {String userName "Tenshine";MessageDigest messageDigest0 MessageDigest…

【C++】基于多态实现员工管理系统

代码 1、主程序&#xff1a; #include<iostream> using namespace std; #include"workerManager.h"#include"worker.h" #include"employee.h" #include"manager.h" #include"boss.h"int main() {WorkerManager wm;i…

Java语言程序设计——篇十三(1)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f333;&#x1f333;&…

C#语言基础速成Day07

“知止而后有定&#xff0c;定而后能静&#xff0c;静而后能安&#xff0c;安而后能虑&#xff0c;虑而后能得。” 目录 前言文章有误敬请斧正 不胜感恩&#xff01;||Day07 C#常见数据结构&#xff1a;1. 集合&#xff08;Collection&#xff09;1.1 **List<T>**1.2 **H…