Compare commits

...

3 Commits

Author SHA1 Message Date
GoCo
a9ae28e3e1 feat: 🎸 查询视频学习进度接口 2025-09-02 04:15:23 +08:00
GoCo
35fb15e2e3 feat: 🎸 课程视频学习进度统计接口 2025-09-02 04:03:17 +08:00
GoCo
40bfe138f0 feat: 🎸 课程增加 ai伴学 字段 2025-09-02 03:00:23 +08:00
7 changed files with 351 additions and 64 deletions

View File

@ -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天过期
}

View File

@ -68,14 +68,13 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
* @param req * @param req
* @return * @return
*/ */
//@AutoLog(value = "课程-分页列表查询") // @AutoLog(value = "课程-分页列表查询")
@Operation(summary = "课程-分页列表查询") @Operation(summary = "课程-分页列表查询")
@GetMapping(value = "/list") @GetMapping(value = "/list")
public Result<IPage<AiolCourse>> queryPageList(AiolCourse aiolCourse, public Result<IPage<AiolCourse>> queryPageList(AiolCourse aiolCourse,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) { HttpServletRequest req) {
QueryWrapper<AiolCourse> queryWrapper = QueryGenerator.initQueryWrapper(aiolCourse, req.getParameterMap()); QueryWrapper<AiolCourse> queryWrapper = QueryGenerator.initQueryWrapper(aiolCourse, req.getParameterMap());
Page<AiolCourse> page = new Page<AiolCourse>(pageNo, pageSize); Page<AiolCourse> page = new Page<AiolCourse>(pageNo, pageSize);
@ -108,7 +107,7 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
@AutoLog(value = "课程-编辑") @AutoLog(value = "课程-编辑")
@Operation(summary = "课程-编辑") @Operation(summary = "课程-编辑")
@RequiresPermissions("aiol:aiol_course:edit") @RequiresPermissions("aiol:aiol_course:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) @RequestMapping(value = "/edit", method = { RequestMethod.PUT, RequestMethod.POST })
public Result<String> edit(@RequestBody AiolCourse aiolCourse) { public Result<String> edit(@RequestBody AiolCourse aiolCourse) {
aiolCourseService.updateById(aiolCourse); aiolCourseService.updateById(aiolCourse);
return Result.OK("编辑成功!"); return Result.OK("编辑成功!");
@ -150,7 +149,7 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
* @param id * @param id
* @return * @return
*/ */
//@AutoLog(value = "课程-通过id查询") // @AutoLog(value = "课程-通过id查询")
@Operation(summary = "课程-通过id查询") @Operation(summary = "课程-通过id查询")
@GetMapping(value = "/queryById") @GetMapping(value = "/queryById")
public Result<AiolCourse> queryById(@RequestParam(name = "id", required = true) String id) { public Result<AiolCourse> queryById(@RequestParam(name = "id", required = true) String id) {
@ -202,12 +201,15 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
switch (sort) { switch (sort) {
case "hottest": case "hottest":
list = list.stream() list = list.stream()
.sorted(Comparator.comparing(AiolCourse::getEnrollCount, Comparator.nullsLast(Integer::compareTo)).reversed()) .sorted(Comparator
.comparing(AiolCourse::getEnrollCount, Comparator.nullsLast(Integer::compareTo))
.reversed())
.collect(Collectors.toList()); .collect(Collectors.toList());
break; break;
case "latest": case "latest":
list = list.stream() list = list.stream()
.sorted(Comparator.comparing(AiolCourse::getCreateTime, Comparator.nullsLast(java.util.Date::compareTo)).reversed()) .sorted(Comparator.comparing(AiolCourse::getCreateTime,
Comparator.nullsLast(java.util.Date::compareTo)).reversed())
.collect(Collectors.toList()); .collect(Collectors.toList());
break; break;
case "recommend": case "recommend":
@ -273,7 +275,8 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
@GetMapping("/{courseId}/section_video/{sectionId}") @GetMapping("/{courseId}/section_video/{sectionId}")
@Operation(summary = "查询视频章节详情", description = "该接口需要携带用户登录token。根据章节id查询章节详情不同类型的章节返回的内容不同") @Operation(summary = "查询视频章节详情", description = "该接口需要携带用户登录token。根据章节id查询章节详情不同类型的章节返回的内容不同")
public Result<List<AiolResource>> querySectionDetail(@PathVariable(value = "courseId") String courseId, @PathVariable(value = "sectionId") String sectionId) { public Result<List<AiolResource>> querySectionDetail(@PathVariable(value = "courseId") String courseId,
@PathVariable(value = "sectionId") String sectionId) {
// TODO GC 获取用户id根据courseId判断当前用户是否报名课程只有已报名的课程才能查看章节详情 // TODO GC 获取用户id根据courseId判断当前用户是否报名课程只有已报名的课程才能查看章节详情
List<AiolResource> list = aiolCourseService.getCourseSectionDetail(0, sectionId, AiolResource.class); List<AiolResource> list = aiolCourseService.getCourseSectionDetail(0, sectionId, AiolResource.class);
@ -282,7 +285,8 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
@GetMapping("/{courseId}/section_document/{sectionId}") @GetMapping("/{courseId}/section_document/{sectionId}")
@Operation(summary = "查询文档章节详情", description = "该接口需要携带用户登录token。根据章节id查询章节详情不同类型的章节返回的内容不同") @Operation(summary = "查询文档章节详情", description = "该接口需要携带用户登录token。根据章节id查询章节详情不同类型的章节返回的内容不同")
public Result<List<AiolResource>> querySectionDocumentDetail(@PathVariable(value = "courseId") String courseId, @PathVariable(value = "sectionId") String sectionId) { public Result<List<AiolResource>> querySectionDocumentDetail(@PathVariable(value = "courseId") String courseId,
@PathVariable(value = "sectionId") String sectionId) {
// TODO GC 获取用户id根据courseId判断当前用户是否报名课程只有已报名的课程才能查看章节详情 // TODO GC 获取用户id根据courseId判断当前用户是否报名课程只有已报名的课程才能查看章节详情
List<AiolResource> list = aiolCourseService.getCourseSectionDetail(1, sectionId, AiolResource.class); List<AiolResource> list = aiolCourseService.getCourseSectionDetail(1, sectionId, AiolResource.class);
@ -291,7 +295,8 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
@GetMapping("/{courseId}/section_homework/{sectionId}") @GetMapping("/{courseId}/section_homework/{sectionId}")
@Operation(summary = "查询作业章节详情", description = "该接口需要携带用户登录token。根据章节id查询章节详情不同类型的章节返回的内容不同") @Operation(summary = "查询作业章节详情", description = "该接口需要携带用户登录token。根据章节id查询章节详情不同类型的章节返回的内容不同")
public Result<List<AiolHomework>> querySectionHomeworkDetail(@PathVariable(value = "courseId") String courseId, @PathVariable(value = "sectionId") String sectionId) { public Result<List<AiolHomework>> querySectionHomeworkDetail(@PathVariable(value = "courseId") String courseId,
@PathVariable(value = "sectionId") String sectionId) {
// TODO GC 获取用户id根据courseId判断当前用户是否报名课程只有已报名的课程才能查看章节详情 // TODO GC 获取用户id根据courseId判断当前用户是否报名课程只有已报名的课程才能查看章节详情
List<AiolHomework> list = aiolCourseService.getCourseSectionDetail(3, sectionId, AiolHomework.class); List<AiolHomework> list = aiolCourseService.getCourseSectionDetail(3, sectionId, AiolHomework.class);
@ -308,7 +313,8 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
@PostMapping("/{courseId}/enroll") @PostMapping("/{courseId}/enroll")
@Operation(summary = "报名课程", description = "该接口需要携带用户登录token。根据课程id报名课程。返回值为报名结果报名成功返回success") @Operation(summary = "报名课程", description = "该接口需要携带用户登录token。根据课程id报名课程。返回值为报名结果报名成功返回success")
public Result<String> enrollCourse(@PathVariable(value = "courseId") String courseId, HttpServletRequest request, HttpServletResponse response) { public Result<String> enrollCourse(@PathVariable(value = "courseId") String courseId, HttpServletRequest request,
HttpServletResponse response) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String username = JwtUtil.getUsername(token); String username = JwtUtil.getUsername(token);
LoginUser sysUser = sysBaseApi.getUserByName(username); LoginUser sysUser = sysBaseApi.getUserByName(username);
@ -318,7 +324,8 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
@GetMapping("/{courseId}/is_enrolled") @GetMapping("/{courseId}/is_enrolled")
@Operation(summary = "查询课程是否已报名", description = "该接口需要携带用户登录token。根据课程id查询课程是否已报名。判断返回值的result是否为true") @Operation(summary = "查询课程是否已报名", description = "该接口需要携带用户登录token。根据课程id查询课程是否已报名。判断返回值的result是否为true")
public Result<Boolean> isEnrolled(@PathVariable(value = "courseId") String courseId, HttpServletRequest request, HttpServletResponse response) { public Result<Boolean> isEnrolled(@PathVariable(value = "courseId") String courseId, HttpServletRequest request,
HttpServletResponse response) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String username = JwtUtil.getUsername(token); String username = JwtUtil.getUsername(token);
LoginUser sysUser = sysBaseApi.getUserByName(username); LoginUser sysUser = sysBaseApi.getUserByName(username);
@ -355,7 +362,8 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
@PostMapping("/{courseId}/add_students") @PostMapping("/{courseId}/add_students")
@Operation(summary = "批量导入学生", description = "请求体为JSON格式包含ids字段ids为逗号分隔的学生ID字符串") @Operation(summary = "批量导入学生", description = "请求体为JSON格式包含ids字段ids为逗号分隔的学生ID字符串")
public Result<Map<String, Object>> addStudents(@PathVariable(value = "courseId") String courseId, @RequestBody Map<String, Object> requestBody, HttpServletRequest request) { public Result<Map<String, Object>> addStudents(@PathVariable(value = "courseId") String courseId,
@RequestBody Map<String, Object> requestBody, HttpServletRequest request) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String username = JwtUtil.getUsername(token); String username = JwtUtil.getUsername(token);
LoginUser sysUser = sysBaseApi.getUserByName(username); LoginUser sysUser = sysBaseApi.getUserByName(username);
@ -371,7 +379,8 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
@GetMapping("/{courseId}/progress") @GetMapping("/{courseId}/progress")
@Operation(summary = "查询课程学习进度") @Operation(summary = "查询课程学习进度")
public Result<Map<String, Object>> queryCourseProgress(@PathVariable(value = "courseId") String courseId, HttpServletRequest request, HttpServletResponse response) { public Result<Map<String, Object>> queryCourseProgress(@PathVariable(value = "courseId") String courseId,
HttpServletRequest request, HttpServletResponse response) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String username = JwtUtil.getUsername(token); String username = JwtUtil.getUsername(token);
LoginUser sysUser = sysBaseApi.getUserByName(username); LoginUser sysUser = sysBaseApi.getUserByName(username);
@ -380,6 +389,38 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
return Result.OK(progress); return Result.OK(progress);
} }
@PostMapping("/{courseId}/record_video_progress")
@Operation(summary = "记录视频学习时长", description = "记录用户观看视频的学习时长当观看时长达到视频总时长的95%时自动标记为已完成")
public Result<Map<String, Object>> 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<String, Object> result = aiolCourseService.recordVideoProgress(courseId, sectionId, duration,
sysUser.getId());
return Result.OK(result);
}
@GetMapping("/{courseId}/get_video_progress")
@Operation(summary = "查询视频学习进度", description = "查询用户在指定视频章节的学习进度,包括已学习时长和视频总时长")
public Result<Map<String, Object>> queryVideoProgress(
@PathVariable(value = "courseId") String courseId,
@RequestParam(value = "sectionId") String sectionId,
HttpServletRequest request) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String username = JwtUtil.getUsername(token);
LoginUser sysUser = sysBaseApi.getUserByName(username);
Map<String, Object> result = aiolCourseService.queryVideoProgress(courseId, sectionId, sysUser.getId());
return Result.OK(result);
}
@GetMapping("/count") @GetMapping("/count")
@Operation(summary = "查询课程总数", description = "返回系统中所有课程的总数量") @Operation(summary = "查询课程总数", description = "返回系统中所有课程的总数量")
@IgnoreAuth @IgnoreAuth
@ -388,7 +429,6 @@ public class AiolCourseController extends JeecgController<AiolCourse, IAiolCours
return Result.OK(count); return Result.OK(count);
} }
@GetMapping("/test") @GetMapping("/test")
@IgnoreAuth @IgnoreAuth
public Result<String> test() { public Result<String> test() {

View File

@ -22,7 +22,7 @@ import lombok.experimental.Accessors;
/** /**
* @Description: 课程 * @Description: 课程
* @Author: jeecg-boot * @Author: jeecg-boot
* @Date: 2025-08-31 * @Date: 2025-09-02
* @Version: V1.0 * @Version: V1.0
*/ */
@Data @Data
@ -36,105 +36,109 @@ public class AiolCourse implements Serializable {
/**主键*/ /**主键*/
@TableId(type = IdType.ASSIGN_ID) @TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键") @Schema(description = "主键")
private String id; private java.lang.String id;
/**课程名*/ /**课程名*/
@Excel(name = "课程名", width = 15) @Excel(name = "课程名", width = 15)
@Schema(description = "课程名") @Schema(description = "课程名")
private String name; private java.lang.String name;
/**封面图*/ /**封面图*/
@Excel(name = "封面图", width = 15) @Excel(name = "封面图", width = 15)
@Schema(description = "封面图") @Schema(description = "封面图")
private String cover; private java.lang.String cover;
/**介绍视频*/ /**介绍视频*/
@Excel(name = "介绍视频", width = 15) @Excel(name = "介绍视频", width = 15)
@Schema(description = "介绍视频") @Schema(description = "介绍视频")
private String video; private java.lang.String video;
/**学校*/ /**学校*/
@Excel(name = "学校", width = 15) @Excel(name = "学校", width = 15)
@Schema(description = "学校") @Schema(description = "学校")
private String school; private java.lang.String school;
/**课程概述*/ /**课程概述*/
@Excel(name = "课程概述", width = 15) @Excel(name = "课程概述", width = 15)
@Schema(description = "课程概述") @Schema(description = "课程概述")
private String description; private java.lang.String description;
/**课程类型*/ /**课程类型*/
@Excel(name = "课程类型", width = 15, dicCode = "course_type") @Excel(name = "课程类型", width = 15, dicCode = "course_type")
@Dict(dicCode = "course_type") @Dict(dicCode = "course_type")
@Schema(description = "课程类型") @Schema(description = "课程类型")
private Integer type; private java.lang.Integer type;
/**授课目标*/ /**授课目标*/
@Excel(name = "授课目标", width = 15) @Excel(name = "授课目标", width = 15)
@Schema(description = "授课目标") @Schema(description = "授课目标")
private String target; private java.lang.String target;
/**课程难度*/ /**课程难度*/
@Excel(name = "课程难度", width = 15, dicCode = "course_difficulty") @Excel(name = "课程难度", width = 15, dicCode = "course_difficulty")
@Dict(dicCode = "course_difficulty") @Dict(dicCode = "course_difficulty")
@Schema(description = "课程难度") @Schema(description = "课程难度")
private Integer difficulty; private java.lang.Integer difficulty;
/**所属专题*/ /**所属专题*/
@Excel(name = "所属专题", width = 15, dicCode = "course_subject") @Excel(name = "所属专题", width = 15, dicCode = "course_subject")
@Dict(dicCode = "course_subject") @Dict(dicCode = "course_subject")
@Schema(description = "所属专题") @Schema(description = "所属专题")
private String subject; private java.lang.String subject;
/**课程大纲*/ /**课程大纲*/
@Excel(name = "课程大纲", width = 15) @Excel(name = "课程大纲", width = 15)
@Schema(description = "课程大纲") @Schema(description = "课程大纲")
private String outline; private java.lang.String outline;
/**预备知识*/ /**预备知识*/
@Excel(name = "预备知识", width = 15) @Excel(name = "预备知识", width = 15)
@Schema(description = "预备知识") @Schema(description = "预备知识")
private String prerequisite; private java.lang.String prerequisite;
/**参考资料*/ /**参考资料*/
@Excel(name = "参考资料", width = 15) @Excel(name = "参考资料", width = 15)
@Schema(description = "参考资料") @Schema(description = "参考资料")
private String reference; private java.lang.String reference;
/**学时安排*/ /**学时安排*/
@Excel(name = "学时安排", width = 15) @Excel(name = "学时安排", width = 15)
@Schema(description = "学时安排") @Schema(description = "学时安排")
private String arrangement; private java.lang.String arrangement;
/**开课时间*/ /**开课时间*/
@Excel(name = "开课时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") @Excel(name = "开课时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "开课时间") @Schema(description = "开课时间")
private Date startTime; private java.util.Date startTime;
/**结课时间*/ /**结课时间*/
@Excel(name = "结课时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") @Excel(name = "结课时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "结课时间") @Schema(description = "结课时间")
private Date endTime; private java.util.Date endTime;
/**已报名人数*/ /**已报名人数*/
@Excel(name = "已报名人数", width = 15) @Excel(name = "已报名人数", width = 15)
@Schema(description = "已报名人数") @Schema(description = "已报名人数")
private Integer enrollCount; private java.lang.Integer enrollCount;
/**最大报名人数*/ /**最大报名人数*/
@Excel(name = "最大报名人数", width = 15) @Excel(name = "最大报名人数", width = 15)
@Schema(description = "最大报名人数") @Schema(description = "最大报名人数")
private Integer maxEnroll; private java.lang.Integer maxEnroll;
/**状态*/ /**状态*/
@Excel(name = "状态", width = 15, dicCode = "course_status") @Excel(name = "状态", width = 15, dicCode = "course_status")
@Dict(dicCode = "course_status") @Dict(dicCode = "course_status")
@Schema(description = "状态") @Schema(description = "状态")
private Integer status; private java.lang.Integer status;
/**常见问题*/ /**常见问题*/
@Excel(name = "常见问题", width = 15) @Excel(name = "常见问题", width = 15)
@Schema(description = "常见问题") @Schema(description = "常见问题")
private String question; private java.lang.String question;
/**是否ai伴学模式*/
@Excel(name = "是否ai伴学模式", width = 15)
@Schema(description = "是否ai伴学模式")
private java.lang.Integer izAi;
/**创建人*/ /**创建人*/
@Schema(description = "创建人") @Schema(description = "创建人")
private String createBy; private java.lang.String createBy;
/**创建时间*/ /**创建时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间") @Schema(description = "创建时间")
private Date createTime; private java.util.Date createTime;
/**更新人*/ /**更新人*/
@Schema(description = "更新人") @Schema(description = "更新人")
private String updateBy; private java.lang.String updateBy;
/**更新时间*/ /**更新时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新时间") @Schema(description = "更新时间")
private Date updateTime; private java.util.Date updateTime;
} }

View File

@ -94,4 +94,24 @@ public interface IAiolCourseService extends IService<AiolCourse> {
Map<String, Object> getCourseProgress(String courseId, String id); Map<String, Object> getCourseProgress(String courseId, String id);
/**
* 记录视频学习时长
* 前端传递course_idsection_idduration单位秒更新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<String, Object> recordVideoProgress(String courseId, String sectionId, Integer duration, String userId);
/**
* 查询视频学习进度
* @param courseId
* @param sectionId
* @param id
* @return
*/
Map<String, Object> queryVideoProgress(String courseId, String sectionId, String userId);
} }

View File

@ -2,12 +2,17 @@ package org.jeecg.modules.aiol.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.CommonUtils; import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.MinioUtil; import org.jeecg.common.util.MinioUtil;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.SpringContextUtils; import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oss.OssBootUtil; import org.jeecg.common.util.oss.OssBootUtil;
import org.jeecg.modules.aiol.constant.EntityLinkConst; 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.CourseWithTeacherInfo;
import org.jeecg.modules.aiol.dto.TeacherInfo; import org.jeecg.modules.aiol.dto.TeacherInfo;
import org.jeecg.modules.aiol.entity.*; import org.jeecg.modules.aiol.entity.*;
@ -38,10 +43,11 @@ import java.util.stream.Stream;
/** /**
* @Description: 课程 * @Description: 课程
* @Author: jeecg-boot * @Author: jeecg-boot
* @Date: 2025-08-31 * @Date: 2025-08-31
* @Version: V1.0 * @Version: V1.0
*/ */
@Service @Service
@Slf4j
public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCourse> implements IAiolCourseService { public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCourse> implements IAiolCourseService {
@Autowired @Autowired
private AiolCourseMapper courseMapper; private AiolCourseMapper courseMapper;
@ -76,6 +82,9 @@ public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCou
@Autowired @Autowired
private AiolLearnProgressMapper learnProgressMapper; private AiolLearnProgressMapper learnProgressMapper;
@Autowired
private RedisUtil redisUtil;
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
/** /**
@ -212,18 +221,18 @@ public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCou
if (i == 0) { if (i == 0) {
// 第一个条件直接开始 // 第一个条件直接开始
wrapper.and(subWrapper -> { wrapper.and(subWrapper -> {
subWrapper.like("subject", topicValue + ",") // 开头匹配 subWrapper.like("subject", topicValue + ",") // 开头匹配
.or().like("subject", "," + topicValue + ",") // 中间匹配 .or().like("subject", "," + topicValue + ",") // 中间匹配
.or().like("subject", "," + topicValue) // 结尾匹配 .or().like("subject", "," + topicValue) // 结尾匹配
.or().eq("subject", topicValue); // 单独匹配 .or().eq("subject", topicValue); // 单独匹配
}); });
} else { } else {
// 后续条件用OR连接表示任一专题匹配即可 // 后续条件用OR连接表示任一专题匹配即可
wrapper.or(subWrapper -> { wrapper.or(subWrapper -> {
subWrapper.like("subject", topicValue + ",") // 开头匹配 subWrapper.like("subject", topicValue + ",") // 开头匹配
.or().like("subject", "," + topicValue + ",") // 中间匹配 .or().like("subject", "," + topicValue + ",") // 中间匹配
.or().like("subject", "," + topicValue) // 结尾匹配 .or().like("subject", "," + topicValue) // 结尾匹配
.or().eq("subject", topicValue); // 单独匹配 .or().eq("subject", topicValue); // 单独匹配
}); });
} }
} }
@ -268,8 +277,10 @@ public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCou
public String uploadHls(MultipartFile file, HttpServletRequest request) throws Exception { public String uploadHls(MultipartFile file, HttpServletRequest request) throws Exception {
// 读取上传类型header 优先 // 读取上传类型header 优先
String headerUploadType = request.getHeader("uploadType"); String headerUploadType = request.getHeader("uploadType");
String configUploadType = SpringContextUtils.getApplicationContext().getEnvironment().getProperty("jeecg.uploadType", "minio"); String configUploadType = SpringContextUtils.getApplicationContext().getEnvironment()
String uploadType = (headerUploadType != null && headerUploadType.trim().length() > 0) ? headerUploadType : configUploadType; .getProperty("jeecg.uploadType", "minio");
String uploadType = (headerUploadType != null && headerUploadType.trim().length() > 0) ? headerUploadType
: configUploadType;
// 1) 保存临时原始视频 // 1) 保存临时原始视频
String uuid = UUID.randomUUID().toString(); String uuid = UUID.randomUUID().toString();
@ -303,7 +314,8 @@ public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCou
String base = "video/hls/" + uuid; String base = "video/hls/" + uuid;
try (Stream<Path> paths = Files.list(hlsDir)) { try (Stream<Path> paths = Files.list(hlsDir)) {
for (Path f : (Iterable<Path>) paths::iterator) { for (Path f : (Iterable<Path>) paths::iterator) {
if (!Files.isRegularFile(f)) continue; if (!Files.isRegularFile(f))
continue;
String rel = base + "/" + f.getFileName().toString(); String rel = base + "/" + f.getFileName().toString();
try (InputStream in = Files.newInputStream(f)) { try (InputStream in = Files.newInputStream(f)) {
if ("minio".equals(uploadType)) { if ("minio".equals(uploadType)) {
@ -317,7 +329,8 @@ public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCou
m3u8Url = rel; // 可在网关拼域名 m3u8Url = rel; // 可在网关拼域名
} }
} else { } else {
String uploadpath = SpringContextUtils.getApplicationContext().getEnvironment().getProperty("jeecg.path.upload"); String uploadpath = SpringContextUtils.getApplicationContext().getEnvironment()
.getProperty("jeecg.path.upload");
Path target = Path.of(uploadpath, rel); Path target = Path.of(uploadpath, rel);
Files.createDirectories(target.getParent()); Files.createDirectories(target.getParent());
Files.copy(f, target, StandardCopyOption.REPLACE_EXISTING); Files.copy(f, target, StandardCopyOption.REPLACE_EXISTING);
@ -338,24 +351,29 @@ public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCou
/** 删除临时目录文件 */ /** 删除临时目录文件 */
private static void deleteQuietly(File file) { private static void deleteQuietly(File file) {
try { try {
if (file == null || !file.exists()) return; if (file == null || !file.exists())
return;
if (file.isDirectory()) { if (file.isDirectory()) {
File[] children = file.listFiles(); File[] children = file.listFiles();
if (children != null) { if (children != null) {
for (File c : children) deleteQuietly(c); for (File c : children)
deleteQuietly(c);
} }
} }
file.delete(); file.delete();
} catch (Exception ignored) {} } catch (Exception ignored) {
}
} }
@Override @Override
public List<TeacherInfo> getCourseTeacherList(String courseId) { public List<TeacherInfo> getCourseTeacherList(String courseId) {
List<AiolCourseTeacher> list = courseTeacherMapper.selectList(new QueryWrapper<AiolCourseTeacher>().eq("course_id", courseId)); List<AiolCourseTeacher> list = courseTeacherMapper
.selectList(new QueryWrapper<AiolCourseTeacher>().eq("course_id", courseId));
List<TeacherInfo> result = new ArrayList<>(); List<TeacherInfo> result = new ArrayList<>();
for (AiolCourseTeacher item : list) { for (AiolCourseTeacher item : list) {
AiolUserInfo userInfo = userInfoMapper.selectOne(new QueryWrapper<AiolUserInfo>().eq("user_id", item.getTeacherId())); AiolUserInfo userInfo = userInfoMapper
.selectOne(new QueryWrapper<AiolUserInfo>().eq("user_id", item.getTeacherId()));
SysUser sysUser = sysUserMapper.selectById(item.getTeacherId()); SysUser sysUser = sysUserMapper.selectById(item.getTeacherId());
TeacherInfo teacherInfo = new TeacherInfo(); TeacherInfo teacherInfo = new TeacherInfo();
@ -512,7 +530,8 @@ public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCou
int currentEnrollCount = course.getEnrollCount() == null ? 0 : course.getEnrollCount(); int currentEnrollCount = course.getEnrollCount() == null ? 0 : course.getEnrollCount();
if (course.getMaxEnroll() != null) { if (course.getMaxEnroll() != null) {
if (currentEnrollCount + newStudentIds.size() > course.getMaxEnroll()) { if (currentEnrollCount + newStudentIds.size() > 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<AiolCourseMapper, AiolCou
} }
// 2. 分别查询视频考试作业章节 // 2. 分别查询视频考试作业章节
List<AiolCourseSection> videoSections = getCourseSectionsByType(courseId, 0); // 视频 List<AiolCourseSection> videoSections = getCourseSectionsByType(courseId, 0); // 视频
List<AiolCourseSection> examSections = getCourseSectionsByType(courseId, 2); // 考试 List<AiolCourseSection> examSections = getCourseSectionsByType(courseId, 2); // 考试
List<AiolCourseSection> homeworkSections = getCourseSectionsByType(courseId, 3); // 作业 List<AiolCourseSection> homeworkSections = getCourseSectionsByType(courseId, 3); // 作业
// 3. 分别查询各类型章节的完成情况 // 3. 分别查询各类型章节的完成情况
@ -647,7 +666,7 @@ public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCou
progressQuery.eq("user_id", userId) progressQuery.eq("user_id", userId)
.eq("course_id", courseId) .eq("course_id", courseId)
.in("section_id", sectionIds) .in("section_id", sectionIds)
.eq("status", 2); // status=2表示学习完成 .eq("status", 2); // status=2表示学习完成
return Math.toIntExact(learnProgressMapper.selectCount(progressQuery)); return Math.toIntExact(learnProgressMapper.selectCount(progressQuery));
} }
@ -661,4 +680,185 @@ public class AiolCourseServiceImpl extends ServiceImpl<AiolCourseMapper, AiolCou
} }
return Math.round((float) completed / total * 100); return Math.round((float) completed / total * 100);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> recordVideoProgress(String courseId, String sectionId, Integer duration, String userId) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 根据section_id查询aiol_entity_link表获取对应的resource_id
String sourceType = EntityLinkConst.SourceType.COURSE_SECTION;
String targetType = EntityLinkConst.TargetType.RESOURCE;
List<String> 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<AiolLearnProgress> 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);
}
}
@Override
public Map<String, Object> queryVideoProgress(String courseId, String sectionId, String userId) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 查询用户在该章节的学习进度
QueryWrapper<AiolLearnProgress> progressQuery = new QueryWrapper<>();
progressQuery.eq("user_id", userId)
.eq("course_id", courseId)
.eq("section_id", sectionId);
AiolLearnProgress learnProgress = learnProgressMapper.selectOne(progressQuery);
// 2. 获取已学习时长
Integer learnedDuration = 0;
if (learnProgress != null) {
learnedDuration = learnProgress.getDuration() != null ? learnProgress.getDuration() : 0;
}
// 3. 从Redis缓存获取视频资源信息如果没有则从数据库查询并缓存
String sourceType = EntityLinkConst.SourceType.COURSE_SECTION;
String targetType = EntityLinkConst.TargetType.RESOURCE;
List<String> resourceIds = entityLinkBizService.listTargetIds(sourceType, sectionId, targetType);
if (resourceIds.isEmpty()) {
result.put("success", false);
result.put("message", "未找到章节对应的视频资源");
return result;
}
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();
// 6. 构建返回结果
result.put("success", true);
result.put("learnedDuration", learnedDuration); // 已学习时长
result.put("totalDuration", totalDuration); // 视频总时长
} 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;
}
} }

View File

@ -74,4 +74,7 @@
<appender-ref ref="FILE_HTML" /> <appender-ref ref="FILE_HTML" />
</root> </root>
<!-- Aiol模块日志输出级别 -->
<logger name="org.jeecg.modules.aiol" level="DEBUG" />
</configuration> </configuration>

View File

@ -101,6 +101,11 @@ export const columns: BasicColumn[] = [
align:"center", align:"center",
dataIndex: 'question', dataIndex: 'question',
}, },
{
title: '是否ai伴学模式',
align:"center",
dataIndex: 'izAi'
},
]; ];
// //
export const searchFormSchema: FormSchema[] = [ export const searchFormSchema: FormSchema[] = [
@ -228,6 +233,11 @@ export const formSchema: FormSchema[] = [
label: '常见问题', label: '常见问题',
field: 'question', field: 'question',
component: 'JEditor', component: 'JEditor',
},
{
label: '是否ai伴学模式',
field: 'izAi',
component: 'InputNumber',
}, },
// TODO ID // TODO ID
{ {
@ -259,6 +269,7 @@ export const superQuerySchema = {
maxEnroll: {title: '最大报名人数',order: 16,view: 'number', type: 'number',}, maxEnroll: {title: '最大报名人数',order: 16,view: 'number', type: 'number',},
status: {title: '状态',order: 17,view: 'number', type: 'number',dictCode: 'course_status',}, status: {title: '状态',order: 17,view: 'number', type: 'number',dictCode: 'course_status',},
question: {title: '常见问题',order: 18,view: 'umeditor', type: 'string',}, question: {title: '常见问题',order: 18,view: 'umeditor', type: 'string',},
izAi: {title: '是否ai伴学模式',order: 19,view: 'number', type: 'number',},
}; };
/** /**