Răsfoiți Sursa

1.资源管理加入可以改名
2.资源讨论加入敏感词拦截
3.修复站内信时间显示问题
4.修改学生操作老师资源的埋点逻辑
5.在文件转换时获取视频文件时长进行保存,并在学习足迹处返回
6.在考试管理列表整合考试提交数与考试批改数

honorfire 3 luni în urmă
părinte
comite
7e5f0981e0
21 a modificat fișierele cu 380 adăugiri și 25 ștergeri
  1. 4 4
      snowy-plugin/snowy-plugin-dev/snowy-plugin-dev-func/src/main/java/vip/xiaonuo/dev/modular/message/entity/DevMessage.java
  2. 5 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/pom.xml
  3. 17 2
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/controller/ResourceRecordController.java
  4. 10 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/controller/UserCommentController.java
  5. 4 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/domain/CourseAuditRecord.java
  6. 5 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/domain/CourseStudentBurialpoint.java
  7. 3 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/domain/ResourceFile.java
  8. 10 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/mapper/CourseStudentBurialpointMapper.java
  9. 8 1
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/mapper/mapping/CourseAuditRecordMapper.xml
  10. 28 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/mapper/mapping/CourseStudentBurialpointMapper.xml
  11. 4 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/param/courseauditrecord/CourseAuditRecordAddParam.java
  12. 4 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/param/courseauditrecord/CourseAuditRecordEditParam.java
  13. 9 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/ResourceUserfileService.java
  14. 2 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/UserCommentService.java
  15. 29 1
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/impl/CourseStudentBurialpointServiceImpl.java
  16. 8 0
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/impl/ResourceUserfileServiceImpl.java
  17. 32 11
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/impl/TranscodingServiceImpl.java
  18. 28 1
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/impl/UserCommentServiceImpl.java
  19. 150 5
      snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/util/VideoConverter.java
  20. 11 0
      snowy-plugin/snowy-plugin-exam/snowy-plugin-exam-func/src/main/java/vip/xiaonuo/exam/mapper/mapping/TExamMapper.xml
  21. 9 0
      snowy-plugin/snowy-plugin-exam/snowy-plugin-exam-func/src/main/java/vip/xiaonuo/exam/vo/TExamVo.java

+ 4 - 4
snowy-plugin/snowy-plugin-dev/snowy-plugin-dev-func/src/main/java/vip/xiaonuo/dev/modular/message/entity/DevMessage.java

@@ -58,8 +58,8 @@ public class DevMessage extends CommonEntity {
     @ApiModelProperty(value = "扩展信息", position = 6)
     private String extJson;
 
-    /** 创建时间 */
-    @ApiModelProperty(value = "创建时间", position = 1000)
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private Date createTime;
+//    /** 创建时间 */
+//    @ApiModelProperty(value = "创建时间", position = 1000)
+//    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+//    private Date createTime;
 }

+ 5 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/pom.xml

@@ -111,6 +111,10 @@
             <groupId>vip.xiaonuo</groupId>
             <artifactId>snowy-plugin-auth-func</artifactId>
         </dependency>
+        <dependency>
+            <groupId>vip.xiaonuo</groupId>
+            <artifactId>snowy-plugin-forum-func</artifactId>
+        </dependency>
 
         <!-- 引入开发工具接口,用于配置信息 -->
         <dependency>
@@ -346,6 +350,7 @@
             <artifactId>ffmpeg</artifactId>
             <version>0.6.2</version>
         </dependency>
+
     </dependencies>
 
     <repositories>

+ 17 - 2
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/controller/ResourceRecordController.java

@@ -38,6 +38,7 @@ import vip.xiaonuo.common.util.PinyinUtils;
 import vip.xiaonuo.disk.domain.CourseAuditRecord;
 import vip.xiaonuo.disk.domain.KeyWord;
 import vip.xiaonuo.disk.domain.ResourceRecordUserRelate;
+import vip.xiaonuo.disk.domain.ResourceUserFile;
 import vip.xiaonuo.disk.param.EditUserFileNameParam;
 import vip.xiaonuo.disk.param.courseauditrecord.CourseAuditRecordAddParam;
 import vip.xiaonuo.disk.param.courseauditrecord.CourseAuditRecordEditParam;
@@ -195,7 +196,7 @@ public class ResourceRecordController {
         Map param =new HashMap();
         param.put("startTime", req.getParameter("startTime"));
         param.put("endTime", req.getParameter("endTime"));
-        //资源中心排序标识,0最新,1热门,默认为0
+        //资源中心排序标识,0最新,1热门,2学习素材,默认为0
         //2025.9.16要求热门不在单纯为排序了,而是只查出热门类型的内容
         String sortflag="0";
         if(StringUtils.isNotEmpty(req.getParameter("sortflag")))sortflag=req.getParameter("sortflag");
@@ -407,6 +408,13 @@ public class ResourceRecordController {
                 resourceRecordUserRelateService.addBatch(resourceRecordUserRelateList);
             }
         }
