zqy
10 小时以前 7c0603315e57e3765270a8ac6b310b5a32af5a40
新增压缩图 和 视频封面 都返回base64
3个文件已修改
1553 ■■■■ 已修改文件
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java 664 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/config/ElasticSearchConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhang-content/src/main/java/com/ruoyi/service/impl/VideoProcessService.java 888 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java
@@ -5,12 +5,18 @@
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.IIOImage;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.websocket.server.PathParam;
@@ -35,14 +41,20 @@
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.framework.config.ServerConfig;
import springfox.bean.validators.plugins.schema.NotNullAnnotationPlugin;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -97,11 +109,19 @@
    @Value("${thumbnail.keep-aspect-ratio:true}")
    private boolean keepAspectRatio;
    // 支持的图片格式
    private static final String[] IMAGE_FORMATS = {"jpg", "jpeg", "png", "gif", "bmp", "webp"};
    // 支持的视频格式
    private static final String[] VIDEO_FORMATS = {"mp4", "avi", "mov", "wmv", "flv", "mkv", "webm"};
    @Autowired
    private NotNullAnnotationPlugin notNullPlugin;
    @Anonymous
    @GetMapping("/generateThumbnail")
    public AjaxResult generateThumbnail(@PathParam(value = "url") String url) throws Exception {
        return AjaxResult.success( videoProcessService.generateThumbnail(url));
        return AjaxResult.success( );
    }
    //    @GetMapping("/downloadFile")
