diff --git a/servers/boe-server-all/src/main/java/com/xboe/school/study/api/StudyCourseApi.java b/servers/boe-server-all/src/main/java/com/xboe/school/study/api/StudyCourseApi.java index 9f041064..386a24d5 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/school/study/api/StudyCourseApi.java +++ b/servers/boe-server-all/src/main/java/com/xboe/school/study/api/StudyCourseApi.java @@ -15,6 +15,7 @@ import com.xboe.constants.CacheName; import com.xboe.module.course.vo.TeacherVo; import com.xboe.module.usergroup.service.IUserGroupService; import com.xboe.school.study.dao.StudyCourseDao; +import com.xboe.school.vo.StudyTimeVo; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -275,19 +276,6 @@ public class StudyCourseApi extends ApiBaseController{ continue; } - // 查询redis上面的key,并解析value获取到lastStudyTime - String lastActive = redisTemplate.opsForValue().get("studyContentId:" + item.getId() + ":last_active"); - if (StringUtil.isNotBlank(lastActive)) { - String[] parts = lastActive.split("&"); - if (parts.length == 2) { - int lastStudyTimeRedis = Integer.parseInt(parts[0]); - log.info("study-video-time-redis获取---lastStudyTimeRedis = " + lastStudyTimeRedis); - if(lastStudyTimeRedis>0){ - item.setLastStudyTime(lastStudyTimeRedis); - } - } - } - BigDecimal lastStudyTime = new BigDecimal(item.getLastStudyTime()); BigDecimal duration = new BigDecimal(content.getDuration()); BigDecimal progress = lastStudyTime.divide(duration, 10, RoundingMode.HALF_UP); @@ -625,7 +613,7 @@ public class StudyCourseApi extends ApiBaseController{ @Deprecated @RequestMapping(value="/appendtime",method = {RequestMethod.GET,RequestMethod.POST}) public JsonResponse appendTime(StudyTime studyTime, HttpServletRequest request){ - + if(StringUtils.isBlank(studyTime.getStudyId())){ return error("参数错误"); } @@ -655,7 +643,91 @@ public class StudyCourseApi extends ApiBaseController{ return error("记录学习时长错误",e.getMessage()); } } - + + /** + * appendtime 于 study-video-time 合并 + * */ + /*@RequestMapping(value="/updateStudyVideoTime1",method = {RequestMethod.GET,RequestMethod.POST}) + public JsonResponse updateStudyVideoTime1(StudyTimeVo studyTime, HttpServletRequest request){ + + // 0 study-video-time , 1 appendtime + if (studyTime.getType() == 0){ + if(StringUtils.isBlank(studyTime.getItemId())){ + return error("参数错误"); + } + if(studyTime.getVideoTime()==null){ + return error("无时间点"); + } + //检查是否已存在 + try { + studyService.updateLastTime(studyTime.getItemId(),studyTime.getVideoTime(), getCurrent().getAccountId()); + if (studyTime.getContentId() != null && studyTime.getCourseId() != null && studyTime.getProgressVideo() != null){ + contentService.updateProcessVideo(studyTime.getContentId(), studyTime.getCourseId(), studyTime.getProgressVideo()); + } + return success("true"); + }catch(Exception e) { + log.error("updateStudyVideoTime type =0 记录最后学习时间错误",e); + return error("updateStudyVideoTime type =0 记录最后学习时间失败 ",e.getMessage()); + } + }else if(studyTime.getType() == 1){ + if(StringUtils.isBlank(studyTime.getStudyId())){ + return error("参数错误"); + } + if(StringUtils.isBlank(studyTime.getCourseId())){ + return error("未指定课程"); + } + if(StringUtils.isBlank(studyTime.getContentId())){ + return error("未指定资源内容"); + } + + String token = request.getHeader("Xboe-Access-Token"); + if (StringUtils.isEmpty(token)) { + token = request.getHeader("token"); + } + try { + studyService.updateStudyDuration(studyTime.getStudyId(),null,studyTime.getContentId(),studyTime.getVideoTime(),studyTime.getCourseId()); + List allUserList = thirdApi.getStudyCourseList(studyTime.getStudyId() ,studyTime.getCourseId(), token); + log.info("updateStudyVideoTime type =1 在线课学习记录 = " + allUserList); + return success(studyTime.getId()); + }catch(Exception e) { + log.error("updateStudyVideoTime type =1 记录学习时长错误",e); + return error("updateStudyVideoTime type =1 记录学习时长错误 ",e.getMessage()); + } + }else{ + return error("type不能为空"); + } + }*/ + + @RequestMapping(value="/updateStudyVideoTime",method = {RequestMethod.GET,RequestMethod.POST}) + public JsonResponse updateStudyVideoTime(StudyTimeVo studyTime, HttpServletRequest request){ + try { + if(StringUtils.isBlank(studyTime.getItemId())){ + return error("参数错误"); + } + if(studyTime.getVideoTime()==null){ + return error("无时间点"); + } + if(StringUtils.isBlank(studyTime.getStudyId())){ + return error("参数错误"); + } + if(StringUtils.isBlank(studyTime.getCourseId())){ + return error("未指定课程"); + } + if(StringUtils.isBlank(studyTime.getContentId())){ + return error("未指定资源内容"); + } + studyService.updateStudyDuration(studyTime.getStudyId(),studyTime.getItemId(),studyTime.getContentId(),studyTime.getVideoTime(),studyTime.getCourseId()); + if (studyTime.getContentId() != null && studyTime.getCourseId() != null && studyTime.getProgressVideo() != null){ + contentService.updateProcessVideo(studyTime.getContentId(), studyTime.getCourseId(), studyTime.getProgressVideo()); + } + + } catch (Exception e) { + log.error("updateStudyVideoTime",e); + return error("学习时长记录失败",e.getMessage(),null); + } + return success("true"); + } + /**获取最后一次的学习内容*/ @GetMapping("/last-study") public JsonResponse> lastStudy(){ diff --git a/servers/boe-server-all/src/main/java/com/xboe/school/study/api/StudyCourseTask.java b/servers/boe-server-all/src/main/java/com/xboe/school/study/api/StudyCourseTask.java index 9236c855..0e70daee 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/school/study/api/StudyCourseTask.java +++ b/servers/boe-server-all/src/main/java/com/xboe/school/study/api/StudyCourseTask.java @@ -1,5 +1,7 @@ package com.xboe.school.study.api; +import com.xboe.api.ThirdApi; +import com.xboe.school.study.entity.StudyCourse; import com.xboe.school.study.service.IStudyService; import com.xxl.job.core.handler.annotation.XxlJob; import lombok.RequiredArgsConstructor; @@ -10,10 +12,13 @@ import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; +import javax.annotation.Resource; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; /** * @author by lyc @@ -26,13 +31,15 @@ public class StudyCourseTask { private final IStudyService studyService; private final StringRedisTemplate redisTemplate; + @Resource + private ThirdApi thirdApi; /** * 定时任务 * 获取redis 中学习结束的数据更新入库 * */ - @XxlJob("saveStudyCourseItemLastTime") - public void saveStudyCourseItemLastTime() { + @XxlJob("saveStudyCourseItemLastTime2") + public void saveStudyCourseItemLastTime2() { // 1. 定义匹配模式(匹配所有目标key) final String KEY_PATTERN = "studyContentId:*:last_active"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); @@ -59,7 +66,6 @@ public class StudyCourseTask { String[] parts = redisKey.split(":"); if (parts.length < 2) continue; String studyContentId = parts[1]; - // 7. 获取存储的时间点(示例逻辑) String redisValue = redisTemplate.opsForValue().get(redisKey); if (redisValue == null) continue; @@ -69,19 +75,15 @@ public class StudyCourseTask { if (partValues.length >= 2){ timestamp = LocalDateTime.parse(partValues[1], formatter); } - // 8. 更新数据库(调用已有服务方法) studyService.updateStudyCourseItemLastTime(studyContentId, lastStudyTime, timestamp); - // 9. 删除Redis键(原子操作) redisTemplate.delete(redisKey); - log.info("处理成功 key: {}, lastStudyTime: {}", redisKey, lastStudyTime); } catch (Exception e) { log.error("处理失败 key: {}", redisKey, e); } } - } cursor.close(); } catch (Exception e) { @@ -102,5 +104,159 @@ public class StudyCourseTask { } + @XxlJob("saveStudyCourseItemLastTime") + public void saveStudyCourseItemLastTime() { + // 1. 定义匹配模式(匹配所有目标key) + final String KEY_PATTERN = "studyId:*:courseId:*:courseContentId:*:studyItemId:*"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + // 2. 使用SCAN安全遍历(避免阻塞) + ScanOptions options = ScanOptions.scanOptions() + .match(KEY_PATTERN) + .count(100) // 分页大小 + .build(); + + try (RedisConnection connection = Objects.requireNonNull(redisTemplate.getConnectionFactory()).getConnection()) { + Cursor cursor = connection.scan(options); + // 3. 遍历处理符合条件的key + while (cursor.hasNext()) { + String redisKey = new String(cursor.next()); + log.info("-定时任务 saveStudyCourseItemLastTime ---redisKey = " + redisKey); + // 4. 获取剩余TTL(秒) + Long ttl = redisTemplate.getExpire(redisKey, TimeUnit.SECONDS); + + // 5. 过滤条件:剩余时间 >= 29天23小时30分钟(转换为秒) + // 总需时间 = (30天 - 30分钟) = 29天23小时30分钟 = 2590200秒 + // 5分钟 300秒 || 2592000 - 300 = 2591700 + if (ttl <= 2590200) { + try { + // 6. 提取studyContentId + String[] parts = redisKey.split(":"); + if (parts.length < 7) continue; + String studyId = parts[1]; + String courseId = parts[3]; + String courseContentId = parts[5]; + String studyItemId = parts[7]; + // 7. 获取存储的时间点(示例逻辑) + String redisValue = redisTemplate.opsForValue().get(redisKey); + log.info("-定时任务 saveStudyCourseItemLastTime ---redisValue = " + redisValue); + if (redisValue == null) continue; + String[] partValues = redisValue.split("&"); + int studyVideoTtime = Integer.parseInt(partValues[0]); + int appendtime = Integer.parseInt(partValues[1]); + LocalDateTime timestamp = null; + if (partValues.length >= 2){ + timestamp = LocalDateTime.parse(partValues[2], formatter); + } + // 8. 更新数据库(调用已有服务方法) + studyService.newAppendStudyDuration(studyId,null,courseContentId,appendtime,timestamp); + log.info("-定时任务 saveStudyCourseItemLastTime ---studyItemId = " + studyItemId); + if (studyItemId != null && !studyItemId.equals("null")){ + log.info("-定时任务 saveStudyCourseItemLastTime --- boolean studyItemId = " + (studyItemId != null)); + // 8. 更新数据库(调用已有服务方法) + studyService.updateStudyCourseItemLastTime(studyItemId, studyVideoTtime, timestamp); + } + List allUserList = thirdApi.getStudyCourseList(studyId , courseId, null); + log.info("处理成功 allUserList: {}", allUserList); + // 9. 删除Redis键(原子操作) + redisTemplate.delete(redisKey); + log.info("处理成功 key: {}, lastStudyTime: {}", redisKey, appendtime); + } catch (Exception e) { + log.error("处理失败 key: {}", redisKey, e); + } + } + } + cursor.close(); + } catch (Exception e) { + log.error("定时任务执行异常", e); + } + + + } + + + @XxlJob("saveStudyCourseItemLastTime1") + public void saveStudyCourseItemLastTime1() { + // 定义需要处理的键模式集合 + processKeys("studyContentId:*:last_active", this::handleLastActiveKey); + processKeys("studyId:*:courseId:*:courseContentId:*", this::handleDurationKey); + } + + private void processKeys(String keyPattern, BiConsumer keyHandler) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + ScanOptions options = ScanOptions.scanOptions() + .match(keyPattern) + .count(100) + .build(); + + try (RedisConnection connection = Objects.requireNonNull(redisTemplate.getConnectionFactory()).getConnection()) { + Cursor cursor = connection.scan(options); + while (cursor.hasNext()) { + String redisKey = new String(cursor.next()); + Long ttl = redisTemplate.getExpire(redisKey, TimeUnit.SECONDS); + + if (ttl != null && ttl <= 2590200) { + try { + String redisValue = redisTemplate.opsForValue().get(redisKey); + if (redisValue != null) { + // 调用对应的处理方法 + keyHandler.accept(redisKey, redisValue); + } + redisTemplate.delete(redisKey); + log.info("Key processed: {}", redisKey); + } catch (Exception e) { + log.error("Process failed [{}]", redisKey, e); + } + } + } + cursor.close(); + } catch (Exception e) { + log.error("Key processing error: {}", keyPattern, e); + } + } + + // 处理 last_active 类型键 + private void handleLastActiveKey(String redisKey, String redisValue) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + String[] parts = redisKey.split(":"); + String studyContentId = parts[1]; + + String[] values = redisValue.split("&"); + int lastStudyTime = Integer.parseInt(values[0]); + LocalDateTime timestamp = values.length >= 2 ? + LocalDateTime.parse(values[1], formatter) : null; + + studyService.updateStudyCourseItemLastTime(studyContentId, lastStudyTime, timestamp); + } + + // 处理 duration 类型键 + private void handleDurationKey(String redisKey, String redisValue) { + String[] parts = redisKey.split(":"); + String studyId = parts[1]; + String courseId = parts[3]; + String courseContentId = parts[5]; + + String[] values = redisValue.split("&"); + int duration = Integer.parseInt(values[0]); + LocalDateTime timestamp = values.length >= 2 ? + LocalDateTime.parse(values[1], DateTimeFormatter.ISO_LOCAL_DATE_TIME) : null; + + studyService.newAppendStudyDuration(studyId, null, courseContentId, duration, timestamp); + + // 保留第三方调用 + List allUserList = thirdApi.getStudyCourseList(studyId, courseId, null); + log.info("Study records synced: {}", allUserList.size()); + } + + + + + + + + + + + + } diff --git a/servers/boe-server-all/src/main/java/com/xboe/school/study/service/IStudyService.java b/servers/boe-server-all/src/main/java/com/xboe/school/study/service/IStudyService.java index cc4a66e4..99341ed7 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/school/study/service/IStudyService.java +++ b/servers/boe-server-all/src/main/java/com/xboe/school/study/service/IStudyService.java @@ -101,4 +101,8 @@ public interface IStudyService { List getList(String courseId, String contentId, String name, Integer status); void updateStudyCourseItemLastTime(String studyContentId, int lastStudyTime, LocalDateTime timestamp); + + void updateStudyDuration(String studyId,String studyItemId, String contentId, Integer videoTime,String courseId); + + void newAppendStudyDuration(String studyId, String studyItemId, String courseContentId, int duration, LocalDateTime timestamp); } diff --git a/servers/boe-server-all/src/main/java/com/xboe/school/study/service/impl/StudyServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/school/study/service/impl/StudyServiceImpl.java index 589547d3..544a0a61 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/school/study/service/impl/StudyServiceImpl.java +++ b/servers/boe-server-all/src/main/java/com/xboe/school/study/service/impl/StudyServiceImpl.java @@ -116,34 +116,74 @@ public class StudyServiceImpl implements IStudyService{ //增加内容的学习时长 if(StringUtils.isNotBlank(studyItemId)) { //直接根据id更新 -// String hql="Update StudyCourseItem set studyDuration=studyDuration+"+duration+",status=(case when status<2 then 2 else status end) where id=?1"; -// scItemDao.update(hql,studyItemId); String sql="Update boe_study_course_item set study_duration=study_duration+"+duration+",status=(case when status<2 then 2 else status end) where id=?1"; scItemDao.sqlUpdate(sql,studyItemId); - //scItemDao.updateMultiFieldById(studyItemId, UpdateBuilder.create("studyDuration", "studyDuration+"+duration,FieldUpdateType.EXPRESSION)); - + }else { - //根据学习id和课程内容id更新 -// scItemDao.update(UpdateBuilder.from(StudyCourseItem.class) -// .addUpdateField("studyDuration", "studyDuration+"+duration,FieldUpdateType.EXPRESSION) -// .addFilter(FieldFilters.eq("studyId", studyId)) -// .addFilter(FieldFilters.eq("contentId", courseContentId)) -// .builder()); -// -// String hql="Update StudyCourseItem set studyDuration=studyDuration+"+duration+",status=(case when status<2 then 2 else status end) where studyId=?1 and contentId=?2"; -// scItemDao.update(hql,studyId,courseContentId); String sql="Update boe_study_course_item set study_duration=study_duration+"+duration+",status=(case when status<2 then 2 else status end) where study_id=?1 and content_id=?2"; scItemDao.sqlUpdate(sql,studyId,courseContentId); } - //追加课程的学习时长 - //scDao.updateMultiFieldById(studyId, UpdateBuilder.create("totalDuration", "totalDuration+"+duration,FieldUpdateType.EXPRESSION)); - String sql="Update boe_study_course set total_duration=total_duration+"+duration+",status=(case when status<2 then 2 else status end),progress=(case when progress=0 then 1 else progress end),last_time = '"+LocalDateTime.now()+"' where id=?1"; scDao.sqlUpdate(sql,studyId); - } + @Override + @Transactional + public void newAppendStudyDuration(String studyId,String studyItemId,String courseContentId, int duration,LocalDateTime timestamp) { + + //增加内容的学习时长 + if(StringUtils.isNotBlank(studyItemId)) { + //直接根据id更新 + String sql="Update boe_study_course_item set study_duration=study_duration+"+duration+",status=(case when status<2 then 2 else status end) where id=?1"; + scItemDao.sqlUpdate(sql,studyItemId); + + }else { + String sql="Update boe_study_course_item set study_duration=study_duration+"+duration+",status=(case when status<2 then 2 else status end) where study_id=?1 and content_id=?2"; + scItemDao.sqlUpdate(sql,studyId,courseContentId); + } + + String sql="Update boe_study_course set total_duration=total_duration+"+duration+",status=(case when status<2 then 2 else status end),progress=(case when progress=0 then 1 else progress end),last_time = '"+timestamp+"' where id=?1"; + scDao.sqlUpdate(sql,studyId); + } + + // 更新 前端传输已学习时长 + @Override + public void updateStudyDuration(String studyId,String studyItemId,String courseContentId, Integer videoTime,String courseId) { + + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + + String key = "studyId:" + studyId + ":courseId:" + courseId + ":courseContentId:" + courseContentId + ":studyItemId:" + studyItemId; + String currentValue = redisTemplate.opsForValue().get(key); + Integer lastDuration = 0; + Integer oldVideoTime = 0; + Integer sum = 10; // 原appendtime改为固定10秒调用一次接口 + if (currentValue != null) { + String[] partValues = currentValue.split("&"); + oldVideoTime = Integer.parseInt(partValues[0]); + lastDuration = Integer.parseInt(partValues[1]); + sum += lastDuration; + if(oldVideoTime > videoTime){ + videoTime = oldVideoTime;// 取最大值最终入库 + } + }; + + String value = videoTime + "&" + sum + "&" + now.format(formatter); // study_video_time & appendtime & time + log.info("-study-video-time-----value = " + value); + + // 20250303 优化 多次更新改一次更新 + // 更新Redis中的最后活跃时间(带30秒过期) + redisTemplate.opsForValue().set( + key, + value, + Duration.ofSeconds(2592000) + ); + log.info("- 合并 updateStudyDuration -redis保存---value = " + value); +// Duration.ofDays(30) 也就是 2592000秒 + } + + @Override @Transactional public void appendStudyDuration(StudyTime st) { @@ -166,7 +206,21 @@ public class StudyServiceImpl implements IStudyService{ @Override public List findByStudyId(String studyId) { - return scItemDao.findList(OrderCondition.desc("lastTime"),FieldFilters.eq("studyId", studyId)); + List list = scItemDao.findList(OrderCondition.desc("lastTime"),FieldFilters.eq("studyId", studyId)); + for (StudyCourseItem item : list){ + log.info("-- studyIndex -查询上次学习的是什么资源。mysql查询---------------- item = " + item); + String redisKey = "studyId:" + studyId + ":courseId:" + item.getCourseId() + ":courseContentId:" + item.getContentId() + ":studyItemId:" + item.getId(); + log.info("-- studyIndex -查询上次学习的是什么资源。查询用户的学习情况---------------- redisKey = " + redisKey); + String redisValue = redisTemplate.opsForValue().get(redisKey); + log.info("-- studyIndex -查询上次学习的是什么资源。查询用户的学习情况---------------- redisValue = " + redisValue); + if (redisValue != null) { + String[] values = redisValue.split("&"); + int duration = Integer.parseInt(values[0]); + item.setLastStudyTime(duration); + log.info("-- studyIndex -----set 结果---------------- LastStudyTime = " + item.getLastStudyTime()); + } + } + return list; } @Override @@ -357,6 +411,7 @@ public class StudyServiceImpl implements IStudyService{ log.info("-study-video-time-mysql保存---studyContentId = " + studyContentId); } + @Override public Map getLast(String aid) { //按lastTime排序,第一条,只是课件内容 diff --git a/servers/boe-server-all/src/main/java/com/xboe/school/vo/StudyTimeVo.java b/servers/boe-server-all/src/main/java/com/xboe/school/vo/StudyTimeVo.java new file mode 100644 index 00000000..388c10d7 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/school/vo/StudyTimeVo.java @@ -0,0 +1,19 @@ +package com.xboe.school.vo; + +import com.xboe.school.study.entity.StudyTime; +import lombok.Data; + +/** appendtime 于 study-video-time 合并 + * appendtime 参数 StudyTime + * study-video-time 参数 是 StudyTimeVo + */ +@Data +public class StudyTimeVo extends StudyTime { + + private String itemId; + private Integer videoTime; +// private String contentId; // 已继承 +// private String courseId; // 已继承 + private Float progressVideo; + private Integer type; // 0 study-video-time , 1 appendtime +}