Bläddra i källkod

修复素材管理后端bug,修复管理查询中时间范围查询bug,修复问答库中相似问个数bug

zmj 2 veckor sedan
förälder
incheckning
dac67cf606

+ 191 - 51
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java

@@ -2,8 +2,12 @@ package com.ruoyi.web.controller.common;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+
+import com.ruoyi.common.utils.ThumbnailUtils;
+import com.ruoyi.common.utils.VideoUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -18,13 +22,13 @@ import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.file.FileUploadUtils;
 import com.ruoyi.common.utils.file.FileUtils;
+import com.ruoyi.common.utils.file.MimeTypeUtils;
 import com.ruoyi.framework.config.ServerConfig;
+import com.ruoyi.base.domain.RobotOpsMediaAsset;
+import com.ruoyi.base.service.IRobotOpsMediaAssetService;
+
+// ... existing code ...
 
-/**
- * 通用请求处理
- * 
- * @author ruoyi
- */
 @RestController
 @RequestMapping("/common")
 public class CommonController
@@ -34,66 +38,124 @@ public class CommonController
     @Autowired
     private ServerConfig serverConfig;
 
+    @Autowired
+    private IRobotOpsMediaAssetService robotOpsMediaAssetService;
+
     private static final String FILE_DELIMITER = ",";
 
+    // ... existing code ...
+
     /**
-     * 通用下载请求
-     * 
-     * @param fileName 文件名称
-     * @param delete 是否删除
+     * 通用上传请求(单个)
      */
-    @GetMapping("/download")
-    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
+    @PostMapping("/upload")
+    public AjaxResult uploadFile(MultipartFile file) throws Exception
     {
         try
         {
-            if (!FileUtils.checkAllowDownload(fileName))
-            {
-                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
-            }
-            String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
-            String filePath = RuoYiConfig.getDownloadPath() + fileName;
-
-            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
-            FileUtils.setAttachmentResponseHeader(response, realFileName);
-            FileUtils.writeBytes(filePath, response.getOutputStream());
-            if (delete)
-            {
-                FileUtils.deleteFile(filePath);
-            }
+            // 上传文件路径
+            String filePath = RuoYiConfig.getUploadPath();
+            // 上传并返回新文件名称
+            String fileName = FileUploadUtils.upload(filePath, file);
+            String url = serverConfig.getUrl() + fileName;
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("url", url);
+            ajax.put("fileName", fileName);
+            ajax.put("newFileName", FileUtils.getName(fileName));
+            ajax.put("originalFilename", file.getOriginalFilename());
+            return ajax;
         }
         catch (Exception e)
         {
-            log.error("下载文件失败", e);
+            return AjaxResult.error(e.getMessage());
         }
     }
 
     /**
-     * 通用上传请求(单个)
+     * 上传素材文件(单个)- 只上传文件,返回文件信息,不保存到数据库
      */
