@FeignClient(contextId = "course", value = "course-service")
public interface CourseClient {
/**
* 根据老师id列表获取老师出题数据和讲课数据
* @param teacherIds 老师id列表
* @return 老师id和老师对应的出题数和教课数
*/
@GetMapping("/course/infoByTeacherIds")
List<SubNumAndCourseNumDTO> infoByTeacherIds(@RequestParam("teacherIds") Iterable<Long> teacherIds);
/**
* 根据小节id获取小节对应的mediaId和课程id
*
* @param sectionId 小节id
* @return 小节对应的mediaId和课程id
*/
@GetMapping("/course/section/{id}")
SectionInfoDTO sectionInfo(@PathVariable("id") Long sectionId);
/**
* 根据媒资Id列表查询媒资被引用的次数
*
* @param mediaIds 媒资id列表
* @return 媒资id和媒资被引用的次数的列表
*/
@GetMapping("/course/media/useInfo")
List<MediaQuoteDTO> mediaUserInfo(@RequestParam("mediaIds") Iterable<Long> mediaIds);
/**
* 根据课程id查询索引库需要的数据
*
* @param id 课程id
* @return 索引库需要的数据
*/
@GetMapping("/course/{id}/searchInfo")
CourseSearchDTO getSearchInfo(@PathVariable("id") Long id);
/**
* 根据课程id集合查询课程简单信息
* @param ids id集合
* @return 课程简单信息的列表
*/
@GetMapping("/courses/simpleInfo/list")
List<CourseSimpleInfoDTO> getSimpleInfoList(@RequestParam("ids") Iterable<Long> ids);
/**
* 根据课程id,获取课程、目录、教师信息
* @param id 课程id
* @return 课程信息、目录信息、教师信息
*/
@GetMapping("/course/{id}")
CourseFullInfoDTO getCourseInfoById(
@PathVariable("id") Long id,
@RequestParam(value = "withCatalogue", required = false) boolean withCatalogue,
@RequestParam(value = "withTeachers", required = false) boolean withTeachers
);
}
day02-我的课表
支付或报名课程后,监听到MQ通知,将课程加入课表。
除此以外,如果用户退款,也应该删除课表中的课程,这里同样是通过MQ通知来实现:
CREATE TABLE learning_lesson (
id bigint NOT NULL COMMENT '主键',
user_id bigint NOT NULL COMMENT '学员id',
course_id bigint NOT NULL COMMENT '课程id',
status tinyint DEFAULT '0' COMMENT '课程状态,0-未学习,1-学习中,2-已学完,3-已失效',
week_freq tinyint DEFAULT NULL COMMENT '每周学习频率,每周3天,每天2节,则频率为6',
plan_status tinyint NOT NULL DEFAULT '0' COMMENT '学习计划状态,0-没有计划,1-计划进行中',
learned_sections int NOT NULL DEFAULT '0' COMMENT '已学习小节数量',
latest_section_id bigint DEFAULT NULL COMMENT '最近一次学习的小节id',
latest_learn_time datetime DEFAULT NULL COMMENT '最近一次学习的时间',
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
expire_time datetime NOT NULL COMMENT '过期时间',
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY idx_user_id (user_id,course_id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='学生课表';
package com.tianji.learning.mq;
import cn.hutool.core.collection.CollUtil;
import com.tianji.api.dto.trade.OrderBasicDTO;
import com.tianji.common.constants.MqConstants;
import com.tianji.learning.service.ILearningLessonService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
@RequiredArgsConstructor
public class LessonChangeListener {
private final ILearningLessonService lessonService;
/***
* MQ消息发送相关代码:
* rabbitMqHelper.send(
* MqConstants.Exchange.ORDER_EXCHANGE, // Exchange
* MqConstants.Key.ORDER_PAY_KEY, // Key
* OrderBasicDTO.builder()
* .orderId(orderId)
* .userId(userId)
* .courseIds(cIds)
* .finishTime(order.getFinishTime())
* .build()
* );
*
* @param dto 接受的参数类型为OrderBasicDTO
*/
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "learning.lesson.pay.queue", durable = "true"),
exchange = @Exchange(value = MqConstants.Exchange.ORDER_EXCHANGE, type = ExchangeTypes.TOPIC),
key = MqConstants.Key.ORDER_PAY_KEY))
public void onMsg(OrderBasicDTO dto) {
log.info("LessonChangeListener接收消息,用户{},添加课程{}", dto.getUserId(), dto.getCourseIds());
// 校验
if (dto.getUserId() == null
|| dto.getOrderId() == null
|| CollUtil.isEmpty(dto.getCourseIds())) {
// 这里是接受MQ消息,中断即可,若抛异常,则自动重试
return;
}
// 保存课程到课表
lessonService.addUserLesson(dto.getUserId(),dto.getCourseIds());
}
/**
* 当用户退款成功时,取消相应课程
*/
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "learning.lesson.refund.queue ",durable = "true"),
exchange = @Exchange(value = MqConstants.Exchange.ORDER_EXCHANGE,type = ExchangeTypes.TOPIC ),
key = MqConstants.Key.ORDER_REFUND_KEY))
public void receiveMsg(OrderBasicDTO dto){
log.info("LessonChangeListener接收消息,用户{},取消课程{}", dto.getUserId(), dto.getCourseIds());
// 校验
if (dto.getUserId() == null
|| dto.getOrderId() == null
|| CollUtil.isEmpty(dto.getCourseIds())) {
// 这里是接受MQ消息,中断即可,若抛异常,则会开启重试
return;
}
// 从课表中删除课程
lessonService.deleteLessionById(dto.getUserId(),dto.getCourseIds());
}
}
添加课程
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.查询课程有效期 通过Feign远程调用课程服务,得到课程信息
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);
}
}
MQ
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在加入课表以后,用户就可以在个人中心查看到这些课程:
因此,这里就需要第二个接口
分页查询个人课程
用户信息存到ThreadLocal
jwt:头部+载体+签名
网关判断权限
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求request信息
ServerHttpRequest request = exchange.getRequest();
String method = request.getMethodValue();
String path = request.getPath().toString();
String antPath = method + ":" + path;
// 2.判断请求路径是否在默认不拦截的路径中
if(isExcludePath(antPath)){
// 直接放行
return chain.filter(exchange);
}
// 3.尝试获取用户信息
List<String> authHeaders = exchange.getRequest().getHeaders().get(AUTHORIZATION_HEADER);
String token = authHeaders == null ? "" : authHeaders.get(0);
R<LoginUserDTO> r = authUtil.parseToken(token);
// 4.如果用户是登录状态,尝试更新请求头,传递用户信息
if(r.success()){
exchange.mutate()
.request(builder -> builder.header(USER_HEADER, r.getData().getUserId().toString()))
//验证通过后将请求头中的"authorization"改成"user_info"
.build();
}
// 5.校验权限
authUtil.checkAuth(antPath, r);
// 6.放行
return chain.filter(exchange);
}
private boolean isExcludePath(String antPath) {
for (String pathPattern : authProperties.getExcludePath()) {
if(antPathMatcher.match(pathPattern, antPath)){
return true;
}
}
return false;
}
拦截器
authsdk.resource.interceptors下:
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.判断是否为空(非法用户 也让访问但是threadlocal中不保存)
if (authorization == null) {
return true;
}
// 3.转为用户id并保存
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();
}
}
package com.tianji.common.utils;
public class UserContext {
private static final ThreadLocal<Long> TL = new ThreadLocal<>();
/**
* 保存用户信息
*/
public static void setUser(Long userId){
TL.set(userId);
}
/**
* 获取用户
*/
public static Long getUser(){
return TL.get();
}
/**
* 移除用户信息
*/
public static void removeUser(){
TL.remove();
}
}
分页查询我的课程
@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;
}
作业
检查课程是否有效
public Long isLessonValid(Long courseId) {
Long userId = UserContext.getUser();
// 获取当前登录用户的userId
// 校验用户课表中是否有该课程
LearningLesson learningLesson = this.lambdaQuery()
.eq(LearningLesson::getUserId, userId).eq(LearningLesson::getCourseId, courseId).one();
// 用户课表中没有该课程
if (learningLesson == null) {
// throw new BizIllegalException("该课程不在用户课表中");
return null;
}
// 校验课程状态是否有效,即是否已过期,根据过期时间字段是否大于当前时间进行判断
LocalDateTime expireTime = learningLesson.getExpireTime();
// 当前时间晚于过期时间,已过期
if (expireTime != null && LocalDateTime.now().isAfter(expireTime)) {
// throw new BizIllegalException("该课程已过期");
return null;
}
return learningLesson.getId();
}
根据id查询指定课程的学习状态
- 对于已经购买的课程:展示为马上学习,并且显示学习的进度、有效期
- 对于未购买的课程:展示为立刻购买或加入购物车
public LearningLessonVO getLessonInfo(Long courseId) {
// 获取当前登录用户的userId
Long userId = UserContext.getUser();
// 校验用户课表中是否有该课程
LearningLesson learningLesson = this.lambdaQuery().eq(LearningLesson::getUserId, userId)
.eq(LearningLesson::getCourseId, courseId).one();
// 用户课表中没有该课程
if (learningLesson == null) {
// throw new BizIllegalException("该课程不在用户课表中");
return null;
}
// 封装数据到vo
LearningLessonVO learningLessonVO = LearningLessonVO.builder().id(learningLesson.getId())
.courseId(learningLesson.getCourseId())
.status(learningLesson.getStatus())
.learnedSections(learningLesson.getLearnedSections())
.createTime(learningLesson.getCreateTime())
.expireTime(learningLesson.getExpireTime())
.planStatus(learningLesson.getPlanStatus())
.build();
return learningLessonVO;
}
public LearningLessonVO now() {
// 获取当前登录用户
Long userId = UserContext.getUser();
if (userId == null) {
throw new BizIllegalException("用户未登录");
}
// 查询当前用户最近学习课表,降序排序取第一条, status为1表示学习中
LearningLesson learningLesson = this.lambdaQuery().eq(LearningLesson::getUserId, userId)
.eq(LearningLesson::getStatus, 1)
.orderByDesc(LearningLesson::getLatestLearnTime)
.last("limit 1 ").one();
if (learningLesson == null) {
return null;
}
// 查询当前用户报名的课程数
Integer courseAmount = this.lambdaQuery().eq(LearningLesson::getUserId, userId).count();
// feign远程调用查询相关课程的课程名、封面url等
CourseFullInfoDTO courseInfo = courseClient.getCourseInfoById(learningLesson.getCourseId(), false, false);
if (Objects.isNull(courseInfo)) {
throw new BizIllegalException("课程不存在");
}
// feign远程调用查询相关小节的小节名称,小节编号
List<CataSimpleInfoDTO> catalogueInfoList = catalogueClient.batchQueryCatalogue(List.of(learningLesson.getLatestSectionId()));
if (CollUtil.isEmpty(catalogueInfoList)) {
throw new BizIllegalException("最新学习小节不存在");
}
// 传参的小节id只有一个,所以可直接使用下标0
CataSimpleInfoDTO catalogueInfo = catalogueInfoList.get(0);
// 将po数据封装到vo
LearningLessonVO learningLessonVO = new LearningLessonVO();
BeanUtil.copyProperties(learningLesson, learningLessonVO);
learningLessonVO.setCourseAmount(courseAmount); // 课程数量
learningLessonVO.setCourseName(courseInfo.getName()); // 最近学习课程名称
learningLessonVO.setCourseCoverUrl(courseInfo.getCoverUrl()); // 最近学习课程封面
learningLessonVO.setSections(courseInfo.getSectionNum()); // 最近学习课程的章节数
// 最近学习的小节id和小节名称
learningLessonVO.setLatestSectionName(catalogueInfo.getName());
learningLessonVO.setLatestSectionIndex(catalogueInfo.getCIndex());
// 返回封装的vo
return learningLessonVO;
}
DAY3
学习计划和进度
保存用户播放到哪里:
learn_lesson有latest_section_id bigint DEFAULT NULL COMMENT ‘最近一次学习的小节id’,
CREATE TABLE IF NOT EXISTS `learning_record` (
`id` bigint NOT NULL COMMENT '学习记录的id',
`lesson_id` bigint NOT NULL COMMENT '对应课表的id',
`section_id` bigint NOT NULL COMMENT '对应小节的id',
`user_id` bigint NOT NULL COMMENT '用户id',
`moment` int DEFAULT '0' COMMENT '视频的当前观看时间点,单位秒',
`finished` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否完成学习,默认false',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '第一次观看时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间(最近一次观看时间)',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_update_time` (`update_time`) USING BTREE,
KEY `idx_user_id` (`user_id`) USING BTREE,
KEY `idx_lesson_id` (`lesson_id`,`section_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学习记录表';
DTO:接收前端参数或者返回时候 用的
public class LearningRecordFormDTO {
@ApiModelProperty("小节类型:1-视频,2-考试")
@NotNull(message = "小节类型不能为空")
@EnumValid(enumeration = {1, 2}, message = "小节类型错误,只能是:1-视频,2-考试")
private SectionType sectionType;
@ApiModelProperty("课表id")
@NotNull(message = "课表id不能为空")
private Long lessonId;
@ApiModelProperty("对应节的id")
@NotNull(message = "节的id不能为空")
private Long sectionId;
@ApiModelProperty("视频总时长,单位秒")
private Integer duration;
@ApiModelProperty("视频的当前观看时长,单位秒,第一次提交填0")
private Integer moment;
@ApiModelProperty("提交时间")
private LocalDateTime commitTime;
}
想循环引用的话,不用注入service,可以注入它的下一层Mapper
查询学习记录
public LearningLessonDTO queryLearningRecordByCourse(Long courseId) {
Long userId = UserContext.getUser();
// 根据用户userId和课程courseId获取最近学习的小节id和课表id
LearningLesson learningLesson = learningLessonService.lambdaQuery()
.eq(LearningLesson::getCourseId, courseId)
.eq(LearningLesson::getUserId, userId).one();
if (Objects.isNull(learningLesson)) {
throw new BizIllegalException("该课程未加入课表");
}
// 根据课表id获取学习记录
List<LearningRecord> learningRecordList = this.lambdaQuery()
.eq(LearningRecord::getLessonId, learningLesson.getId()).list();
// copyToList有判空校验,不再赘余
List<LearningRecordDTO> learningRecordDTOList = BeanUtil.copyToList(learningRecordList, LearningRecordDTO.class);
LearningLessonDTO learningLessonDTO = new LearningLessonDTO();
learningLessonDTO.setId(learningLesson.getId());
learningLessonDTO.setLatestSectionId(learningLesson.getLatestSectionId());
learningLessonDTO.setRecords(learningRecordDTOList);
return learningLessonDTO;
}
提交学习记录
保存用户播放到哪里,续播
参数校验:validator
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
续播如何精确到分钟秒的? 有上一次学习时间
.set(!finished, LearningLesson::getLatestLearnTime, recordDTO.getCommitTime())
创建学习计划
首先,要做到切换设备后还能续播,用户的播放进度必须保存在服务端,而不是客户端。
其次,用户突然断开或者切换设备,续播的时间误差不能超过30秒,那播放进度的记录频率就需要比较高。我们会在前端每隔15秒就发起一次心跳请求,提交最新的播放进度,记录到服务端。这样用户下一次续播时直接读取服务端的播放进度,就可以将时间误差控制在15秒左右。
分页查询我的学习计划
/**
* 分页查询我的学习计划
*
* @param pageQuery 分页参数
*/
@Override
public LearningPlanPageVO queryMyPlans(PageQuery pageQuery) {
Long userId = UserContext.getUser();
if (Objects.isNull(userId)) {
throw new BizIllegalException("用户未登录");
}
// 查询用户正在进行的课表
Page<LearningLesson> learningLessonPage = lambdaQuery()
.eq(LearningLesson::getUserId, userId)
.eq(LearningLesson::getPlanStatus, PlanStatus.PLAN_RUNNING)
.in(LearningLesson::getStatus, LessonStatus.NOT_BEGIN, LessonStatus.LEARNING)
.page(pageQuery.toMpPage("latest_learn_time", false));
// 判断用户是否有正在上的课
if (CollUtil.isEmpty(learningLessonPage.getRecords())) {
// 返回空数据
LearningPlanPageVO emptyVO = new LearningPlanPageVO();
emptyVO.setTotal(0L);
emptyVO.setPages(0L);
emptyVO.setList(Collections.emptyList());
return emptyVO;
}
// TODO 实现本周学习积分,暂未实现,默认0
// 查询课表相关的课程信息并封装到Map
Map<Long, CourseSimpleInfoDTO> simpleInfoDTOMap = getLongCourseSimpleInfoDTOMap(learningLessonPage);
// 封装到VO
return getPlanPageVO(learningLessonPage, simpleInfoDTOMap);
}
/**
* 数据封装
*/
private LearningPlanPageVO getPlanPageVO(Page<LearningLesson> learningLessonPage, Map<Long, CourseSimpleInfoDTO> simpleInfoDTOMap) {
// 遍历课表
List<LearningPlanVO> learningPlanVOList = learningLessonPage.getRecords().stream().map(learningLesson -> {
// 从课程map中取出相应的课程信息
CourseSimpleInfoDTO courseSimpleInfoDTO = simpleInfoDTOMap.get(learningLesson.getCourseId());
//LearningPlanVO learningPlanVO = BeanUtils.copyBean(learningLesson, LearningPlanVO.class);
LearningPlanVO learningPlanVO = LearningPlanVO.builder()
.courseId(learningLesson.getCourseId()) // 课程id
.weekFreq(learningLesson.getWeekFreq()) // 本周计划学习数量
.learnedSections(learningLesson.getLearnedSections()) // 已学习小节数量
.latestLearnTime(learningLesson.getLatestLearnTime()) // 最近一次学习时间
.build();
if (courseSimpleInfoDTO != null) {
// 赋值课程名和总小节数量属性
learningPlanVO.setCourseName(courseSimpleInfoDTO.getName()); // 课程名
learningPlanVO.setSections(courseSimpleInfoDTO.getSectionNum()); // 课程总小节数量
}
// 查询该课程本周已学完的小节数
LocalDate now = LocalDate.now();
LocalDateTime weekBeginTime = DateUtils.getWeekBeginTime(now);
LocalDateTime weekEndTime = DateUtils.getWeekEndTime(now);
// 避免循环依赖,用Mapper不用service
// 查询该课程本周已学习的小节数量:即查一周内该用户该课程有多少条学习记录
Integer weekLearnedSections = learningRecordMapper.getWeekLearnedSections(learningLesson.getId(),
weekBeginTime,weekEndTime);
// 封装到planVO
learningPlanVO.setWeekLearnedSections(weekLearnedSections);
return learningPlanVO;
}).collect(Collectors.toList());
// 累加计算本周计划完成的小节数和已学完的小节数量
Integer weekFinished = 0; // 本周已学完小节数量
Integer weekTotalPlan = 0; // 本周计划学习小节数量
for (LearningPlanVO learningPlanVO : learningPlanVOList) {
weekFinished += learningPlanVO.getWeekLearnedSections();
weekTotalPlan += learningPlanVO.getWeekFreq();
}
LearningPlanPageVO planPageVO = LearningPlanPageVO.builder()
.weekFinished(weekFinished)
.weekTotalPlan(weekTotalPlan)
// TODO 学习积分暂为0
.weekPoints(0)
.build();
return planPageVO.pageInfo(learningLessonPage.getTotal(), learningLessonPage.getPages(), learningPlanVOList);
}
/**
* 查询课表相关的课程信息并封装到Map
*/
private Map<Long, CourseSimpleInfoDTO> getLongCourseSimpleInfoDTOMap(Page<LearningLesson> learningLessonPage) {
List<Long> courseIds = learningLessonPage.getRecords().stream()
.map(LearningLesson::getCourseId).collect(Collectors.toList());
List<CourseSimpleInfoDTO> simpleInfoList = courseClient.getSimpleInfoList(courseIds);
// 校验课表相关的课程信息
if (CollUtil.isEmpty(simpleInfoList)) {
throw new BizIllegalException("未查询到课表中相关课程");
}
// 封装为map,方便后面取出,空间换时间
Map<Long, CourseSimpleInfoDTO> simpleInfoDTOMap = simpleInfoList.stream()
.collect(Collectors.toMap(CourseSimpleInfoDTO::getId, courseSimpleInfoDTO -> courseSimpleInfoDTO));
return simpleInfoDTOMap;
}
基础知识
使用 Builder 模式可以让对象的创建更加清晰和灵活,避免了传统构造函数参数过多时的复杂性和可读性问题。在代码中使用 .builder() 方法是一种简洁的方式来创建对象,同时可以提高代码的可维护性和可扩展性。
LearningPlanPageVO planPageVO = LearningPlanPageVO.builder()
.weekFinished(weekFinished)
.weekTotalPlan(weekTotalPlan)
.weekPoints(0)
.build();
// LearningPlanVO learningPlanVO = BeanUtils.copyBean(learningLesson, LearningPlanVO.class);
LearningPlanVO learningPlanVO = LearningPlanVO.builder()
.courseId(learningLesson.getCourseId()) // 课程id
.weekFreq(learningLesson.getWeekFreq()) // 本周计划学习数量
.learnedSections(learningLesson.getLearnedSections()) // 已学习小节数量
.latestLearnTime(learningLesson.getLatestLearnTime()) // 最近一次学习时间
.build();
3.1.课程过期
编写一个SpringTask定时任务,定期检查learning_lesson表中的课程是否过期,如果过期则将课程状态修改为已过期。
启动类上加@EnableScheduling // 开启定时任务
task上加@Scheduled(cron = “0 0 3 1 * ?”) //秒分时日月周年
3.2.方案思考
思考题:思考一下目前提交学习记录功能可能存在哪些问题?有哪些可以改进的方向?