//    public void fileDownload(@PathParam("path") String path, HttpServletResponse response)
@@ -493,452 +513,99 @@
    // 支持的图片格式
    private static final Set<String> SUPPORTED_IMAGE_FORMATS =
        new HashSet<>(Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff"));    // 缩略图缓存
    private final Map<String, Long> thumbnailCache = new HashMap<>();
    /**
     * 动态生成缩略图 - 主入口
     * 参数说明:
     *   path: 图片路径(必需)
     *   width: 缩略图宽度(可选,默认300)
     *   height: 缩略图高度(可选,默认200)
     *   mode: 生成模式(可选,crop=裁剪,scale=缩放,默认scale)
     *   quality: 图片质量(可选,0-1,默认0.8)
     *   format: 输出格式(可选,jpg/png等,默认jpg)
     * 压缩图片或提取视频封面并返回Base64
     *
     * @param file 图片或视频文件
     * @param width 目标宽度,默认100
     * @param height 目标高度,默认100
     * @param quality 图片质量 0.1-1.0,默认0.8
     * @return Map包含压缩结果和Base64
     */
    @Anonymous
    @GetMapping("/thumbnail")
    public void generateThumbnail(
        @RequestParam("path") String imagePath,
        @RequestParam(value = "width", required = false) Integer width,
        @RequestParam(value = "height", required = false) Integer height,
        @RequestParam(value = "mode", defaultValue = "scale") String mode,
        @RequestParam(value = "quality", required = false) Double quality,
        @RequestParam(value = "format", required = false) String format,
    @PostMapping(value = "/zip", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Map<String, Object> compressMediaToBase64(
        @RequestParam(value = "file", required = false) MultipartFile file,
        @RequestParam(value = "filePath", required = false) String filePath,
        @RequestParam(value = "width", defaultValue = "0") int width,
        @RequestParam(value = "height", defaultValue = "0") int height,
        @RequestParam(value = "quality", defaultValue = "0.8") float quality) {
        Map<String, Object> result = new HashMap<>();
        HttpServletResponse response) {
        File trueFile;
        boolean deleteY = true;
        try {
            // 1. 参数验证和设置默认值
            String decodedPath = URLDecoder.decode(imagePath, "UTF-8").replace("/profile","");
            int targetWidth = width != null ? Math.min(width, maxThumbnailWidth) : defaultThumbnailWidth;
            int targetHeight = height != null ? Math.min(height, maxThumbnailHeight) : defaultThumbnailHeight;
            double targetQuality = quality != null ? Math.max(0.1, Math.min(1.0, quality)) : thumbnailQuality;
            String targetFormat = format != null && SUPPORTED_IMAGE_FORMATS.contains(format.toLowerCase())
                ? format.toLowerCase() : thumbnailFormat;
            // 2. 安全检查
            if (!FileUtils.checkAllowDownload(decodedPath)) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "禁止访问该路径");
                return;
            // 验证文件
            if ((file == null || file.isEmpty()) && filePath == null ) {
                result.put("success", false);
                result.put("message", "文件不能为空");
                return result;
            }
            // 3. 获取原图文件
            String fullPath = RuoYiConfig.getProfile() + decodedPath;
            File originalFile = new File(fullPath);
            if (!originalFile.exists()) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND, "图片不存在: " + decodedPath);
                return;
            if ((file == null || file.isEmpty())){
                deleteY = false;
                String fileUel = RuoYiConfig.getProfile() + filePath.replace("/profile","");
                trueFile = new File(fileUel);
            }else {
                trueFile = videoProcessService.convertToFile(file);
            }
            String fileName = trueFile.getName();
            if (fileName.isEmpty() && filePath == null) {
                result.put("success", false);
                result.put("message", "文件名不能为空");
                return result;
            }
            if (!isImageFile(originalFile)) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "不是图片文件: " + originalFile.getName());
                return;
            }
            System.out.println("-----------"+fileName);
            // 获取文件扩展名
            String extension = getFileExtension(fileName).toLowerCase();
            System.out.println("-----------"+extension);
            // 5. 生成缩略图
            BufferedImage thumbnail = generateThumbnailImage(
                originalFile, targetWidth, targetHeight, mode, targetQuality, targetFormat
            );
            if (thumbnail == null) {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "生成缩略图失败");
                return;
            }
            // 7. 输出缩略图
            sendThumbnailResponse(thumbnail, targetFormat, targetQuality, response);
        } catch (Exception e) {
            log.error("生成缩略图失败: path={}, error={}", imagePath, e.getMessage(), e);
            try {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "生成缩略图失败: " + e.getMessage());
            } catch (IOException ex) {
                log.error("发送错误响应失败", ex);
            }
        }
    }
    /**
     * 批量生成缩略图 - 通过逗号分隔的路径
     * 参数说明:
     *   paths: 用逗号分隔的图片路径列表(必需)
     *   width: 缩略图宽度(可选,默认300)
     *   height: 缩略图高度(可选,默认200)
     *   mode: 生成模式(可选,crop=裁剪,scale=缩放,默认scale)
     *   quality: 图片质量(可选,0-1,默认0.8)
     *   format: 输出格式(可选,jpg/png等,默认jpg)
     */
    @Anonymous
    @PostMapping("/batchThumbnailByPaths")
    public AjaxResult generateBatchThumbnailByPaths(
        @RequestParam("paths") String imagePaths,
        @RequestParam(value = "width", required = false) Integer width,
        @RequestParam(value = "height", required = false) Integer height,
        @RequestParam(value = "mode", defaultValue = "scale") String mode,
        @RequestParam(value = "quality", required = false) Double quality,
        @RequestParam(value = "format", required = false) String format) {
        List<Map<String, Object>> results = new ArrayList<>();
        List<String> thumbnailUrls = new ArrayList<>(); // 存储所有缩略图URL
        try {
            // 1. 分割路径
            String[] pathArray = imagePaths.split(",");
            if (pathArray.length == 0) {
                return AjaxResult.error("图片路径不能为空");
            }
            // 2. 设置压缩参数
            int targetWidth = width != null ? Math.min(width, maxThumbnailWidth) : defaultThumbnailWidth;
            int targetHeight = height != null ? Math.min(height, maxThumbnailHeight) : defaultThumbnailHeight;
            double targetQuality = quality != null ? Math.max(0.1, Math.min(1.0, quality)) : thumbnailQuality;
            String targetFormat = format != null && SUPPORTED_IMAGE_FORMATS.contains(format.toLowerCase())
                ? format.toLowerCase() : thumbnailFormat;
            int successCount = 0;
            int failCount = 0;
            // 3. 处理每个路径
            for (String path : pathArray) {
                Map<String, Object> result = new HashMap<>();
                String trimmedPath = path.trim();
                if (trimmedPath.isEmpty()) {
                    continue;
                }
                result.put("originalPath", trimmedPath);
                try {
                    // 解码路径
                    String decodedPath = URLDecoder.decode(trimmedPath, "UTF-8").replace("/profile","");
                    // 安全检查
                    if (!FileUtils.checkAllowDownload(decodedPath)) {
                        result.put("success", false);
                        result.put("error", "禁止访问该路径");
                        result.put("code", 403);
                        results.add(result);
                        failCount++;
                        continue;
                    }
                    // 获取原图
                    String fullPath = RuoYiConfig.getProfile() + decodedPath;
                    File originalFile = new File(fullPath);
                    if (!originalFile.exists()) {
                        result.put("success", false);
                        result.put("error", "图片不存在");
                        result.put("code", 404);
                        results.add(result);
                        failCount++;
                        continue;
                    }
                    if (!isImageFile(originalFile)) {
                        result.put("success", false);
                        result.put("error", "不是图片文件");
                        result.put("code", 400);
                        results.add(result);
                        failCount++;
                        continue;
                    }
                    // 生成缩略图URL
                    String thumbnailUrl = buildThumbnailUrlWithParams(
                        trimmedPath, targetWidth, targetHeight, mode, targetQuality, targetFormat
                    );
                    result.put("success", true);
                    result.put("thumbnailUrl", thumbnailUrl);
                    result.put("originalUrl", serverConfig.getUrl() + decodedPath);
                    result.put("fileName", originalFile.getName());
                    result.put("code", 200);
                    // 将成功的缩略图URL添加到列表
                    thumbnailUrls.add(thumbnailUrl);
                    // 获取图片信息
                    BufferedImage originalImage = ImageIO.read(originalFile);
                    if (originalImage != null) {
                        result.put("originalWidth", originalImage.getWidth());
                        result.put("originalHeight", originalImage.getHeight());
                    }
                    // 压缩参数
                    Map<String, Object> compressParams = new HashMap<>();
                    compressParams.put("width", targetWidth);
                    compressParams.put("height", targetHeight);
                    compressParams.put("mode", mode);
                    compressParams.put("quality", targetQuality);
                    compressParams.put("format", targetFormat);
                    result.put("compressParams", compressParams);
                    successCount++;
                } catch (Exception e) {
                    log.error("处理缩略图请求失败: {}", trimmedPath, e);
                    result.put("success", false);
                    result.put("error", e.getMessage());
                    result.put("code", 500);
                    failCount++;
                }
                results.add(result);
            }
            // 4. 构建返回结果
            Map<String, Object> responseData = new HashMap<>();
            responseData.put("results", results);
            // 将所有成功的缩略图URL用逗号连接
            if (!thumbnailUrls.isEmpty()) {
                responseData.put("thumbnailUrls", String.join(",", thumbnailUrls));
            // 判断文件类型
            if (isImageFile(extension)) {
                // 处理图片文件
                return videoProcessService.processImage(trueFile, width, height, quality, extension,deleteY);
            } else if (isVideoFile(extension)) {
                // 处理视频文件
                return videoProcessService.processVideo(trueFile,width,height,quality,extension,deleteY);
            } else {
                responseData.put("thumbnailUrls", "");
            }
            // 汇总信息
            Map<String, Object> summary = new HashMap<>();
            summary.put("total", pathArray.length);
            summary.put("success", successCount);
            summary.put("fail", failCount);
            Map<String, Object> compressParams = new HashMap<>();
            compressParams.put("width", targetWidth);
            compressParams.put("height", targetHeight);
            compressParams.put("mode", mode);
            compressParams.put("quality", targetQuality);
            compressParams.put("format", targetFormat);
            summary.put("compressParams", compressParams);
            responseData.put("summary", summary);
            return AjaxResult.success(
                String.format("批量处理完成,成功%s个,失败%s个", successCount, failCount),
                responseData
            );
        } catch (Exception e) {
            log.error("批量生成缩略图失败", e);
            return AjaxResult.error("批量处理失败: " + e.getMessage());
        }
    }
    /**
     * 批量生成缩略图并直接压缩图片 - 返回压缩后的图片信息
     */
    @Anonymous
    @PostMapping("/batchCompressImages")
    public void batchCompressImages(
        @RequestParam("paths") String imagePaths,
        @RequestParam(value = "width", required = false) Integer width,
        @RequestParam(value = "height", required = false) Integer height,
        @RequestParam(value = "mode", defaultValue = "scale") String mode,
        @RequestParam(value = "quality", required = false) Double quality,
        @RequestParam(value = "format", required = false) String format,
        HttpServletResponse response) {
        try {
            // 1. 分割路径
            String[] pathArray = imagePaths.split(",");
            if (pathArray.length == 0) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "图片路径不能为空");
                return;
            }
            // 2. 设置压缩参数
            int targetWidth = width != null ? Math.min(width, maxThumbnailWidth) : defaultThumbnailWidth;
            int targetHeight = height != null ? Math.min(height, maxThumbnailHeight) : defaultThumbnailHeight;
            double targetQuality = quality != null ? Math.max(0.1, Math.min(1.0, quality)) : thumbnailQuality;
            String targetFormat = format != null && SUPPORTED_IMAGE_FORMATS.contains(format.toLowerCase())
                ? format.toLowerCase() : thumbnailFormat;
            // 3. 生成ZIP压缩包
            response.setContentType("application/zip");
            response.setHeader("Content-Disposition", "attachment; filename=\"compressed_images.zip\"");
            try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) {
                int processedCount = 0;
                for (String path : pathArray) {
                    String trimmedPath = path.trim();
                    if (trimmedPath.isEmpty()) {
                        continue;
                    }
                    try {
                        // 解码路径
                        String decodedPath = URLDecoder.decode(trimmedPath, "UTF-8").replace("/profile","");
                        // 安全检查
                        if (!FileUtils.checkAllowDownload(decodedPath)) {
                            log.warn("禁止访问路径: {}", decodedPath);
                            continue;
                        }
                        // 获取原图
                        String fullPath = RuoYiConfig.getProfile() + decodedPath;
                        File originalFile = new File(fullPath);
                        if (!originalFile.exists() || !isImageFile(originalFile)) {
                            log.warn("图片不存在或不是图片文件: {}", decodedPath);
                            continue;
                        }
                        // 生成缩略图
                        BufferedImage thumbnail = generateThumbnailImage(
                            originalFile, targetWidth, targetHeight, mode, targetQuality, targetFormat
                        );
                        if (thumbnail != null) {
                            // 添加到ZIP
                            String fileName = getFileNameWithoutExtension(originalFile.getName()) +
                                "_" + targetWidth + "x" + targetHeight +
                                "." + targetFormat;
                            ZipEntry zipEntry = new ZipEntry(fileName);
                            zipOut.putNextEntry(zipEntry);
                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
                            ImageIO.write(thumbnail, targetFormat, baos);
                            zipOut.write(baos.toByteArray());
                            zipOut.closeEntry();
                            processedCount++;
                        }
                    } catch (Exception e) {
                        log.error("处理图片失败: {}", trimmedPath, e);
                    }
                }
                if (processedCount == 0) {
                    response.reset(); // 清空响应
                    response.setContentType("application/json");
                    response.getWriter().write("{\"code\": 500, \"msg\": \"没有图片处理成功\"}");
                }
            } catch (Exception e) {
                log.error("生成压缩包失败", e);
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "生成压缩包失败");
                result.put("success", false);
                result.put("message", "不支持的文件格式:" + extension);
                return result;
            }
        } catch (Exception e) {
            log.error("批量压缩图片失败", e);
            try {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "处理失败: " + e.getMessage());
            } catch (IOException ex) {
                log.error("发送错误响应失败", ex);
            }
            log.error("文件处理失败", e);
            result.put("success", false);
            result.put("message", "处理失败: " + e.getMessage());
            return result;
        }
    }
    /**
     * 获取图片信息(包含缩略图URL)
     * 判断是否是图片文件
     */
    @Anonymous
    @GetMapping("/imageInfo")
    public AjaxResult getImageInfo(
        @RequestParam("path") String imagePath,
        @RequestParam(value = "width", required = false) Integer width,
        @RequestParam(value = "height", required = false) Integer height) {
        try {
            String decodedPath = URLDecoder.decode(imagePath, "UTF-8");
            // 安全检查
            if (!FileUtils.checkAllowDownload(decodedPath)) {
                return AjaxResult.error("禁止访问该路径");
    private boolean isImageFile(String extension) {
        for (String format : IMAGE_FORMATS) {
            if (format.equalsIgnoreCase(extension)) {
                return true;
            }
            // 获取原图
            String fullPath = RuoYiConfig.getProfile() + decodedPath;
            File originalFile = new File(fullPath);
            if (!originalFile.exists()) {
                return AjaxResult.error("图片不存在");
            }
            if (!isImageFile(originalFile)) {
                return AjaxResult.error("不是图片文件");
            }
            // 读取图片信息
            BufferedImage image = ImageIO.read(originalFile);
            if (image == null) {
                return AjaxResult.error("无法读取图片");
            }
            // 构建返回信息
            Map<String, Object> info = new HashMap<>();
            info.put("originalUrl", serverConfig.getUrl() + decodedPath);
            info.put("originalPath", decodedPath);
            info.put("fileName", originalFile.getName());
            info.put("fileSize", originalFile.length());
            info.put("fileType", getFileExtension(originalFile.getName()));
            info.put("width", image.getWidth());
            info.put("height", image.getHeight());
            info.put("lastModified", originalFile.lastModified());
            // 构建缩略图URL
            int targetWidth = width != null ? width : defaultThumbnailWidth;
            int targetHeight = height != null ? height : defaultThumbnailHeight;
            String thumbnailUrl = buildThumbnailUrl(decodedPath, targetWidth, targetHeight);
            info.put("thumbnailUrl", thumbnailUrl);
            // 不同尺寸的缩略图URL
            info.put("smallThumbnailUrl", buildThumbnailUrl(decodedPath, 150, 100));
            info.put("mediumThumbnailUrl", buildThumbnailUrl(decodedPath, 300, 200));
            info.put("largeThumbnailUrl", buildThumbnailUrl(decodedPath, 600, 400));
            return AjaxResult.success("获取成功", info);
        } catch (Exception e) {
            log.error("获取图片信息失败: {}", imagePath, e);
            return AjaxResult.error("获取失败: " + e.getMessage());
        }
        return false;
    }
    /**
     * 检查是否为图片文件
     * 判断是否是视频文件
     */
    private boolean isImageFile(File file) {
        if (file == null || !file.exists()) {
            return false;
    private boolean isVideoFile(String extension) {
        for (String format : VIDEO_FORMATS) {
            if (format.equalsIgnoreCase(extension)) {
                return true;
            }
        }
        String fileName = file.getName().toLowerCase();
        String extension = getFileExtension(fileName);
        return SUPPORTED_IMAGE_FORMATS.contains(extension);
        return false;
    }
    /**
@@ -957,158 +624,5 @@
    }
    /**
     * 生成缩略图
     */
    private BufferedImage generateThumbnailImage(File originalFile, int width, int height,
                                                 String mode, double quality, String format) {
        try {
            BufferedImage originalImage = ImageIO.read(originalFile);
            if (originalImage == null) {
                return null;
            }
            int originalWidth = originalImage.getWidth();
            int originalHeight = originalImage.getHeight();
            // 计算目标尺寸
            int targetWidth = width;
            int targetHeight = height;
            if (keepAspectRatio && mode.equals("scale")) {
                // 保持宽高比缩放
                double widthRatio = (double) width / originalWidth;
                double heightRatio = (double) height / originalHeight;
                double ratio = Math.min(widthRatio, heightRatio);
                targetWidth = (int) (originalWidth * ratio);
                targetHeight = (int) (originalHeight * ratio);
            } else if (mode.equals("crop")) {
                // 裁剪模式
                double widthRatio = (double) width / originalWidth;
                double heightRatio = (double) height / originalHeight;
                double ratio = Math.max(widthRatio, heightRatio);
                int cropWidth = (int) (width / ratio);
                int cropHeight = (int) (height / ratio);
                // 居中裁剪
                int cropX = (originalWidth - cropWidth) / 2;
                int cropY = (originalHeight - cropHeight) / 2;
                BufferedImage cropped = originalImage.getSubimage(
                    Math.max(0, cropX),
                    Math.max(0, cropY),
                    Math.min(cropWidth, originalWidth - cropX),
                    Math.min(cropHeight, originalHeight - cropY)
                );
                originalImage = cropped;
            }
            // 创建目标图片
            BufferedImage thumbnail = new BufferedImage(targetWidth, targetHeight,
                format.equals("png") ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
            // 绘制缩略图
            Graphics2D g2d = thumbnail.createGraphics();
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
            g2d.dispose();
            return thumbnail;
        } catch (Exception e) {
            log.error("生成缩略图失败: {}", originalFile.getAbsolutePath(), e);
            return null;
        }
    }
    /**
     * 发送缩略图响应
     */
    private void sendThumbnailResponse(BufferedImage thumbnail, String format,
                                       double quality, HttpServletResponse response) throws IOException {
        response.setContentType("image/" + format);
        response.setHeader("Cache-Control", "public, max-age=31536000"); // 缓存1年
        response.setHeader("X-Thumbnail-Cache", "MISS");
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(thumbnail, format, baos);
        byte[] imageBytes = baos.toByteArray();
        response.setContentLength(imageBytes.length);
        response.getOutputStream().write(imageBytes);
    }
    /**
     * 构建缩略图URL
     */
    private String buildThumbnailUrl(String imagePath, int width, int height) {
        try {
            return serverConfig.getUrl() + "/common/thumbnail?" +
                "path=" + URLEncoder.encode(imagePath, "UTF-8") +
                "&width=" + width +
                "&height=" + height;
        } catch (UnsupportedEncodingException e) {
            return "";
        }
    }
    /**
     * 构建带参数的缩略图URL
     */
    private String buildThumbnailUrlWithParams(String imagePath, int width, int height,
                                               String mode, double quality, String format)
        throws UnsupportedEncodingException {
        StringBuilder url = new StringBuilder();
        url.append(serverConfig.getUrl()).append("/common/thumbnail?");
        url.append("path=").append(URLEncoder.encode(imagePath, "UTF-8"));
        url.append("&width=").append(width);
        url.append("&height=").append(height);
        url.append("&mode=").append(mode);
        url.append("&quality=").append(quality);
        url.append("&format=").append(format);
        return url.toString();
    }
    /**
     * 获取不带扩展名的文件名
     */
    private String getFileNameWithoutExtension(String fileName) {
        if (fileName == null) {
            return "";
        }
        int lastDot = fileName.lastIndexOf('.');
        if (lastDot > 0) {
            return fileName.substring(0, lastDot);
        }
        return fileName;
    }
    /**
     * 缩略图请求参数类
     */
    @Data
    static class ThumbnailRequest {
        private String path;      // 图片路径
        private Integer width;    // 宽度
        private Integer height;   // 高度
        private String mode;      // 模式:scale/crop
        private Double quality;   // 质量:0-1
        private String format;    // 格式
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/config/ElasticSearchConfig.java
@@ -36,7 +36,6 @@
                        "localhost",
                        8087,
//
                    "http"
                )
zhang-content/src/main/java/com/ruoyi/service/impl/VideoProcessService.java
@@ -1,6 +1,7 @@
package com.ruoyi.service.impl;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
@@ -8,13 +9,20 @@
import org.springframework.web.multipart.MultipartFile;
import ws.schild.jave.*;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
@@ -22,9 +30,6 @@
    @Value("${video.upload-dir:./uploads/videos}")
    private String uploadDir;
    @Value("${video.thumbnail-dir:/profile/thumbnails}")
    private String thumbnailDir;
    @Value("${video.thumbnail.width:320}")
    private int thumbnailWidth;
@@ -38,64 +43,575 @@
    @Value("${video.thumbnail.format:jpg}")
    private String thumbnailFormat;
    @Value("${video.thumbnail.count:1}")
    private int thumbnailCount; // 生成封面数量
    // 支持的视频格式
    private static final Set<String> SUPPORTED_VIDEO_FORMATS =
        new HashSet<>(Arrays.asList("mp4", "avi", "mov", "mkv", "flv", "wmv", "webm", "mpeg", "mpg", "3gp", "m4v"));
    public File convertToFile(MultipartFile multipartFile) throws IOException {
        String originalFilename = multipartFile.getOriginalFilename();
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        File tempFile = File.createTempFile("video_", extension);
        multipartFile.transferTo(tempFile);
        tempFile.deleteOnExit();
        return tempFile;
    }
    /**
     * 处理视频文件:只生成封面
     * 处理图片文件
     */
    public VideoProcessResult processVideo(MultipartFile file) {
        VideoProcessResult result = new VideoProcessResult();
    public Map<String, Object> processImage(File file, int width, int height,
                                            float quality, String format,boolean deleteY) throws IOException {
        Map<String, Object> result = new HashMap<>();
        // 读取图片
        BufferedImage image = ImageIO.read(file);
        if (image == null) {
            result.put("success", false);
            result.put("message", "无法读取图片文件");
            return result;
        }
        int originalWidth = image.getWidth();
        int originalHeight = image.getHeight();
        // 调整尺寸
        if (width > 0 && height > 0) {
            image = resizeImage(image, width, height, true);
        }
        // 压缩并转换为Base64
        byte[] compressedBytes = compressImage(image, quality, format);
        String base64 = Base64.getEncoder().encodeToString(compressedBytes);
        String dataUrl = "data:image/" + format + ";base64," + base64;
        // 返回结果
        result.put("success", true);
        result.put("type", "image");
        result.put("format", format);
        result.put("compressedSize", compressedBytes.length);
        result.put("compressedWidth", width > 0 ? width : originalWidth);
        result.put("compressedHeight", height > 0 ? height : originalHeight);
        result.put("base64", dataUrl);
        result.put("message", "图片压缩成功");
        if (deleteY) {
            file.delete();
            result.put("originalDeleted", false);
            result.put("message", "图片压缩成功");
        }
        return result;
    }
    /**
     * 调整图片大小
     */
    private BufferedImage resizeImage(BufferedImage originalImage, int targetWidth,
                                      int targetHeight, boolean keepAspectRatio) {
        int originalWidth = originalImage.getWidth();
        int originalHeight = originalImage.getHeight();
        // 如果宽高都为0,则不调整
        if (targetWidth <= 0 && targetHeight <= 0) {
            return originalImage;
        }
        int newWidth, newHeight;
        if (keepAspectRatio) {
            // 保持宽高比
            if (targetWidth <= 0) {
                // 只给高度
                double scale = (double) targetHeight / originalHeight;
                newWidth = (int) (originalWidth * scale);
                newHeight = targetHeight;
            } else if (targetHeight <= 0) {
                // 只给宽度
                double scale = (double) targetWidth / originalWidth;
                newWidth = targetWidth;
                newHeight = (int) (originalHeight * scale);
            } else {
                // 两个维度都给了,保持宽高比缩放
                double widthRatio = (double) targetWidth / originalWidth;
                double heightRatio = (double) targetHeight / originalHeight;
                double scale = Math.min(widthRatio, heightRatio);
                newWidth = (int) (originalWidth * scale);
                newHeight = (int) (originalHeight * scale);
            }
        } else {
            // 不保持宽高比
            newWidth = targetWidth > 0 ? targetWidth : originalWidth;
            newHeight = targetHeight > 0 ? targetHeight : originalHeight;
        }
        // 确保最小为1
        newWidth = Math.max(1, newWidth);
        newHeight = Math.max(1, newHeight);
        BufferedImage resizedImage = new BufferedImage(newWidth, newHeight,
            originalImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : originalImage.getType());
        Graphics2D g = resizedImage.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.setRenderingHint(RenderingHints.KEY_RENDERING,
            RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
        g.dispose();
        return resizedImage;
    }
    /**
     * 压缩图片
     */
    private byte[] compressImage(BufferedImage image, float quality, String format) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 根据格式选择压缩方式
        if ("jpg".equalsIgnoreCase(format) || "jpeg".equalsIgnoreCase(format)) {
            compressAsJpeg(image, baos, quality);
        } else if ("png".equalsIgnoreCase(format)) {
            compressAsPng(image, baos, 6); // PNG压缩级别6
        } else {
            // 其他格式使用默认方式
            ImageIO.write(image, format, baos);
        }
        return baos.toByteArray();
    }
    /**
     * 压缩为JPEG格式
     */
    private void compressAsJpeg(BufferedImage image, ByteArrayOutputStream output, float quality)
        throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpeg");
        if (!writers.hasNext()) {
            ImageIO.write(image, "jpg", output);
            return;
        }
        ImageWriter writer = writers.next();
        ImageWriteParam param = writer.getDefaultWriteParam();
        // 设置压缩质量
        if (quality > 0) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionQuality(Math.max(0.1f, Math.min(1.0f, quality)));
        }
        writer.setOutput(new MemoryCacheImageOutputStream(output));
        writer.write(null, new IIOImage(image, null, null), param);
        writer.dispose();
    }
    /**
     * 压缩为PNG格式
     */
    private void compressAsPng(BufferedImage image, ByteArrayOutputStream output, int compressionLevel)
        throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
        if (!writers.hasNext()) {
            ImageIO.write(image, "png", output);
            return;
        }
        ImageWriter writer = writers.next();
        ImageWriteParam param = writer.getDefaultWriteParam();
        // PNG的压缩级别(0-9,0最快但压缩率低,9最慢但压缩率高)
        if (compressionLevel >= 0) {
            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            param.setCompressionType("Deflate");
            param.setCompressionQuality(Math.max(0.0f, Math.min(1.0f, compressionLevel / 9.0f)));
        }
        writer.setOutput(new MemoryCacheImageOutputStream(output));
        writer.write(null, new IIOImage(image, null, null), param);
        writer.dispose();
    }
    /**
     * 处理视频文件:生成Base64编码的封面
     */
    public Map<String, Object> processVideo(File file, int width, int height,
                                            float quality, String format,boolean deleteY) {
        Map<String, Object> result = new HashMap<>();
        File tempVideoFile = file;
        System.out.println();
        try {
            // 1. 验证视频格式
            if (!isVideoFile(file)) {
                result.setSuccess(false);
                result.setMessage("不支持的视频格式");
                result.put("success", false);
                result.put("message", "不支持的视频格式");
                return result;
            }
            // 2. 生成唯一文件名
            String originalFilename = file.getOriginalFilename();
            String fileExtension = getFileExtension(originalFilename);
            String uuid = UUID.randomUUID().toString().replace("-", "");
            String fileName = uuid + fileExtension;
            // 2. 保存原始文件到临时目录
//            String originalFilename = file.getName();
//            tempVideoFile = File.createTempFile("video_", ".temp");
            // 3. 创建目录
            createDirectories();
            // 3. 获取视频信息
//            MultimediaInfo info = getVideoInfo(tempVideoFile);
//            double durationSeconds = info.getDuration() / 1000.0;
//            String videoResolution = info.getVideo().getSize().getWidth() + "x" + info.getVideo().getSize().getHeight();
            // 4. 保存原始文件
            Path originalPath = Paths.get(uploadDir, fileName);
            file.transferTo(originalPath.toFile());
            // 4. 生成Base64格式的封面
            String base64Thumbnail = generateBase64ThumbnailFromFile(
                tempVideoFile,
                width > 0 ? width : thumbnailWidth,
                height > 0 ? height : thumbnailHeight,
                (int) quality,
                format != null ? format : thumbnailFormat
            );
            // 5. 获取视频信息
            MultimediaInfo info = getVideoInfo(originalPath.toFile());
            if (base64Thumbnail == null || base64Thumbnail.isEmpty()) {
                result.put("success", false);
                result.put("message", "封面生成失败");
                return result;
            }
            // 设置结果
            result.setSuccess(true);
            result.setMessage("封面生成成功");
            result.setOriginalPath(originalPath.toString());
            result.setOriginalFilename(originalFilename);
            result.setFileSize(file.getSize());
            result.setDuration(info.getDuration() / 1000.0); // 转换为秒
            result.setResolution(info.getVideo().getSize().getWidth() + "x" + info.getVideo().getSize().getHeight());
            result.setVideoFormat(info.getFormat());
            // 5. 获取缩略图大小
            String thumbnailSize = extractImageSizeFromBase64(base64Thumbnail);
            String[] sizeParts = thumbnailSize.split("x");
            int thumbWidth = sizeParts.length > 0 ? Integer.parseInt(sizeParts[0]) : 0;
            int thumbHeight = sizeParts.length > 1 ? Integer.parseInt(sizeParts[1]) : 0;
            // 6. 生成封面
            String thumbnailPath = generateThumbnail(originalPath.toFile().getPath());
            result.setThumbnailPath(thumbnailPath);
            // 6. 返回结果
            result.put("success", true);
            result.put("type", "video");
            result.put("format", format != null ? format : thumbnailFormat);
//            result.put("originalFilename", originalFilename);
//            result.put("originalWidth", info.getVideo().getSize().getWidth());
//            result.put("originalHeight", info.getVideo().getSize().getHeight());
//            result.put("originalResolution", videoResolution);
//            result.put("duration", durationSeconds);
//            result.put("videoFormat", info.getFormat());
//            result.put("compressedWidth", thumbWidth);
//            result.put("compressedHeight", thumbHeight);
            result.put("base64", base64Thumbnail);
            result.put("message", "视频封面生成成功");
        } catch (Exception e) {
            log.error("视频封面生成失败", e);
            result.setSuccess(false);
            result.setMessage("封面生成失败: " + e.getMessage());
            result.put("success", false);
            result.put("message", "封面生成失败: " + e.getMessage());
        } finally {
            // 清理临时文件
            if (tempVideoFile != null && tempVideoFile.exists() && deleteY) {
                try {
                    tempVideoFile.delete();
                } catch (Exception e) {
                    log.warn("删除临时视频文件时出错", e);
                }
            }
        }
        return result;
    }
    /**
     * 生成Base64格式的封面(可自定义参数)
     */
    private String generateBase64ThumbnailFromFile(File videoFile, int width, int height,
                                                   int quality, String format) {
        File tempThumbnailFile = null;
        Process process = null;
        try {
            if (!videoFile.exists()) {
                log.error("视频文件不存在: {}", videoFile.getAbsolutePath());
                return null;
            }
            // 1. 创建临时文件用于保存缩略图
            String ext = format != null ? format : thumbnailFormat;
            tempThumbnailFile = File.createTempFile("thumb_", "." + ext);
            tempThumbnailFile.deleteOnExit();
            // 2. 获取视频时长,计算截图时间点
            MultimediaInfo info = getVideoInfo(videoFile);
            double durationSeconds = info.getDuration() / 1000.0;
            double screenshotTime = calculateScreenshotTime(durationSeconds);
            // 3. 构建FFmpeg命令
            String ffmpegCmd = String.format(
                "ffmpeg -y -i \"%s\" -ss %.2f -vframes 1 -q:v %d -s %dx%d -f image2 \"%s\"",
                videoFile.getAbsolutePath(),
                screenshotTime,
                quality,
                width,
                height,
                tempThumbnailFile.getAbsolutePath()
            );
            // 4. 执行FFmpeg命令
            ProcessBuilder processBuilder = new ProcessBuilder();
            String[] command = isWindows() ?
                new String[]{"cmd.exe", "/c", ffmpegCmd} :
                new String[]{"bash", "-c", ffmpegCmd};
            processBuilder.command(command);
            processBuilder.redirectErrorStream(true);
            process = processBuilder.start();
            // 5. 等待命令完成
            boolean finished = process.waitFor(30, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                return null;
            }
            int exitCode = process.waitFor();
            if (exitCode != 0 || !tempThumbnailFile.exists() || tempThumbnailFile.length() == 0) {
                return null;
            }
            // 6. 读取图片文件并转换为Base64
            byte[] fileContent = Files.readAllBytes(tempThumbnailFile.toPath());
            if (!isValidImage(fileContent)) {
                return null;
            }
            // 7. 转换为Base64
            String base64 = Base64.getEncoder().encodeToString(fileContent);
            String mimeType = getMimeType(ext);
            return "data:" + mimeType + ";base64," + base64;
        } catch (Exception e) {
            log.error("生成Base64封面失败", e);
            return null;
        } finally {
            // 清理资源
            if (process != null && process.isAlive()) {
                process.destroy();
            }
            if (tempThumbnailFile != null && tempThumbnailFile.exists()) {
                try {
                    tempThumbnailFile.delete();
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    }
    /**
     * 生成Base64格式的封面(不存储文件)
     */
    public String generateBase64ThumbnailFromFile(String videoPath) {
        File videoFile = new File(videoPath);
        if (!videoFile.exists()) {
            log.error("视频文件不存在: {}", videoPath);
            return null;
        }
        return generateBase64ThumbnailFromFile(videoFile);
    }
    /**
     * 生成Base64格式的封面(不存储文件)
     */
    public String generateBase64ThumbnailFromFile(File videoFile) {
        File tempThumbnailFile = null;
        Process process = null;
        try {
            if (!videoFile.exists()) {
                log.error("视频文件不存在: {}", videoFile.getAbsolutePath());
                return null;
            }
            // 1. 创建临时文件用于保存缩略图
            tempThumbnailFile = File.createTempFile("thumb_", "." + thumbnailFormat);
            tempThumbnailFile.deleteOnExit(); // 确保文件会被删除
            // 2. 获取视频时长,计算截图时间点
            MultimediaInfo info = getVideoInfo(videoFile);
            double durationSeconds = info.getDuration() / 1000.0;
            double screenshotTime = calculateScreenshotTime(durationSeconds);
            log.debug("视频截图信息 - 文件名: {}, 时长: {}s, 截图时间点: {}s, 目标尺寸: {}x{}, 质量: {}",
                videoFile.getName(), durationSeconds, screenshotTime,
                thumbnailWidth, thumbnailHeight, thumbnailQuality);
            // 3. 构建FFmpeg命令
            String ffmpegCmd = String.format(
                "ffmpeg -y -i \"%s\" -ss %.2f -vframes 1 -q:v %d -s %dx%d -f image2 \"%s\"",
                videoFile.getAbsolutePath(),  // 输入文件
                screenshotTime,                // 跳转到的时间点(秒)
                thumbnailQuality,              // 输出质量 (2-31,值越小质量越高)
                thumbnailWidth,                // 输出宽度
                thumbnailHeight,               // 输出高度
                tempThumbnailFile.getAbsolutePath() // 输出文件
            );
            log.debug("执行FFmpeg命令: {}", ffmpegCmd);
            // 4. 执行FFmpeg命令
            ProcessBuilder processBuilder = new ProcessBuilder();
            String[] command = isWindows() ?
                new String[]{"cmd.exe", "/c", ffmpegCmd} :
                new String[]{"bash", "-c", ffmpegCmd};
            processBuilder.command(command);
            processBuilder.redirectErrorStream(true);
            process = processBuilder.start();
            // 5. 读取输出(用于调试)
            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");
                }
            }
            // 6. 等待命令完成(带超时)
            boolean finished = process.waitFor(30, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                log.error("FFmpeg命令执行超时,视频文件: {}", videoFile.getName());
                return null;
            }
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                log.error("FFmpeg命令执行失败,退出码: {}, 视频文件: {}, 输出: {}",
                    exitCode, videoFile.getName(), output.toString());
                return null;
            }
            // 7. 检查生成的图片文件
            if (!tempThumbnailFile.exists() || tempThumbnailFile.length() == 0) {
                log.error("生成的缩略图为空或不存在,视频文件: {}", videoFile.getName());
                return null;
            }
            // 8. 读取图片文件并转换为Base64
            byte[] fileContent = Files.readAllBytes(tempThumbnailFile.toPath());
            // 9. 验证图片有效性
            if (!isValidImage(fileContent)) {
                log.error("生成的缩略图不是有效的图片,视频文件: {}", videoFile.getName());
                return null;
            }
            // 10. 转换为Base64
            String base64 = Base64.getEncoder().encodeToString(fileContent);
            String mimeType = getMimeType(thumbnailFormat);
            return "data:" + mimeType + ";base64," + base64;
        } catch (Exception e) {
            log.error("生成Base64封面失败,视频文件: {}", videoFile.getName(), e);
            return null;
        } finally {
            // 清理资源
            if (process != null && process.isAlive()) {
                process.destroy();
            }
            // 删除临时文件
            if (tempThumbnailFile != null && tempThumbnailFile.exists()) {
                try {
                    boolean deleted = tempThumbnailFile.delete();
                    if (deleted) {
                        log.debug("已删除临时缩略图文件: {}", tempThumbnailFile.getAbsolutePath());
                    } else {
                        log.warn("无法删除临时缩略图文件: {}", tempThumbnailFile.getAbsolutePath());
                    }
                } catch (Exception e) {
                    log.warn("删除临时缩略图文件时出错", e);
                }
            }
        }
    }
    /**
     * 从Base64字符串中提取图片尺寸
     */
    private String extractImageSizeFromBase64(String base64) {
        try {
            // Base64字符串格式: data:image/jpeg;base64,/9j/4AAQSkZJRgABA...
            String base64Data = base64.split(",")[1];
            byte[] imageBytes = Base64.getDecoder().decode(base64Data);
            try (ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes)) {
                BufferedImage image = ImageIO.read(bis);
                if (image != null) {
                    return image.getWidth() + "x" + image.getHeight();
                }
            }
        } catch (Exception e) {
            log.warn("无法从Base64中提取图片尺寸", e);
        }
        return thumbnailWidth + "x" + thumbnailHeight; // 返回目标尺寸
    }
    /**
     * 计算截图时间点
     */
    private double calculateScreenshotTime(double durationSeconds) {
        if (durationSeconds <= 0) {
            return 0;
        }
        // 如果视频长度小于1秒,在中间截图
        if (durationSeconds < 1) {
            return durationSeconds / 2;
        }
        // 如果视频长度小于3秒,在第1秒截图
        if (durationSeconds < 3) {
            return 1.0;
        }
        // 视频较长时,在10%的位置截图(但不超过30秒)
        double time = durationSeconds * 0.1;
        return Math.min(time, 30.0);
    }
    /**
     * 获取视频信息
     */
    private MultimediaInfo getVideoInfo(File videoFile) throws Exception {
        try {
            MultimediaObject multimediaObject = new MultimediaObject(videoFile);
            return multimediaObject.getInfo();
        } catch (Exception e) {
            log.error("获取视频信息失败: {}", videoFile.getAbsolutePath(), e);
            throw new Exception("无法获取视频信息: " + e.getMessage());
        }
    }
    /**
@@ -129,194 +645,46 @@
    }
    /**
     * 生成封面(缩略图)
     * 批量生成Base64格式的缩略图
     */
    public String generateThumbnail(String videoPath) throws Exception {
        File videoFile = new File(RuoYiConfig.getProfile() + videoPath.replace("/profile", ""));
        if (!videoFile.exists()) {
            throw new FileNotFoundException("视频文件不存在: " + videoPath);
        }
        // 生成唯一的缩略图文件名(与视频同目录)
        String originalName = videoFile.getName();
        String baseName = getFileNameWithoutExtension(originalName);
        String thumbnailName = "thumb_" + baseName + "_" +
            UUID.randomUUID().toString().substring(0, 8) +
            "." + thumbnailFormat;
        // *** 关键修改:缩略图生成在视频文件所在目录 ***
        // 获取视频文件所在的目录
        File videoDir = videoFile.getParentFile();
        if (videoDir == null) {
            throw new RuntimeException("无法获取视频文件所在目录: " + videoFile.getAbsolutePath());
        }
        // 确保视频目录存在
        if (!videoDir.exists()) {
            videoDir.mkdirs();
        }
        // 创建缩略图文件对象(在视频同目录)
        File thumbnailFile = new File(videoDir, thumbnailName);
        System.out.println("------------------------缩略图文件名: " + thumbnailName);
        System.out.println("------------------------缩略图完整路径: " + thumbnailFile.getAbsolutePath());
        System.out.println("------------------------视频文件目录: " + videoDir.getAbsolutePath());
        // 1. 获取视频时长,计算截图时间点
        MultimediaInfo info = getVideoInfo(videoFile);
        double durationSeconds = info.getDuration() / 1000.0;
        double screenshotTime = calculateScreenshotTime(durationSeconds);
        System.out.println("------------------------截图时间点: " + screenshotTime);
        // 2. 构建并执行 FFmpeg 命令
        String ffmpegCmd = String.format(
            "ffmpeg -i \"%s\" -ss %.2f -vframes 1 -q:v %d -s %dx%d \"%s\"",
            videoFile.getAbsolutePath(),  // 输入文件
            screenshotTime,               // 跳转到的时间点(秒)
            thumbnailQuality,             // 输出质量 (2-31,值越小质量越高)
            thumbnailWidth,               // 输出宽度
            thumbnailHeight,              // 输出高度
            thumbnailFile.getAbsolutePath() // 输出文件(与视频同目录)
        );
        Process process = Runtime.getRuntime().exec(ffmpegCmd);
        // 3. 读取并打印命令输出(用于调试)
        try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
             BufferedReader inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = errorReader.readLine()) != null) {
                System.out.println("[FFmpeg Error] " + line);
            }
            while ((line = inputReader.readLine()) != null) {
                System.out.println("[FFmpeg Output] " + line);
            }
        }
        int exitCode = process.waitFor();
        System.out.println("------------------------FFmpeg 退出码: " + exitCode);
        if (exitCode != 0) {
            throw new RuntimeException("FFmpeg 截图失败,退出码: " + exitCode);
        }
        if (!thumbnailFile.exists() || thumbnailFile.length() == 0) {
            throw new RuntimeException("生成的缩略图为空或不存在");
        }
        // 获取缩略图的绝对路径
        String absolutePath = thumbnailFile.getAbsolutePath();
        // 将绝对路径转换为相对profilePath的相对路径
        String relativePath = absolutePath.substring(RuoYiConfig.getProfile().length()).replace("\\", "/");
        // 确保相对路径以 / 开头
        if (!relativePath.startsWith("/")) {
            relativePath = "/" + relativePath;
        }
        System.out.println("------------------------生成的相对路径: " + relativePath);
        return "/profile"+relativePath;
    }
    /**
     * 计算截图时间点
     */
    private double calculateScreenshotTime(double durationSeconds) {
        // 1. 尝试在第1秒
        // 2. 如果视频不足1秒,在中间截图
        // 3. 如果视频很短(<0.5秒),在开头截图
        double screenshotTime = 1.0; // 默认第1秒
        if (durationSeconds < 1.0) {
            screenshotTime = durationSeconds / 2; // 中间
        }
        if (durationSeconds < 0.5) {
            screenshotTime = 0; // 开头
        }
        return screenshotTime;
    }
    /**
     * 获取视频信息
     */
    private MultimediaInfo getVideoInfo(File videoFile) throws Exception {
        MultimediaObject multimediaObject = new MultimediaObject(videoFile);
        return multimediaObject.getInfo();
    }
    /**
     * 批量生成缩略图
     */
    public BatchThumbnailResult batchGenerateThumbnails(List<MultipartFile> files) {
        BatchThumbnailResult result = new BatchThumbnailResult();
        List<VideoProcessResult> results = new ArrayList<>();
        int successCount = 0;
        int failCount = 0;
        for (MultipartFile file : files) {
            try {
                VideoProcessResult singleResult = processVideo(file);
                results.add(singleResult);
                if (singleResult.isSuccess()) {
                    successCount++;
                } else {
                    failCount++;
                }
            } catch (Exception e) {
                log.error("处理视频失败: {}", file.getOriginalFilename(), e);
                failCount++;
            }
        }
        result.setResults(results);
        result.setTotal(files.size());
        result.setSuccess(successCount);
        result.setFail(failCount);
        return result;
    }
    /**
     * 从视频目录批量生成缩略图
     */
    public void generateThumbnailsFromDirectory(File videoDir) throws Exception {
        if (!videoDir.exists() || !videoDir.isDirectory()) {
            throw new IllegalArgumentException("视频目录不存在");
        }
        File[] videoFiles = videoDir.listFiles((dir, name) -> isVideoFile(new File(dir, name)));
        if (videoFiles == null || videoFiles.length == 0) {
            log.warn("视频目录中没有找到支持的视频文件");
            return;
        }
        log.info("开始批量生成缩略图,共{}个视频", videoFiles.length);
        int successCount = 0;
        int failCount = 0;
        for (File videoFile : videoFiles) {
            try {
                generateThumbnail(videoFile.getPath());
                successCount++;
                log.info("已生成缩略图: {}", videoFile.getName());
            } catch (Exception e) {
                failCount++;
                log.error("生成缩略图失败: {}", videoFile.getName(), e);
            }
        }
        log.info("批量生成缩略图完成,成功{}个,失败{}个", successCount, failCount);
    }