-    @PostMapping("/upload")
-    public AjaxResult uploadFile(MultipartFile file) throws Exception
+    @PostMapping("/uploadMediaFile")
+    public AjaxResult uploadMediaFile(MultipartFile file) throws Exception
     {
         try
         {
-            // 上传文件路径
+            // 1. 上传文件
             String filePath = RuoYiConfig.getUploadPath();
-            // 上传并返回新文件名称
             String fileName = FileUploadUtils.upload(filePath, file);
             String url = serverConfig.getUrl() + fileName;
-            AjaxResult ajax = AjaxResult.success();
-            ajax.put("url", url);
+
+            // 2. 构建返回数据
+            AjaxResult ajax = AjaxResult.success("上传成功");
+            ajax.put("fileUrl", url);
             ajax.put("fileName", fileName);
-            ajax.put("newFileName", FileUtils.getName(fileName));
             ajax.put("originalFilename", file.getOriginalFilename());
+            ajax.put("fileSize", file.getSize());
+            ajax.put("mimeType", file.getContentType());
+            ajax.put("fileFormat", FileUtils.getExtension(file));
+
+            // 3. 根据文件类型处理
+            String contentType = file.getContentType();
+
+            if (contentType != null && contentType.startsWith("image/"))
+            {
+                // 图片:生成缩略图和获取分辨率
+                String thumbnailUrl = ThumbnailUtils.generateThumbnail(file);
+                ajax.put("thumbnailUrl", thumbnailUrl);
+
+                try
+                {
+                    java.awt.image.BufferedImage image = javax.imageio.ImageIO.read(file.getInputStream());
+                    if (image != null)
+                    {
+                        String resolution = image.getWidth() + "x" + image.getHeight();
+                        ajax.put("resolution", resolution);
+                    }
+                }
+                catch (Exception e)
+                {
+                    log.warn("获取图片分辨率失败", e);
+                }
+            }
+            else if (contentType != null && contentType.startsWith("video/"))
+            {
+                // 视频:生成缩略图、获取时长和分辨率
+                try
+                {
+                    // 获取实际文件路径
+                    String realFilePath = RuoYiConfig.getProfile() + FileUtils.stripPrefix(url);
+
+                    // 生成视频缩略图(截取第1秒的画面)
+                    String thumbnailUrl = VideoUtils.generateVideoThumbnail(realFilePath, 1);
+                    ajax.put("thumbnailUrl", thumbnailUrl);
+
+                    // 获取视频信息(时长和分辨率)
+                    Map<String, Object> videoInfo = VideoUtils.getVideoInfo(realFilePath);
+                    if (videoInfo.containsKey("durationSeconds"))
+                    {
+                        ajax.put("durationSeconds", videoInfo.get("durationSeconds"));
+                    }
+                    if (videoInfo.containsKey("resolution"))
+                    {
+                        ajax.put("resolution", videoInfo.get("resolution"));
+                    }
+                }
+                catch (Exception e)
+                {
+                    log.warn("处理视频文件失败", e);
+                }
+            }
+
             return ajax;
         }
         catch (Exception e)
         {
-            return AjaxResult.error(e.getMessage());
+            log.error("上传素材文件失败", e);
+            return AjaxResult.error("上传失败: " + e.getMessage());
         }
     }
 
+    // ... existing code ...
+
     /**
      * 通用上传请求(多个)
      */
@@ -132,31 +194,109 @@ public class CommonController
     }
 
     /**
-     * 本地资源通用下载
+     * 批量上传素材文件 - 只上传文件,返回文件信息列表,不保存到数据库
      */
-    @GetMapping("/download/resource")
-    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
-            throws Exception
+    @PostMapping("/uploadMediaFiles")
+    public AjaxResult uploadMediaFiles(List<MultipartFile> files) throws Exception
     {
+        List<AjaxResult> fileInfoList = new ArrayList<>();
+        int successCount = 0;
+        int failedCount = 0;
+
         try
         {
-            if (!FileUtils.checkAllowDownload(resource))
+            String filePath = RuoYiConfig.getUploadPath();
+
+            for (MultipartFile file : files)
             {
-                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
+                try
+                {
+                    // 1. 上传文件
+                    String fileName = FileUploadUtils.upload(filePath, file);
+                    String url = serverConfig.getUrl() + fileName;
+
+                    // 2. 构建文件信息
+                    AjaxResult fileInfo = AjaxResult.success();
+                    fileInfo.put("fileUrl", url);
+                    fileInfo.put("fileName", fileName);
+                    fileInfo.put("originalFilename", file.getOriginalFilename());
+                    fileInfo.put("fileSize", file.getSize());
+                    fileInfo.put("mimeType", file.getContentType());
+                    fileInfo.put("fileFormat", FileUtils.getExtension(file));
+
+                    // 3. 根据文件类型处理
+                    String contentType = file.getContentType();
+
+                    if (contentType != null && contentType.startsWith("image/"))
+                    {
+                        // 图片处理
+                        String thumbnailUrl = ThumbnailUtils.generateThumbnail(file);
+                        fileInfo.put("thumbnailUrl", thumbnailUrl);
+
+                        try
+                        {
+                            java.awt.image.BufferedImage image = javax.imageio.ImageIO.read(file.getInputStream());
+                            if (image != null)
+                            {
+                                String resolution = image.getWidth() + "x" + image.getHeight();
+                                fileInfo.put("resolution", resolution);
+                            }
+                        }
+                        catch (Exception e)
+                        {
+                            log.warn("获取图片分辨率失败", e);
+                        }
+                    }
+                    else if (contentType != null && contentType.startsWith("video/"))
+                    {
+                        // 视频处理
+                        try
+                        {
+                            String realFilePath = RuoYiConfig.getProfile() + FileUtils.stripPrefix(url);
+
+                            // 生成视频缩略图
+                            String thumbnailUrl = VideoUtils.generateVideoThumbnail(realFilePath, 1);
+                            fileInfo.put("thumbnailUrl", thumbnailUrl);
+
+                            // 获取视频信息
+                            Map<String, Object> videoInfo = VideoUtils.getVideoInfo(realFilePath);
+                            if (videoInfo.containsKey("durationSeconds"))
+                            {
+                                fileInfo.put("durationSeconds", videoInfo.get("durationSeconds"));
+                            }
+                            if (videoInfo.containsKey("resolution"))
+                            {
+                                fileInfo.put("resolution", videoInfo.get("resolution"));
+                            }
+                        }
+                        catch (Exception e)
+                        {
+                            log.warn("处理视频文件失败", e);
+                        }
+                    }
+
+                    fileInfoList.add(fileInfo);
+                    successCount++;
+                }
+                catch (Exception e)
+                {
+                    log.error("上传文件失败: {}", file.getOriginalFilename(), e);
+                    failedCount++;
+                }
             }
-            // 本地资源路径
-            String localPath = RuoYiConfig.getProfile();
-            // 数据库资源地址
-            String downloadPath = localPath + FileUtils.stripPrefix(resource);
-            // 下载名称
-            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
-            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
-            FileUtils.setAttachmentResponseHeader(response, downloadName);
-            FileUtils.writeBytes(downloadPath, response.getOutputStream());
+
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("files", fileInfoList);
+            ajax.put("successCount", successCount);
+            ajax.put("failedCount", failedCount);
+            return ajax;
         }
         catch (Exception e)
         {
-            log.error("下载文件失败", e);
+            log.error("批量上传素材文件失败", e);
+            return AjaxResult.error("批量上传失败: " + e.getMessage());
         }
     }
