package com.ruoyi.basic.utils;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
import com.ruoyi.basic.constant.ApplicationType;
|
import com.ruoyi.basic.constant.RecordType;
|
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 lombok.RequiredArgsConstructor;
|
import org.springframework.beans.BeanUtils;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.stereotype.Component;
|
import org.springframework.util.StringUtils;
|
|
import java.math.BigDecimal;
|
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 StorageBlobMapper storageBlobMapper;
|
private final StringRedisTemplate stringRedisTemplate;
|
|
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 storageBlobVOS 文件信息
|
*/
|
public void saveStorageAttachment(ApplicationType application, RecordType recordType, Long recordId, List<StorageBlobVO> storageBlobVOS) {
|
if (CollectionUtils.isEmpty(storageBlobVOS)) {
|
throw new RuntimeException("文件信息不能为空");
|
}
|
if (!application.isValid()) {
|
throw new RuntimeException("文件用途不能为空");
|
}
|
if (!recordType.isValid()) {
|
throw new RuntimeException("关联记录类型不能为空");
|
}
|
if (recordId == null || recordId <= 0) {
|
throw new RuntimeException("关联记录id不能为空");
|
}
|
// 删除旧附件信息
|
deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
|
List<StorageAttachment> storageAttachments = new ArrayList<>();
|
for (StorageBlobVO storageBlobVO : storageBlobVOS) {
|
StorageAttachment storageAttachment = new StorageAttachment();
|
storageAttachment.setApplication(application.getType());
|
storageAttachment.setRecordType(recordType.getType());
|
storageAttachment.setRecordId(recordId);
|
storageAttachment.setStorageBlobId(storageBlobVO.getId());
|
storageAttachment.setDeleted(0L);
|
}
|
// todo fileChange
|
// storageAttachmentMapper.insert(storageAttachments);
|
}
|
|
/**
|
* 删除文件信息
|
*
|
* @param storageBlobIds 文件id
|
*/
|
public void deleteStorageBlobs(List<Long> storageBlobIds) {
|
// todo fileChange
|
// storageBlobMapper.deleteByIds(storageBlobIds);
|
}
|
|
/**
|
* 通过文件关联id删除文件信息
|
*
|
* @param storageAttachmentIds 文件id
|
*/
|
public void deleteStorageBlobsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
|
// todo fileChange
|
// 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 recordId 关联记录id
|
*/
|
public void deleteStorageBlobsByApplicationAndRecordTypeAndRecordId(ApplicationType application, RecordType recordType, Long recordId) {
|
if (!application.isValid()) {
|
throw new RuntimeException("文件用途不能为空");
|
}
|
if (!recordType.isValid()) {
|
throw new RuntimeException("关联记录类型不能为空");
|
}
|
if (recordId == null || recordId <= 0) {
|
throw new RuntimeException("关联记录id不能为空");
|
}
|
List<StorageAttachment> storageAttachments = storageAttachmentMapper.selectList(new LambdaQueryWrapper<StorageAttachment>()
|
.eq(StorageAttachment::getRecordType, recordType.getType())
|
.eq(StorageAttachment::getRecordId, recordId)
|
.eq(StorageAttachment::getApplication, application.getType()));
|
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);
|
// todo fileChange
|
// storageAttachmentMapper.deleteByIds(storageAttachmentIds);
|
}
|
|
public void deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationType application, RecordType recordType, Long recordId) {
|
if (!application.isValid()) {
|
throw new RuntimeException("文件用途不能为空");
|
}
|
if (!recordType.isValid()) {
|
throw new RuntimeException("关联记录类型不能为空");
|
}
|
if (recordId == null || recordId <= 0) {
|
throw new RuntimeException("关联记录id不能为空");
|
}
|
deleteStorageBlobsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
|
storageAttachmentMapper.delete(new LambdaQueryWrapper<StorageAttachment>()
|
.eq(StorageAttachment::getRecordType, recordType.getType())
|
.eq(StorageAttachment::getRecordId, recordId)
|
.eq(StorageAttachment::getApplication, application.getType()));
|
}
|
|
/**
|
* 通过文件关联id获取文件信息
|
*
|
* @param storageAttachmentIds 文件id
|
*/
|
public List<StorageAttachment> getStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
|
if (CollectionUtils.isEmpty(storageAttachmentIds)) {
|
throw new RuntimeException("文件id不能为空");
|
}
|
// todo fileChange
|
// return storageAttachmentMapper.selectByIds(storageAttachmentIds);
|
return new ArrayList<>();
|
}
|
|
/**
|
* 通过文件用途、关联记录类型、关联记录id获取文件关联信息
|
*
|
* @param application 文件用途
|
* @param recordType 关联记录类型
|
* @param recordId 关联记录id
|
*/
|
public List<StorageAttachment> getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationType application, RecordType recordType, Long recordId) {
|
if (!application.isValid()) {
|
throw new RuntimeException("文件用途不能为空");
|
}
|
if (!recordType.isValid()) {
|
throw new RuntimeException("关联记录类型不能为空");
|
}
|
if (recordId == null || recordId <= 0) {
|
throw new RuntimeException("关联记录id不能为空");
|
}
|
return storageAttachmentMapper.selectList(new LambdaQueryWrapper<StorageAttachment>()
|
.eq(StorageAttachment::getRecordType, recordType.getType())
|
.eq(StorageAttachment::getRecordId, recordId)
|
.eq(StorageAttachment::getApplication, application.getType()));
|
}
|
|
/**
|
* 通过文件关联id获取文件信息
|
*
|
* @param storageAttachmentIds 文件id
|
*/
|
public List<StorageBlobVO> getStorageBlobDTOsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
|
List<StorageAttachment> storageAttachments = getStorageAttachmentsByStorageAttachmentIds(storageAttachmentIds);
|
if (CollectionUtils.isEmpty(storageAttachments)) {
|
return null;
|
}
|
List<Long> storageBlobIds = storageAttachments.stream().map(StorageAttachment::getStorageBlobId).collect(Collectors.toList());
|
// todo fileChange
|
// List<StorageBlob> storageBlobs = storageBlobMapper.selectByIds(storageBlobIds);
|
// List<StorageBlobDTO> storageBlobDTOS = new ArrayList<>();
|
// for (StorageBlob storageBlob : storageBlobs) {
|
// StorageBlobDTO storageBlobDTO = new StorageBlobDTO();
|
// BeanUtils.copyProperties(storageBlob, storageBlobDTO);
|
// storageBlobDTO.setPreviewURL(buildSignedPreviewUrl(storageBlobDTO));
|
// storageBlobDTO.setDownloadURL(buildSignedDownloadUrl(storageBlobDTO));
|
// storageBlobDTOS.add(storageBlobDTO);
|
// }
|
return new ArrayList<>();
|
}
|
|
/**
|
* 通过文件关联id获取文件信息存在过期时间
|
*
|
* @param storageAttachmentIds 文件id
|
* @param expired 过期时间
|
*/
|
public List<StorageBlobVO> getStorageBlobDTOsByStorageAttachmentIds(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());
|
// todo fileChange
|
// List<StorageBlob> storageBlobs = storageBlobMapper.selectByIds(storageBlobIds);
|
// List<StorageBlobDTO> storageBlobDTOS = new ArrayList<>();
|
// for (StorageBlob storageBlob : storageBlobs) {
|
// StorageBlobDTO storageBlobDTO = new StorageBlobDTO();
|
// BeanUtils.copyProperties(storageBlob, storageBlobDTO);
|
// storageBlobDTO.setPreviewURL(buildSignedUrl(storageBlobDTO, "/preview/", expired));
|
// storageBlobDTO.setDownloadURL(buildSignedUrl(storageBlobDTO, "/download/", expired));
|
// storageBlobDTOS.add(storageBlobDTO);
|
// }
|
// return storageBlobDTOS;
|
return new ArrayList<>();
|
}
|
|
/**
|
* 通过文件关联id获取文件信息
|
*
|
* @param storageAttachmentIds 文件id
|
*/
|
public List<StorageAttachmentDTO> getStorageAttachmentDTOsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
|
List<StorageAttachment> storageAttachments = getStorageAttachmentsByStorageAttachmentIds(storageAttachmentIds);
|
if (CollectionUtils.isEmpty(storageAttachments)) {
|
return new ArrayList<>();
|
}
|
List<StorageAttachmentDTO> storageAttachmentDTOS = new ArrayList<>();
|
for (StorageAttachment storageAttachment : storageAttachments) {
|
StorageAttachmentDTO storageAttachmentDTO = new StorageAttachmentDTO();
|
BeanUtils.copyProperties(storageAttachment, storageAttachmentDTO);
|
List<StorageBlobVO> storageBlobVOS = getStorageBlobDTOsByStorageAttachmentIds(Collections.singletonList(storageAttachment.getId()));
|
if (CollectionUtils.isEmpty(storageBlobVOS)) {
|
storageAttachmentDTO.setStorageBlobVOS(new ArrayList<>());
|
} else {
|
storageAttachmentDTO.setStorageBlobVOS(storageBlobVOS);
|
}
|
storageAttachmentDTOS.add(storageAttachmentDTO);
|
}
|
return storageAttachmentDTOS;
|
}
|
|
/**
|
* 通过文件关联id获取文件信息存在过期时间
|
*
|
* @param storageAttachmentIds 文件id
|
* @param expired 过期时间
|
*/
|
public List<StorageAttachmentDTO> getStorageAttachmentDTOsByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired) {
|
List<StorageAttachment> storageAttachments = getStorageAttachmentsByStorageAttachmentIds(storageAttachmentIds);
|
if (CollectionUtils.isEmpty(storageAttachments)) {
|
return new ArrayList<>();
|
}
|
List<StorageAttachmentDTO> storageAttachmentDTOS = new ArrayList<>();
|
for (StorageAttachment storageAttachment : storageAttachments) {
|
StorageAttachmentDTO storageAttachmentDTO = new StorageAttachmentDTO();
|
BeanUtils.copyProperties(storageAttachment, storageAttachmentDTO);
|
List<StorageBlobVO> storageBlobVOS = getStorageBlobDTOsByStorageAttachmentIds(Collections.singletonList(storageAttachment.getId()), expired);
|
if (CollectionUtils.isEmpty(storageBlobVOS)) {
|
storageAttachmentDTO.setStorageBlobVOS(new ArrayList<>());
|
} else {
|
storageAttachmentDTO.setStorageBlobVOS(storageBlobVOS);
|
}
|
storageAttachmentDTOS.add(storageAttachmentDTO);
|
}
|
return storageAttachmentDTOS;
|
}
|
|
/**
|
* 通过文件关联id获取文件信息
|
*
|
* @param application 应用
|
* @param recordType 记录类型
|
* @param recordId 记录id
|
*/
|
public List<StorageAttachmentDTO> getStorageAttachmentDTOsByApplicationAndRecordTypeAndRecordId(ApplicationType application, RecordType recordType, Long recordId) {
|
List<StorageAttachment> storageAttachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
|
if (CollectionUtils.isEmpty(storageAttachments)) {
|
return new ArrayList<>();
|
}
|
List<StorageAttachmentDTO> storageAttachmentDTOS = new ArrayList<>();
|
for (StorageAttachment storageAttachment : storageAttachments) {
|
StorageAttachmentDTO storageAttachmentDTO = new StorageAttachmentDTO();
|
BeanUtils.copyProperties(storageAttachment, storageAttachmentDTO);
|
List<StorageBlobVO> storageBlobVOS = getStorageBlobDTOsByStorageAttachmentIds(Collections.singletonList(storageAttachment.getId()));
|
if (CollectionUtils.isEmpty(storageBlobVOS)) {
|
storageAttachmentDTO.setStorageBlobVOS(new ArrayList<>());
|
} else {
|
storageAttachmentDTO.setStorageBlobVOS(storageBlobVOS);
|
}
|
storageAttachmentDTOS.add(storageAttachmentDTO);
|
}
|
return storageAttachmentDTOS;
|
}
|
|
/**
|
* 通过文件关联id获取文件信息存在过期时间
|
*
|
* @param application 应用
|
* @param recordType 记录类型
|
* @param recordId 记录id
|
* @param expired 过期时间
|
*/
|
public List<StorageAttachmentDTO> getStorageAttachmentDTOsByApplicationAndRecordTypeAndRecordId(ApplicationType application, RecordType recordType, Long recordId, BigDecimal expired) {
|
List<StorageAttachment> storageAttachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
|
if (CollectionUtils.isEmpty(storageAttachments)) {
|
return new ArrayList<>();
|
}
|
List<StorageAttachmentDTO> storageAttachmentDTOS = new ArrayList<>();
|
for (StorageAttachment storageAttachment : storageAttachments) {
|
StorageAttachmentDTO storageAttachmentDTO = new StorageAttachmentDTO();
|
BeanUtils.copyProperties(storageAttachment, storageAttachmentDTO);
|
List<StorageBlobVO> storageBlobVOS = getStorageBlobDTOsByStorageAttachmentIds(Collections.singletonList(storageAttachment.getId()), expired);
|
if (CollectionUtils.isEmpty(storageBlobVOS)) {
|
storageAttachmentDTO.setStorageBlobVOS(new ArrayList<>());
|
} else {
|
storageAttachmentDTO.setStorageBlobVOS(storageBlobVOS);
|
}
|
storageAttachmentDTOS.add(storageAttachmentDTO);
|
}
|
return storageAttachmentDTOS;
|
}
|
|
/**
|
* 通过文件关联id获取文件预览地址
|
*
|
* @param storageAttachmentIds 文件关联id
|
*/
|
public List<String> getFilePreviewURLByStorageAttachmentIds(List<Long> storageAttachmentIds) {
|
List<String> res = new ArrayList<>();
|
List<StorageBlobVO> storageBlobVOS = getStorageBlobDTOsByStorageAttachmentIds(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 = getStorageBlobDTOsByStorageAttachmentIds(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 = getStorageBlobDTOsByStorageAttachmentIds(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 = getStorageBlobDTOsByStorageAttachmentIds(storageAttachmentIds);
|
for (StorageBlobVO storageBlobVO : storageBlobVOS) {
|
res.add(buildSignedUrl(storageBlobVO, "/download/", expired));
|
}
|
return res;
|
}
|
|
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("文件信息不完整");
|
}
|
long now = System.currentTimeMillis();
|
long expiredMillis = expired.multiply(new BigDecimal("60000")).longValue();
|
if (expiredMillis <= 0L) {
|
expiredMillis = 2L * 60L * 60L * 1000L;
|
}
|
Date issuedAt = new Date(now);
|
Date expiration = new Date(now + expiredMillis);
|
String token = Jwts.builder()
|
.setSubject(storageBlob.getUidFilename())
|
.setIssuedAt(issuedAt)
|
.setExpiration(expiration)
|
.claim("path", storageBlob.getPath())
|
.claim("resourceKey", storageBlob.getResourceKey())
|
.signWith(SignatureAlgorithm.HS256, properties.getJwtSecret())
|
.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;
|
}
|
|
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();
|
}
|
|
}
|