liyong
11 小时以前 1ca5584d7e3200a9af65a099bd26d3593e2ba702
src/main/java/com/ruoyi/basic/utils/FileUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,847 @@
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;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.mapper.StorageAttachmentMapper;
import com.ruoyi.basic.mapper.StorageBlobMapper;
import com.ruoyi.basic.pojo.StorageAttachment;
import com.ruoyi.basic.pojo.StorageBlob;
import com.ruoyi.common.config.FileProperties;
import io.jsonwebtoken.Jwts;
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.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class FileUtil {
    private final FileProperties properties;
    private final StorageAttachmentMapper storageAttachmentMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final StorageBlobMapper storageBlobMapper;
    private static final String TOKEN_USAGE_KEY_PREFIX = "file:token:usage:";
    private static final DateTimeFormatter YEAR_PATH_FORMATTER = DateTimeFormatter.ofPattern("yyyy");
    private static final DateTimeFormatter MONTH_DAY_PATH_FORMATTER = DateTimeFormatter.ofPattern("MMdd");
    /**
     * ä¿å­˜é™„件信息
     *
     * @param application     æ–‡ä»¶ç”¨é€”
     * @param recordType      å…³è”记录类型
     * @param recordId        å…³è”记录id
     * @param storageBlobDTOS æ–‡ä»¶ä¿¡æ¯
     */
    public void saveStorageAttachment(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, List<StorageBlobDTO> storageBlobDTOS) {
        if (application == null) {
            throw new RuntimeException("文件用途不能为空");
        }
        if (recordType == null) {
            throw new RuntimeException("关联记录类型不能为空");
        }
        if (recordId == null || recordId <= 0) {
            throw new RuntimeException("关联记录id不能为空");
        }
        // åˆ é™¤æ—§é™„件信息
        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());
            storageAttachment.setDeleted(0L);
            storageAttachments.add(storageAttachment);
        }
        storageAttachmentMapper.insert(storageAttachments);
    }
    /**
     * åˆ é™¤æ–‡ä»¶ä¿¡æ¯
     *
     * @param storageBlobIds æ–‡ä»¶id
     */
    public void deleteStorageBlobs(List<Long> storageBlobIds) {
        storageBlobMapper.deleteByIds(storageBlobIds);
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id删除文件信息
     *
     * @param storageAttachmentIds æ–‡ä»¶id
     */
    public void deleteStorageBlobsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
        List<StorageAttachment> storageAttachments = storageAttachmentMapper.selectByIds(storageAttachmentIds);
        List<Long> storageBlobIds = storageAttachments.stream().map(StorageAttachment::getStorageBlobId).collect(Collectors.toList());
        deleteStorageBlobs(storageBlobIds);
    }
    /**
     * é€šè¿‡æ–‡ä»¶ç”¨é€”、关联记录类型、关联记录id删除文件信息
     *
     * @param application æ–‡ä»¶ç”¨é€”
     * @param recordType  å…³è”记录类型
     * @param recordIds    å…³è”记录id
     */
    public void deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List<Long> recordIds) {
        if (recordIds == null || recordIds.isEmpty()) {
            throw new RuntimeException("关联记录id不能为空");
        }
        if (application == null) {
            throw new RuntimeException("文件用途不能为空");
        }
        if (recordType == null) {
            throw new RuntimeException("关联记录类型不能为空");
        }
        List<StorageAttachment> storageAttachments = storageAttachmentMapper.selectList(new LambdaQueryWrapper<StorageAttachment>()
                .eq(StorageAttachment::getRecordType, recordType.getType())
                .in(StorageAttachment::getRecordId, recordIds)
                .eq(StorageAttachment::getApplication, application.getType()));
        if (CollectionUtils.isNotEmpty(storageAttachments)) {
            List<Long> storageAttachmentIds = storageAttachments.stream().map(StorageAttachment::getStorageBlobId)
                    .collect(Collectors.toList());
            deleteStorageBlobsByStorageAttachmentIds(storageAttachmentIds);
        }
    }
    /**
     * é€šè¿‡å…³è”记录类型、关联记录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
     */
    public void deleteStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
        deleteStorageBlobsByStorageAttachmentIds(storageAttachmentIds);
        storageAttachmentMapper.deleteByIds(storageAttachmentIds);
    }
    /**
     * åˆ é™¤æ–‡ä»¶å…³è”信息
     *
     * @param application æ–‡ä»¶ç”¨é€”
     * @param recordType  å…³è”记录类型
     * @param recordId    å…³è”记录id
     */
    public void deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId) {
        if (recordId == null || recordId <= 0) {
            throw new RuntimeException("关联记录id不能为空");
        }
        if (application == null) {
            throw new RuntimeException("文件用途不能为空");
        }
        if (recordType == null) {
            throw new RuntimeException("关联记录类型不能为空");
        }
        deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(application, recordType, Arrays.asList(recordId));
        storageAttachmentMapper.delete(new LambdaQueryWrapper<StorageAttachment>()
                .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));
    }
    /**
     * æ‰¹é‡åˆ é™¤æ–‡ä»¶å…³è”信息 attachment
     *
     * @param application æ–‡ä»¶ç”¨é€”
     * @param recordType  å…³è”记录类型
     * @param recordIds   å…³è”记录id
     */
    public void deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List<Long> recordIds) {
        if (recordIds == null || recordIds.isEmpty()) {
            throw new RuntimeException("关联记录id不能为空");
        }
        if (application == null) {
            throw new RuntimeException("文件用途不能为空");
        }
        if (recordType == null) {
            throw new RuntimeException("关联记录类型不能为空");
        }
        deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(application, recordType, recordIds);
        storageAttachmentMapper.delete(new LambdaQueryWrapper<StorageAttachment>()
                .eq(StorageAttachment::getRecordType, recordType.getType())
                .in(StorageAttachment::getRecordId, recordIds)
                .eq(StorageAttachment::getApplication, application.getType()));
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件信息 attachment
     *
     * @param storageAttachmentIds æ–‡ä»¶id
     */
    public List<StorageAttachment> getStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
        if (CollectionUtils.isEmpty(storageAttachmentIds)) {
            throw new RuntimeException("文件id不能为空");
        }
        return storageAttachmentMapper.selectByIds(storageAttachmentIds);
    }
    /**
     * é€šè¿‡è®°å½•类型获取文件信息 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 æ–‡ä»¶ç”¨é€”
     * @param recordType  å…³è”记录类型
     * @param recordId    å…³è”记录id
     */
    public List<StorageAttachment> getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId) {
        if (recordId == null || recordId <= 0) {
            throw new RuntimeException("关联记录id不能为空");
        }        if (application == null) {
            throw new RuntimeException("文件用途不能为空");
        }
        if (recordType == null) {
            throw new RuntimeException("关联记录类型不能为空");
        }
        return storageAttachmentMapper.selectList(new LambdaQueryWrapper<StorageAttachment>()
                .eq(StorageAttachment::getRecordType, recordType.getType())
                .eq(StorageAttachment::getRecordId, recordId)
                .eq(StorageAttachment::getApplication, application.getType()));
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件信息 blob
     *
     * @param storageAttachmentIds æ–‡ä»¶id
     */
    public List<StorageBlobVO> getStorageBlobVOsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByStorageAttachmentIds(storageAttachmentIds);
        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<>();
        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获取文件关联信息 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));
    }
    /**
     * é€šè¿‡æ–‡ä»¶ç”¨é€”、关联记录类型、关联记录id获取文件信息 blob
     *
     * @param application æ–‡ä»¶ç”¨é€”
     * @param recordType  å…³è”记录类型
     * @param recordId    å…³è”记录id
     */
    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;
        }
        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));
            storageBlobDTOS.add(storageBlobVO);
        }
        return storageBlobDTOS;
    }
    /**
     * é€šè¿‡æ–‡ä»¶ç”¨é€”、关联记录类型、关联记录id获取文件信息 è‡ªå®šä¹‰è¿‡æœŸæ—¶é—´ï¼ˆåˆ†é’Ÿï¼‰ blob
     *
     * @param application æ–‡ä»¶ç”¨é€”
     * @param recordType  å…³è”记录类型
     * @param recordId    å…³è”记录id
     * @param expired     è¿‡æœŸæ—¶é—´
     */
    public List<StorageBlobVO> getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired) {
        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(buildSignedUrl(storageBlobVO, "/preview/", expired));
            storageBlobVO.setDownloadURL(buildSignedUrl(storageBlobVO, "/download/", expired));
            storageBlobVO.setStorageAttachmentId(blobIdToAttachmentIdMap.get(storageBlob.getId()));
            storageBlobDTOS.add(storageBlobVO);
        }
        return storageBlobDTOS;
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件信息存在过期时间 è‡ªå®šä¹‰è¿‡æœŸæ—¶é—´ï¼ˆåˆ†é’Ÿï¼‰ blob
     *
     * @param storageAttachmentIds æ–‡ä»¶id
     * @param expired              è¿‡æœŸæ—¶é—´
     */
    public List<StorageBlobVO> getStorageBlobVOsByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByStorageAttachmentIds(storageAttachmentIds);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return null;
        }
        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(buildSignedUrl(storageBlobVO, "/preview/", expired));
            storageBlobVO.setDownloadURL(buildSignedUrl(storageBlobVO, "/download/", expired));
            storageBlobDTOS.add(storageBlobVO);
        }
        return storageBlobDTOS;
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件信息 attachment
     *
     * @param storageAttachmentIds æ–‡ä»¶id
     */
    public List<StorageAttachmentVO> getStorageAttachmentVOSByStorageAttachmentIds(List<Long> storageAttachmentIds) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByStorageAttachmentIds(storageAttachmentIds);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return new ArrayList<>();
        }
        List<StorageAttachmentVO> storageAttachmentVOS = new ArrayList<>();
        for (StorageAttachment storageAttachment : storageAttachments) {
            StorageAttachmentVO storageAttachmentVO = new StorageAttachmentVO();
            BeanUtils.copyProperties(storageAttachment, storageAttachmentVO);
            List<StorageBlobVO> storageBlobVOS = getStorageBlobVOsByStorageAttachmentIds(Collections.singletonList(storageAttachment.getId()));
            if (CollectionUtils.isEmpty(storageBlobVOS)) {
                storageAttachmentVO.setStorageBlobVOS(new ArrayList<>());
            } else {
                storageAttachmentVO.setStorageBlobVOS(storageBlobVOS);
            }
            storageAttachmentVOS.add(storageAttachmentVO);
        }
        return storageAttachmentVOS;
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件信息存在过期时间 è‡ªå®šä¹‰è¿‡æœŸæ—¶é—´ï¼ˆåˆ†é’Ÿï¼‰ attachment
     *
     * @param storageAttachmentIds æ–‡ä»¶id
     * @param expired              è¿‡æœŸæ—¶é—´
     */
    public List<StorageAttachmentVO> getStorageAttachmentVOSByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByStorageAttachmentIds(storageAttachmentIds);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return new ArrayList<>();
        }
        List<StorageAttachmentVO> storageAttachmentVOS = new ArrayList<>();
        for (StorageAttachment storageAttachment : storageAttachments) {
            StorageAttachmentVO storageAttachmentVO = new StorageAttachmentVO();
            BeanUtils.copyProperties(storageAttachment, storageAttachmentVO);
            List<StorageBlobVO> storageBlobVOS = getStorageBlobVOsByStorageAttachmentIds(Collections.singletonList(storageAttachment.getId()), expired);
            if (CollectionUtils.isEmpty(storageBlobVOS)) {
                storageAttachmentVO.setStorageBlobVOS(new ArrayList<>());
            } else {
                storageAttachmentVO.setStorageBlobVOS(storageBlobVOS);
            }
            storageAttachmentVOS.add(storageAttachmentVO);
        }
        return storageAttachmentVOS;
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件信息 attachment
     *
     * @param application åº”用
     * @param recordType  è®°å½•类型
     * @param recordId    è®°å½•id
     */
    public List<StorageAttachmentVO> getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return new ArrayList<>();
        }
        List<StorageAttachmentVO> storageAttachmentVOS = new ArrayList<>();
        for (StorageAttachment storageAttachment : storageAttachments) {
            StorageAttachmentVO storageAttachmentVO = new StorageAttachmentVO();
            BeanUtils.copyProperties(storageAttachment, storageAttachmentVO);
            List<StorageBlobVO> storageBlobVOS = getStorageBlobVOsByStorageAttachmentIds(Collections.singletonList(storageAttachment.getId()));
            if (CollectionUtils.isEmpty(storageBlobVOS)) {
                storageAttachmentVO.setStorageBlobVOS(new ArrayList<>());
            } else {
                storageAttachmentVO.setStorageBlobVOS(storageBlobVOS);
            }
            storageAttachmentVOS.add(storageAttachmentVO);
        }
        return storageAttachmentVOS;
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件信息存在过期时间 è‡ªå®šä¹‰è¿‡æœŸæ—¶é—´ï¼ˆåˆ†é’Ÿï¼‰ attachment
     *
     * @param application åº”用
     * @param recordType  è®°å½•类型
     * @param recordId    è®°å½•id
     * @param expired     è¿‡æœŸæ—¶é—´
     */
    public List<StorageAttachmentVO> getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired) {
        List<StorageAttachment> storageAttachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
        if (CollectionUtils.isEmpty(storageAttachments)) {
            return new ArrayList<>();
        }
        List<StorageAttachmentVO> storageAttachmentVOS = new ArrayList<>();
        for (StorageAttachment storageAttachment : storageAttachments) {
            StorageAttachmentVO storageAttachmentVO = new StorageAttachmentVO();
            BeanUtils.copyProperties(storageAttachment, storageAttachmentVO);
            List<StorageBlobVO> storageBlobVOS = getStorageBlobVOsByStorageAttachmentIds(Collections.singletonList(storageAttachment.getId()), expired);
            if (CollectionUtils.isEmpty(storageBlobVOS)) {
                storageAttachmentVO.setStorageBlobVOS(new ArrayList<>());
            } else {
                storageAttachmentVO.setStorageBlobVOS(storageBlobVOS);
            }
            storageAttachmentVOS.add(storageAttachmentVO);
        }
        return storageAttachmentVOS;
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件预览地址
     *
     * @param storageAttachmentIds æ–‡ä»¶å…³è”id
     */
    public List<String> getFilePreviewURLByStorageAttachmentIds(List<Long> storageAttachmentIds) {
        List<String> res = new ArrayList<>();
        List<StorageBlobVO> storageBlobVOS = getStorageBlobVOsByStorageAttachmentIds(storageAttachmentIds);
        for (StorageBlobVO storageBlobVO : storageBlobVOS) {
            res.add(buildSignedPreviewUrl(storageBlobVO));
        }
        return res;
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件预览地址存在过期时间 è‡ªå®šä¹‰è¿‡æœŸæ—¶é—´ï¼ˆåˆ†é’Ÿï¼‰
     *
     * @param storageAttachmentIds æ–‡ä»¶å…³è”id
     * @param expired              è¿‡æœŸæ—¶é—´
     */
    public List<String> getFilePreviewURLByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired) {
        List<String> res = new ArrayList<>();
        List<StorageBlobVO> storageBlobVOS = getStorageBlobVOsByStorageAttachmentIds(storageAttachmentIds);
        for (StorageBlobVO storageBlobVO : storageBlobVOS) {
            res.add(buildSignedUrl(storageBlobVO, "/preview/", expired));
        }
        return res;
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件下载地址
     *
     * @param storageAttachmentIds æ–‡ä»¶å…³è”id
     */
    public List<String> getFileDownloadURLByStorageAttachmentIds(List<Long> storageAttachmentIds) {
        List<String> res = new ArrayList<>();
        List<StorageBlobVO> storageBlobVOS = getStorageBlobVOsByStorageAttachmentIds(storageAttachmentIds);
        for (StorageBlobVO storageBlobVO : storageBlobVOS) {
            res.add(buildSignedDownloadUrl(storageBlobVO));
        }
        return res;
    }
    /**
     * é€šè¿‡æ–‡ä»¶å…³è”id获取文件下载地址存在过期时间 è‡ªå®šä¹‰è¿‡æœŸæ—¶é—´ï¼ˆåˆ†é’Ÿï¼‰
     *
     * @param storageAttachmentIds æ–‡ä»¶å…³è”id
     * @param expired              è¿‡æœŸæ—¶é—´
     */
    public List<String> getFileDownloadURLByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired) {
        List<String> res = new ArrayList<>();
        List<StorageBlobVO> storageBlobVOS = getStorageBlobVOsByStorageAttachmentIds(storageAttachmentIds);
        for (StorageBlobVO storageBlobVO : storageBlobVOS) {
            res.add(buildSignedUrl(storageBlobVO, "/download/", expired));
        }
        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());
    }
    public String buildSignedDownloadUrl(StorageBlobVO storageBlob) {
        return buildSignedUrl(storageBlob, "/download/", properties.getExpired());
    }
    /**
     * æž„建带签名的URL
     *
     * @param storageBlob æ–‡ä»¶å…ƒæ•°æ®
     * @param actionPath  æ“ä½œè·¯å¾„ "/preview/" or "/download/"
     * @param expired     è¿‡æœŸæ—¶é—´ å¦‚果不配置,不传参,将使用默认值120分钟
     * @return å¸¦ç­¾åçš„URL
     */
    public String buildSignedUrl(StorageBlobVO storageBlob, String actionPath, BigDecimal expired) {
        if (!Arrays.asList("/preview/", "/download/").contains(actionPath)) {
            throw new IllegalArgumentException("操作路径参数错误");
        }
        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();
        BigDecimal expiredValue = expired == null ? new BigDecimal("120") : expired;
        long expiredMillis = expiredValue.multiply(new BigDecimal("60000")).longValue();
        if (expiredMillis <= 0L) {
            expiredMillis = 2L * 60L * 60L * 1000L;
        }
        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()
                .subject(storageBlob.getUidFilename())
                .issuedAt(issuedAt)       // æ–°ç‰ˆå»ºè®®ç›´æŽ¥è°ƒç”¨ .issuedAt()
                .expiration(expiration)   // æ–°ç‰ˆå»ºè®®ç›´æŽ¥è°ƒç”¨ .expiration()
                .claim("path", storageBlob.getPath())
                .claim("resourceKey", storageBlob.getResourceKey())
                .signWith(key)            // é‡ç‚¹ï¼šä¼ å…¥ä¸Šé¢ç”Ÿæˆçš„ key å¯¹è±¡ï¼Œè€Œä¸æ˜¯ String
                .compact();
        cacheTokenUsage(token, expiredMillis);
        return baseUrl + "?token=" + token;
    }
    private void cacheTokenUsage(String token, long expiredMillis) {
        if (!StringUtils.hasText(token)) {
            return;
        }
        long ttl = expiredMillis > 0L ? expiredMillis : 2L * 60L * 60L * 1000L;
        stringRedisTemplate.opsForValue().set(buildTokenUsageKey(token), "0", ttl, TimeUnit.MILLISECONDS);
    }
    private String buildTokenUsageKey(String token) {
        return TOKEN_USAGE_KEY_PREFIX + token;
    }
    public String buildRelativePath() {
        LocalDate now = LocalDate.now();
        return now.format(YEAR_PATH_FORMATTER) + "/" + now.format(MONTH_DAY_PATH_FORMATTER);
    }
    public void validateTokenUsage(String token) {
        String redisKey = buildTokenUsageKey(token);
        String currentCountValue = stringRedisTemplate.opsForValue().get(redisKey);
        if (!StringUtils.hasText(currentCountValue)) {
            throw new IllegalArgumentException("链接已过期或达到使用次数失效");
        }
        long currentCount = Long.parseLong(currentCountValue);
        int limit = resolveLimit();
        if (currentCount >= limit) {
            stringRedisTemplate.delete(redisKey);
            throw new IllegalArgumentException("链接达到使用次数失效");
        }
        Long updatedCount = stringRedisTemplate.opsForValue().increment(redisKey);
        if (updatedCount != null && updatedCount >= limit) {
            stringRedisTemplate.delete(redisKey);
        }
    }
    private int resolveLimit() {
        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);
    }
}