项目背景
项目需要做一个记录视频播放进度的功能,有以下几点需要着重注意:
1.点击视频,播放到几小时几分几秒,下次同一个人点击进来依然是当前时间段
2.当一个维度下有多个视频可以看,分开记录当前视频或者文档是否已经看完。比如学习维度下有两个视频,一个文档,文档或者视频看完,直接显示当前视频已看完,但学习维度还显示正在学习,除非当前维度下的所有视频或者文档全部显示为已学完。
3.文档点击进入页面可以直接认定为已看完,视频看完必须要看完时长的三分之二,才能显示当前视频已看完。
4.记录当前天每个人实际学习时间总时长(额外需求开发)。
单个媒体视频学完之后,直接已学完,这种逻辑很好实现。多个媒体在同一个维度下,比如娱乐维度下有三个视频,学习维度下有五个学习视频,只要当前维度下的所有视频全部显示已学完,才可以让当前维度更新数据库显示已学完,怎么统计呢?
解决办法:当前维度下的单个视频,每次第一次进入就去更新是否已学完。还有当单个媒体最新状态显示已学完,只有这两个情况才进入方法去更新当前维度下的学习状态。
需求分析
会出现同一个人在不同的页面或者不同的浏览器打开同一门课的情况,当前情况可以能用redis分布式锁解决。
首先记录学习【媒体播放进度表】
course_id :课程维度主键id:唯一性。
section_id :课程下媒体或者文档主键id:唯一性。
user_id : 学习用户。
learned_duration : 已学习时长:用于判断当前是否已学完,当时长等于总长度三分二时。
media_progress : 媒体播放进度:用于返回给前端,上次已播放的位子。
media_duration : 媒体视频总时长:用于判断是否已学完,还是正在学。
learned_status : 学习状态:已学完COMPALETED,正在学LEARNING。
create_date : 创建时间。
update_date : 修改时间。
//学习进度表
DROP TABLE IF EXISTS `学习进度表`;
CREATE TABLE `学习进度表` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`course_id` int(11) COMMENT '课程ID',
`section_id` int(11) COMMENT '节ID',
`type` varchar(255) COMMENT '类型',
`user_id` varchar(64) COMMENT '用户ID',
`learned_duration` bigint(20) COMMENT '已学习时长',
`media_progress` bigint(20) COMMENT '媒体进度',
`media_duration` bigint(20) COMMENT '媒体总时长',
`learned_status` varchar(32) COMMENT '\'已学完,学习中\'',
`create_time` datetime(0) COMMENT '新建时间',
`update_time` datetime(0) COMMENT '更新时间',
PRIMARY KEY (`id`)
);
【课程维度】表
上面表记录单个视频是否已学完,课程维度表记录当前维度下的所有视频是否已学完。
- user_id: 用户id。
- `type` '类型' 记录哪个模块的课程
- course_id : 课程主键id。
- last_section_id : 课程下媒体最后学习id。
- learned_section_count: 已学完总的视频媒体数量。
- learn_status:学习状态:已学完,未学完。
- create_time: 创建时间。
- update_time 修改时间。
课程维度
CREATE TABLE `课程维度` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(64) COMMENT '用户ID',
`type` varchar(255) COMMENT '类型',
`course_id` int(11) comment '课程ID',
`last_section_id` comment '课程下媒体最后学习id',
`learned_section_count` bigint(20) comment '课程ID',
`learn_status` COMMENT '已学完compalate,学习ing',
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`)
)
【学习时长】表
- user_id: 学习用户。
- section_id: 课程下媒体或者文档主键id:唯一性。
- course_id: 课程ID
- type: 类型,哪个模块的学习
- duration: 学习总时长
- learn_status: 学习类型,视频or其他
- 创建时间。
- 修改时间。
学习总时长表
CREATE TABLE `学习总时长表` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(64) COMMENT '用户ID',
`section_id` int(11) COMMENT '节ID',
`course_id` int(11) COMMENT '课程ID',
`type` varchar(255) '类型',
`duration` bigint(20) COMMENT '学习总时长',
`learn_status` varchar(32) COMMENT '学习类型',
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`)
)
Controller
/**
* 媒体进度
*
* @author keying
*/
@RestController
@RequestMapping("/section")
public class SectionController {
@Autowired
private ProgressService progressService;
@PostMapping("/{id}/progress")
public void progress(@PathVariable Long id, @RequestBody @Valid ProgressRequest request) {
/* if ((DateUtils.truncatedCompareToDateUtils.addMilliseconds(learningRecordDO.getGmtModified(),(int)SectionConstants.MIN_DELTA_DURATION), new Date(), Calendar.MILLISECOND) > 0) {
throw new BizException("访问过于频繁。");
}*/
progressService.progress(id, request);
}
}
ServiceImpl
package com.alibaba.first.service.lmpl;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import com.alibaba.first.mapper.UserCourseLearnedMapper;
import com.alibaba.first.mapper.UserLearnStatsMapper;
import com.alibaba.first.mapper.UserLearnedMapper;
import com.alibaba.first.model.ProgressRequest;
import com.alibaba.first.model.UserCourseLearned;
import com.alibaba.first.model.UserLearnStats;
import com.alibaba.first.model.UserLearned;
import com.alibaba.first.service.ProgressService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author keying
* @date 2021/9/1
*/
@Service
@Slf4j
public class ProgressServiceImpl implements ProgressService {
//默认用户
public final String NAME = "keying";
@Autowired
private UserLearnStatsMapper userLearnStatsMapper;
@Autowired
private UserLearnedMapper userLearnedMapper;
@Autowired
private UserCourseLearnedMapper userCourseLearnedMapper;
@Override
public void progress(Long id, ProgressRequest request) {
//存入学习总时长
progressUserLearned(id, request);
//统计当前课程 & 节是否已学完
insertUserLearnStats(id, request);
}
private void progressUserLearned(Long id, ProgressRequest request) {
//总学习时长增加
UserLearned userLearnedSelect = new UserLearned();
userLearnedSelect.setCourseId(request.getCourseId());
userLearnedSelect.setSectionId(id);
userLearnedSelect.setUserId(NAME);
//获取到历史数据
UserLearned userLearned = userLearnedMapper.selectOne(userLearnedSelect);
if (!Objects.isNull(userLearned)) {
//修改学习时长
UserLearned userLearnedUpdate = new UserLearned();
userLearnedUpdate.setId(userLearned.getId());
userLearnedUpdate.setUpdateDate(new Date());
userLearnedUpdate.setDuration(userLearned.getDuration() + request.getDeltaDuration());
userLearnedMapper.updateById(userLearnedUpdate);
return;
}
//新增学习时长
UserLearned userLearnedInsert = new UserLearned();
userLearnedInsert.setUserId(NAME);
userLearnedInsert.setCourseId(request.getCourseId());
userLearnedInsert.setSectionId(id);
userLearnedInsert.setCreateDate(new Date());
userLearnedInsert.setUpdateDate(new Date());
userLearnedInsert.setDuration(request.getDeltaDuration());
userLearnedMapper.insert(userLearnedInsert);
}
private void insertUserLearnStats(Long id, ProgressRequest request) {
//处理节,媒体是否已学完
UserLearnStats userLearnStatsSelect = new UserLearnStats();
userLearnStatsSelect.setUserId(NAME);
userLearnStatsSelect.setSectionId(id);
userLearnStatsSelect.setCourseId(request.getCourseId());
//获取到历史数据
UserLearnStats userLearnStats = userLearnStatsMapper.selectOne(userLearnStatsSelect);
//节状态
if (!Objects.isNull(userLearnStats)) {
//修改
UserLearnStats updateUserLearnStats = new UserLearnStats();
updateUserLearnStats.setId(userLearnStats.getId());
updateUserLearnStats.setUpdateDate(new Date());
//获取是否已学完
boolean flag = calculateLearned(request, updateUserLearnStats.getMediaDuration(),
request.getMediaProgress() + request.getMediaProgress());
updateUserLearnStats.setLearnedStatus("LEARNING");
if (flag) {
updateUserLearnStats.setLearnedStatus("COMPALETE");
}
userLearnStatsMapper.updateById(updateUserLearnStats);
//处理课程 是否学完
courseRedisson(Boolean.FALSE, request, userLearnStats.getLearnedStatus(),
updateUserLearnStats.getLearnedStatus());
return;
}
//无历史数据则创建
UserLearnStats insertUserLearnStats = new UserLearnStats();
insertUserLearnStats.setCourseId(request.getCourseId());
insertUserLearnStats.setCreateDate(new Date());
insertUserLearnStats.setUpdateDate(new Date());
insertUserLearnStats.setLearnedDuration(request.getDeltaDuration());
//总时长默认都60*1000毫秒
insertUserLearnStats.setMediaDuration(60 * 1000L);
//获取是否已学完
boolean flag = calculateLearned(request, insertUserLearnStats.getMediaDuration(),
request.getMediaProgress() + request.getMediaProgress());
insertUserLearnStats.setLearnedStatus("LEARNING");
if (flag) {
insertUserLearnStats.setLearnedStatus("COMPALETE");
}
insertUserLearnStats.setMediaProgress(request.getMediaProgress());
insertUserLearnStats.setSectionId(id);
insertUserLearnStats.setUserId(NAME);
userLearnStatsMapper.insert(insertUserLearnStats);
//处理课程 是否学完
courseRedisson(Boolean.TRUE, request, null,
insertUserLearnStats.getLearnedStatus());
}
private void courseRedisson(Boolean aFalse, ProgressRequest request, String oldLearnedStatus,
String newLearnedStatus) {
// 默认连接上127.0.0.1:6379
RedissonClient redissonClient = Redisson.create();
// 一个分布式锁,指明锁的名称
RLock rLock = redissonClient.getLock(NAME + request.getCourseId());
try {
if (rLock.tryLock(1, 1, TimeUnit.MINUTES)) {
log.info("获取到锁");
courseProgress(Boolean.FALSE, request, oldLearnedStatus,
newLearnedStatus);
}
} catch (Exception e) {
} finally {
rLock.unlock();
}
}
/**
* @param aFalse 是否第一次进入
* @param request
* @param oldLearnedStatus 老的学习状态
* @param newLearnedStatus 新的学习状态
*/
private void courseProgress(Boolean aFalse, ProgressRequest request, String oldLearnedStatus,
String newLearnedStatus) {
//限制进入条件:
// 当是第一次进入的时候,前面的百分百是false,导致全部为false。
//当第二次进入的时候,前面的百分之百为true。后面的就必须为false,才能跳过执行业务代码。
boolean flag = !aFalse && (newLearnedStatus.equals("LEARNING") || oldLearnedStatus.equals("COMPALETE")
&& newLearnedStatus.equals("COMPALETE"));
//当为false的时候跳过,不执行return
if (flag) {
return;
}
Date now = new Date();
//查询是否是第一次
UserCourseLearned userCourseLearnedSelect = new UserCourseLearned();
userCourseLearnedSelect.setCourseId(request.getCourseId());
userCourseLearnedSelect.setUserId(NAME);
UserCourseLearned userCourseLearnedOne = userCourseLearnedMapper.selectOne(userCourseLearnedSelect);
//存在则修改
if (!Objects.isNull(userCourseLearnedOne)) {
UserCourseLearned updateUserCourseLearned = new UserCourseLearned();
updateUserCourseLearned.setId(userCourseLearnedOne.getId());
updateUserCourseLearned.setUpdateDate(now);
updateUserCourseLearned.setLastSectionId(request.getSectionId());
//填充学习状态和学习完的节数量
populateCourseStatus(updateUserCourseLearned, userCourseLearnedOne.getLearnedSectionCount(), newLearnedStatus);
userCourseLearnedMapper.updateById(updateUserCourseLearned);
return;
}
//不存在则新增
UserCourseLearned updateUserCourseInsert = new UserCourseLearned();
updateUserCourseInsert.setUserId(NAME);
updateUserCourseInsert.setCourseId(request.getCourseId());
updateUserCourseInsert.setCreateDate(now);
updateUserCourseInsert.setLastSectionId(request.getSectionId());
updateUserCourseInsert.setUpdateDate(now);
//填充学习状态和学习完的节数量
populateCourseStatus(updateUserCourseInsert, 0L, newLearnedStatus);
userCourseLearnedMapper.insert(updateUserCourseInsert);
}
/**
* @param updateUserCourseInsert 参数
* @param count 旧的已学完节
* @param newLearnedStatus 新的学习状态
*/
private void populateCourseStatus(UserCourseLearned updateUserCourseInsert, Long count, String newLearnedStatus) {
//已学完
if (newLearnedStatus.equals("COMPALETE")) {
//新增一节数量
updateUserCourseInsert.setLearnedSectionCount(count + 1L);
//先查询课程下的总节数是多少,这里代码演示,没有建立课程表,直接 写死每个课程总节数是5
Long sectionLearnedCount = getSectionLearnedCount();
if(updateUserCourseInsert.getLearnedSectionCount() >= sectionLearnedCount){
updateUserCourseInsert.setLearnedStatus("COMPALETE");
}else{
updateUserCourseInsert.setLearnedStatus("LEARNING");
}
return;
}
//未学完
updateUserCourseInsert.setLearnedSectionCount(count);
updateUserCourseInsert.setLearnedStatus("LEARNING");
}
private Long getSectionLearnedCount() {
return 5L;
}
private boolean calculateLearned(ProgressRequest request, Long allDuration, Long learnedDuration) {
if (request.getSectionType().equals("TEXT")) {
return Boolean.TRUE;
}
if (request.getSectionType().equals("VIDEO")) {
if (learnedDuration >= (allDuration * 2 / 3)) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
}
BO
package com.premiere.module.schedule.entity.bo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel
public class ProgressRequestBO {
/**
* 节id(媒体视频id)
*/
@ApiModelProperty(value = "媒体视频id", name = "媒体视频id", required = true)
private Integer sectionId;
/**
* 代表媒体视频或者文档属于哪个维度下的id,比如学习维度,娱乐维度
*/
@ApiModelProperty(value = "课程id", name = "课程id", required = true)
private Integer courseId;
@ApiModelProperty(value = "视频来源类型", name = "视频来源类型", required = true)
private String type;
/**
* 类型,代表当前是媒体视频还是文档
*/
@ApiModelProperty(value = "类型(媒体视频还是文档)", name = "类型(媒体视频还是文档)", required = false)
private String sectionType;
@ApiModelProperty(value = "视频总时长", name = "视频总时长", required = false)
private Long duration;
/**
* 增量时间,非视频为0,视频就传新增的看视频时长
*/
@ApiModelProperty(value = "增量时间(非视频为0,视频就传新增的看视频时长)", name = "增量时间(非视频为0,视频就传新增的看视频时长)", required = false)
private Long deltaDuration;
/**
* 视频播放节点,非视频为0,视频就传已看到的视频节点
*/
@ApiModelProperty(value = "视频播放节点,非视频为0,视频就传已看到的视频节点", name = "视频播放节点,非视频为0,视频就传已看到的视频节点", required = false)
private Long mediaProgress;
/**
* 是否第一次打开当前媒体。(方便以后扩展使用)
*/
@ApiModelProperty(value = "是否第一次打开当前媒体", name = "是否第一次打开当前媒体", required = false)
private Boolean first;
}
————————————————
版权声明:本文为CSDN博主「后端从入门到精通」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ke1ying/article/details/120039189