+
+    // ... existing code ...
 }

+ 10 - 3
ruoyi-common/pom.xml

@@ -52,13 +52,13 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
         </dependency>
-  
+
         <!-- JSON工具类 -->
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
         </dependency>
-        
+
         <!-- 阿里JSON解析器 -->
         <dependency>
             <groupId>com.alibaba.fastjson2</groupId>
@@ -114,6 +114,13 @@
             <artifactId>javax.servlet-api</artifactId>
         </dependency>
 
+        <!-- JavaCV 视频处理 -->
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>javacv-platform</artifactId>
+            <version>1.5.9</version>
+        </dependency>
+
     </dependencies>
 
-</project>
+</project>

+ 188 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/ThumbnailUtils.java

@@ -0,0 +1,188 @@
+package com.ruoyi.common.utils;
+
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import javax.imageio.ImageIO;
+
+import com.ruoyi.common.utils.file.FileUploadUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.uuid.IdUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+/**
+ * 缩略图处理工具类(仅处理图片)
+ *
+ * @author ruoyi
+ */
+public class ThumbnailUtils
+{
+    private static final Logger log = LoggerFactory.getLogger(ThumbnailUtils.class);
+
+    /**
+     * 默认缩略图宽度
+     */
+    private static final int DEFAULT_THUMBNAIL_WIDTH = 200;
+
+    /**
+     * 默认缩略图高度
+     */
+    private static final int DEFAULT_THUMBNAIL_HEIGHT = 200;
+
+    /**
+     * 生成缩略图
+     *
+     * @param file 原始文件
+     * @return 缩略图文件路径,如果生成失败返回null
+     */
+    public static String generateThumbnail(MultipartFile file)
+    {
+        try
+        {
+            BufferedImage originalImage = ImageIO.read(file.getInputStream());
+            if (originalImage == null)
+            {
+                log.warn("无法读取图片文件: {}", file.getOriginalFilename());
+                return null;
+            }
+
+            return generateThumbnail(originalImage, file.getOriginalFilename(), DEFAULT_THUMBNAIL_WIDTH, DEFAULT_THUMBNAIL_HEIGHT);
+        }
+        catch (IOException e)
+        {
+            log.error("生成缩略图失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 生成缩略图
+     *
+     * @param file 原始文件
+     * @param width 缩略图宽度
+     * @param height 缩略图高度
+     * @return 缩略图文件路径,如果生成失败返回null
+     */
+    public static String generateThumbnail(MultipartFile file, int width, int height)
+    {
+        try
+        {
+            BufferedImage originalImage = ImageIO.read(file.getInputStream());
+            if (originalImage == null)
+            {
+                log.warn("无法读取图片文件: {}", file.getOriginalFilename());
+                return null;
+            }
+
+            return generateThumbnail(originalImage, file.getOriginalFilename(), width, height);
+        }
+        catch (IOException e)
+        {
+            log.error("生成缩略图失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 生成缩略图
+     *
+     * @param imagePath 原始图片路径
+     * @return 缩略图文件路径,如果生成失败返回null
+     */
+    public static String generateThumbnail(String imagePath)
+    {
+        try
+        {
+            File imageFile = new File(imagePath);
+            if (!imageFile.exists())
+            {
+                log.warn("图片文件不存在: {}", imagePath);
+                return null;
+            }
+
+            BufferedImage originalImage = ImageIO.read(imageFile);
+            if (originalImage == null)
+            {
+                log.warn("无法读取图片文件: {}", imagePath);
+                return null;
+            }
+
+            String fileName = imageFile.getName();
+            return generateThumbnail(originalImage, fileName, DEFAULT_THUMBNAIL_WIDTH, DEFAULT_THUMBNAIL_HEIGHT);
+        }
+        catch (IOException e)
+        {
+            log.error("生成缩略图失败: {}", imagePath, e);
+            return null;
+        }
+    }
+
+    /**
+     * 生成缩略图(内部方法)
+     */
+    private static String generateThumbnail(BufferedImage originalImage, String originalFileName, int width, int height)
+    {
+        try
+        {
+            int originalWidth = originalImage.getWidth();
+            int originalHeight = originalImage.getHeight();
+
+            // 计算缩放比例,保持宽高比
+            double scale = Math.min((double) width / originalWidth, (double) height / originalHeight);
+            int scaledWidth = (int) (originalWidth * scale);
+            int scaledHeight = (int) (originalHeight * scale);
+
+            // 创建缩略图
+            BufferedImage thumbnail = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
+            Graphics2D g2d = thumbnail.createGraphics();
+
+            // 设置图片平滑渲染
+            g2d.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION,
+                    java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+            g2d.drawImage(originalImage.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH), 0, 0, null);
+            g2d.dispose();
+
+            // 生成缩略图文件名
+            String extension = getFileExtension(originalFileName);
+            String thumbnailFileName = "thumb_" + IdUtils.fastSimpleUUID() + "." + extension;
+            String datePath = DateUtils.datePath();
+            String thumbnailPath = datePath + "/" + thumbnailFileName;
+
+            // 保存缩略图
+            String uploadDir = RuoYiConfig.getUploadPath();
+            File thumbnailFile = FileUploadUtils.getAbsoluteFile(uploadDir, thumbnailPath);
+            ImageIO.write(thumbnail, extension, thumbnailFile);
+
+            // 返回访问路径
+            return FileUploadUtils.getPathFileName(uploadDir, thumbnailPath);
+        }
+        catch (Exception e)
+        {
+            log.error("生成缩略图失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取文件扩展名
+     */
+    private static String getFileExtension(String fileName)
+    {
+        if (StringUtils.isEmpty(fileName))
+        {
+            return "jpg";
+        }
+        int lastDotIndex = fileName.lastIndexOf('.');
+        if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1)
+        {
+            return fileName.substring(lastDotIndex + 1).toLowerCase();
+        }
+        return "jpg";
+    }
+}

+ 262 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/VideoUtils.java

@@ -0,0 +1,262 @@
+package com.ruoyi.common.utils;
+
+
+
+import com.ruoyi.common.utils.file.FileUploadUtils;
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.bytedeco.javacv.Frame;
+import org.bytedeco.javacv.Java2DFrameConverter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 视频处理工具类(基于JavaCV)
+ *
+ * @author ruoyi
+ */
+public class VideoUtils
+{
+    private static final Logger log = LoggerFactory.getLogger(VideoUtils.class);
+
+    /**
+     * 获取视频信息
+     *
+     * @param videoPath 视频文件路径
+     * @return 视频信息Map,包含duration(时长秒)、width(宽度)、height(高度)
+     */
+    public static Map<String, Object> getVideoInfo(String videoPath)
+    {
+        Map<String, Object> videoInfo = new HashMap<>();
+        FFmpegFrameGrabber grabber = null;
+
+        try
+        {
+            grabber = new FFmpegFrameGrabber(videoPath);
+            grabber.start();
+
+            // 获取视频时长(微秒转换为秒)
+            long durationMicroseconds = grabber.getLengthInTime();
+            if (durationMicroseconds > 0)
+            {
+                long durationSeconds = durationMicroseconds / 1000000;
+                videoInfo.put("durationSeconds", durationSeconds);
+            }
+
+            // 获取视频帧数
+            int frameNumber = grabber.getLengthInFrames();
+
+            // 获取视频宽度和高度
+            int width = grabber.getImageWidth();
+            int height = grabber.getImageHeight();
+
+            if (width > 0 && height > 0)
+            {
+                videoInfo.put("resolution", width + "x" + height);
+                videoInfo.put("width", width);
+                videoInfo.put("height", height);
+            }
+
+            // 获取帧率
+            double frameRate = grabber.getFrameRate();
+            if (frameRate > 0)
+            {
+                videoInfo.put("frameRate", frameRate);
+            }
+        }
+        catch (Exception e)
+        {
+            log.error("获取视频信息失败: {}", videoPath, e);
+        }
+        finally
+        {
+            if (grabber != null)
+            {
+                try
+                {
+                    grabber.stop();
+                    grabber.release();
+                }
+                catch (Exception e)
+                {
+                    log.warn("释放视频资源失败", e);
+                }
+            }
+        }
+
+        return videoInfo;
+    }
+
+    /**
+     * 获取视频时长(秒)
+     *
+     * @param videoPath 视频文件路径
+     * @return 时长(秒),失败返回null
+     */
+    public static Long getVideoDuration(String videoPath)
+    {
+        Map<String, Object> videoInfo = getVideoInfo(videoPath);
+        return (Long) videoInfo.get("durationSeconds");
+    }
+
+    /**
+     * 获取视频分辨率
+     *
+     * @param videoPath 视频文件路径
+     * @return 分辨率(如1920x1080),失败返回null
+     */
+    public static String getVideoResolution(String videoPath)
+    {
+        Map<String, Object> videoInfo = getVideoInfo(videoPath);
+        return (String) videoInfo.get("resolution");
+    }
+
+    /**
+     * 从视频文件生成缩略图
+     *
+     * @param videoPath 视频文件路径
+     * @return 缩略图文件路径,如果生成失败返回null
+     */
+    public static String generateVideoThumbnail(String videoPath)
+    {
+        return generateVideoThumbnail(videoPath, 1);
+    }
+
+    /**
+     * 从视频文件生成缩略图
+     *
+     * @param videoPath 视频文件路径
+     * @param captureTime 截取时间点(秒)
+     * @return 缩略图文件路径,如果生成失败返回null
+     */
+    public static String generateVideoThumbnail(String videoPath, int captureTime)
+    {
+        return generateVideoThumbnail(videoPath, captureTime, 200, 200);
+    }
+
+    /**
+     * 从视频文件生成缩略图(自定义尺寸)
+     *
+     * @param videoPath 视频文件路径
+     * @param captureTime 截取时间点(秒)
+     * @param width 缩略图宽度
+     * @param height 缩略图高度
+     * @return 缩略图文件路径,如果生成失败返回null
+     */
+    public static String generateVideoThumbnail(String videoPath, int captureTime, int width, int height)
+    {
+        FFmpegFrameGrabber grabber = null;
+        Java2DFrameConverter converter = null;
+
+        try
+        {
+            grabber = new FFmpegFrameGrabber(videoPath);
+            grabber.start();
+
+            // 计算目标帧位置
+            int frameNumber = (int) (captureTime * grabber.getFrameRate());
+            if (frameNumber >= grabber.getLengthInFrames())
+            {
+                frameNumber = grabber.getLengthInFrames() - 1;
+            }
+
+            // 跳转到指定帧
+            grabber.setFrameNumber(frameNumber);
+            Frame frame = grabber.grabImage();
+
+            if (frame == null)
+            {
+                log.warn("无法抓取视频帧: {}", videoPath);
+                return null;
+            }
+
+            // 转换Frame为BufferedImage
+            converter = new Java2DFrameConverter();
+            BufferedImage bufferedImage = converter.convert(frame);
+
+            if (bufferedImage == null)
+            {
+                log.warn("转换视频帧失败: {}", videoPath);
+                return null;
+            }
+
+            // 缩放图片
+            BufferedImage thumbnail = resizeImage(bufferedImage, width, height);
+
+            // 生成缩略图文件名
+            String extension = "jpg";
+            String thumbnailFileName = "thumb_" + com.ruoyi.common.utils.uuid.IdUtils.fastSimpleUUID() + "." + extension;
+            String datePath = com.ruoyi.common.utils.DateUtils.datePath();
+            String thumbnailPath = datePath + "/" + thumbnailFileName;
+
+            // 保存缩略图
+            String uploadDir = com.ruoyi.common.config.RuoYiConfig.getUploadPath();
+            File thumbnailFile = FileUploadUtils.getAbsoluteFile(uploadDir, thumbnailPath);
+            ImageIO.write(thumbnail, extension, thumbnailFile);
+
+            // 返回访问路径
+            return FileUploadUtils.getPathFileName(uploadDir, thumbnailPath);
+        }
+        catch (Exception e)
+        {
+            log.error("生成视频缩略图失败: {}", videoPath, e);
+            return null;
+        }
+        finally
+        {
+            if (converter != null)
+            {
+                converter.close();
+            }
+            if (grabber != null)
+            {
+                try
+                {
+                    grabber.stop();
+                    grabber.release();
+                }
+                catch (Exception e)
+                {
+                    log.warn("释放视频资源失败", e);
+                }
+            }
+        }
+    }
+
+    /**
+     * 缩放图片(保持宽高比)
+     *
+     * @param original 原始图片
+     * @param targetWidth 目标宽度
+     * @param targetHeight 目标高度
+     * @return 缩放后的图片
+     */
+    private static BufferedImage resizeImage(BufferedImage original, int targetWidth, int targetHeight)
+    {
+        int originalWidth = original.getWidth();
+        int originalHeight = original.getHeight();
+
+        // 计算缩放比例,保持宽高比
+        double scale = Math.min((double) targetWidth / originalWidth, (double) targetHeight / originalHeight);
+        int scaledWidth = (int) (originalWidth * scale);
+        int scaledHeight = (int) (originalHeight * scale);
+
+        // 创建缩放后的图片
+        BufferedImage resized = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
+        java.awt.Graphics2D g2d = resized.createGraphics();
+
+        // 设置图片平滑渲染
+        g2d.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION,
+                java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+        g2d.drawImage(original.getScaledInstance(scaledWidth, scaledHeight, java.awt.Image.SCALE_SMOOTH),
+                0, 0, null);
+        g2d.dispose();
+
+        return resized;
+    }
+}

+ 29 - 10
ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java

@@ -19,10 +19,11 @@ import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.utils.DateUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.uuid.IdUtils;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 文件处理工具类
- * 
+ *
  * @author ruoyi
  */
 public class FileUtils
@@ -31,7 +32,7 @@ public class FileUtils
 
     /**
      * 输出指定文件的byte数组
-     * 
+     *
      * @param filePath 文件路径
      * @param os 输出流
      * @return
@@ -106,7 +107,7 @@ public class FileUtils
 
     /**
      * 移除路径中的请求前缀片段
-     * 
+     *
      * @param filePath 文件路径
      * @return 移除后的文件路径
      */
@@ -117,7 +118,7 @@ public class FileUtils
 
     /**
      * 删除文件
-     * 
+     *
      * @param filePath 文件
      * @return
      */
@@ -135,7 +136,7 @@ public class FileUtils
 
     /**
      * 文件名称验证
-     * 
+     *
      * @param filename 文件名称
      * @return true 正常 false 非法
      */
@@ -146,7 +147,7 @@ public class FileUtils
 
     /**
      * 检查文件是否可下载
-     * 
+     *
      * @param resource 需要下载的文件
      * @return true 正常 false 非法
      */
@@ -170,7 +171,7 @@ public class FileUtils
 
     /**
      * 下载文件名重新编码
-     * 
+     *
      * @param request 请求对象
      * @param fileName 文件名
      * @return 编码后的文件名
@@ -240,7 +241,7 @@ public class FileUtils
 
     /**
      * 获取图像后缀
-     * 
+     *
      * @param photoByte 图像数据
      * @return 后缀名
      */
@@ -269,7 +270,7 @@ public class FileUtils
 
     /**
      * 获取文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi.png
-     * 
+     *
      * @param fileName 路径名称
      * @return 没有文件路径的名称
      */
@@ -287,7 +288,7 @@ public class FileUtils
 
     /**
      * 获取不带后缀文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi
-     * 
+     *
      * @param fileName 路径名称
      * @return 没有文件路径和后缀的名称
      */
@@ -300,4 +301,22 @@ public class FileUtils
         String baseName = FilenameUtils.getBaseName(fileName);
         return baseName;
     }
+
+
+
+    /**
+     * 获取文件扩展名
+     *
+     * @param file 表单文件
+     * @return 扩展名
+     */
+    public static String getExtension(MultipartFile file)
+    {
+        String extension = FilenameUtils.getExtension(file.getOriginalFilename());
+        if (StringUtils.isEmpty(extension))
+        {
+            extension = MimeTypeUtils.getExtension(file.getContentType());
+        }
+        return extension;
+    }
 }

+ 41 - 26
ruoyi-system/src/main/java/com/ruoyi/base/domain/RobotOpsFaq.java

@@ -8,7 +8,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
 
 /**
  * 问答库管理对象 robot_ops_faq
- * 
+ *
  * @author ruoyi
  * @date 2026-05-07
  */
@@ -39,65 +39,69 @@ public class RobotOpsFaq extends BaseEntity
     @Excel(name = "启用状态:0停用,1启用")
     private String status;
 
+    /** 相似问个数 */
+    @Excel(name = "相似问个数")
+    private Integer similarCount;
+
     /** 问答相似问信息 */
     private List<RobotOpsFaqSimilar> robotOpsFaqSimilarList;
 
-    public void setId(Long id) 
+    public void setId(Long id)
     {
         this.id = id;
     }
 
-    public Long getId() 
+    public Long getId()
     {
         return id;
     }
 
-    public void setCategoryType(String categoryType) 
+    public void setCategoryType(String categoryType)
     {
         this.categoryType = categoryType;
     }
 
-    public String getCategoryType() 
+    public String getCategoryType()
     {
         return categoryType;
     }
 
-    public void setQuestion(String question) 
+    public void setQuestion(String question)
     {
         this.question = question;
     }
 
-    public String getQuestion() 
+    public String getQuestion()
     {
         return question;
     }
 
-    public void setAnswer(String answer) 
+    public void setAnswer(String answer)
     {
         this.answer = answer;
     }
 
-    public String getAnswer() 
+    public String getAnswer()
     {
         return answer;
     }
 
-    public void setSortNo(Long sortNo) 
+    public void setSortNo(Long sortNo)
     {
         this.sortNo = sortNo;
     }
 
-    public Long getSortNo() 
+    public Long getSortNo()
     {
         return sortNo;
     }
 
-    public void setStatus(String status) 
+    public void setStatus(String status)
     {
         this.status = status;
     }
 
-    public String getStatus() 
+    public String getStatus()
     {
         return status;
     }
@@ -107,6 +111,16 @@ public class RobotOpsFaq extends BaseEntity
         return robotOpsFaqSimilarList;
     }
 
+    public void setSimilarCount(Integer similarCount)
+    {
+        this.similarCount = similarCount;
+    }
+
+    public Integer getSimilarCount()
+    {
+        return similarCount;
+    }
+
     public void setRobotOpsFaqSimilarList(List<RobotOpsFaqSimilar> robotOpsFaqSimilarList)
     {
         this.robotOpsFaqSimilarList = robotOpsFaqSimilarList;
@@ -115,18 +129,19 @@ public class RobotOpsFaq extends BaseEntity
     @Override
     public String toString() {
         return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
-            .append("id", getId())
-            .append("categoryType", getCategoryType())
-            .append("question", getQuestion())
-            .append("answer", getAnswer())
-            .append("sortNo", getSortNo())
-            .append("status", getStatus())
-            .append("remark", getRemark())
-            .append("createTime", getCreateTime())
-            .append("updateTime", getUpdateTime())
-            .append("createBy", getCreateBy())
-            .append("updateBy", getUpdateBy())
-            .append("robotOpsFaqSimilarList", getRobotOpsFaqSimilarList())
-            .toString();
+                .append("id", getId())
+                .append("categoryType", getCategoryType())
+                .append("question", getQuestion())
+                .append("answer", getAnswer())
+                .append("sortNo", getSortNo())
+                .append("status", getStatus())
+                .append("similarCount", getSimilarCount())
+                .append("remark", getRemark())
+                .append("createTime", getCreateTime())
+                .append("updateTime", getUpdateTime())
+                .append("createBy", getCreateBy())
+                .append("updateBy", getUpdateBy())
+                .append("robotOpsFaqSimilarList", getRobotOpsFaqSimilarList())
+                .toString();
     }
 }

+ 8 - 4
ruoyi-system/src/main/resources/mapper/base/RobotOpsFaqMapper.xml

@@ -11,6 +11,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="answer"    column="answer"    />
         <result property="sortNo"    column="sort_no"    />
         <result property="status"    column="status"    />
+        <result property="similarCount"    column="similar_count"    />
         <result property="remark"    column="remark"    />
         <result property="createTime"    column="create_time"    />
         <result property="updateTime"    column="update_time"    />
@@ -32,7 +33,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectRobotOpsFaqVo">
-        select id, category_type, question, answer, sort_no, status, remark, create_time, update_time, create_by, update_by from robot_ops_faq
+        select f.id, f.category_type, f.question, f.answer, f.sort_no, f.status, f.remark, f.create_time, f.update_time, f.create_by, f.update_by,
+               (select count(*) from robot_ops_faq_similar s where s.faq_id = f.id) as similar_count
+        from robot_ops_faq f
     </sql>
 
     <select id="selectRobotOpsFaqList" parameterType="RobotOpsFaq" resultMap="RobotOpsFaqResult">
@@ -47,9 +50,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </select>
 
     <select id="selectRobotOpsFaqById" parameterType="Long" resultMap="RobotOpsFaqRobotOpsFaqSimilarResult">
-        select id, category_type, question, answer, sort_no, status, remark, create_time, update_time, create_by, update_by
-        from robot_ops_faq
-        where id = #{id}
+        select f.id, f.category_type, f.question, f.answer, f.sort_no, f.status, f.remark, f.create_time, f.update_time, f.create_by, f.update_by,
+               (select count(*) from robot_ops_faq_similar s where s.faq_id = f.id) as similar_count
+        from robot_ops_faq f
+        where f.id = #{id}
     </select>
 
     <select id="selectRobotOpsFaqSimilarList" resultMap="RobotOpsFaqSimilarResult">

+ 11 - 5
ruoyi-system/src/main/resources/mapper/base/RobotOpsMediaAssetMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.ruoyi.base.mapper.RobotOpsMediaAssetMapper">
-    
+
     <resultMap type="RobotOpsMediaAsset" id="RobotOpsMediaAssetResult">
         <result property="id"    column="id"    />
         <result property="assetName"    column="asset_name"    />
@@ -30,15 +30,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectRobotOpsMediaAssetList" parameterType="RobotOpsMediaAsset" resultMap="RobotOpsMediaAssetResult">
         <include refid="selectRobotOpsMediaAssetVo"/>
-        <where>  
+        <where>
             <if test="assetName != null  and assetName != ''"> and asset_name like concat('%', #{assetName}, '%')</if>
             <if test="assetType != null  and assetType != ''"> and asset_type = #{assetType}</if>
             <if test="mimeType != null  and mimeType != ''"> and mime_type = #{mimeType}</if>
             <if test="status != null  and status != ''"> and status = #{status}</if>
             <if test="quotedFlag != null  and quotedFlag != ''"> and quoted_flag = #{quotedFlag}</if>
+            <if test="params.beginCreateTime != null and params.beginCreateTime != ''">
+                and date_format(create_time,'%Y%m%d') &gt;= date_format(#{params.beginCreateTime},'%Y%m%d')
+            </if>
+            <if test="params.endCreateTime != null and params.endCreateTime != ''">
+                and date_format(create_time,'%Y%m%d') &lt;= date_format(#{params.endCreateTime},'%Y%m%d')
+            </if>
         </where>
     </select>
-    
+
     <select id="selectRobotOpsMediaAssetById" parameterType="Long" resultMap="RobotOpsMediaAssetResult">
         <include refid="selectRobotOpsMediaAssetVo"/>
         where id = #{id}
@@ -112,9 +118,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteRobotOpsMediaAssetByIds" parameterType="String">
-        delete from robot_ops_media_asset where id in 
+        delete from robot_ops_media_asset where id in
         <foreach item="id" collection="array" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>
-</mapper>
+</mapper>