chenhj
2026-04-22 55a16a2b0144c30277c7ac01712f347eedfaab41
feat(common): 优化文件预览接口实现及文件压缩功能

- 调整CommonController中preview接口,实现返回ResponseEntity支持文件压缩和流式响应
- 引入FileUtil工具类,新增文件压缩compressFile方法,针对图片类型文件进行压缩处理
- FileProperties中新增文件压缩相关配置项包括compress、needCompressSize和compressQuality
- FileUtil中优化JWT签名密钥生成,改用SecretKey替代字符串秘钥签名
- 添加thumbnailator依赖支持图片压缩功能
- 更新pom.xml,添加thumbnailator及相关版本依赖并整理项目依赖
- 修正StorageBlobServiceImpl中JWT秘钥获取逻辑,确保使用正确的密钥字符串
- application-dev-pro.yml新增多项项目和服务配置,包括minio、quartz、redis和token等配置项
已修改6个文件
108 ■■■■ 文件已修改
pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/StorageBlobServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/utils/FileUtil.java 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/config/FileProperties.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/common/CommonController.java 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev-pro.yml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -46,6 +46,7 @@
        <mybatis-plus.version>3.5.16</mybatis-plus.version>
        <getui-sdk.version>1.0.7.0</getui-sdk.version>
        <jsqlparser.version>4.9</jsqlparser.version>
        <thumbnailator.version>0.4.20</thumbnailator.version>
    </properties>
    <dependencies>
@@ -360,6 +361,12 @@
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
        <dependency>
            <groupId>net.coobird</groupId>
            <artifactId>thumbnailator</artifactId>
            <version>${thumbnailator.version}</version>
        </dependency>
    </dependencies>
    <build>
src/main/java/com/ruoyi/basic/service/impl/StorageBlobServiceImpl.java
@@ -89,9 +89,7 @@
            throw new IllegalArgumentException("token不能为空");
        }
        String secretStr = StringUtils.hasText(properties.getJwtSecret())
                ? properties.getJwtSecret()
                : "local-file-jwt-secret";
        String secretStr = properties.getJwtSecret();
        SecretKey key = Keys.hmacShaKeyFor(secretStr.getBytes(StandardCharsets.UTF_8));
        Claims claims = Jwts.parser()
src/main/java/com/ruoyi/basic/utils/FileUtil.java
@@ -7,18 +7,21 @@
import com.ruoyi.basic.dto.StorageAttachmentDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.mapper.StorageAttachmentMapper;
import com.ruoyi.basic.mapper.StorageBlobMapper;
import com.ruoyi.basic.pojo.StorageAttachment;
import com.ruoyi.common.config.FileProperties;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.io.File;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
@@ -30,7 +33,6 @@
public class FileUtil {
    private final FileProperties properties;
    private final StorageAttachmentMapper storageAttachmentMapper;
    private final StorageBlobMapper storageBlobMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private static final String TOKEN_USAGE_KEY_PREFIX = "file:token:usage:";
@@ -431,13 +433,15 @@
        }
        Date issuedAt = new Date(now);
        Date expiration = new Date(now + expiredMillis);
        SecretKey key = Keys.hmacShaKeyFor(properties.getJwtSecret().getBytes(StandardCharsets.UTF_8));
        String token = Jwts.builder()
                .setSubject(storageBlob.getUidFilename())
                .setIssuedAt(issuedAt)
                .setExpiration(expiration)
                .subject(storageBlob.getUidFilename())
                .issuedAt(issuedAt)       // 新版建议直接调用 .issuedAt()
                .expiration(expiration)   // 新版建议直接调用 .expiration()
                .claim("path", storageBlob.getPath())
                .claim("resourceKey", storageBlob.getResourceKey())
                .signWith(SignatureAlgorithm.HS256, properties.getJwtSecret())
                .signWith(key)            // 重点:传入上面生成的 key 对象,而不是 String
                .compact();
        cacheTokenUsage(token, expiredMillis);
        String domain = StringUtils.trimTrailingCharacter(properties.getDomain(), '/');
