2 天以前 a77decc3df26449414639082293e2ee6722e643c
src/main/java/com/ruoyi/basic/utils/FileUtil.java
@@ -1,7 +1,10 @@
package com.ruoyi.basic.utils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.dto.StorageAttachmentDTO;
import com.ruoyi.basic.dto.StorageAttachmentVO;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
@@ -52,9 +55,6 @@
     * @param storageBlobDTOS 文件信息
     */
    public void saveStorageAttachment(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, List<StorageBlobDTO> storageBlobDTOS) {
        if (CollectionUtils.isEmpty(storageBlobDTOS)) {
            throw new RuntimeException("文件信息不能为空");
        }
        if (application == null) {
            throw new RuntimeException("文件用途不能为空");
        }
@@ -66,10 +66,54 @@
        }
        // 删除旧附件信息
        deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
        if (CollectionUtils.isEmpty(storageBlobDTOS)) {
            return;
        }
        List<StorageAttachment> storageAttachments = new ArrayList<>();
        for (StorageBlobDTO storageBlobDTO : storageBlobDTOS) {
            StorageAttachment storageAttachment = new StorageAttachment();
            storageAttachment.setApplication(application.getType());
            storageAttachment.setRecordType(recordType.getType());
            storageAttachment.setRecordId(recordId);
            storageAttachment.setStorageBlobId(storageBlobDTO.getId());
            storageAttachment.setDeleted(0L);
            storageAttachments.add(storageAttachment);
        }
        storageAttachmentMapper.insert(storageAttachments);
    }
    /**
     * 保存附件信息
     *
     * @param recordType      关联记录类型
     * @param recordId        关联记录id
     * @param storageBlobDTOS 文件信息
     */
    public void saveStorageAttachmentByRecordTypeAndRecordId(String application,RecordTypeEnum recordType, Long recordId, List<StorageBlobDTO> storageBlobDTOS) {
        if (recordType == null) {
            throw new RuntimeException("关联记录类型不能为空");
        }
        if (recordId == null) {
            throw new RuntimeException("关联记录id不能为空");
        }
        // 删除旧附件信息
        if (application == null) {
            for (StorageBlobDTO storageBlobDTO : storageBlobDTOS) {
                deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.getByType(storageBlobDTO.getApplication()), recordType, recordId);
            }
        } else {
            deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.getByType(application), recordType, recordId);
        }
        if (CollectionUtils.isEmpty(storageBlobDTOS)) {
            deleteStorageAttachmentsByRecordTypeAndRecordId(recordType, recordId);
        }
        List<StorageAttachment> storageAttachments = new ArrayList<>();
        for (StorageBlobDTO storageBlobDTO : storageBlobDTOS) {
            StorageAttachment storageAttachment = new StorageAttachment();
            storageAttachment.setApplication(Objects.requireNonNullElseGet(application, () -> ApplicationTypeEnum.getByType(storageBlobDTO.getApplication()).getType()));
            storageAttachment.setRecordType(recordType.getType());
            storageAttachment.setRecordId(recordId);
            storageAttachment.setStorageBlobId(storageBlobDTO.getId());
@@ -128,6 +172,29 @@
    }
    /**
     * 通过关联记录类型、关联记录id删除文件信息
     *
     * @param recordType  关联记录类型
     * @param recordId    关联记录id
     */
    public void deleteStorageBlobsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId) {
        if (recordId == null || recordId <= 0) {
            throw new RuntimeException("关联记录id不能为空");
        }
        if (recordType == null) {
            throw new RuntimeException("关联记录类型不能为空");
        }
        List<StorageAttachment> storageAttachments = storageAttachmentMapper.selectList(new LambdaQueryWrapper<StorageAttachment>()
                .eq(StorageAttachment::getRecordType, recordType.getType())
                .eq(StorageAttachment::getRecordId, recordId));
        if (CollectionUtils.isNotEmpty(storageAttachments)) {
            List<Long> storageAttachmentIds = storageAttachments.stream().map(StorageAttachment::getStorageBlobId)
                    .collect(Collectors.toList());
            deleteStorageBlobsByStorageAttachmentIds(storageAttachmentIds);
        }
    }
    /**
     * 删除文件关联信息
     *
     * @param storageAttachmentIds 文件关联id
@@ -159,6 +226,25 @@
                .eq(StorageAttachment::getRecordType, recordType.getType())
                .eq(StorageAttachment::getRecordId, recordId)
                .eq(StorageAttachment::getApplication, application.getType()));
    }
    /**
     * 删除文件关联信息
     *
     * @param recordType  关联记录类型
     * @param recordId    关联记录id
     */
    public void deleteStorageAttachmentsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId) {
        if (recordId == null || recordId <= 0) {
            throw new RuntimeException("关联记录id不能为空");
        }
        if (recordType == null) {
            throw new RuntimeException("关联记录类型不能为空");
        }
        deleteStorageBlobsByRecordTypeAndRecordId(recordType, recordId);
        storageAttachmentMapper.delete(new LambdaQueryWrapper<StorageAttachment>()
                .eq(StorageAttachment::getRecordType, recordType.getType())
                .eq(StorageAttachment::getRecordId, recordId));
    }
    /**
@@ -198,6 +284,25 @@
    }
    /**
     * 通过记录类型获取文件信息 attachment(分页)
     *
     * @param storageAttachmentDTO 关联记录信息
     */
    public List<StorageBlobVO> getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(StorageAttachmentDTO storageAttachmentDTO) {
        LambdaQueryWrapper<StorageAttachment> queryWrapper = new LambdaQueryWrapper<StorageAttachment>()
                .eq(StorageAttachment::getRecordType, storageAttachmentDTO.getRecordType())
                .eq(StorageAttachment::getRecordId, storageAttachmentDTO.getRecordId());
        if (storageAttachmentDTO.getApplication() != null) {
            queryWrapper.eq(StorageAttachment::getApplication, storageAttachmentDTO.getApplication());
        }
        List<StorageAttachment> storageAttachments = storageAttachmentMapper.selectList(queryWrapper);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return null;
        }
        return getStorageBlobVOsByStorageAttachmentIds(storageAttachments.stream().map(StorageAttachment::getId).collect(Collectors.toList()));
    }
    /**
     * 通过文件用途、关联记录类型、关联记录id获取文件关联信息 attachment
     *
     * @param application 文件用途
@@ -229,6 +334,9 @@
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return null;
        }
        Map<Long, Long> blobIdToAttachmentIdMap = storageAttachments.stream()
                .collect(Collectors.toMap(StorageAttachment::getStorageBlobId, StorageAttachment::getId));
        List<Long> storageBlobIds = storageAttachments.stream().map(StorageAttachment::getStorageBlobId).collect(Collectors.toList());
        List<StorageBlob> storageBlobs = storageBlobMapper.selectByIds(storageBlobIds);
        List<StorageBlobVO> storageBlobDTOS = new ArrayList<>();
@@ -237,9 +345,28 @@
            BeanUtils.copyProperties(storageBlob, storageBlobVO);
            storageBlobVO.setPreviewURL(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setDownloadURL(buildSignedDownloadUrl(storageBlobVO));
            storageBlobVO.setStorageAttachmentId(blobIdToAttachmentIdMap.get(storageBlob.getId()));
            storageBlobDTOS.add(storageBlobVO);
        }
        return storageBlobDTOS;
    }
    /**
     * 通过文件用途、关联记录类型、关联记录id获取文件关联信息 attachment
     *
     * @param recordType  关联记录类型
     * @param recordId    关联记录id
     */
    public List<StorageAttachment> getStorageAttachmentsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId) {
        if (recordId == null || recordId <= 0) {
            throw new RuntimeException("关联记录id不能为空");
        }
        if (recordType == null) {
            throw new RuntimeException("关联记录类型不能为空");
        }
        return storageAttachmentMapper.selectList(new LambdaQueryWrapper<StorageAttachment>()
                .eq(StorageAttachment::getRecordType, recordType.getType())
                .eq(StorageAttachment::getRecordId, recordId));
    }
    /**
@@ -251,6 +378,35 @@
     */
    public List<StorageBlobVO> getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return null;
        }
        // 构建 storageBlobId -> storageAttachmentId 的映射
        Map<Long, Long> blobIdToAttachmentIdMap = storageAttachments.stream()
                .collect(Collectors.toMap(StorageAttachment::getStorageBlobId, StorageAttachment::getId));
        List<Long> storageBlobIds = storageAttachments.stream().map(StorageAttachment::getStorageBlobId).collect(Collectors.toList());
        List<StorageBlob> storageBlobs = storageBlobMapper.selectByIds(storageBlobIds);
        List<StorageBlobVO> storageBlobDTOS = new ArrayList<>();
        for (StorageBlob storageBlob : storageBlobs) {
            StorageBlobVO storageBlobVO = new StorageBlobVO();
            BeanUtils.copyProperties(storageBlob, storageBlobVO);
            storageBlobVO.setPreviewURL(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setDownloadURL(buildSignedDownloadUrl(storageBlobVO));
            storageBlobVO.setStorageAttachmentId(blobIdToAttachmentIdMap.get(storageBlob.getId()));
            storageBlobDTOS.add(storageBlobVO);
        }
        return storageBlobDTOS;
    }
    /**
     * 通过文件用途、关联记录类型、关联记录id获取文件信息 blob
     *
     * @param recordType  关联记录类型
     * @param recordId    关联记录id
     */
    public List<StorageBlobVO> getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByRecordTypeAndRecordId(recordType, recordId);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return null;
        }