+        //如果传入资源名称,则对resource_userfile表的名称进行同步
+        if(StringUtils.isNotEmpty(courseAuditRecordEditParam.getResourceName()))
+        {
+            ResourceUserFile resourceUserfile=resourceUserfileService.queryEntity(oldCourseAuditRecord.getUserfileId());
+            resourceUserfile.setFileName(courseAuditRecordEditParam.getResourceName());
+            resourceUserfileService.editOne(resourceUserfile);
+        }
 
         return CommonResult.ok();
     }
@@ -822,7 +830,7 @@ public class ResourceRecordController {
         List<String> addIdList=new ArrayList<>();
         for (String userFileId : userFileIdList) {
             CourseAuditRecord courseAuditRecord = BeanUtil.toBean(courseAuditRecordAddParam, CourseAuditRecord.class);
-            //2025.10.30学生超链接功能下,应甲方需要,学生在此添加资源,强行把资源归为老师上传,但是把学生信息本地埋点至fileName字段用于追溯
+            //2025.10.30学生超链接功能下,应甲方需要,学生在此添加资源,强行把资源归为老师上传,但是把学生信息也做埋点
             if("1".equals(stulinkType))
             {
                 courseAuditRecord.setCreateUser(teacherId);
@@ -952,6 +960,13 @@ public class ResourceRecordController {
                 resourceRecordUserRelateService.addBatch(resourceRecordUserRelateList);
             }
         }
+        //如果传入资源名称,则对resource_userfile表的名称进行同步
+        if(StringUtils.isNotEmpty(courseAuditRecordEditParam.getResourceName()))
+        {
+            ResourceUserFile resourceUserfile=resourceUserfileService.queryEntity(oldCourseAuditRecord.getUserfileId());
+            resourceUserfile.setFileName(courseAuditRecordEditParam.getResourceName());
+            resourceUserfileService.editOne(resourceUserfile);
+        }
 
         return CommonResult.ok();
     }

+ 10 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/controller/UserCommentController.java

@@ -90,6 +90,11 @@ public class UserCommentController {
     @CommonLog("添加user_comment")
     @PostMapping("/disk/comment/add")
     public CommonResult<String> add(@RequestBody @Valid UserCommentAddParam userCommentAddParam) {
+        // 检查敏感词
+        String titleIsIllegal = userCommentService.filterSensitiveWordsTitle(userCommentAddParam.getCommentName());
+        if("1".equals(titleIsIllegal)) {
+            return CommonResult.error("内容含有敏感词,请重新编辑发送");
+        }
         userCommentService.add(userCommentAddParam);
         return CommonResult.ok();
     }
@@ -105,6 +110,11 @@ public class UserCommentController {
     @CommonLog("编辑user_comment")
     @PostMapping("/disk/comment/edit")
     public CommonResult<String> edit(@RequestBody @Valid UserCommentEditParam userCommentEditParam) {
+        // 检查敏感词
+        String titleIsIllegal = userCommentService.filterSensitiveWordsTitle(userCommentEditParam.getCommentName());
+        if("1".equals(titleIsIllegal)) {
+            return CommonResult.error("内容含有敏感词,请重新编辑发送");
+        }
         userCommentService.edit(userCommentEditParam);
         return CommonResult.ok();
     }

+ 4 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/domain/CourseAuditRecord.java

@@ -121,4 +121,8 @@ public class CourseAuditRecord extends CommonEntity {
     /** 权限类型,0公开1私密 */
     @ApiModelProperty(value = "权限类型,0公开1私密", position = 7)
     private String authType;
+
+    /** 学员超链接,学生id埋点 */
+    @ApiModelProperty(value = "学员超链接,学生id埋点", position = 2)
+    private String linkStuId;
 }

+ 5 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/domain/CourseStudentBurialpoint.java

@@ -120,4 +120,9 @@ public class CourseStudentBurialpoint {
     private String resourceRecord;
     /** 文件扩展名 */
     private String extendName;
+
+    /** 视频时长 */
+    @ApiModelProperty(value = "视频时长", position = 4)
+    @TableField(select = false,insertStrategy = FieldStrategy.IGNORED, updateStrategy = FieldStrategy.IGNORED)
+    private String duration;
 }

+ 3 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/domain/ResourceFile.java

@@ -83,6 +83,9 @@ public class ResourceFile {
     @TableField(value="PRIVIEW_FILE_URL")
     private String previewFileUrl;
 
+    @TableField(value="DURATION")
+    private String duration;
+
     public ResourceFile(){
 
     }

+ 10 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/mapper/CourseStudentBurialpointMapper.java

@@ -15,6 +15,9 @@ package vip.xiaonuo.disk.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import vip.xiaonuo.disk.domain.CourseStudentBurialpoint;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * 课程学生-学习进度表Mapper接口
  *
@@ -22,4 +25,11 @@ import vip.xiaonuo.disk.domain.CourseStudentBurialpoint;
  * @date  2025/07/30 14:57
  **/
 public interface CourseStudentBurialpointMapper extends BaseMapper<CourseStudentBurialpoint> {
+
+    /**
+     * 获取埋点其他信息
+     * (用于补全信息)
+     * */
+    List<Map<String, Object>> getOtherInfoList(Map param);
+
 }

+ 8 - 1
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/mapper/mapping/CourseAuditRecordMapper.xml

@@ -33,6 +33,10 @@
             IFNULL (RTRIM(CONCAT(rt.NAME,',',rt2.NAME,',',rt3.NAME),','),'') AS resourceALLTypeName,
             IFNULL (t1.COURSE_ID,'') as courseId,
             IFNULL (t3.COURSE_NAME,'') AS courseIdName,
+            CASE
+            WHEN rf.PRIVIEW_FILE_URL IS NOT null  AND rf.PRIVIEW_FILE_URL !='' THEN '1'
+            ELSE '0'
+            END AS isConvert,
             IFNULL (TRIM(t1.IS_RECOMMEND),'') AS isRecommend,
             IFNULL (TRIM(t1.IS_HOT),'') AS isHot
         FROM RESOURCE_RECORD t1
@@ -184,6 +188,9 @@
             <if test=" param.sortflag == 1">
                 and t1.IS_HOT=1
             </if>
+            <if test=" param.sortflag == 2">
+                and t2.FUNC_TYPE ='1'
+            </if>
         </if>
         AND (
             t1.AUTH_TYPE ='0'
@@ -249,7 +256,7 @@
             IFNULL (CAST(t7.AVATAR AS VARCHAR),'') AS avatar,
             IFNULL (CAST(t6.DOWNLOAD_PATH AS VARCHAR),'') AS coverImagePath,
             CASE
-                WHEN rf.PRIVIEW_FILE_URL IS NOT null THEN '1'
+                WHEN rf.PRIVIEW_FILE_URL IS NOT null AND rf.PRIVIEW_FILE_URL !='' THEN '1'
                 ELSE '0'
             END AS isConvert,
             IFNULL (TRIM(t1.IS_RECOMMEND),'') AS isRecommend,

+ 28 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/mapper/mapping/CourseStudentBurialpointMapper.xml

@@ -2,4 +2,32 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="vip.xiaonuo.disk.mapper.CourseStudentBurialpointMapper">
 
+
+    <select id="getOtherInfoList" resultType="java.util.Map">
+        SELECT
+            t1.csbId,
+            CASE
+            WHEN FLOOR(t1.duration / 3600000) = 0 THEN '0'
+            ELSE REGEXP_REPLACE(LPAD(FLOOR(t1.duration / 3600000), 5, '0'), '^0+', '')
+            END || '小时' ||
+            LPAD(FLOOR((t1.duration % 3600000) / 60000), 2, '0') || '分钟' AS durationTime
+        FROM (
+            SELECT
+                csb.ID AS csbId,
+                IFNULL(rf.DURATION,'0') AS duration
+            FROM COURSE_STUDENT_BURIALPOINT csb
+            LEFT JOIN COURSE_CLASSHOUR cch ON csb.HOUR_ID =cch.ID AND cch.DELETE_FLAG ='NOT_DELETE'
+            LEFT JOIN COURSE_RELATE cr ON cr.MAIN_ID =cch.ID AND cr.FUNC_TYPE ='2' AND cr.INFO_TYPE ='0' AND cr.CHAPTERHOUR_TYPE ='1'
+            LEFT JOIN RESOURCE_RECORD rr ON rr.ID =cr.RELATE_ID AND rr.DELETE_FLAG ='NOT_DELETE'
+            LEFT JOIN RESOURCE_USERFILE ru ON rr.USERFILE_ID =ru.USER_FILE_ID AND ru.DELETE_FLAG ='NOT_DELETE'
+            LEFT JOIN RESOURCE_FILE rf ON rf.FILE_ID =ru.FILE_ID
+            WHERE csb.DELETE_FLAG ='NOT_DELETE'
+            <if test="burialpointIdList !=null and burialpointIdList.size()>0">
+                and csb.ID in
+                <foreach collection=" burialpointIdList" close=")" index="index" item="item" open="(" separator=",">
+                    #{item}
+                </foreach>
+            </if>
+        )t1
+    </select>
 </mapper>

+ 4 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/param/courseauditrecord/CourseAuditRecordAddParam.java

@@ -107,4 +107,8 @@ public class CourseAuditRecordAddParam {
     /** 老师id(学生超链接功能所用) */
     @ApiModelProperty(value = "老师id", position = 7)
     private String teacherId;
+
+    /** 资源名称 */
+    @ApiModelProperty(value = "资源名称", position = 7)
+    private String resourceName;
 }

+ 4 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/param/courseauditrecord/CourseAuditRecordEditParam.java

@@ -117,4 +117,8 @@ public class CourseAuditRecordEditParam {
     @ApiModelProperty(value = "老师id", position = 7)
     private String teacherId;
 
+    /** 资源名称 */
+    @ApiModelProperty(value = "资源名称", position = 7)
+    private String resourceName;
+
 }

+ 9 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/ResourceUserfileService.java

@@ -15,6 +15,7 @@ package vip.xiaonuo.disk.service;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import org.apache.ibatis.annotations.Param;
+import vip.xiaonuo.disk.domain.CourseAuditRecord;
 import vip.xiaonuo.disk.domain.ResourceUserFile;
 import vip.xiaonuo.disk.param.resourceuserfile.ResourceUserFileAddParam;
 import vip.xiaonuo.disk.param.resourceuserfile.ResourceUserFileEditParam;
@@ -55,6 +56,14 @@ public interface ResourceUserfileService extends IService<ResourceUserFile> {
      */
     void edit(ResourceUserFileEditParam ResourceUserFileEditParam);
 
+    /**
+     * RESOURCE_USERFILE-编辑
+     *
+     * @author honorfire
+     * @date  2025/06/20 14:58
+     */
+    ResourceUserFile editOne(ResourceUserFile resourceUserFile);
+
     /**
      * 删除RESOURCE_USERFILE
      *

+ 2 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/UserCommentService.java

@@ -79,4 +79,6 @@ public interface UserCommentService extends IService<UserComment> {
     String giveCancel(@Valid @NotNull(message="评论id不能为空") Integer id);
 
     Page<UserCommentVo> pageList(UserCommentPageParam userCommentPageParam);
+
+    public String filterSensitiveWordsTitle(String title);
 }

+ 29 - 1
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/impl/CourseStudentBurialpointServiceImpl.java

@@ -24,6 +24,7 @@ import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
 import vip.xiaonuo.common.exception.CommonException;
 import vip.xiaonuo.common.page.CommonPageRequest;
 import vip.xiaonuo.common.pojo.CommonResult;
+import vip.xiaonuo.disk.domain.Chapter;
 import vip.xiaonuo.disk.domain.CourseStudentBurialpoint;
 import vip.xiaonuo.disk.mapper.CourseStudentBurialpointMapper;
 import vip.xiaonuo.disk.param.coursestudentprogress.CourseStudentProgressAddParam;
@@ -32,6 +33,8 @@ import vip.xiaonuo.disk.param.coursestudentprogress.CourseStudentProgressIdParam
 import vip.xiaonuo.disk.param.coursestudentprogress.CourseStudentProgressPageParam;
 import vip.xiaonuo.disk.service.CourseStudentBurialpointService;
 
+import javax.annotation.Resource;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -44,14 +47,39 @@ import java.util.Map;
 @Service
 public class CourseStudentBurialpointServiceImpl extends ServiceImpl<CourseStudentBurialpointMapper, CourseStudentBurialpoint> implements CourseStudentBurialpointService {
 
+    @Resource
+    private CourseStudentBurialpointMapper courseStudentBurialpointMapper;
 
     @Override
     public Page<CourseStudentBurialpoint> page(CourseStudentProgressPageParam courseStudentProgressPageParam) {
+        Map param=new HashMap();
         QueryWrapper<CourseStudentBurialpoint> queryWrapper = new QueryWrapper<>();
         queryWrapper.lambda().eq(CourseStudentBurialpoint::getCreateUser, StpLoginUserUtil.getLoginUser().getId());
         queryWrapper.lambda().eq(CourseStudentBurialpoint::getDeleteFlag, "NOT_DELETE");
         queryWrapper.lambda().orderByDesc(CourseStudentBurialpoint::getCreateTime);
-        return this.page(CommonPageRequest.defaultPage(), queryWrapper);
+        Page page=this.page(CommonPageRequest.defaultPage(), queryWrapper);
+        List<CourseStudentBurialpoint> records = page.getRecords();
+        List<String> burialpointIdList= CollStreamUtil.toList(records, CourseStudentBurialpoint::getId);
+        param.put("burialpointIdList", burialpointIdList);
+        List<Map<String, Object>> otherInfoList=courseStudentBurialpointMapper.getOtherInfoList(param);
+        if (otherInfoList.size()>0)
+        {
+            for (CourseStudentBurialpoint one:records)
+            {
+                for (int i = otherInfoList.size() - 1; i >= 0; i--)
+                {
+                    Map<String,Object> otherInfo = otherInfoList.get(i);
+                    if (otherInfo == null || otherInfo.get("csbId") == null) continue;
+
+                    if (one.getId().equals(otherInfo.get("csbId")))
+                    {
+                        one.setDuration(otherInfo.get("durationTime").toString());
+                        otherInfoList.remove(i);          // 从原列表删除(安全操作)
+                    }
+                }
+            }
+        }
+        return page;
     }
 
     /**

+ 8 - 0
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/impl/ResourceUserfileServiceImpl.java

@@ -29,6 +29,7 @@ import org.springframework.transaction.annotation.Transactional;
 import vip.xiaonuo.common.enums.CommonSortOrderEnum;
 import vip.xiaonuo.common.exception.CommonException;
 import vip.xiaonuo.common.page.CommonPageRequest;
+import vip.xiaonuo.disk.domain.CourseAuditRecord;
 import vip.xiaonuo.disk.domain.ResourceUserFile;
 import vip.xiaonuo.disk.param.resourceuserfile.ResourceUserFileAddParam;
 import vip.xiaonuo.disk.param.resourceuserfile.ResourceUserFileEditParam;
@@ -86,6 +87,13 @@ public class ResourceUserfileServiceImpl extends ServiceImpl<ResourceUserFileMap
         this.updateById(ResourceUserFile);
     }
 
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public ResourceUserFile editOne(ResourceUserFile resourceUserFile) {
+        this.updateById(resourceUserFile);
+        return resourceUserFile;
+    }
+
     @Transactional(rollbackFor = Exception.class)
     @Override
     public void delete(List<ResourceUserFileIdParam> ResourceUserFileIdParamList) {

+ 32 - 11
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/impl/TranscodingServiceImpl.java

@@ -22,10 +22,7 @@ import vip.xiaonuo.disk.util.VideoConverter;
 
 import javax.annotation.Resource;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
 
 /**
  * 资源转码 service
@@ -127,11 +124,11 @@ public class TranscodingServiceImpl implements TranscodingService {
         insertResourceUserfileConvert(transcodingResourceReqDTO.getUserFileId(), userFile.getFileId(),userFile.getFileName(), userFile.getExtendName(), userFile.getFilePath(), ResourceUserfileConvertEnum.ING.getValue(), userId,null);
 
 
-        if (userFile == null || !userFile.getUserId().equals(userId)) {
-            System.out.println("文件不存在或不是本人的");
-            insertResourceUserfileConvert(transcodingResourceReqDTO.getUserFileId(), userFile.getFileId(),userFile.getFileName(), userFile.getExtendName(), userFile.getFilePath(), ResourceUserfileConvertEnum.FAIL.getValue(), userId,"文件不存在或不是本人的");
-            return CommonResult.error("文件不存在或不是本人的");
-        }
+//        if (userFile == null || !userFile.getUserId().equals(userId)) {
+//            System.out.println("文件不存在或不是本人的");
+//            insertResourceUserfileConvert(transcodingResourceReqDTO.getUserFileId(), userFile.getFileId(),userFile.getFileName(), userFile.getExtendName(), userFile.getFilePath(), ResourceUserfileConvertEnum.FAIL.getValue(), userId,"文件不存在或不是本人的");
+//            return CommonResult.error("文件不存在或不是本人的");
+//        }
 
         List<String> videoList= Arrays.asList("wmv","avi","flv","mpeg","mpg","rmvb","mov","mkv");
 
@@ -167,12 +164,13 @@ public class TranscodingServiceImpl implements TranscodingService {
                 try {
                     // 转码视频
                     // mp4 wmv avi flv mpeg mpg rmvb mov 互相转
-                    videoConverter.convertAndUpload(fileBean.getFileUrl(), fileBean.getFileId(), new String[]{transcodingResourceReqDTO.getFormat()});
+                    Map result=videoConverter.convertAndUpload(fileBean.getFileUrl(), fileBean.getFileId(), new String[]{transcodingResourceReqDTO.getFormat()});
                     //根据文件id修改预览地址
                     if(videoList.contains(userFile.getExtendName())||wordList.contains(userFile.getExtendName())){
                         newFile = new ResourceFile();
                         newFile.setFileId(fileBean.getFileId());
                         newFile.setPreviewFileUrl("converted/" + getCurrentDate()+"/"+fileBean.getFileId() + "." + transcodingResourceReqDTO.getFormat());
+                        newFile.setDuration(result.get("duration").toString());
                         resourceFileMapper.updateById(newFile);
                     }
                 } catch (Exception ex) {
@@ -212,6 +210,17 @@ public class TranscodingServiceImpl implements TranscodingService {
         else
         {
             newFile = new ResourceFile();
+            //如果是mp4,不做转换和重新上传,只单纯下载获取视频时长
+            if("mp4".equals(userFile.getExtendName()))
+            {
+                try {
+                    Map result=videoConverter.getDuration(fileBean.getFileUrl(), fileBean.getFileId(), new String[]{userFile.getExtendName()});
+                    newFile.setDuration(result.get("duration").toString());
+                } catch (Exception e) {
+                    log.error(e.getMessage());
+                    System.out.println("mp4获取视频时长失败");
+                }
+            }
             newFile.setFileId(fileBean.getFileId());
             newFile.setPreviewFileUrl(fileBean.getFileUrl());
             resourceFileMapper.updateById(newFile);
@@ -279,12 +288,13 @@ public class TranscodingServiceImpl implements TranscodingService {
                         try {
                             // 转码视频
                             // mp4 wmv avi flv mpeg mpg rmvb mov 互相转
-                            videoConverter.convertAndUpload(fileBean.getFileUrl(), fileBean.getFileId(), new String[]{transcodingResourceReqDTO.getFormat()});
+                            Map result=videoConverter.convertAndUpload(fileBean.getFileUrl(), fileBean.getFileId(), new String[]{transcodingResourceReqDTO.getFormat()});
                             //根据文件id修改预览地址
                             if(videoList.contains(userFile.getExtendName())||wordList.contains(userFile.getExtendName())){
                                 newFile = new ResourceFile();
                                 newFile.setFileId(fileBean.getFileId());
                                 newFile.setPreviewFileUrl("converted/" + getCurrentDate()+"/"+fileBean.getFileId() + "." + transcodingResourceReqDTO.getFormat());
+                                newFile.setDuration(result.get("duration").toString());
                                 resourceFileMapper.updateById(newFile);
                             }
                         } catch (Exception ex) {
@@ -324,6 +334,17 @@ public class TranscodingServiceImpl implements TranscodingService {
                 else
                 {
                     newFile = new ResourceFile();
+                    //如果是mp4,不做转换和重新上传,只单纯下载获取视频时长
+                    if("mp4".equals(userFile.getExtendName()))
+                    {
+                        try {
+                            Map result=videoConverter.getDuration(fileBean.getFileUrl(), fileBean.getFileId(), new String[]{userFile.getExtendName()});
+                            newFile.setDuration(result.get("duration").toString());
+                        } catch (Exception e) {
+                            log.error(e.getMessage());
+                            System.out.println("mp4获取视频时长失败");
+                        }
+                    }
                     newFile.setFileId(fileBean.getFileId());
                     newFile.setPreviewFileUrl(fileBean.getFileUrl());
                     resourceFileMapper.updateById(newFile);

+ 28 - 1
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/service/impl/UserCommentServiceImpl.java

@@ -15,6 +15,7 @@ package vip.xiaonuo.disk.service.impl;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollStreamUtil;
 import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -23,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional;
 import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
 import vip.xiaonuo.common.exception.CommonException;
 import vip.xiaonuo.common.page.CommonPageRequest;
+import vip.xiaonuo.common.pojo.CommonResult;
 import vip.xiaonuo.disk.domain.UserComment;
 import vip.xiaonuo.disk.domain.UserCommentGive;
 import vip.xiaonuo.disk.mapper.UserCommentGiveMapper;
@@ -33,7 +35,10 @@ import vip.xiaonuo.disk.param.UserCommentIdParam;
 import vip.xiaonuo.disk.param.UserCommentPageParam;
 import vip.xiaonuo.disk.service.UserCommentService;
 import vip.xiaonuo.disk.vo.comment.UserCommentVo;
+import vip.xiaonuo.forum.modular.sensitivity.entity.ForumSensitivity;
+import vip.xiaonuo.forum.modular.sensitivity.mapper.ForumSensitivityMapper;
 
+import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -50,10 +55,12 @@ public class UserCommentServiceImpl extends ServiceImpl<UserCommentMapper, UserC
     @Autowired
     private  UserCommentMapper  userCommentMapper;
 
-
     @Autowired
     private UserCommentGiveMapper userCommentGiveMapper;
 
+    @Resource
+    private ForumSensitivityMapper forumSensitivityMapper;
+
 
     @Override
     public Page<UserCommentVo> page(UserCommentPageParam userCommentPageParam) {
@@ -187,5 +194,25 @@ public class UserCommentServiceImpl extends ServiceImpl<UserCommentMapper, UserC
         return userComment;
     }
 
+    /**
+     * 检查敏感词
+     *
+     */
+    @Override
+    public String filterSensitiveWordsTitle(String title) {
+        String isIllegal="0";
+        List<ForumSensitivity> forumSensitivities = forumSensitivityMapper.selectList(new QueryWrapper<>());
+        if (forumSensitivities.size() > 0) {
+            // 过滤敏感词
+            for (ForumSensitivity forumSensitivity : forumSensitivities) {
+                // 过滤逻辑
+                if (title.contains(forumSensitivity.getSensitivityWord())) {
+                    isIllegal="1";
+                }
+            }
+        }
+        return isIllegal;
+    }
+
 
 }

+ 150 - 5
snowy-plugin/snowy-plugin-disk/snowy-plugin-disk-func/src/main/java/vip/xiaonuo/disk/util/VideoConverter.java

@@ -10,10 +10,7 @@ import java.io.*;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
 
 @Component
 public class VideoConverter {
@@ -33,7 +30,8 @@ public class VideoConverter {
     private final String tempDir = System.getProperty("java.io.tmpdir") + "/minio-converter/";
 
 
-    public void convertAndUpload(String inputKey, String outputPrefix, String[] formats) throws Exception {
+    public Map convertAndUpload(String inputKey, String outputPrefix, String[] formats) throws Exception {
+        Map result = new HashMap<>();
         MinioClient minioClient = MinioClient.builder()
                 .endpoint(MINIO_ENDPOINT)
                 .credentials(ACCESS_KEY, SECRET_KEY)
@@ -46,6 +44,7 @@ public class VideoConverter {
 //        if (!tempDir.exists()) {
 //            tempDir.mkdir();
 //        }
+        String duration="";
         String fileName = new File(inputKey).getName();
         String uniqueName = UUID.randomUUID() + "_" + fileName;
         File tempFile = new File(tempDir, uniqueName);
@@ -55,6 +54,7 @@ public class VideoConverter {
             System.out.println("下载文件:" + tempFile);
             downloadFromMinio(minioClient, BUCKET_NAME, inputKey, tempFile);
             filesToDelete.add(tempFile); // 添加到删除列表
+            duration=getVideoDurationMillis(tempFile.getAbsolutePath());
             // 执行格式转换
             for (String format : formats) {
                 File outputFile = new File(tempDir, outputPrefix + "." + format);
@@ -64,6 +64,7 @@ public class VideoConverter {
                 uploadToMinio(minioClient, BUCKET_NAME, outputFile.getName(), outputFile);
                 filesToDelete.add(outputFile); // 添加到删除列表
             }
+            result.put("duration",duration);
         } finally {
             // 清理临时文件(保留目录结构)
             for (File file : filesToDelete) {
@@ -72,6 +73,45 @@ public class VideoConverter {
                 }
             }
         }
+        return result;
+    }
+
+    public Map getDuration(String inputKey, String outputPrefix, String[] formats) throws Exception {
+        Map result = new HashMap<>();
+        MinioClient minioClient = MinioClient.builder()
+                .endpoint(MINIO_ENDPOINT)
+                .credentials(ACCESS_KEY, SECRET_KEY)
+                .build();
+
+
+        List<File> filesToDelete = new ArrayList<>(); // 待删除文件列表
+        // 创建临时目录
+//        File tempDir = new File("temp");
+//        if (!tempDir.exists()) {
+//            tempDir.mkdir();
+//        }
+        String duration="";
+        String fileName = new File(inputKey).getName();
+        String uniqueName = UUID.randomUUID() + "_" + fileName;
+        File tempFile = new File(tempDir, uniqueName);
+        try {
+            System.out.println("下载路径:" + tempDir);
+            System.out.println("下载名称:" + uniqueName);
+            System.out.println("下载文件:" + tempFile);
+            downloadFromMinio(minioClient, BUCKET_NAME, inputKey, tempFile);
+            filesToDelete.add(tempFile); // 添加到删除列表
+            duration=getVideoDurationMillis(tempFile.getAbsolutePath());
+
+            result.put("duration",duration);
+        } finally {
+            // 清理临时文件(保留目录结构)
+            for (File file : filesToDelete) {
+                if (file.exists() && !file.delete()) {
+                    System.err.println("警告:无法删除临时文件 " + file.getAbsolutePath());
+                }
+            }
+        }
+        return result;
     }
 
 //    private void convertVideo(String inputPath, String outputPath, String format) throws IOException, InterruptedException {
@@ -165,6 +205,111 @@ public class VideoConverter {
         );
     }
 
+    /**
+     * 获取视频文件时长(毫秒)
+     * @param inputPath 视频文件路径
+     * @return 时长毫秒的字符串,失败返回 null
+     */
+    private String getVideoDurationMillis(String inputPath) {
+        List<String> command = new ArrayList<>();
+        command.add(FFMPEG_PATH);
+        command.add("-i");
+        command.add(inputPath);
+        // 不进行实际转码,只探测信息
+        command.add("-f");
+        command.add("null");
+        command.add("-");
+
+        ProcessBuilder pb = new ProcessBuilder(command);
+        // 将错误流重定向到输入流,因为ffmpeg的输出信息在stderr
+        pb.redirectErrorStream(true);
+
+        try {
+            Process process = pb.start();
+            StringBuilder output = new StringBuilder();
+
+            // 读取输出信息
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    output.append(line).append("\n");
+                    // 实时解析包含 Duration 的行,找到就可以提前退出
+                    if (line.contains("Duration:")) {
+                        String durationStr = extractDuration(line);
+                        if (durationStr != null) {
+                            // 等待进程结束(但我们已经拿到需要的信息了)
+                            process.destroy();
+                            return durationStr;
+                        }
+                    }
+                }
+            }
+
+            int exitCode = process.waitFor();
+            if (exitCode != 0) {
+                System.err.println("FFmpeg探测失败,退出码:" + exitCode);
+            }
+
+            // 如果上面没找到,尝试从完整输出中再找一次
+            String durationStr = extractDuration(output.toString());
+            if (durationStr != null) {
+                return durationStr;
+            }
+
+        } catch (IOException | InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    /**
+     * 从ffmpeg输出行中提取时长并转换为毫秒字符串
+     */
+    private String extractDuration(String line) {
+        // 匹配格式:Duration: 00:01:30.45, start: 0.000000, bitrate: 320 kb/s
+        if (line.contains("Duration:")) {
+            try {
+                // 提取时间部分 "00:01:30.45"
+                String[] parts = line.split("Duration:\\s*");
+                if (parts.length > 1) {
+                    String timePart = parts[1].split(",")[0].trim(); // "00:01:30.45"
+
+                    // 解析时分秒毫秒
+                    String[] timeParts = timePart.split(":");
+                    if (timeParts.length >= 3) {
+                        int hours = Integer.parseInt(timeParts[0]);
+                        int minutes = Integer.parseInt(timeParts[1]);
+
+                        // 秒和毫秒可能用点分隔
+                        String[] secondsParts = timeParts[2].split("\\.");
+                        int seconds = Integer.parseInt(secondsParts[0]);
+                        int millis = 0;
+                        if (secondsParts.length > 1) {
+                            // 处理小数秒,可能是 .45 或 .450 等形式
+                            String millisStr = secondsParts[1];
+                            if (millisStr.length() == 1) {
+                                millis = Integer.parseInt(millisStr) * 100;
+                            } else if (millisStr.length() == 2) {
+                                millis = Integer.parseInt(millisStr) * 10;
+                            } else if (millisStr.length() >= 3) {
+                                millis = Integer.parseInt(millisStr.substring(0, 3));
+                            }
+                        }
+
+                        // 计算总毫秒数
+                        long totalMillis = (hours * 3600L + minutes * 60L + seconds) * 1000 + millis;
+                        return String.valueOf(totalMillis);
+                    }
+                }
+            } catch (Exception e) {
+                System.err.println("解析时长失败,行内容: " + line);
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
     private static String getMimeType(String fileName) {
         return switch (fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase()) {
             case "mp4" -> "video/mp4";

+ 11 - 0
snowy-plugin/snowy-plugin-exam/snowy-plugin-exam-func/src/main/java/vip/xiaonuo/exam/mapper/mapping/TExamMapper.xml

@@ -20,10 +20,21 @@
            a.EXAM_TYPE examType,
            a.SEMESTER_ID semesterId,
            c.name semesterName,
+           IFNULL(t1.submitCount,0) AS submitCount,
+           IFNULL(t1.judgeCount,0) AS judgeCount,
            a.course_id courseId
        FROM
            T_EXAM a
        LEFT JOIN  SEMESTER c ON a.SEMESTER_ID =c.ID
+       LEFT JOIN (
+           select
+               tepa.exam_paper_id AS tepaId,
+               count(tepa."id") AS submitCount,
+               count(CASE WHEN tepa.status = '2' THEN 1 END) AS judgeCount
+           from t_exam_paper_answer tepa
+           WHERE 1=1
+           GROUP BY tepa.exam_paper_id
+       )t1 ON t1.tepaId=a.PAPER_ID
     <where>
        <if test="tExamPageParam.examName != null and tExamPageParam.examName != ''">
            a.EXAM_NAME like '%'||#{tExamPageParam.examName}||'%'

+ 9 - 0
snowy-plugin/snowy-plugin-exam/snowy-plugin-exam-func/src/main/java/vip/xiaonuo/exam/vo/TExamVo.java

@@ -103,5 +103,14 @@ public class TExamVo {
      */
     private String gradesids;
 
+    /**
+     * 提交人数
+     */
+    private String submitCount;
+
+    /**
+     * 批改人数
+     */
+    private String judgeCount;
 
 }