开发接口
添加课程到课表
需求分析: 用户购买课程后,交易服务会通过MQ通知学习服务,学习服务将课程加入用户课表中
接下来,我们来分析一下添加课表逻辑的业务流程。首先来对比一下请求参数和数据库字段:
一个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)中,就有有效期信息:
功能实现
我们在tj-learning服务中定义一个MQ的监听器:
package com.tianji.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中实现的,首先是接口声明:
package com.tianji.learning.service; /** * <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 添加课程信息到用户课程表 } }现在,我们正式实现LearningLessonServiceImpl中的addUserLessons方法:
package com.tianji.learning.service.impl; // 略 @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); } }功能测试
- 检查队列
- 检查交换机
- 修改该课程的结束时间, 不然报名会失败, course表, java泛型课程
查看消息: 表示消费成功
查看日志
查看数据库
登录用户传递流程
天机学堂是基于JWT实现登录的,登录信息就保存在请求头的token中。因此要获取当前登录用户,只要获取请求头,解析其中的token即可。
但是,每个微服务都可能需要登录用户信息,在每个微服务都做token解析就属于重复编码了。因此我们的把token解析的行为放到了网关中,然后由网关把用户信息放入请求头,传递给下游微服务。
每个微服务要从请求头拿出用户信息,在业务中使用,也比较麻烦,所以我们定义了一个HandlerInterceptor,拦截进入微服务的请求,并获取用户信息,存入UserContext(底层基于ThreadLocal)。这样后续的业务处理时就能直接从UserContext中获取用户了:
以上就是天机学堂中获取用户信息的基本实现思路。
查看代码: 以后需要用户ID时, 使用UserContext.getUser();工具类获取
分页查询我的课表
需求: 在个人中心-我的课程页面,可以分页查询用户的课表及学习状态信息
接口信息
课表VO属性分析
万事具备,接下来根据我们分析的接口来定义和实现接口。
package com.tianji.learning.controller; /** * <p> * 学生课程表 前端控制器 * </p> * * @author author * @since 2025-11-26 */ @RestController @RequestMapping("/lessons") @Api(tags = "我的课程相关接口") @RequiredArgsConstructor public class LearningLessonController { private final ILearningLessonService lessonService; @GetMapping("/page") @ApiOperation("分页查询我的课程列表") public PageDTO<LearningLessonVO> pageMyLessons(PageQuery query) { return lessonService.queryMyLessons(query); } }package com.tianji.learning.service; /** * <p> * 学生课程表 服务类 * </p> * * @author author * @since 2025-11-26 */ public interface ILearningLessonService extends IService<LearningLesson> { PageDTO<LearningLessonVO> queryMyLessons(PageQuery query); }package com.tianji.learning.service.impl; /** * <p> * 学生课程表 服务实现类 * </p> * * @author author * @since 2025-11-26 */ @Service @RequiredArgsConstructor @Slf4j public class LearningLessonServiceImpl extends ServiceImpl<LearningLessonMapper, LearningLesson> implements ILearningLessonService { private final CourseClient courseClient; @Override public PageDTO<LearningLessonVO> queryMyLessons(PageQuery query) { // 获取当前登录用户 Long userId = UserContext.getUser(); // 分页查询 // select * from learning_lesson where user_id = ? order by latest_learn_time desc limit ?,? Page<LearningLesson> page = lambdaQuery() .eq(LearningLesson::getUserId, userId) .page(query.toMpPage("latest_learn_time", false)); List<LearningLesson> records = page.getRecords(); if(CollUtils.isEmpty(records)) { return PageDTO.empty(page); } // 查询课程信息 Set<Long> cIds = records.stream().map(LearningLesson::getCourseId).collect(Collectors.toSet()); List<CourseSimpleInfoDTO> cInfoList = courseClient.getSimpleInfoList(cIds); if (CollUtils.isEmpty(cInfoList)) { throw new BadRequestException("课程信息不存在!"); } Map<Long, CourseSimpleInfoDTO> cMap = cInfoList.stream() .collect(Collectors.toMap(CourseSimpleInfoDTO::getId, c -> c)); // 封装VO返回 ArrayList<LearningLessonVO> list = new ArrayList<>(records.size()); for (LearningLesson r : records) { LearningLessonVO vo = BeanUtils.copyBean(r, LearningLessonVO.class); 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); } }接口测试
由于我使用的JDK17, 访问接口后b报错:
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.lang.Class java.lang.invoke.SerializedLambda.capturingClass accessible: module java.base does not "opens java.lang.invoke" to unnamed module这表明Java不允许访问SerializedLambda类的私有字段。
问题出现在MyBatis-Plus的SerializedLambdaMeta类初始化过程中,它尝试通过反射设置字段可访问。
在启动应用时添加以下JVM参数:
--add-opens java.base/java.lang.invoke=ALL-UNNAMED如果你使用的是IDE(如IntelliJ IDEA),可以在运行配置的VM options中添加这个参数。
如果是Maven项目,可以在pom.xml中配置:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <jvmArguments>--add-opens java.base/java.lang.invoke=ALL-UNNAMED</jvmArguments> </configuration> </plugin>继续测试
前端页面
查询我正在学习的课程
package com.tianji.learning.controller; /** * <p> * 学生课程表 前端控制器 * </p> * * @author author * @since 2025-11-26 */ @RestController @RequestMapping("/lessons") @Api(tags = "我的课程相关接口") @RequiredArgsConstructor public class LearningLessonController { private final ILearningLessonService lessonService; @GetMapping("/now") @ApiOperation("查询我正在学习的课程") public LearningLessonVO queryMyCurrentLesson() { return lessonService.queryMyCurrentLesson(); } }package com.tianji.learning.service; /** * <p> * 学生课程表 服务类 * </p> * * @author author * @since 2025-11-26 */ public interface ILearningLessonService extends IService<LearningLesson> { LearningLessonVO queryMyCurrentLesson(); }package com.tianji.learning.service.impl; /** * <p> * 学生课程表 服务实现类 * </p> * * @author author * @since 2025-11-26 */ @Service @RequiredArgsConstructor @Slf4j public class LearningLessonServiceImpl extends ServiceImpl<LearningLessonMapper, LearningLesson> implements ILearningLessonService { 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; } }代码提交
- 提交代码到远程仓库
- 把开发分支合并到dev分支
- 先切换到dev分支, 再把开发分支合并到dev
- 提交刚合并过来的代码
- 删除开发分支
- 查看tjxt-dev-build是否构建
- 构建完成启动tj-learning服务, 简单测试