|
|
@@ -22,6 +22,7 @@ import com.fs.company.domain.CompanyUser;
|
|
|
import com.fs.company.mapper.CompanyMapper;
|
|
|
import com.fs.course.config.CourseConfig;
|
|
|
import com.fs.course.config.RedisKeyScanner;
|
|
|
+import com.fs.course.utils.H5WxUserWatchRedisUtil;
|
|
|
import com.fs.course.domain.*;
|
|
|
import com.fs.course.mapper.*;
|
|
|
import com.fs.course.param.*;
|
|
|
@@ -107,6 +108,8 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
@Autowired
|
|
|
private RedisCache redisCache;
|
|
|
@Autowired
|
|
|
+ private H5WxUserWatchRedisUtil h5WxUserWatchRedisUtil;
|
|
|
+ @Autowired
|
|
|
private IQwExternalContactCacheService qwExternalContactCacheService;
|
|
|
@Autowired
|
|
|
private QwWatchLogMapper qwWatchLogMapper;
|
|
|
@@ -381,69 +384,141 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
@Override
|
|
|
public void scheduleUpdateDurationToDatabase() {
|
|
|
log.info("WXH5-开始更新会员看课时长,检查完课>>>>>>");
|
|
|
- //读取所有的key
|
|
|
- Collection<String> keys = redisCache.keys("h5wxuser:watch:duration:*");
|
|
|
-
|
|
|
- //读取看课配置
|
|
|
- String json = configService.selectConfigByKey("course.config");
|
|
|
- CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
|
|
|
-
|
|
|
+ Set<String> keys = h5WxUserWatchRedisUtil.listDurationKeys();
|
|
|
+ CourseConfig config = loadCourseConfig();
|
|
|
List<FsCourseWatchLog> logs = new ArrayList<>();
|
|
|
for (String key : keys) {
|
|
|
- //取key中数据
|
|
|
String[] parts = key.split(":");
|
|
|
- Long userId=null;
|
|
|
- Long videoId=null;
|
|
|
- Long companyUserId=null;
|
|
|
+ Long userId;
|
|
|
+ Long videoId;
|
|
|
+ Long companyUserId;
|
|
|
try {
|
|
|
userId = Long.parseLong(parts[3]);
|
|
|
videoId = Long.parseLong(parts[4]);
|
|
|
companyUserId = Long.parseLong(parts[5]);
|
|
|
-
|
|
|
- }catch (Exception e){
|
|
|
+ } catch (Exception e) {
|
|
|
log.error("key中id为null:{}", key);
|
|
|
continue;
|
|
|
}
|
|
|
String durationStr = redisCache.getCacheObject(key);
|
|
|
if (durationStr == null) {
|
|
|
log.error("key中数据为null:{}", key);
|
|
|
- continue; // 如果 Redis 中没有记录,跳过
|
|
|
+ h5WxUserWatchRedisUtil.untrackDuration(key);
|
|
|
+ continue;
|
|
|
}
|
|
|
Long duration = Long.valueOf(durationStr);
|
|
|
-
|
|
|
- FsCourseWatchLog watchLog = new FsCourseWatchLog();
|
|
|
- watchLog.setVideoId(videoId);
|
|
|
- watchLog.setUserId(userId);
|
|
|
- watchLog.setCompanyUserId(companyUserId);
|
|
|
- watchLog.setDuration(duration);
|
|
|
-
|
|
|
- //取对应视频的时长
|
|
|
- Long videoDuration = 0L;
|
|
|
- try {
|
|
|
- videoDuration = getFsUserVideoDuration(videoId);
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("视频时长识别错误:{}", key);
|
|
|
+ FsCourseWatchLog watchLog = buildH5WxDurationWatchLogUpdate(userId, videoId, companyUserId, duration, config, key);
|
|
|
+ if (watchLog == null) {
|
|
|
continue;
|
|
|
}
|
|
|
- if (videoDuration != null && videoDuration != 0) {
|
|
|
- //判断是否完课
|
|
|
- long percentage = (duration * 100 / videoDuration);
|
|
|
- if (percentage >= config.getAnswerRate()) {
|
|
|
- watchLog.setLogType(2); // 设置状态为“已完成”checkFsUserWatchStatus
|
|
|
- watchLog.setFinishTime(new Date());
|
|
|
- String heartbeatKey = "h5wxuser:watch:heartbeat:" + userId + ":" + videoId + ":" + companyUserId;
|
|
|
- // 完课删除心跳记录
|
|
|
- redisCache.deleteObject(heartbeatKey);
|
|
|
- // 完课删除看课时长记录
|
|
|
- redisCache.deleteObject(key);
|
|
|
- }
|
|
|
- }
|
|
|
- //集合中增加
|
|
|
logs.add(watchLog);
|
|
|
}
|
|
|
batchUpdateFsUserCourseWatchLog(logs, 100);
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public void syncH5WxUserWatchProgressOnFinish(Long userId, Long videoId, Long companyUserId, Long duration) {
|
|
|
+ if (userId == null || videoId == null || companyUserId == null || duration == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ CourseConfig config = loadCourseConfig();
|
|
|
+ String durationKey = H5WxUserWatchRedisUtil.durationKey(userId, videoId, companyUserId);
|
|
|
+ FsCourseWatchLog watchLog = buildH5WxDurationWatchLogUpdate(userId, videoId, companyUserId, duration, config, durationKey);
|
|
|
+ if (watchLog == null || watchLog.getLogType() == null || watchLog.getLogType() != 2) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ batchUpdateFsUserCourseWatchLog(Collections.singletonList(watchLog), 100);
|
|
|
+ log.info("H5微信看课已达完课阈值,已同步写库: userId={}, videoId={}, companyUserId={}, duration={}",
|
|
|
+ userId, videoId, companyUserId, duration);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void syncQwUserWatchProgressOnFinish(Long qwUserId, Long qwExternalContactId, Long videoId, Long duration) {
|
|
|
+ if (qwUserId == null || qwExternalContactId == null || videoId == null || duration == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ CourseConfig config = loadCourseConfig();
|
|
|
+ String durationKey = "h5user:watch:duration:" + qwUserId + ":" + qwExternalContactId + ":" + videoId;
|
|
|
+ FsCourseWatchLog watchLog = buildQwDurationWatchLogUpdate(
|
|
|
+ qwUserId, qwExternalContactId, videoId, duration, config, durationKey);
|
|
|
+ if (watchLog.getLogType() == null || watchLog.getLogType() != 2) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ batchUpdateFsCourseWatchLog(Collections.singletonList(watchLog), 100);
|
|
|
+ log.info("企微看课已达完课阈值,已同步写库: qwUserId={}, qwExternalContactId={}, videoId={}, duration={}",
|
|
|
+ qwUserId, qwExternalContactId, videoId, duration);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 与 scheduleBatchUpdateToDatabase / processKeyBatch(type=1) 单条处理逻辑一致
|
|
|
+ */
|
|
|
+ private FsCourseWatchLog buildQwDurationWatchLogUpdate(Long qwUserId, Long externalId, Long videoId,
|
|
|
+ Long duration, CourseConfig config, String durationRedisKey) {
|
|
|
+ FsCourseWatchLog watchLog = new FsCourseWatchLog();
|
|
|
+ watchLog.setVideoId(videoId);
|
|
|
+ watchLog.setQwUserId(qwUserId);
|
|
|
+ watchLog.setQwExternalContactId(externalId);
|
|
|
+ watchLog.setDuration(duration);
|
|
|
+
|
|
|
+ Long videoDuration;
|
|
|
+ try {
|
|
|
+ videoDuration = getVideoDuration(videoId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("视频时长识别错误:{}", durationRedisKey);
|
|
|
+ return watchLog;
|
|
|
+ }
|
|
|
+ if (videoDuration != null && videoDuration != 0) {
|
|
|
+ long percentage = (duration * 100 / videoDuration);
|
|
|
+ if (percentage >= config.getAnswerRate()) {
|
|
|
+ watchLog.setLogType(2);
|
|
|
+ watchLog.setFinishTime(new Date());
|
|
|
+ String heartbeatKey = "h5user:watch:heartbeat:" + qwUserId + ":" + externalId + ":" + videoId;
|
|
|
+ redisCache.deleteObject(heartbeatKey);
|
|
|
+ redisCache.deleteObject(durationRedisKey);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return watchLog;
|
|
|
+ }
|
|
|
+
|
|
|
+ private CourseConfig loadCourseConfig() {
|
|
|
+ String json = configService.selectConfigByKey("course.config");
|
|
|
+ return JSONUtil.toBean(json, CourseConfig.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 与 scheduleUpdateDurationToDatabase 单条处理逻辑一致(百分比达标即完课并清理 Redis)
|
|
|
+ */
|
|
|
+ private FsCourseWatchLog buildH5WxDurationWatchLogUpdate(Long userId, Long videoId, Long companyUserId,
|
|
|
+ Long duration, CourseConfig config, String durationRedisKey) {
|
|
|
+ FsCourseWatchLog watchLog = new FsCourseWatchLog();
|
|
|
+ watchLog.setVideoId(videoId);
|
|
|
+ watchLog.setUserId(userId);
|
|
|
+ watchLog.setCompanyUserId(companyUserId);
|
|
|
+ watchLog.setDuration(duration);
|
|
|
+
|
|
|
+ Long videoDuration;
|
|
|
+ try {
|
|
|
+ videoDuration = getFsUserVideoDuration(videoId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("视频时长识别错误:{}", durationRedisKey);
|
|
|
+ return watchLog;
|
|
|
+ }
|
|
|
+ if (videoDuration == null || videoDuration == 0 || config == null || config.getAnswerRate() == null) {
|
|
|
+ return watchLog;
|
|
|
+ }
|
|
|
+ long percentage = (duration * 100 / videoDuration);
|
|
|
+ if (percentage >= config.getAnswerRate()) {
|
|
|
+ watchLog.setLogType(2);
|
|
|
+ watchLog.setFinishTime(new Date());
|
|
|
+ String heartbeatKey = H5WxUserWatchRedisUtil.heartbeatKey(userId, videoId, companyUserId);
|
|
|
+ redisCache.deleteObject(heartbeatKey);
|
|
|
+ h5WxUserWatchRedisUtil.untrackHeartbeat(heartbeatKey);
|
|
|
+ redisCache.deleteObject(durationRedisKey);
|
|
|
+ h5WxUserWatchRedisUtil.untrackDuration(durationRedisKey);
|
|
|
+ }
|
|
|
+ return watchLog;
|
|
|
+ }
|
|
|
+
|
|
|
public Long getFsUserVideoDuration(Long videoId) {
|
|
|
//将视频时长也存到redis
|
|
|
String videoRedisKey = "h5wxuser:video:duration:" + videoId;
|
|
|
@@ -468,8 +543,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
@Override
|
|
|
public void checkFsUserWatchStatus() {
|
|
|
log.info("WXH5-开始更新会员看课中断记录>>>>>");
|
|
|
- // 从 Redis 中获取所有正在看课的用户记录
|
|
|
- Collection<String> keys = redisCache.keys("h5wxuser:watch:heartbeat:*");
|
|
|
+ Set<String> keys = h5WxUserWatchRedisUtil.listHeartbeatKeys();
|
|
|
LocalDateTime now = LocalDateTime.now();
|
|
|
List<FsCourseWatchLog> logs = new ArrayList<>();
|
|
|
for (String key : keys) {
|
|
|
@@ -481,6 +555,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
// 获取最后心跳时间
|
|
|
String lastHeartbeatStr = redisCache.getCacheObject(key);
|
|
|
if (lastHeartbeatStr == null) {
|
|
|
+ h5WxUserWatchRedisUtil.untrackHeartbeat(key);
|
|
|
continue; // 如果 Redis 中没有记录,跳过
|
|
|
}
|
|
|
LocalDateTime lastHeartbeatTime = LocalDateTime.parse(lastHeartbeatStr);
|
|
|
@@ -494,6 +569,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
watchLog.setLogType(4);
|
|
|
// 从 Redis 中删除该记录
|
|
|
redisCache.deleteObject(key);
|
|
|
+ h5WxUserWatchRedisUtil.untrackHeartbeat(key);
|
|
|
} else {
|
|
|
watchLog.setLogType(1);
|
|
|
}
|
|
|
@@ -1100,32 +1176,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
continue; // 如果 Redis 中没有记录,跳过
|
|
|
}
|
|
|
Long duration = Long.valueOf(durationStr);
|
|
|
-
|
|
|
- watchLog.setDuration(duration);
|
|
|
-
|
|
|
- // 取对应视频的时长
|
|
|
- Long videoDuration;
|
|
|
- try {
|
|
|
- videoDuration = getVideoDuration(videoId);
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("视频时长识别错误:{}", key);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- if (videoDuration != null && videoDuration != 0) {
|
|
|
- // 判断是否完课
|
|
|
- long percentage = (duration * 100 / videoDuration);
|
|
|
- if (percentage >= config.getAnswerRate()) {
|
|
|
- watchLog.setLogType(2); // 设置状态为"已完成"
|
|
|
- watchLog.setFinishTime(new Date());
|
|
|
- String heartbeatKey = "h5user:watch:heartbeat:" + qwUserId + ":" + externalId + ":" + videoId;
|
|
|
- // 完课删除心跳记录
|
|
|
- redisCache.deleteObject(heartbeatKey);
|
|
|
- // 完课删除看课时长记录
|
|
|
- redisCache.deleteObject(key);
|
|
|
- }
|
|
|
- }
|
|
|
+ watchLog = buildQwDurationWatchLogUpdate(qwUserId, externalId, videoId, duration, config, key);
|
|
|
}else{
|
|
|
//检查看课中断
|
|
|
// 获取最后心跳时间
|