Browse Source

完课定时任务增加一次检查,减少完课误差

xw 3 ngày trước cách đây
mục cha
commit
c89401d131

+ 1 - 1
fs-qw-task/src/main/java/com/fs/app/task/CourseWatchLogScheduler.java

@@ -175,7 +175,7 @@ public class CourseWatchLogScheduler {
 
     }
 
-    @Scheduled(fixedRate = 30000) // 每分钟执行一次
+    @Scheduled(fixedRate = 30000) // 每30s执行一次
     public void checkFsUserWatchStatus() {
         // 尝试设置标志为 true,表示任务开始执行
         if (!isRunning4.compareAndSet(false, true)) {

+ 10 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java

@@ -129,6 +129,16 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
 
     void scheduleUpdateDurationToDatabase();
 
+    /**
+     * H5 微信看课:上报时长时若已达完课条件则立即写库(逻辑与定时任务一致,仅提前完课落库)
+     */
+    void syncH5WxUserWatchProgressOnFinish(Long userId, Long videoId, Long companyUserId, Long duration);
+
+    /**
+     * 企微看课:上报时长时若已达完课条件则立即写库(逻辑与 scheduleBatchUpdateToDatabase 单条一致)
+     */
+    void syncQwUserWatchProgressOnFinish(Long qwUserId, Long qwExternalContactId, Long videoId, Long duration);
+
     void checkFsUserWatchStatus();
 
     /**

+ 112 - 67
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -385,24 +385,18 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     public void scheduleUpdateDurationToDatabase() {
         log.info("WXH5-开始更新会员看课时长,检查完课>>>>>>");
         Set<String> keys = h5WxUserWatchRedisUtil.listDurationKeys();
-
-        //读取看课配置
-        String json = configService.selectConfigByKey("course.config");
-        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-
+        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;
             }
@@ -410,45 +404,121 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
             if (durationStr == null) {
                 log.error("key中数据为null:{}", key);
                 h5WxUserWatchRedisUtil.untrackDuration(key);
-                continue;  // 如果 Redis 中没有记录,跳过
+                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 = H5WxUserWatchRedisUtil.heartbeatKey(userId, videoId, companyUserId);
-                    // 完课删除心跳记录
-                    redisCache.deleteObject(heartbeatKey);
-                    h5WxUserWatchRedisUtil.untrackHeartbeat(heartbeatKey);
-                    // 完课删除看课时长记录
-                    redisCache.deleteObject(key);
-                    h5WxUserWatchRedisUtil.untrackDuration(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;
@@ -1106,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{
                     //检查看课中断
                     // 获取最后心跳时间

+ 17 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -178,6 +178,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     private FsCourseWatchLogMapper courseWatchLogMapper;
     @Autowired
+    private IFsCourseWatchLogService courseWatchLogService;
+    @Autowired
     private ISopUserLogsInfoService iSopUserLogsInfoService;
     @Autowired
     private FsCourseLinkMapper fsCourseLinkMapper;
@@ -524,6 +526,16 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 redisCache.setCacheObject(redisKey, param.getDuration().toString(), 2, TimeUnit.HOURS);
             }
             updateHeartbeat(param);
+            if (param.getDuration() != null && param.getQwUserId() != null
+                    && param.getQwExternalId() != null && param.getVideoId() != null) {
+                try {
+                    Long qwUserId = Long.parseLong(param.getQwUserId());
+                    courseWatchLogService.syncQwUserWatchProgressOnFinish(
+                            qwUserId, param.getQwExternalId(), param.getVideoId(), param.getDuration());
+                } catch (NumberFormatException e) {
+                    logger.warn("企微看课完课同步跳过,qwUserId: {}", param.getQwUserId());
+                }
+            }
             return R.ok();
         } catch (Exception e) {
             e.printStackTrace();
@@ -3323,6 +3335,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
             //更新缓存中的心跳时间
             updateHeartbeatWx(param);
+            // 已达完课阈值时立即写库,避免仅依赖定时任务产生十几秒延迟
+            if (param.getDuration() != null) {
+                courseWatchLogService.syncH5WxUserWatchProgressOnFinish(
+                        param.getUserId(), param.getVideoId(), param.getCompanyUserId(), param.getDuration());
+            }
             return R.ok();
         } catch (Exception e) {
             logger.error("更新看课时长失败:{}", redisKey, e.getMessage());