@@ -280,6 +436,10 @@
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return null;
        }
        // 构建 storageBlobId -> storageAttachmentId 的映射
        Map<Long, Long> blobIdToAttachmentIdMap = storageAttachments.stream()
                .collect(Collectors.toMap(StorageAttachment::getStorageBlobId, StorageAttachment::getId));
        List<Long> storageBlobIds = storageAttachments.stream().map(StorageAttachment::getStorageBlobId).collect(Collectors.toList());
        List<StorageBlob> storageBlobs = storageBlobMapper.selectByIds(storageBlobIds);
        List<StorageBlobVO> storageBlobDTOS = new ArrayList<>();
@@ -288,6 +448,7 @@
            BeanUtils.copyProperties(storageBlob, storageBlobVO);
            storageBlobVO.setPreviewURL(buildSignedUrl(storageBlobVO, "/preview/", expired));
            storageBlobVO.setDownloadURL(buildSignedUrl(storageBlobVO, "/download/", expired));
            storageBlobVO.setStorageAttachmentId(blobIdToAttachmentIdMap.get(storageBlob.getId()));
            storageBlobDTOS.add(storageBlobVO);
        }
        return storageBlobDTOS;
@@ -481,6 +642,68 @@
        return res;
    }
    /**
     * 通过文件关联id获取文件预览地址
     *
     * @param application 应用
     * @param recordType  记录类型
     * @param recordId    记录id
     */
    public List<String> getFilePreviewURLByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return new ArrayList<>();
        }
        return getFilePreviewURLByStorageAttachmentIds(storageAttachments.stream().map(StorageAttachment::getId).collect(Collectors.toList()));
    }
    /**
     * 通过文件关联id获取文件预览地址存在过期时间
     *
     * @param application 应用
     * @param recordType  记录类型
     * @param recordId    记录id
     * @param expired     过期时间(分钟)
     */
    public List<String> getFilePreviewURLByApplicationAndRecordTypeAndRecordIdAndExpired(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return new ArrayList<>();
        }
        return getFilePreviewURLByStorageAttachmentIds(storageAttachments.stream().map(StorageAttachment::getId).collect(Collectors.toList()), expired);
    }
    /**
     * 通过文件关联id获取文件下载地址
     *
     * @param application 应用
     * @param recordType  记录类型
     * @param recordId    记录id
     */
    public List<String> getFileDownloadURLByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return new ArrayList<>();
        }
        return getFileDownloadURLByStorageAttachmentIds(storageAttachments.stream().map(StorageAttachment::getId).collect(Collectors.toList()));
    }
    /**
     * 通过文件关联id获取文件下载地址存在过期时间
     *
     * @param application 应用
     * @param recordType  记录类型
     * @param recordId    记录id
     * @param expired     过期时间(分钟)
     */
    public List<String> getFileDownloadURLByApplicationAndRecordTypeAndRecordIdAndExpired(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return new ArrayList<>();
        }
        return getFileDownloadURLByStorageAttachmentIds(storageAttachments.stream().map(StorageAttachment::getId).collect(Collectors.toList()), expired);
    }
    public String buildSignedPreviewUrl(StorageBlobVO storageBlob) {
        return buildSignedUrl(storageBlob, "/preview/", properties.getExpired());
    }
@@ -503,8 +726,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;
        }
@@ -521,16 +764,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) {