SpringCloud天机学堂:我的课表(三)
文章目录
- SpringCloud天机学堂:我的课表(三)
- 1、添加课程到课表
- 2、分页查询课表
- 3、查询正在学习的课程
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.订单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();
}
其中,通知报名成功的逻辑是这部分:
由此,我们可以得知发送消息的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 {@Overridepublic 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@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、分页查询课表
在加入课表以后,用户就可以在个人中心查看到这些课程:
因此,这里就需要第二个接口:
分页查询我的课表
当然,在这个页面大家还能看到跟学习计划有关的按钮,不过本节课我们暂时不讨论学习计划的相关功能实现。
另外,当课程学完后,可以选择删除课程:
所以,还要有删除课程的接口:
删除指定课程
除此以外,如果用户退款,也应该删除课表中的课程,这里同样是通过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, 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 | ||
请求参数 | 无参,程序从登录凭证中获取当前用户 | ||
返回值 | 字段名 | 类型 | 说明 |
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 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;
}