Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

lixiaolong 3 месяцев назад
Родитель
Сommit
88d3e28079

+ 70 - 19
commons/src/main/java/com/jzg/commons/util/FileUtils.java

@@ -8,9 +8,18 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Formatter;
 
+/**
+ * 文件工具类
+ * 提供文件哈希值计算(MD5、ETag)等功能
+ */
 public class FileUtils {
 
-    // 将字节数组转换为十六进制字符串
+    /**
+     * 将字节数组转换为十六进制字符串
+     *
+     * @param byteArray 字节数组
+     * @return 十六进制字符串
+     */
     private static String byteArrayToHex(byte[] byteArray) {
         try (Formatter formatter = new Formatter()) {
             for (byte b : byteArray) {
@@ -21,11 +30,12 @@ public class FileUtils {
     }
 
     /**
-     * 获取文件ETAG
-     * @param file
-     * @return
-     * @throws NoSuchAlgorithmException
-     * @throws IOException
+     * 获取文件的 ETag(基于 MD5 计算)
+     *
+     * @param file 上传的文件
+     * @return 文件的 ETag 值
+     * @throws NoSuchAlgorithmException MD5 算法不可用时抛出
+     * @throws IOException              文件读取失败时抛出
      */
     public static String getFileETag(MultipartFile file) throws NoSuchAlgorithmException, IOException {
         MessageDigest md5Digest = MessageDigest.getInstance("MD5");
@@ -44,38 +54,79 @@ public class FileUtils {
     }
 
     /**
-     * 获取文件md5
-     * @param file
-     * @return
-     * @throws IOException
-     * @throws NoSuchAlgorithmException
+     * 获取文件的 MD5 值
+     *
+     * @param file 上传的文件
+     * @return 文件的 MD5 值
+     * @throws IOException              文件读取失败时抛出
+     * @throws NoSuchAlgorithmException MD5 算法不可用时抛出
      */
     public static String getFileMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
         // 获取文件输入流
         InputStream inputStream = file.getInputStream();
+        try {
+            return getFileMD5(inputStream);
+        } finally {
+            inputStream.close();
+        }
+    }
 
-        // 创建 MessageDigest 对象用于计算 MD5
+    /**
+     * 获取字节数组的 MD5 值
+     * 用于压缩后的图片数据计算哈希值
+     *
+     * @param data 字节数组
+     * @return MD5 值
+     * @throws NoSuchAlgorithmException MD5 算法不可用时抛出
+     */
+    public static String getFileMD5(byte[] data) throws NoSuchAlgorithmException {
         MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+        messageDigest.update(data);
+        byte[] md5Bytes = messageDigest.digest();
+        StringBuilder sb = new StringBuilder();
+        for (byte b : md5Bytes) {
+            sb.append(String.format("%02x", b & 0xff));
+        }
+        return sb.toString();
+    }
 
+    /**
+     * 获取输入流的 MD5 值(内部方法)
+     *
+     * @param inputStream 输入流
+     * @return MD5 值
+     * @throws NoSuchAlgorithmException MD5 算法不可用时抛出
+     * @throws IOException              读取失败时抛出
+     */
+    private static String getFileMD5(InputStream inputStream) throws NoSuchAlgorithmException, IOException {
+        MessageDigest messageDigest = MessageDigest.getInstance("MD5");
         // 使用 1KB 缓冲区读取文件内容
         byte[] buffer = new byte[1024];
         int bytesRead;
         while ((bytesRead = inputStream.read(buffer)) != -1) {
             messageDigest.update(buffer, 0, bytesRead);
         }
-
-        // 关闭输入流
-        inputStream.close();
-
-        // 获取 MD5 校验和
         byte[] md5Bytes = messageDigest.digest();
-
         // 转换为 16 进制字符串
         StringBuilder sb = new StringBuilder();
         for (byte b : md5Bytes) {
             sb.append(String.format("%02x", b & 0xff));
         }
+        return sb.toString();
+    }
 
-        return sb.toString();  // 返回 MD5 字符串
+    /**
+     * 获取字节数组的 ETag 值(基于 MD5 计算)
+     * 用于压缩后的图片数据计算 ETag
+     *
+     * @param data 字节数组
+     * @return ETag 值
+     * @throws NoSuchAlgorithmException MD5 算法不可用时抛出
+     */
+    public static String getFileETag(byte[] data) throws NoSuchAlgorithmException {
+        MessageDigest md5Digest = MessageDigest.getInstance("MD5");
+        md5Digest.update(data);
+        byte[] md5Bytes = md5Digest.digest();
+        return byteArrayToHex(md5Bytes);
     }
 }

+ 152 - 4
commons/src/main/java/com/jzg/commons/util/ImageUtil.java

@@ -4,12 +4,15 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.imageio.ImageIO;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.ImageWriter;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.awt.*;
 import java.awt.image.BufferedImage;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
+import java.io.*;
 import java.util.Base64;
+import java.util.Iterator;
 
 /**
  * 图片工具类
@@ -121,4 +124,149 @@ public class ImageUtil {
         return ".jpg"; // 默认返回 jpg
     }
 
+    /**
+     * 图片压缩参数配置
+     */
+    private static final int MAX_WIDTH = 1920;
+    private static final int MAX_HEIGHT = 1080;
+    private static final float QUALITY = 0.7f;
+
+    /**
+     * 判断文件是否为图片类型(通过 Content-Type 判断)
+     *
+     * @param contentType 文件的 Content-Type
+     * @return 是否为图片类型
+     */
+    public static boolean isImage(String contentType) {
+        return contentType != null && contentType.startsWith("image/");
+    }
+
+    /**
+     * 判断文件扩展名是否为图片类型
+     *
+     * @param extension 文件扩展名(含点号,如 ".jpg")
+     * @return 是否为图片扩展名
+     */
+    public static boolean isImageExtension(String extension) {
+        if (extension == null) {
+            return false;
+        }
+        String ext = extension.toLowerCase();
+        return ext.equals(".jpg") || ext.equals(".jpeg") || ext.equals(".png") ||
+               ext.equals(".gif") || ext.equals(".bmp") || ext.equals(".webp");
+    }
+
+    /**
+     * 图片压缩方法
+     * 压缩策略:
+     * 1. 如果图片尺寸超过 MAX_WIDTH x MAX_HEIGHT,先按比例缩放
+     * 2. 无论是否缩放,都进行质量压缩(压缩质量为 QUALITY)
+     * 3. 保持原图片格式,PNG 保持透明通道
+     *
+     * @param file 上传的图片文件
+     * @return 压缩后的字节数组,压缩失败返回 null
+     */
+    public static byte[] compressImage(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            return null;
+        }
+
+        try (InputStream is = file.getInputStream()) {
+            BufferedImage originalImage = ImageIO.read(is);
+            if (originalImage == null) {
+                return null;
+            }
+
+            int width = originalImage.getWidth();
+            int height = originalImage.getHeight();
+
+            // 如果图片尺寸在限制范围内,直接进行质量压缩
+            if (width <= MAX_WIDTH && height <= MAX_HEIGHT) {
+                return compressWithQuality(originalImage, file.getOriginalFilename());
+            }
+
+            // 计算缩放比例,保持宽高比
+            double scale = Math.min((double) MAX_WIDTH / width, (double) MAX_HEIGHT / height);
+            int newWidth = (int) (width * scale);
+            int newHeight = (int) (height * scale);
+
+            // 创建缩放后的图片,使用双线性插值算法保证清晰度
+            BufferedImage resizedImage = new BufferedImage(newWidth, newHeight, originalImage.getType());
+            Graphics2D g2d = resizedImage.createGraphics();
+            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+            g2d.drawImage(originalImage, 0, 0, newWidth, newHeight, null);
+            g2d.dispose();
+
+            // 对缩放后的图片进行质量压缩
+            return compressWithQuality(resizedImage, file.getOriginalFilename());
+        } catch (IOException e) {
+            log.error("图片压缩失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 图片质量压缩
+     * 使用 ImageWriter 进行高质量压缩,保持原格式
+     *
+     * @param image    待压缩的图片对象
+     * @param filename 原文件名,用于确定图片格式
+     * @return 压缩后的字节数组
+     */
+    private static byte[] compressWithQuality(BufferedImage image, String filename) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            String format = getImageFormat(filename);
+            Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(format);
+
+            // 如果找不到对应格式的 ImageWriter,使用默认方式写入
+            if (!writers.hasNext()) {
+                ImageIO.write(image, format, baos);
+                return baos.toByteArray();
+            }
+
+            // 使用 ImageWriter 进行高质量压缩
+            ImageWriter writer = writers.next();
+            ImageWriteParam writeParam = writer.getDefaultWriteParam();
+
+            if (writeParam.canWriteCompressed()) {
+                writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+                writeParam.setCompressionQuality(QUALITY);
+            }
+
+            writer.setOutput(ImageIO.createImageOutputStream(baos));
+            writer.write(null, new javax.imageio.IIOImage(image, null, null), writeParam);
+            writer.dispose();
+
+            return baos.toByteArray();
+        } catch (IOException e) {
+            log.error("图片质量压缩失败", e);
+            try {
+                // 压缩失败时回退到普通写入方式
+                ImageIO.write(image, getImageFormat(filename), baos);
+                return baos.toByteArray();
+            } catch (IOException ex) {
+                log.error("图片回退保存失败", ex);
+                return null;
+            }
+        }
+    }
+
+    /**
+     * 根据文件名获取图片格式
+     *
+     * @param filename 文件名
+     * @return 图片格式(如 "jpg", "png", "gif")
+     */
+    private static String getImageFormat(String filename) {
+        if (filename == null) {
+            return "jpg";
+        }
+        String ext = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
+        if ("jpeg".equals(ext)) {
+            return "jpg";
+        }
+        return ext;
+    }
+
 }

+ 73 - 34
platform/src/main/java/com/jzg/controller/FileUploadController.java

@@ -5,25 +5,23 @@ import com.jzg.commons.core.base.BaseController;
 import com.jzg.commons.core.page.HttpResult;
 import com.jzg.commons.entity.po.SysUploadFiles;
 import com.jzg.commons.exception.SystemException;
-import com.jzg.commons.request.RequestSingleParam;
 import com.jzg.commons.util.FileUtils;
+import com.jzg.commons.util.ImageUtil;
 import com.jzg.commons.util.MinioUtils;
 import com.jzg.commons.util.idgen.IdGenerate;
 import com.jzg.mapper.SysUploadFilesMapper;
 import io.minio.*;
 import io.swagger.v3.oas.annotations.Operation;
+import org.jetbrains.annotations.NotNull;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLEncoder;
+import java.net.*;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 
@@ -47,24 +45,60 @@ public class FileUploadController extends BaseController {
     private String bucketName;
 
 
+    /**
+     * 文件上传接口
+     * 支持图片自动压缩:上传图片时先判断类型,若是图片则进行尺寸和质量压缩后再上传
+     *
+     * @param file 上传的文件
+     * @return 上传结果,包含文件信息和下载链接
+     */
     @PostMapping("/upload")
-    @Operation(summary = "文件上传接口", description = "文件上传接口")
+    @Operation(summary = "文件上传接口", description = "文件上传接口,图片自动压缩")
     public HttpResult uploadFile(@RequestParam("file") MultipartFile file) {
         System.out.printf("调用文件上传接口");
         try {
             // 获取文件原始名称和扩展名
             String originalFileName = file.getOriginalFilename();
             String extension = getFileExtension(originalFileName);
-            // MD5,ETag
-            String fileMd5 = FileUtils.getFileMD5(file);
-            String objectName = generateObjectName(originalFileName, fileMd5, extension);
-            String fileEtag = FileUtils.getFileETag(file);
-            // 查找文件是否已经存在于MinIO
+
+            // 判断是否为图片文件,若是则进行压缩
+            // 双重校验:通过 Content-Type 和文件扩展名判断
+            byte[] compressedBytes = null;
+            if (ImageUtil.isImage(file.getContentType()) || ImageUtil.isImageExtension(extension)) {
+                compressedBytes = ImageUtil.compressImage(file);
+            }
+
+            // 根据是否压缩成功,计算文件的 MD5 和 ETag
+            // 注意:MD5/ETag 必须基于最终上传的文件内容计算,确保与 MinIO 存储一致
+            String fileMd5;
+            String fileEtag;
+            String objectName;
+
+            if (compressedBytes != null && compressedBytes.length > 0) {
+                // 使用压缩后的字节数组计算哈希值
+                fileMd5 = FileUtils.getFileMD5(compressedBytes);
+                fileEtag = FileUtils.getFileETag(compressedBytes);
+                objectName = generateObjectName(originalFileName, fileMd5, extension);
+            } else {
+                // 使用原始文件计算哈希值(非图片或压缩失败时回退)
+                fileMd5 = FileUtils.getFileMD5(file);
+                fileEtag = FileUtils.getFileETag(file);
+                objectName = generateObjectName(originalFileName, fileMd5, extension);
+            }
+
 //            if (isFileExistsInMinIo(objectName, fileEtag)) {
 //                return HttpResult.ok(getFileFromDatabase(fileEtag,objectName, extension));
 //            }
-            //如果文件不存在,执行上传操作
-            SysUploadFiles sysUploadFiles = uploadFileToMinIo(file, objectName, fileEtag, extension);
+
+            // 根据是否压缩成功,选择对应的上传方式
+            SysUploadFiles sysUploadFiles;
+            if (compressedBytes != null && compressedBytes.length > 0) {
+                // 上传压缩后的图片字节数组
+                sysUploadFiles = uploadFileToMinIo(compressedBytes, objectName, fileEtag, extension, file.getContentType());
+            } else {
+                // 上传原始文件(非图片或压缩失败时回退)
+                sysUploadFiles = uploadFileToMinIo(file, objectName, fileEtag, extension);
+            }
             return HttpResult.ok(sysUploadFiles);
         } catch (Exception e) {
             e.printStackTrace();
@@ -87,8 +121,7 @@ public class FileUploadController extends BaseController {
             String contentType = connection.getContentType();
             String extension = "";
 
-            if(contentType.contains("application/pdf") || contentType.contains("text/html"))
-            {
+            if (contentType.contains("application/pdf") || contentType.contains("text/html")) {
                 extension = ".pdf";
             }
             try (InputStream inputStream = connection.getInputStream()) {
@@ -96,7 +129,7 @@ public class FileUploadController extends BaseController {
             } catch (Exception e) {
                 throw new RuntimeException(e);
             }
-        }catch (MalformedURLException e){
+        } catch (MalformedURLException e) {
             throw new RuntimeException("url格式错误");
         } catch (IOException e) {
             throw new SystemException("url错误");
@@ -169,7 +202,7 @@ public class FileUploadController extends BaseController {
         }
     }
 
-    private SysUploadFiles getFileFromDatabase(String fileEtag,String fileName,String fileType) {
+    private SysUploadFiles getFileFromDatabase(String fileEtag, String fileName, String fileType) {
         SysUploadFiles sysUploadFiles = sysUploadFilesMapper.selectOne(
                 new LambdaQueryWrapper<SysUploadFiles>().eq(SysUploadFiles::getEtag, fileEtag));
 
@@ -182,11 +215,11 @@ public class FileUploadController extends BaseController {
             sysUploadFiles.setEtag(fileEtag);
             sysUploadFiles.setCreateTime(LocalDateTime.now());
             sysUploadFiles.setCreateBy(getUserName());
-            StatObjectResponse fileContent = MinioUtils.getFileStatusInfo(bucketName,fileName);
+            StatObjectResponse fileContent = MinioUtils.getFileStatusInfo(bucketName, fileName);
             sysUploadFiles.setFileSize(fileContent.size() + "");
             sysUploadFilesMapper.insert(sysUploadFiles);
         }
-        sysUploadFiles.setDownloadUrl(MinioUtils.getPresignedObjectUrl(bucketName,fileName));
+        sysUploadFiles.setDownloadUrl(MinioUtils.getPresignedObjectUrl(bucketName, fileName));
         return sysUploadFiles;
     }
 
@@ -200,18 +233,19 @@ public class FileUploadController extends BaseController {
                 .contentType(file.getContentType())
                 .build());
 
-        SysUploadFiles sysUploadFiles = new SysUploadFiles();
-        sysUploadFiles.setId(IdGenerate.nextId());
-        sysUploadFiles.setFileName(objectName);
-        sysUploadFiles.setFileType(extension.substring(1));
-        StatObjectResponse fileContent = MinioUtils.getFileStatusInfo(bucketName,objectName);
-        sysUploadFiles.setFileSize(fileContent.size() + "");
-        sysUploadFiles.setBucketName(bucketName);
-        sysUploadFiles.setEtag(objectWriteResponse.etag());
-        sysUploadFiles.setCreateTime(LocalDateTime.now());
-        sysUploadFiles.setDownloadUrl(MinioUtils.getPresignedObjectUrl(bucketName,objectName));
-        sysUploadFilesMapper.insert(sysUploadFiles);
-        return sysUploadFiles;
+        return insertSysUploadFiles(objectName, extension, objectWriteResponse);
+    }
+
+    private SysUploadFiles uploadFileToMinIo(byte[] fileBytes, String objectName, String fileEtag, String extension, String contentType) throws Exception {
+        InputStream fileInputStream = new ByteArrayInputStream(fileBytes);
+        ObjectWriteResponse objectWriteResponse = minioClient.putObject(PutObjectArgs.builder()
+                .bucket(bucketName)
+                .object(objectName)
+                .stream(fileInputStream, fileBytes.length, -1)
+                .contentType(contentType)
+                .build());
+
+        return insertSysUploadFiles(objectName, extension, objectWriteResponse);
     }
 
 
@@ -224,16 +258,21 @@ public class FileUploadController extends BaseController {
                 .contentType(contentType)
                 .build());
 
+        return insertSysUploadFiles(objectName, extension, objectWriteResponse);
+    }
+
+    @NotNull
+    private SysUploadFiles insertSysUploadFiles(String objectName, String extension, ObjectWriteResponse objectWriteResponse) {
         SysUploadFiles sysUploadFiles = new SysUploadFiles();
         sysUploadFiles.setId(IdGenerate.nextId());
         sysUploadFiles.setFileName(objectName);
         sysUploadFiles.setFileType(extension.substring(1));
-        StatObjectResponse fileContent = MinioUtils.getFileStatusInfo(bucketName,objectName);
+        StatObjectResponse fileContent = MinioUtils.getFileStatusInfo(bucketName, objectName);
         sysUploadFiles.setFileSize(fileContent.size() + "");
         sysUploadFiles.setBucketName(bucketName);
         sysUploadFiles.setEtag(objectWriteResponse.etag());
         sysUploadFiles.setCreateTime(LocalDateTime.now());
-        sysUploadFiles.setDownloadUrl(MinioUtils.getPresignedObjectUrl(bucketName,objectName));
+        sysUploadFiles.setDownloadUrl(MinioUtils.getPresignedObjectUrl(bucketName, objectName));
         sysUploadFilesMapper.insert(sysUploadFiles);
         return sysUploadFiles;
     }