chenhj
2026-04-25 34320634733b77a773af6836a2e3082092068ec9
增加永久文件接口
已修改5个文件
117 ■■■■ 文件已修改
src/main/java/com/ruoyi/basic/service/StorageBlobService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/StorageBlobServiceImpl.java 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/utils/FileUtil.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/common/CommonController.java 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/StorageBlobService.java
@@ -23,9 +23,11 @@
     * @param files 文件列表
     * @return 上传结果
     */
    List<StorageBlobVO> upload(List<MultipartFile> files);
    List<StorageBlobVO> upload(List<MultipartFile> files, Boolean isPublic);
    File getFileByToken(String fileName, String token);
    public String getDownloadFileName(String fileName);
    File getPublicFile(String fileName, String publicKey);
    String getDownloadFileName(String fileName);
}
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java
@@ -25,7 +25,10 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
src/main/java/com/ruoyi/basic/service/impl/StorageBlobServiceImpl.java
@@ -20,6 +20,7 @@
import javax.crypto.SecretKey;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -39,7 +40,7 @@
    private final FileUtil fileUtil;
    @Override
    public List<StorageBlobVO> upload(List<MultipartFile> files) {
    public List<StorageBlobVO> upload(List<MultipartFile> files, Boolean isPublic) {
        if (CollectionUtils.isEmpty(files)) {
            throw new IllegalArgumentException("文件不能为空");
        }
@@ -65,7 +66,7 @@
            StorageBlobVO storageBlob;
            try {
                file.transferTo(dest);
                storageBlob = getStorageBlob(file, originalFileName, fileName, relativePath);
                storageBlob = getStorageBlob(file, originalFileName, fileName, relativePath, isPublic);
                if (storageBlob == null || storageBlob.getId() == null) {
                    throw new RuntimeException("文件元数据保存失败");
                }
@@ -109,13 +110,35 @@
        return new File(new File(properties.getPath(), path), fileName);
    }
    @Override
    public File getPublicFile(String fileName, String publicKey) {
        if (!StringUtils.hasText(fileName)) {
            throw new IllegalArgumentException("文件名不能为空");
        }
        if (!StringUtils.hasText(publicKey)) {
            throw new IllegalArgumentException("publicKey不能为空");
        }
        StorageBlob storageBlob = storageBlobMapper.selectOne(new LambdaQueryWrapper<StorageBlob>()
                .eq(StorageBlob::getUidFilename, fileName)
                .eq(StorageBlob::getResourceKey, publicKey)
                .last("limit 1"));
        if (storageBlob == null) {
            throw new IllegalArgumentException("公开文件不存在或publicKey不匹配");
        }
        String path = storageBlob.getPath();
        if (!StringUtils.hasText(path)) {
            return new File(properties.getPath(), fileName);
        }
        return new File(new File(properties.getPath(), path), fileName);
    }
    private StorageBlob findStorageBlob(String fileName) {
        return storageBlobMapper.selectOne(new LambdaQueryWrapper<StorageBlob>()
                .eq(StorageBlob::getUidFilename, fileName)
                .last("limit 1"));
    }
    private StorageBlobVO getStorageBlob(MultipartFile file, String originalFileName, String fileName, String relativePath) {
    private StorageBlobVO getStorageBlob(MultipartFile file, String originalFileName, String fileName, String relativePath, Boolean isPublic) {
        StorageBlobVO storageBlob = new StorageBlobVO();
        storageBlob.setResourceKey(UUID.randomUUID().toString().replace("-", ""));
        storageBlob.setContentType(file.getContentType());
@@ -123,8 +146,13 @@
        storageBlob.setUidFilename(fileName);
        storageBlob.setByteSize(file.getSize());
        storageBlob.setPath(relativePath);
        storageBlob.setPreviewURL(fileUtil.buildSignedPreviewUrl(storageBlob));
        storageBlob.setDownloadURL(fileUtil.buildSignedDownloadUrl(storageBlob));
        if (isPublic) {
            storageBlob.setPreviewURL(fileUtil.buildSignedUrl(storageBlob, "/preview/", BigDecimal.valueOf(-1)));
            storageBlob.setDownloadURL(fileUtil.buildSignedUrl(storageBlob, "/download/", BigDecimal.valueOf(-1)));
        } else {
            storageBlob.setPreviewURL(fileUtil.buildSignedPreviewUrl(storageBlob));
            storageBlob.setDownloadURL(fileUtil.buildSignedDownloadUrl(storageBlob));
        }
        int affectedRows = storageBlobMapper.insert(storageBlob);
        if (affectedRows <= 0) {
            throw new RuntimeException("文件元数据保存失败");
src/main/java/com/ruoyi/basic/utils/FileUtil.java
@@ -565,8 +565,28 @@
        if (storageBlob == null || !StringUtils.hasText(storageBlob.getUidFilename())) {
            throw new IllegalArgumentException("文件信息不完整");
        }
        String domain = StringUtils.trimTrailingCharacter(properties.getDomain(), '/');
        String prefix = properties.getUrlPrefix().startsWith("/") ? properties.getUrlPrefix() : "/" + properties.getUrlPrefix();
        String normalizedActionPath = StringUtils.hasText(actionPath) ? actionPath : "/preview/";
        if (!normalizedActionPath.startsWith("/")) {
            normalizedActionPath = "/" + normalizedActionPath;
        }
        if (!normalizedActionPath.endsWith("/")) {
            normalizedActionPath = normalizedActionPath + "/";
        }
        String baseUrl = domain + prefix + normalizedActionPath + storageBlob.getUidFilename();
        // -1 表示永久有效,不生成 token,改为 publicKey 组合校验
        if (expired != null && BigDecimal.valueOf(-1L).compareTo(expired) == 0) {
            if (!StringUtils.hasText(storageBlob.getResourceKey())) {
                throw new IllegalArgumentException("公开链接缺少publicKey");
            }
            return baseUrl + "?publicKey=" + storageBlob.getResourceKey();
        }
        long now = System.currentTimeMillis();
        long expiredMillis = expired.multiply(new BigDecimal("60000")).longValue();
        BigDecimal expiredValue = expired == null ? new BigDecimal("120") : expired;
        long expiredMillis = expiredValue.multiply(new BigDecimal("60000")).longValue();
        if (expiredMillis <= 0L) {
            expiredMillis = 2L * 60L * 60L * 1000L;
        }
@@ -583,16 +603,7 @@
                .signWith(key)            // 重点:传入上面生成的 key 对象,而不是 String
                .compact();
        cacheTokenUsage(token, expiredMillis);
        String domain = StringUtils.trimTrailingCharacter(properties.getDomain(), '/');
        String prefix = properties.getUrlPrefix().startsWith("/") ? properties.getUrlPrefix() : "/" + properties.getUrlPrefix();
        String normalizedActionPath = StringUtils.hasText(actionPath) ? actionPath : "/preview/";
        if (!normalizedActionPath.startsWith("/")) {
            normalizedActionPath = "/" + normalizedActionPath;
        }
        if (!normalizedActionPath.endsWith("/")) {
            normalizedActionPath = normalizedActionPath + "/";
        }
        return domain + prefix + normalizedActionPath + storageBlob.getUidFilename() + "?token=" + token;
        return baseUrl + "?token=" + token;
    }
    private void cacheTokenUsage(String token, long expiredMillis) {
src/main/java/com/ruoyi/project/common/CommonController.java
@@ -128,15 +128,32 @@
//    }
    @PostMapping({"/upload"})
    @Operation(summary = "文件上传")
    public R upload(@RequestParam("files") List<MultipartFile> files) throws Exception {
        return R.ok(storageBlobService.upload(files));
    public R upload(@RequestParam("files") List<MultipartFile> files) {
        return R.ok(storageBlobService.upload(files, false));
    }
    /**
     * 公共文件上传
     * 此接口上传的文件永久有效,慎用
     */
    @PostMapping({"/public/upload"})
    @Operation(summary = "文件上传")
    public R publicUpload(@RequestParam("files") List<MultipartFile> files) {
        return R.ok(storageBlobService.upload(files, true));
    }
    @GetMapping("/download/{fileName}")
    @Anonymous
    public void download(@PathVariable String fileName, @RequestParam("token") String token, HttpServletResponse response) throws Exception {
        File file = storageBlobService.getFileByToken(fileName, token);
    public void download(@PathVariable String fileName,
                         @RequestParam(value = "token", required = false) String token,
                         @RequestParam(value = "publicKey", required = false) String publicKey,
                         HttpServletResponse response) throws Exception {
        File file;
        if (publicKey != null) {
            file = fileUtil.compressFile(storageBlobService.getPublicFile(fileName, publicKey));
        } else {
            file = fileUtil.compressFile(storageBlobService.getFileByToken(fileName, token));
        }
        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);
@@ -147,8 +164,14 @@
    @GetMapping("/preview/{fileName}")
    @Anonymous
    public ResponseEntity<FileSystemResource> preview(@PathVariable String fileName,
                                                      @RequestParam("token") String token) throws Exception {
        File file = fileUtil.compressFile(storageBlobService.getFileByToken(fileName, token));
                                                      @RequestParam(value = "token", required = false) String token,
                                                      @RequestParam(value = "publicKey", required = false) String publicKey) throws Exception {
        File file;
        if (publicKey != null) {
            file = fileUtil.compressFile(storageBlobService.getPublicFile(fileName, publicKey));
        } else {
            file = fileUtil.compressFile(storageBlobService.getFileByToken(fileName, token));
        }
        String contentType = Files.probeContentType(file.toPath());
        ContentDisposition contentDisposition = ContentDisposition.inline()