@@ -491,4 +495,42 @@
        return properties.getUseLimit() == null || properties.getUseLimit() <= 0 ? 10 : properties.getUseLimit();
    }
    /**
     * 压缩文件
     *
     * @param file 文件
     * @return 压缩后的文件
     */
    public File compressFile(File file) {
        if (properties.getCompress() && isImage(file.getName()) && (file.length() > properties.getNeedCompressSize().toBytes())) {
            try {
                // 创建一个临时文件存放压缩后的图片,避免破坏原图
                File compressedFile = new File(file.getParent(), "thumb_" + file.getName());
                // 1. 如果已经存在压缩过的文件,直接返回,不再消耗 CPU 压缩
                if (compressedFile.exists()) {
                    return compressedFile;
                }
                // 使用 Thumbnailator 进行压缩
                Thumbnails.of(file)
                        .scale(1.0f)                // 保持原尺寸
                        .outputQuality(properties.getCompressQuality())        // 核心:设置画质 (0.0~1.0)
                        .toFile(compressedFile);
                return compressedFile;
            } catch (Exception e) {
                // 如果压缩失败,降级处理:返回原图
                return file;
            }
        }
        return file;
    }
    // 简单的后缀判断
    private boolean isImage(String fileName) {
        String ext = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
        return "jpg".equals(ext) || "jpeg".equals(ext) || "png".equals(ext);
    }
}
src/main/java/com/ruoyi/common/config/FileProperties.java
@@ -5,12 +5,13 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.util.unit.DataSize;
import java.math.BigDecimal;
@Configuration
@Component
@ConfigurationProperties(prefix = "file")
@ConfigurationProperties(prefix = "file", ignoreUnknownFields = true)
@Data
public class FileProperties {
    private String path = "D:/upload";
@@ -21,4 +22,7 @@
    // 令牌秘钥
    @Value("${token.secret}")
    private String jwtSecret;
    private Boolean compress;
    private DataSize needCompressSize;
    private float compressQuality;
}
src/main/java/com/ruoyi/project/common/CommonController.java
@@ -1,18 +1,23 @@
package com.ruoyi.project.common;
import com.ruoyi.basic.service.StorageBlobService;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Anonymous;
import com.ruoyi.framework.config.ServerConfig;
import com.ruoyi.framework.web.domain.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@@ -36,6 +41,7 @@
    private static final String FILE_DELIMETER = ",";
    private final StorageBlobService storageBlobService;
    private final FileUtil fileUtil;
//    /**
@@ -171,7 +177,6 @@
//            log.error("下载文件失败", e);
//        }
//    }
    @PostMapping({"/upload"})
    @ApiOperation(value = "文件上传")
    public R upload(@RequestParam("files") List<MultipartFile> files) throws Exception {
@@ -186,17 +191,25 @@
        String originalFileName = storageBlobService.getDownloadFileName(fileName);
        String encodedFileName = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
        response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + encodedFileName);
        response.setContentLengthLong(file.length());
        Files.copy(file.toPath(), response.getOutputStream());
    }
    @GetMapping("/preview/{fileName}")
    @Anonymous
    public void preview(@PathVariable String fileName, @RequestParam("token") String token, HttpServletResponse response) throws Exception {
        File file = storageBlobService.getFileByToken(fileName, token);
    public ResponseEntity<FileSystemResource> preview(@PathVariable String fileName,
                                                      @RequestParam("token") String token) throws Exception {
        File file = fileUtil.compressFile(storageBlobService.getFileByToken(fileName, token));
        String contentType = Files.probeContentType(file.toPath());
        response.setContentType(contentType == null ? "application/octet-stream" : contentType);
        String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
        response.setHeader("Content-Disposition", "inline;filename*=UTF-8''" + encodedFileName);
        Files.copy(file.toPath(), response.getOutputStream());
        ContentDisposition contentDisposition = ContentDisposition.inline()
                .filename(fileName, StandardCharsets.UTF_8)
                .build();
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType != null ? contentType : "application/octet-stream"))
                .contentLength(file.length())
                .header("Content-Disposition", contentDisposition.toString())
                .body(new FileSystemResource(file));
    }
}
src/main/resources/application-dev-pro.yml
@@ -37,6 +37,9 @@
    uri-encoding: UTF-8
    # 连接数满后的排队数,默认为100
    accept-count: 1000
    max-swallow-size: -1          # -1 表示不限制
    max-http-form-post-size: -1   # POST 请求体不限制
    connection-timeout: 60000     # 连接超时时间(毫秒)
    threads:
      # tomcat最大线程数,默认为200
      max: 800
@@ -261,3 +264,6 @@
  domain: http://127.0.0.1:7003 # 域名前缀
  expired: 120 # 过期时间(单位:分钟)
  useLimit: 10 # 使用次数
  compress: true # 是否压缩
  needCompressSize: 10MB # 压缩阈值
  compressQuality: 0.1 # 压缩质量(0.0-1.0)