zqy
1 天以前 e1dc6930a9d217da8d87e2838208eb0e7eca2a2a
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java
@@ -7,8 +7,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -20,6 +19,7 @@
import com.ruoyi.common.utils.RenamedMultipartFile;
import com.ruoyi.common.utils.uuid.UUID;
import com.ruoyi.service.DownLoadFileService;
import com.ruoyi.service.impl.VideoProcessService;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,9 +36,20 @@
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.framework.config.ServerConfig;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
 * 通用请求处理
 *
 *
 * @author ruoyi
 */
@RestController
@@ -52,12 +63,47 @@
    @Autowired
    private DownLoadFileService downLoadFileService;
    @Autowired
    private VideoProcessService videoProcessService;
    private static final String FILE_DELIMETER = ",";
    private static final Pattern CHINESE_PATTERN = Pattern.compile("[\u4e00-\u9fa5]");
    // 缩略图配置
    @Value("${thumbnail.default-width:300}")
    private int defaultThumbnailWidth;
//    @GetMapping("/downloadFile")
    @Value("${thumbnail.default-height:200}")
    private int defaultThumbnailHeight;
    @Value("${thumbnail.quality:0.8}")
    private double thumbnailQuality;
    @Value("${thumbnail.cache-dir:./cache/thumbnails}")
    private String thumbnailCacheDir;
    @Value("${thumbnail.max-width:1920}")
    private int maxThumbnailWidth;
    @Value("${thumbnail.max-height:1080}")
    private int maxThumbnailHeight;
    @Value("${thumbnail.format:jpg}")
    private String thumbnailFormat;
    @Value("${thumbnail.keep-aspect-ratio:true}")
    private boolean keepAspectRatio;
    @Anonymous
    @GetMapping("/generateThumbnail")
    public AjaxResult generateThumbnail(@PathParam(value = "url") String url) throws Exception {
        return AjaxResult.success( videoProcessService.generateThumbnail(url));
    }
    //    @GetMapping("/downloadFile")
//    public void fileDownload(@PathParam("path") String path, HttpServletResponse response)
//    {
//        path=path.substring(8);
@@ -444,4 +490,625 @@
        }
    }
}
    // 支持的图片格式
    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)
     */
    @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,
        HttpServletResponse response) {
        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;
            }
            // 3. 获取原图文件
            String fullPath = RuoYiConfig.getProfile() + decodedPath;
            File originalFile = new File(fullPath);
            if (!originalFile.exists()) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND, "图片不存在: " + decodedPath);
                return;
            }
            if (!isImageFile(originalFile)) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "不是图片文件: " + originalFile.getName());
                return;
            }
            // 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));
            } 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, "生成压缩包失败");
            }
        } catch (Exception e) {
            log.error("批量压缩图片失败", e);
            try {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "处理失败: " + e.getMessage());
            } catch (IOException ex) {
                log.error("发送错误响应失败", ex);
            }
        }
    }
    /**
     * 获取图片信息(包含缩略图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("禁止访问该路径");
            }
            // 获取原图
            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());
        }
    }
    /**
     * 检查是否为图片文件
     */
    private boolean isImageFile(File file) {
        if (file == null || !file.exists()) {
            return false;
        }
        String fileName = file.getName().toLowerCase();
        String extension = getFileExtension(fileName);
        return SUPPORTED_IMAGE_FORMATS.contains(extension);
    }
    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String fileName) {
        if (StringUtils.isEmpty(fileName)) {
            return "";
        }
        int lastDot = fileName.lastIndexOf('.');
        if (lastDot > 0 && lastDot < fileName.length() - 1) {
            return fileName.substring(lastDot + 1).toLowerCase();
        }
        return "";
    }
    /**
     * 生成缩略图
     */
    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;    // 格式
    }
}