SpringCloud天机学堂:我的课表(三)
文章目录
1、添加课程到课表
首先,用户支付完成后,需要将购买的课程加入课表:
而支付成功后,交易服务会基于MQ通知的方式,通知学习服务来执行加入课表的动作。因此,我们要实现的第一个接口就是:
支付或报名课程后,监听到MQ通知,将课程加入课表。
在trade-service
的OrderController
中,有一个报名免费课程的接口:
@ApiOperation("免费课立刻报名接口")
@PostMapping("/freeCourse/{courseId}")
public PlaceOrderResultVO enrolledFreeCourse(
@ApiParam("免费课程id") @PathVariable("courseId") Long courseId) {
return orderService.enrolledFreeCourse(courseId);
}
可以看到这里调用了OrderService
的enrolledFreeCourse()
方法:
@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();
}
其中,通知报名成功的逻辑是这部分:
由此,我们可以得知发送消息的Exchange、RoutingKey,以及消息体。消息体的格式是OrderBasicDTO,包含四个字段:
- orderId:订单id
- userId:下单的用户id
- courseIds:购买的课程id集合
- finishTime:支付完成时间
因此,在学习服务,我们需要编写的消息监听接口规范如下:
接口说明 | 当用户购买/报名课程后,交易服务(trade-service)会通过MQ消息通知其它微服务。学习服务(learning-service)需要监听该通知,将用户报名的课程加入我的课表中。 |
---|---|
请求方式 | MQ异步通知:exchange :MqConstants.Exchange.ORDER_EXCHANGE routingKey :MqConstants.Key.ORDER_PAY_KEY |
请求路径 | – |
请求参数格式 | { "orderId": "1578558664933920770", // 订单id "userId": "2", // 用户id "courseIds": [ "1549025085494521857" // 购买的课程id集合 ], "finishTime": "2023-02-21" // 支付完成时间 } |
返回值格式 | – |
我们在tj-learning服务中定义一个MQ的监听器:
代码如下:
@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
数据表:
一个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
)。课程表结构如图:
因此,我们要做的事情就是根据courseId集合查询课程信息,然后分别计算每个课程的有效期,组织多个LearingLesson的数据,形成集合。最终批量新增到数据库即可。
流程如图:
那么问题来了,我们该如何根据课程id查询课程信息呢?
获取课程信息
课程(course)的信息是由课程服务(course-service)来维护的,目前已经开发完成并部署到了虚拟机的开发环境中。
我们现在需要查询课程信息,自然需要调用课程服务暴露的Feign接口。如果没有这样的接口,则需要联系维护该服务的同事,协商开发相关接口。
在咱们的项目中,课程微服务已经暴露了一些接口。我们有三种方式可以查看已经开放的接口:
- 与开发的同事交流沟通
- 通过网关中的Swagger文档来查看
- 直接查看课程服务的源码
首先,我们来看一下swagger文档:
不过这种方式查看到的接口数量非常多,有很多是给前端用的。不一定有对应的Feign接口。
要查看Feign接口,需要到tj-api
中查看:
检索其中的API,可以发现一个这样的接口:
根据id批量查询课程的基本信息,而在课程基本信息(CourseSimpleInfoDTO
)中,就有有效期信息:
实现添加课程到课表
现在,我们正式实现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、分页查询课表
在加入课表以后,用户就可以在个人中心查看到这些课程:
因此,这里就需要第二个接口:
分页查询我的课表
当然,在这个页面大家还能看到跟学习计划有关的按钮,不过本节课我们暂时不讨论学习计划的相关功能实现。
另外,当课程学完后,可以选择删除课程:
所以,还要有删除课程的接口:
删除指定课程
除此以外,如果用户退款,也应该删除课表中的课程,这里同样是通过MQ通知来实现:
退款后,监听到MQ通知,删除指定课程
修改之前的tj-learning
中的LearningLessonServiceImpl
的queryMyLessons
方法:
@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 | ||
请求参数 | 无参,程序从登录凭证中获取当前用户 | ||
返回值 | 字段名 | 类型 | 说明 |
courseId | String | 课程id | |
courseName | String | 课程名称 | |
sections | int | 课程总课时数 | |
learnedSections | int | 已学习课时数 | |
createTime | LocalDateTime | 加入课表时间 | |
expireTime | LocalDateTime | 过期时间 | |
courseAmount | long | 课表中课程总数 | |
latestSectionName | String | 最近一次学习的小节名称 | |
latestSectionIndex | int | 最近一次学习的小节序号 |
可以看到返回值结果与分页查询的课表VO基本类似,因此这里可以复用LearningLessonVO实体,但是需要添加几个字段:
- courseAmount
- latestSectionName
- latestSectionIndex
查询章节信息
小节名称、序号信息都在课程微服务(course-service)中,因此可以通过课程微服务提供的接口来查询:
接口:
其中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;
}
标签:天机,SpringCloud,userId,id,课表,课程,vo,order
From: https://blog.csdn.net/weixin_53961667/article/details/141111685