开发的流程
那为什么要先设计接口呢?原因有两点:
-
第一:目前企业开发的模式往往是前后端分离,前后端并行开发。前端开发需要调用后端接口,后端需要开发接口返回数据给前端,要想并行开发,就必须有一套接口标准,前后端基于接口完成开发。
-
第二:设计接口的过程就是分析业务的过程,弄清楚了业务的细节,更有助于我们设计数据库结构,开发接口功能。
通过产品原型和需求分析,对需要的接口进行了统计。
然后逐个对接口进行了分析,进行接口的设计。
接口设计的核心要素包括:
-
请求方式
-
请求路径
-
请求参数格式
-
返回值格式
分析完接口,定义相应的请求和返回实体。
设计数据结构
首先通过需求和接口分析明确表格中包含的信息字段
然后寻找数据关系去设计表格
为什么要创建分支
一般开发新功能都需要创建一个feature类型分支,不能在DEV分支直接开发,因此这里我们新建一个功能分支。
代码简化-MP的使用
基于创建的表格使用MP生成代码
代码优化-枚举
前面分析得到的字段里面有状态,如果每次编码都手写很容易写错,因此一般都会定义出枚举
实现接口功能
MQ监听器
在前面的接口分析中我理解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());
}
调用Feign接口
因为需要调用课程的详细信息,所以需要远程调用
通过token获取登录用户
天机学堂是基于JWT实现登录的,登录信息就保存在请求头的token中。因此要获取当前登录用户,只要获取请求头,解析其中的token即可。
微服务定义拦截器获取用户信息
@Slf4j
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.尝试获取头信息中的用户信息
String authorization = request.getHeader(JwtConstants.USER_HEADER);
// 2.判断是否为空
if (authorization == null) {
return true;
}
// 3.转为用户id并保存到UserContext中
try {
Long userId = Long.valueOf(authorization);
UserContext.setUser(userId);
return true;
} catch (NumberFormatException e) {
log.error("用户身份信息格式不正确,{}, 原因:{}", authorization, e.getMessage());
return true;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理用户信息
UserContext.removeUser();
}
}
ThreadLocal
在这个拦截器中,获取到用户信息后保存到了UserContext
中,这是一个基于ThreadLocal
的工具,可以确保不同的请求之间互不干扰,避免线程安全问题发生
文件流
@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;
}
课后作业
删除课表中课程
@Override
@Transactional
public void deleteCourseFromLesson(Long userId, Long courseIds) {
if (userId == null) {
userId = UserContext.getUser();
}
// delete * from learning_lesson where user_id = #{userId} and course_id in (#{courseIds})
remove(buildUserIdAndCourseIdWrapper(userId, courseIds));
}
private LambdaQueryWrapper<LearningLesson> buildUserIdAndCourseIdWrapper(Long userId, Long courseIds) {
return new QueryWrapper<LearningLesson>().lambda()
.eq(LearningLesson::getUserId, userId)
.in(LearningLesson::getCourseId, courseIds);
}
检查课程是否有效
@Override
public Long isLessonValid(Long courseId) {
// 1.获取登录用户
Long userId = UserContext.getUser();
if (userId == null) {
return null;
}
// 2.查询课程
LearningLesson lesson = lambdaQuery()
.eq(LearningLesson::getUserId, UserContext.getUser())
.eq(LearningLesson::getCourseId, courseId)
.one();
if (lesson == null) {
return null;
}
if(lesson.getStatus()==LessonStatus.EXPIRED){
return null;
}
return lesson.getId();
}
查询用户课表中指定课程状态
@Override
public LearningLessonVO LessonQuey(Long courseId) {
// 1.获取当前登录的用户
Long userId = UserContext.getUser();
// 2.查询指定课程的
LearningLesson lesson = lambdaQuery()
.eq(LearningLesson::getUserId, userId)
.eq(LearningLesson::getCourseId,courseId)
.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());
return vo;
}
统计课程的学习人数
@Override
public Integer countLearningLessonByCourse(Long courseId) {
Integer courseAmount = lambdaQuery()
.eq(LearningLesson::getCourseId, courseId)
.count();
return courseAmount;
}