zqy
2 天以前 e1dc6930a9d217da8d87e2838208eb0e7eca2a2a
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java
@@ -1,13 +1,26 @@
package com.ruoyi.web.controller.common;
import java.util.ArrayList;
import java.util.HashMap;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.websocket.server.PathParam;
import com.ruoyi.common.annotation.Anonymous;
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;
import org.springframework.beans.factory.annotation.Autowired;
@@ -23,15 +36,25 @@
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
@RequestMapping("/common")
public class CommonController
{
public class CommonController {
    private static final Logger log = LoggerFactory.getLogger(CommonController.class);
    @Autowired
@@ -40,9 +63,47 @@
    @Autowired
    private DownLoadFileService downLoadFileService;
    @Autowired
    private VideoProcessService videoProcessService;
    private static final String FILE_DELIMETER = ",";
//    @GetMapping("/downloadFile")
    private static final Pattern CHINESE_PATTERN = Pattern.compile("[\u4e00-\u9fa5]");
    // 缩略图配置
    @Value("${thumbnail.default-width:300}")
    private int defaultThumbnailWidth;
    @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);
@@ -67,9 +128,7 @@
//    }
//    /**
    //    /**
//     * 通用下载请求
//     *
//     * @param fileName 文件名称
@@ -100,10 +159,11 @@
//            log.error("下载文件失败", e);
//        }
//    }
    @Anonymous
    @GetMapping("/downLoadFile")
    public void downLoadFile(@PathParam("path") String path, HttpServletResponse response) throws Exception {
        downLoadFileService.downLoadFile(path,response);
        downLoadFileService.downLoadFile(path, response);
    }
@@ -111,14 +171,12 @@
     * 通用上传请求(单个)
     */
    @PostMapping("/upload")
    public AjaxResult uploadFile(@RequestParam("uploadFile") MultipartFile file,String fname) throws Exception
    {
        try
        {
    public AjaxResult uploadFile(@RequestParam("uploadFile") MultipartFile file, String fname) throws Exception {
        try {
            // 上传文件路径
            String filePath = RuoYiConfig.getUploadPath();
            // 上传并返回新文件名称
            String fileName = FileUploadUtils.upload(filePath, file,fname);
            String fileName = FileUploadUtils.upload(filePath, file, fname);
            String url = serverConfig.getUrl() + fileName;
            AjaxResult ajax = AjaxResult.success();
@@ -129,15 +187,14 @@
            data.put("newFileName", FileUtils.getName(fileName));
            data.put("originalFilename", file.getOriginalFilename());
            ajax.put("msg","操作成功");
            ajax.put("data",data);
            ajax.put("msg", "操作成功");
            ajax.put("data", data);
            return ajax;
        }
        catch (Exception e)
        {
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    /**
     * 通用上传请求(多个)
@@ -154,15 +211,15 @@
            List<String> fileNames = new ArrayList<String>();
            List<String> newFileNames = new ArrayList<String>();
            List<String> originalFilenames = new ArrayList<String>();
           // System.out.println("99999999999999999999999990000000000000000");
          //  System.out.println(files);
          //  System.out.println(files.size());
            // System.out.println("99999999999999999999999990000000000000000");
            //  System.out.println(files);
            //  System.out.println(files.size());
            for (MultipartFile file : files)
            {
                // 上传并返回新文件名称
                String filename = "";
             //   System.out.println("1122123330+++++++++++++++++++++++++++++");
                //   System.out.println("1122123330+++++++++++++++++++++++++++++");
                String fileName = FileUploadUtils.upload(filePath, file, filename);
                String url = serverConfig.getUrl() + fileName;
@@ -172,13 +229,13 @@
                originalFilenames.add(file.getOriginalFilename());
            }
            AjaxResult ajax = AjaxResult.success();
          //  System.out.println("99999999999999999999999990000000000000000");
            //  System.out.println("99999999999999999999999990000000000000000");
            ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
            ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
            ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
            ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
          //  System.out.println("1122123330+++++++++++++++++++++++++++++");
            //  System.out.println("1122123330+++++++++++++++++++++++++++++");
            return ajax;
        }
        catch (Exception e)
@@ -188,16 +245,235 @@
    }
    /**
     * 通用上传请求(多个) 将中文修改为其他
     */
    @PostMapping("/noChinese/uploads")
    public AjaxResult noChineseUploadFiles(@RequestParam("files") List<MultipartFile> files) throws Exception {
        //System.out.println("99999999999999999999999990000000000000000");
        try {
            // 上传文件路径
            String filePath = RuoYiConfig.getUploadPath();
            List<String> urls = new ArrayList<String>();
            List<String> fileNames = new ArrayList<String>();
            List<String> newFileNames = new ArrayList<String>();
            List<String> originalFilenames = new ArrayList<String>();
            for (MultipartFile file : files) {
                originalFilenames.add(file.getOriginalFilename());
                String safeFilename = generateSafeFilename(file.getOriginalFilename());
                MultipartFile renamedFile = new RenamedMultipartFile(file, safeFilename);
                String lastName="";
                String fileName = FileUploadUtils.upload(filePath, renamedFile, lastName);
                String url = serverConfig.getUrl() + fileName;
                urls.add(url);
                fileNames.add(fileName);
                newFileNames.add(FileUtils.getName(fileName));
            }
            AjaxResult ajax = AjaxResult.success();
            ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
            ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
            ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
            ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
            return ajax;
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    /**
     * 生成安全文件名(只替换中文部分)
     */
    private String generateSafeFilename(String originalName) {
        if (originalName == null) {
            return "";
        }
        // 1. 获取文件扩展名
        String extension = "";
        int dotIndex = originalName.lastIndexOf('.');
        if (dotIndex > 0) {
            extension = originalName.substring(dotIndex);
            originalName = originalName.substring(0, dotIndex);
        }
        // 2. 只替换中文部分
        StringBuilder safeName = new StringBuilder();
        Matcher matcher = CHINESE_PATTERN.matcher(originalName);
        int lastEnd = 0;
        while (matcher.find()) {
            // 添加非中文部分
            safeName.append(originalName, lastEnd, matcher.start());
            // 添加随机字符串替换中文
            safeName.append(generateRandomString(4));
            lastEnd = matcher.end();
        }
        // 添加剩余部分
        safeName.append(originalName.substring(lastEnd));
        String noSpaceName = safeName.toString().replaceAll("\\s", "");
        // 3. 添加扩展名
        return noSpaceName + extension;
    }
    /**
     * 生成随机字符串(字母+数字)
     */
    private String generateRandomString(int length) {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        return uuid.substring(0, Math.min(length, uuid.length()));
    }
    @PostMapping("/uploads1")
    public AjaxResult uploadFiles1(@RequestParam("files") List<MultipartFile> files) {
        try {
            String filePath = RuoYiConfig.getUploadPath();
            List<String> urls = new ArrayList<>();
            List<String> fileNames = new ArrayList<>();
            List<String> newFileNames = new ArrayList<>();
            List<String> originalFilenames = new ArrayList<>();
            List<String> httpSafePaths = new ArrayList<>();
            for (MultipartFile file : files) {
                // 1. 上传文件
                String fileName = FileUploadUtils.upload(filePath, file, "");
                String originalFilename = file.getOriginalFilename();
                // 2. 获取HTTP安全路径
                String httpSafePath = toHttpPath(fileName);
                // 3. 构建完整URL(确保有斜杠分隔)
                String baseUrl = serverConfig.getUrl();
                if (!baseUrl.endsWith("/") && !httpSafePath.startsWith("/")) {
                    baseUrl += "/";
                }
                String url = baseUrl + httpSafePath;
                urls.add(url);
                fileNames.add(fileName);
                newFileNames.add(FileUtils.getName(fileName));
                originalFilenames.add(originalFilename);
                httpSafePaths.add(httpSafePath);
            }
            AjaxResult ajax = AjaxResult.success();
            ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
            ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
            ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
            ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
            ajax.put("httpSafePaths", StringUtils.join(httpSafePaths, FILE_DELIMETER));
            return ajax;
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    /**
     * 将包含中文的文件路径转换为 HTTP 安全的 URL 路径
     */
    public String toHttpPath(String filePath) {
        try {
            // 1. 标准化路径
            Path normalizedPath = Paths.get(filePath).normalize();
            // 2. 统一使用正斜杠
            String pathStr = normalizedPath.toString().replace("\\", "/");
            // 3. 分割路径组件
            String[] parts = pathStr.split("/");
            StringBuilder encodedPath = new StringBuilder();
            // 4. 对每个组件单独编码并处理空格
            for (String part : parts) {
                if (!part.isEmpty()) {
                    // 编码并替换空格为 %20
                    String encodedPart = URLEncoder.encode(part, StandardCharsets.UTF_8.name())
                            .replace("+", "%20");
                    encodedPath.append("/").append(encodedPart);
                }
            }
            // 5. 处理绝对路径和相对路径
            return filePath.startsWith("/") || filePath.startsWith("\\") ?
                    encodedPath.toString() :
                    encodedPath.substring(1);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 encoding not supported", e);
        }
        }
    /**
     * 从 HTTP URL 路径还原原始中文路径
     */
    @PostMapping("/getFileName")
    public String extractFileName(@RequestBody String httpPath) {
        try {
            // 1. 处理空值
            if (httpPath == null || httpPath.trim().isEmpty()) {
                return "";
            }
            // 2. 移除URL协议、域名和查询参数
            String pathOnly = httpPath;
            // 移除协议和域名
            if (pathOnly.contains("://")) {
                pathOnly = pathOnly.substring(pathOnly.indexOf("://") + 3);
                pathOnly = pathOnly.substring(pathOnly.indexOf('/'));
            }
            // 移除查询参数(如 ?token=123)
            int queryStart = pathOnly.indexOf('?');
            if (queryStart > 0) {
                pathOnly = pathOnly.substring(0, queryStart);
            }
            // 3. URL解码
            String decodedPath = URLDecoder.decode(pathOnly, StandardCharsets.UTF_8.name());
            // 4. 提取文件名(处理Windows路径)
            decodedPath = decodedPath.replace("\\", "/");
            // 获取最后一个非空路径组件
            int lastSlash = decodedPath.lastIndexOf('/');
            String fileName = (lastSlash >= 0 && lastSlash < decodedPath.length() - 1) ?
                    decodedPath.substring(lastSlash + 1) : decodedPath;
            // 5. 处理特殊情况(如结尾斜杠)
            if (fileName.isEmpty()) {
                // 尝试获取倒数第二个组件
                int prevSlash = decodedPath.lastIndexOf('/', lastSlash - 1);
                if (prevSlash >= 0) {
                    fileName = decodedPath.substring(prevSlash + 1, lastSlash);
                }
            }
            return fileName;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 encoding not supported", e);
        }
    }
    /**
     * 本地资源通用下载
     */
    @GetMapping("/download/resource")
    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
            throws Exception
    {
        try
        {
            if (!FileUtils.checkAllowDownload(resource))
            {
            throws Exception {
        try {
            if (!FileUtils.checkAllowDownload(resource)) {
                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
            }
            // 本地资源路径
@@ -209,10 +485,630 @@
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            FileUtils.setAttachmentResponseHeader(response, downloadName);
            FileUtils.writeBytes(downloadPath, response.getOutputStream());
        }
        catch (Exception e)
        {
        } catch (Exception e) {
            log.error("下载文件失败", e);
        }
    }
    // 支持的图片格式
    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;    // 格式
    }
}