From 35fb15e2e3679ce22d386a656c2572c3408bba14 Mon Sep 17 00:00:00 2001 From: GoCo Date: Tue, 2 Sep 2025 04:03:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=AD=A6=E4=B9=A0=E8=BF=9B=E5=BA=A6=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/aiol/constant/RedisConst.java | 9 + .../aiol/controller/AiolCourseController.java | 59 ++++-- .../aiol/service/IAiolCourseService.java | 11 + .../service/impl/AiolCourseServiceImpl.java | 188 ++++++++++++++++-- .../src/main/resources/logback-spring.xml | 3 + 5 files changed, 231 insertions(+), 39 deletions(-) create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/constant/RedisConst.java diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/constant/RedisConst.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/constant/RedisConst.java new file mode 100644 index 00000000..b178aa9f --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/constant/RedisConst.java @@ -0,0 +1,9 @@ +package org.jeecg.modules.aiol.constant; + +public class RedisConst { + // 视频资源信息 + public static final String VIDEO_RESOURCE_CACHE_KEY_PREFIX = "aiol:video_resource:"; + public static final int VIDEO_RESOURCE_CACHE_EXPIRE = 30 * 24 * 60 * 60; // 30天过期 + + +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolCourseController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolCourseController.java index ed2fd762..aadef894 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolCourseController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolCourseController.java @@ -68,14 +68,13 @@ public class AiolCourseController extends JeecgController> queryPageList(AiolCourse aiolCourse, - @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, - @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, - HttpServletRequest req) { - + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(aiolCourse, req.getParameterMap()); Page page = new Page(pageNo, pageSize); @@ -108,7 +107,7 @@ public class AiolCourseController extends JeecgController edit(@RequestBody AiolCourse aiolCourse) { aiolCourseService.updateById(aiolCourse); return Result.OK("编辑成功!"); @@ -150,7 +149,7 @@ public class AiolCourseController extends JeecgController queryById(@RequestParam(name = "id", required = true) String id) { @@ -202,12 +201,15 @@ public class AiolCourseController extends JeecgController> querySectionDetail(@PathVariable(value = "courseId") String courseId, @PathVariable(value = "sectionId") String sectionId) { + public Result> querySectionDetail(@PathVariable(value = "courseId") String courseId, + @PathVariable(value = "sectionId") String sectionId) { // TODO GC 获取用户id,根据courseId判断当前用户是否报名课程,只有已报名的课程才能查看章节详情 List list = aiolCourseService.getCourseSectionDetail(0, sectionId, AiolResource.class); @@ -282,7 +285,8 @@ public class AiolCourseController extends JeecgController> querySectionDocumentDetail(@PathVariable(value = "courseId") String courseId, @PathVariable(value = "sectionId") String sectionId) { + public Result> querySectionDocumentDetail(@PathVariable(value = "courseId") String courseId, + @PathVariable(value = "sectionId") String sectionId) { // TODO GC 获取用户id,根据courseId判断当前用户是否报名课程,只有已报名的课程才能查看章节详情 List list = aiolCourseService.getCourseSectionDetail(1, sectionId, AiolResource.class); @@ -291,7 +295,8 @@ public class AiolCourseController extends JeecgController> querySectionHomeworkDetail(@PathVariable(value = "courseId") String courseId, @PathVariable(value = "sectionId") String sectionId) { + public Result> querySectionHomeworkDetail(@PathVariable(value = "courseId") String courseId, + @PathVariable(value = "sectionId") String sectionId) { // TODO GC 获取用户id,根据courseId判断当前用户是否报名课程,只有已报名的课程才能查看章节详情 List list = aiolCourseService.getCourseSectionDetail(3, sectionId, AiolHomework.class); @@ -308,7 +313,8 @@ public class AiolCourseController extends JeecgController enrollCourse(@PathVariable(value = "courseId") String courseId, HttpServletRequest request, HttpServletResponse response) { + public Result enrollCourse(@PathVariable(value = "courseId") String courseId, HttpServletRequest request, + HttpServletResponse response) { String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); String username = JwtUtil.getUsername(token); LoginUser sysUser = sysBaseApi.getUserByName(username); @@ -318,7 +324,8 @@ public class AiolCourseController extends JeecgController isEnrolled(@PathVariable(value = "courseId") String courseId, HttpServletRequest request, HttpServletResponse response) { + public Result isEnrolled(@PathVariable(value = "courseId") String courseId, HttpServletRequest request, + HttpServletResponse response) { String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); String username = JwtUtil.getUsername(token); LoginUser sysUser = sysBaseApi.getUserByName(username); @@ -355,7 +362,8 @@ public class AiolCourseController extends JeecgController> addStudents(@PathVariable(value = "courseId") String courseId, @RequestBody Map requestBody, HttpServletRequest request) { + public Result> addStudents(@PathVariable(value = "courseId") String courseId, + @RequestBody Map requestBody, HttpServletRequest request) { String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); String username = JwtUtil.getUsername(token); LoginUser sysUser = sysBaseApi.getUserByName(username); @@ -371,7 +379,8 @@ public class AiolCourseController extends JeecgController> queryCourseProgress(@PathVariable(value = "courseId") String courseId, HttpServletRequest request, HttpServletResponse response) { + public Result> queryCourseProgress(@PathVariable(value = "courseId") String courseId, + HttpServletRequest request, HttpServletResponse response) { String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); String username = JwtUtil.getUsername(token); LoginUser sysUser = sysBaseApi.getUserByName(username); @@ -380,6 +389,23 @@ public class AiolCourseController extends JeecgController> recordVideoProgress( + @PathVariable(value = "courseId") String courseId, + @RequestParam(value = "sectionId") String sectionId, + @RequestParam(value = "duration") Integer duration, + HttpServletRequest request) { + + String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); + String username = JwtUtil.getUsername(token); + LoginUser sysUser = sysBaseApi.getUserByName(username); + + Map result = aiolCourseService.recordVideoProgress(courseId, sectionId, duration, + sysUser.getId()); + return Result.OK(result); + } + @GetMapping("/count") @Operation(summary = "查询课程总数", description = "返回系统中所有课程的总数量") @IgnoreAuth @@ -388,7 +414,6 @@ public class AiolCourseController extends JeecgController test() { diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/IAiolCourseService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/IAiolCourseService.java index 3a16c0d4..acdf8269 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/IAiolCourseService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/IAiolCourseService.java @@ -94,4 +94,15 @@ public interface IAiolCourseService extends IService { Map getCourseProgress(String courseId, String id); + + /** + * 记录视频学习时长 + * 前端传递course_id、section_id、duration(单位秒),更新aiol_learn_progress。同时,后端需要根据section_id,先去aiol_entity_link表,根据source_type=course_section和source_id=section_id,查询对应target_type=resource的target_id,再根据target_id去aiol_resource表查询对应文件信息的duration(视频总时长),判断前端传递的duration是否>=视频duration的95%,如果大于等于,则更新aiol_learn_progress表的status=2(已完成) + * @param courseId + * @param sectionId + * @param duration + * @param id + * @return + */ + Map recordVideoProgress(String courseId, String sectionId, Integer duration, String id); } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/impl/AiolCourseServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/impl/AiolCourseServiceImpl.java index 22960727..08464218 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/impl/AiolCourseServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/impl/AiolCourseServiceImpl.java @@ -2,12 +2,17 @@ package org.jeecg.modules.aiol.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; + import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.util.CommonUtils; import org.jeecg.common.util.MinioUtil; +import org.jeecg.common.util.RedisUtil; import org.jeecg.common.util.SpringContextUtils; import org.jeecg.common.util.oss.OssBootUtil; import org.jeecg.modules.aiol.constant.EntityLinkConst; +import org.jeecg.modules.aiol.constant.RedisConst; import org.jeecg.modules.aiol.dto.CourseWithTeacherInfo; import org.jeecg.modules.aiol.dto.TeacherInfo; import org.jeecg.modules.aiol.entity.*; @@ -38,10 +43,11 @@ import java.util.stream.Stream; /** * @Description: 课程 * @Author: jeecg-boot - * @Date: 2025-08-31 + * @Date: 2025-08-31 * @Version: V1.0 */ @Service +@Slf4j public class AiolCourseServiceImpl extends ServiceImpl implements IAiolCourseService { @Autowired private AiolCourseMapper courseMapper; @@ -76,6 +82,9 @@ public class AiolCourseServiceImpl extends ServiceImpl { - subWrapper.like("subject", topicValue + ",") // 开头匹配 - .or().like("subject", "," + topicValue + ",") // 中间匹配 - .or().like("subject", "," + topicValue) // 结尾匹配 - .or().eq("subject", topicValue); // 单独匹配 + subWrapper.like("subject", topicValue + ",") // 开头匹配 + .or().like("subject", "," + topicValue + ",") // 中间匹配 + .or().like("subject", "," + topicValue) // 结尾匹配 + .or().eq("subject", topicValue); // 单独匹配 }); } else { // 后续条件用OR连接,表示任一专题匹配即可 wrapper.or(subWrapper -> { - subWrapper.like("subject", topicValue + ",") // 开头匹配 - .or().like("subject", "," + topicValue + ",") // 中间匹配 - .or().like("subject", "," + topicValue) // 结尾匹配 - .or().eq("subject", topicValue); // 单独匹配 + subWrapper.like("subject", topicValue + ",") // 开头匹配 + .or().like("subject", "," + topicValue + ",") // 中间匹配 + .or().like("subject", "," + topicValue) // 结尾匹配 + .or().eq("subject", topicValue); // 单独匹配 }); } } @@ -268,8 +277,10 @@ public class AiolCourseServiceImpl extends ServiceImpl 0) ? headerUploadType : configUploadType; + String configUploadType = SpringContextUtils.getApplicationContext().getEnvironment() + .getProperty("jeecg.uploadType", "minio"); + String uploadType = (headerUploadType != null && headerUploadType.trim().length() > 0) ? headerUploadType + : configUploadType; // 1) 保存临时原始视频 String uuid = UUID.randomUUID().toString(); @@ -303,7 +314,8 @@ public class AiolCourseServiceImpl extends ServiceImpl paths = Files.list(hlsDir)) { for (Path f : (Iterable) paths::iterator) { - if (!Files.isRegularFile(f)) continue; + if (!Files.isRegularFile(f)) + continue; String rel = base + "/" + f.getFileName().toString(); try (InputStream in = Files.newInputStream(f)) { if ("minio".equals(uploadType)) { @@ -317,7 +329,8 @@ public class AiolCourseServiceImpl extends ServiceImpl getCourseTeacherList(String courseId) { - List list = courseTeacherMapper.selectList(new QueryWrapper().eq("course_id", courseId)); + List list = courseTeacherMapper + .selectList(new QueryWrapper().eq("course_id", courseId)); List result = new ArrayList<>(); for (AiolCourseTeacher item : list) { - AiolUserInfo userInfo = userInfoMapper.selectOne(new QueryWrapper().eq("user_id", item.getTeacherId())); + AiolUserInfo userInfo = userInfoMapper + .selectOne(new QueryWrapper().eq("user_id", item.getTeacherId())); SysUser sysUser = sysUserMapper.selectById(item.getTeacherId()); TeacherInfo teacherInfo = new TeacherInfo(); @@ -512,7 +530,8 @@ public class AiolCourseServiceImpl extends ServiceImpl course.getMaxEnroll()) { - throw new RuntimeException("添加学生后将超过最大报名人数限制,当前:" + currentEnrollCount + ",最大:" + course.getMaxEnroll() + ",新增:" + newStudentIds.size()); + throw new RuntimeException("添加学生后将超过最大报名人数限制,当前:" + currentEnrollCount + ",最大:" + course.getMaxEnroll() + + ",新增:" + newStudentIds.size()); } } @@ -569,8 +588,8 @@ public class AiolCourseServiceImpl extends ServiceImpl videoSections = getCourseSectionsByType(courseId, 0); // 视频 - List examSections = getCourseSectionsByType(courseId, 2); // 考试 + List videoSections = getCourseSectionsByType(courseId, 0); // 视频 + List examSections = getCourseSectionsByType(courseId, 2); // 考试 List homeworkSections = getCourseSectionsByType(courseId, 3); // 作业 // 3. 分别查询各类型章节的完成情况 @@ -647,7 +666,7 @@ public class AiolCourseServiceImpl extends ServiceImpl recordVideoProgress(String courseId, String sectionId, Integer duration, String userId) { + Map result = new HashMap<>(); + + try { + // 1. 根据section_id查询aiol_entity_link表,获取对应的resource_id + String sourceType = EntityLinkConst.SourceType.COURSE_SECTION; + String targetType = EntityLinkConst.TargetType.RESOURCE; + + List resourceIds = entityLinkBizService.listTargetIds(sourceType, sectionId, targetType); + if (resourceIds.isEmpty()) { + result.put("success", false); + result.put("message", "未找到章节对应的视频资源"); + return result; + } + + // 2. 从Redis缓存获取视频资源信息,如果没有则从数据库查询并缓存 + String resourceId = resourceIds.get(0); // 一个章节对应一个视频资源,取第一个即可 + AiolResource resource = getVideoResourceFromCache(resourceId); + + if (resource == null || resource.getDuration() == null) { + result.put("success", false); + result.put("message", "视频资源信息不完整"); + return result; + } + + Integer totalDuration = resource.getDuration(); + Integer thresholdDuration = (int) Math.ceil(totalDuration * 0.95); // 95%阈值 + + // 3. 判断学习时长是否达到完成阈值 + boolean isCompleted = duration >= thresholdDuration; + + // 4. 查询或创建学习进度记录 + QueryWrapper progressQuery = new QueryWrapper<>(); + progressQuery.eq("user_id", userId) + .eq("course_id", courseId) + .eq("section_id", sectionId); + + AiolLearnProgress learnProgress = learnProgressMapper.selectOne(progressQuery); + + if (learnProgress == null) { + // 创建新的学习进度记录 + learnProgress = new AiolLearnProgress(); + learnProgress.setUserId(userId); + learnProgress.setCourseId(courseId); + learnProgress.setSectionId(sectionId); + learnProgress.setCreateTime(new Date()); + learnProgress.setCreateBy(userId); + } + + // 5. 更新学习进度信息 + learnProgress.setUpdateTime(new Date()); + learnProgress.setUpdateBy(userId); + learnProgress.setDuration(duration); + + if (isCompleted) { + learnProgress.setStatus(2); // 已完成 + learnProgress.setUpdateTime(new Date()); + } else { + learnProgress.setStatus(1); // 学习中 + } + + // 6. 保存或更新学习进度 + if (learnProgress.getId() == null) { + learnProgressMapper.insert(learnProgress); + } else { + learnProgressMapper.updateById(learnProgress); + } + + // 7. 构建返回结果 + result.put("success", true); + result.put("isCompleted", isCompleted); + result.put("currentDuration", duration); + result.put("totalDuration", totalDuration); + result.put("thresholdDuration", thresholdDuration); + result.put("progress", Math.min(100, (duration * 100) / totalDuration)); + result.put("message", isCompleted ? "视频学习完成!" : "学习进度已记录"); + + } catch (Exception e) { + log.error("记录视频学习进度失败: courseId={}, sectionId={}, userId={}, error={}", + courseId, sectionId, userId, e.getMessage(), e); + result.put("success", false); + result.put("message", "记录学习进度失败: " + e.getMessage()); + } + + return result; + } + + /** + * 从Redis缓存获取视频资源信息,如果没有则从数据库查询并缓存 + * + * @param resourceId 资源ID + * @return 视频资源信息 + */ + private AiolResource getVideoResourceFromCache(String resourceId) { + String cacheKey = RedisConst.VIDEO_RESOURCE_CACHE_KEY_PREFIX + resourceId; + + try { + // 1. 尝试从Redis获取缓存 + Object cachedResource = redisUtil.get(cacheKey); + if (cachedResource != null) { + log.debug("从Redis缓存获取视频资源: resourceId={}", resourceId); + return (AiolResource) cachedResource; + } + + // 2. 缓存未命中,从数据库查询 + log.debug("Redis缓存未命中,从数据库查询视频资源: resourceId={}", resourceId); + AiolResource resource = resourceMapper.selectById(resourceId); + + if (resource != null) { + // 3. 将资源信息存入Redis缓存 + redisUtil.set(cacheKey, resource, RedisConst.VIDEO_RESOURCE_CACHE_EXPIRE); + log.debug("视频资源已缓存到Redis: resourceId={}, duration={}", resourceId, resource.getDuration()); + } + + return resource; + + } catch (Exception e) { + log.error("获取视频资源缓存失败: resourceId={}, error={}", resourceId, e.getMessage(), e); + // 缓存异常时,直接从数据库查询 + return resourceMapper.selectById(resourceId); + } + } } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/logback-spring.xml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/logback-spring.xml index acad24b3..3a7e87e2 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/logback-spring.xml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/logback-spring.xml @@ -74,4 +74,7 @@ + + + \ No newline at end of file