//    public BatchThumbnailResult batchGenerateThumbnails(List<MultipartFile> files) {
//        BatchThumbnailResult result = new BatchThumbnailResult();
//        List<VideoProcessResult> results = new ArrayList<>();
//
//        int successCount = 0;
//        int failCount = 0;
//
//        for (MultipartFile file : files) {
//            try {
//                VideoProcessResult singleResult = processVideo(file);
//                results.add(singleResult);
//
//                if (singleResult.isSuccess()) {
//                    successCount++;
//                } else {
//                    failCount++;
//                }
//
//            } catch (Exception e) {
//                log.error("处理视频失败: {}", file.getOriginalFilename(), e);
//
//                VideoProcessResult errorResult = new VideoProcessResult();
//                errorResult.setSuccess(false);
//                errorResult.setMessage("处理失败: " + e.getMessage());
//                errorResult.setOriginalFilename(file.getOriginalFilename());
//                results.add(errorResult);
//
//                failCount++;
//            }
//        }
//
//        result.setResults(results);
//        result.setTotal(files.size());
//        result.setSuccess(successCount);
//        result.setFail(failCount);
//
//        return result;
//    }
    /**
     * 获取视频时长
@@ -361,7 +729,7 @@
        if (fileName == null) return "";
        int lastDot = fileName.lastIndexOf('.');
        if (lastDot > 0 && lastDot < fileName.length() - 1) {
            return fileName.substring(lastDot + 1);
            return fileName.substring(lastDot + 1).toLowerCase();
        }
        return "";
    }
@@ -379,63 +747,58 @@
    }
    /**
     * 根据图片格式获取编码器
     * 验证图片是否有效
     */
    private String getCodecForFormat(String format) {
    private boolean isValidImage(byte[] imageData) {
        try (ByteArrayInputStream bis = new ByteArrayInputStream(imageData)) {
            BufferedImage image = ImageIO.read(bis);
            return image != null;
        } catch (Exception e) {
            return false;
        }
    }
    /**
     * 获取MIME类型
     */
    private String getMimeType(String format) {
        switch (format.toLowerCase()) {
            case "jpg":
            case "jpeg":
                return "mjpeg";
                return "image/jpeg";
            case "png":
                return "png";
                return "image/png";
            case "gif":
                return "gif";
                return "image/gif";
            case "bmp":
                return "bmp";
                return "image/bmp";
            case "webp":
                return "image/webp";
            default:
                return "png";
                return "image/jpeg";
        }
    }
    /**
     * 根据扩展名获取格式
     * 判断操作系统是否是Windows
     */
    private String getFormatForExtension(String extension) {
        switch (extension.toLowerCase()) {
            case "jpg":
            case "jpeg":
                return "image2";
            case "png":
                return "image2";
            case "gif":
                return "gif";
            case "bmp":
                return "bmp";
            default:
                return "image2";
        }
    }
    /**
     * 创建目录
     */
    private void createDirectories() throws IOException {
        Files.createDirectories(Paths.get(uploadDir));
        Files.createDirectories(Paths.get(thumbnailDir));
    private boolean isWindows() {
        String os = System.getProperty("os.name").toLowerCase();
        return os.contains("win");
    }
    @Data
    public static class VideoProcessResult {
        private boolean success;
        private String message;
        private String originalPath;
        private String originalFilename;
        private String thumbnailPath;
        private List<String> thumbnailPaths; // 多个封面路径
        private String thumbnailBase64;  // Base64编码的封面
        private String mimeType;         // 封面MIME类型
        private long fileSize;          // 原始文件大小
        private double duration;        // 视频时长(秒)
        private String resolution;      // 视频分辨率
        private String videoFormat;     // 视频格式
        private String thumbnailSize;   // 缩略图尺寸
        public String getFileSizeFormatted() {
            return formatFileSize(fileSize);
@@ -511,4 +874,3 @@
        }
    }
}