news 2026/4/16 12:56:20

[天机学堂]-04我的课表2

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[天机学堂]-04我的课表2

开发接口

添加课程到课表

需求分析: 用户购买课程后,交易服务会通过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); } }

功能测试

  1. 检查队列

  1. 检查交换机

  1. 修改该课程的结束时间, 不然报名会失败, 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; } }

代码提交

  1. 提交代码到远程仓库

  1. 把开发分支合并到dev分支
  • 先切换到dev分支, 再把开发分支合并到dev

  • 提交刚合并过来的代码

  • 删除开发分支

  1. 查看tjxt-dev-build是否构建

  1. 构建完成启动tj-learning服务, 简单测试


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 10:10:01

宽论框架下量化交易三大工具的协同作战

宽论作为一种科学、系统的交易理念&#xff0c;其量化交易的三大工具 —— 弹论、CDVA 分型以及带鱼短鱼理论&#xff0c;在市场实战中相互配合、协同作战&#xff0c;为投资者构建了一个强大的交易体系。深入探究这三大工具的协同机制&#xff0c;对投资者提升交易水平具有重要…

作者头像 李华
网站建设 2026/4/14 14:39:36

Path of Building:流放之路角色构建的艺术与科学

在《流放之路》这个充满无限可能的游戏世界里&#xff0c;每个玩家都是自己角色的建筑师。而Path of Building&#xff0c;这个被誉为"流放者必备工具"的离线构建工具&#xff0c;正是将这种建筑艺术推向极致的魔法画笔。它不仅仅是一个工具&#xff0c;更是一位懂你…

作者头像 李华
网站建设 2026/4/14 21:05:51

Beyond Compare 5密钥生成技术深度解析:逆向工程与数字签名机制

在软件授权验证领域&#xff0c;Beyond Compare 5作为一款专业的文件对比工具&#xff0c;其授权机制采用了复杂的RSA数字签名技术。本文将从技术原理、安全机制和实现方法三个维度&#xff0c;深入剖析该软件的密钥生成技术。 【免费下载链接】BCompare_Keygen Keygen for BCo…

作者头像 李华
网站建设 2026/4/16 7:09:05

达梦数据库中视图与索引的创建及使用详解

索引&#xff1a;在数据库管理与应用开发过程中&#xff0c;视图和索引是两个非常重要的数据库对象。视图能够简化复杂查询、保障数据安全&#xff0c;索引则可以大幅提升数据查询效率。本文将针对达梦&#xff08;DM&#xff09;数据库&#xff0c;详细介绍视图和索引的概念、…

作者头像 李华
网站建设 2026/4/15 9:58:51

macOS NTFS磁盘读写解决方案:技术实现与操作实践

macOS NTFS磁盘读写解决方案&#xff1a;技术实现与操作实践 【免费下载链接】ntfstool A ntfs tool for mac 项目地址: https://gitcode.com/gh_mirrors/nt/ntfstool 在跨平台数据交换日益频繁的今天&#xff0c;macOS用户面临着一个持续存在的技术挑战&#xff1a;对N…

作者头像 李华