package cn.iocoder.yudao.module.system.util.storage;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO;
|
import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentRespVO;
|
import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentSaveReqVO;
|
import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageAttachmentDO;
|
import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO;
|
import cn.iocoder.yudao.module.system.dal.mysql.storage.SystemStorageAttachmentMapper;
|
import cn.iocoder.yudao.module.system.dal.mysql.storage.SystemStorageBlobMapper;
|
import cn.iocoder.yudao.module.system.enums.storage.StorageApplicationTypeEnum;
|
import cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum;
|
import cn.iocoder.yudao.module.system.framework.storage.config.StorageProperties;
|
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.security.Keys;
|
import lombok.RequiredArgsConstructor;
|
import net.coobird.thumbnailator.Thumbnails;
|
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;
|
|
/**
|
* 文件上传体系核心工具类
|
*
|
* 主要职责:
|
* - 文件与业务记录绑定(保存附件关系)
|
* - 文件与附件删除
|
* - 附件查询
|
* - 预览/下载地址生成(带JWT签名)
|
* - token使用次数控制(Redis)
|
* - 图片压缩
|
*
|
* @author 芋道源码
|
*/
|
@Component
|
@RequiredArgsConstructor
|
public class StorageFileUtil {
|
|
private final StorageProperties properties;
|
private final SystemStorageAttachmentMapper storageAttachmentMapper;
|
private final SystemStorageBlobMapper storageBlobMapper;
|
private final StringRedisTemplate stringRedisTemplate;
|
|
private static final String TOKEN_USAGE_KEY_PREFIX = "storage:token:usage:";
|
private static final DateTimeFormatter YEAR_PATH_FORMATTER = DateTimeFormatter.ofPattern("yyyy");
|
private static final DateTimeFormatter MONTH_DAY_PATH_FORMATTER = DateTimeFormatter.ofPattern("MMdd");
|
|
// ==================== 保存附件关系 ====================
|
|
/**
|
* 按"文件用途 + 记录类型 + 记录id"保存附件关系
|
* 逻辑:先删除旧附件,再批量插入新附件(整体替换模式)
|
*/
|
public void saveStorageAttachment(StorageApplicationTypeEnum application, StorageRecordTypeEnum recordType,
|
Long recordId, List<SystemStorageAttachmentSaveReqVO.BlobItem> blobItems) {
|
if (application == null) {
|
throw new IllegalArgumentException("文件用途不能为空");
|
}
|
if (recordType == null) {
|
throw new IllegalArgumentException("关联记录类型不能为空");
|
}
|
if (recordId == null || recordId <= 0) {
|
throw new IllegalArgumentException("关联记录id不能为空");
|
}
|
// 删除旧附件信息
|
deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application.getType(), recordType.getType(), recordId);
|
if (CollUtil.isEmpty(blobItems)) {
|
return;
|
}
|
List<SystemStorageAttachmentDO> attachments = new ArrayList<>();
|
for (SystemStorageAttachmentSaveReqVO.BlobItem item : blobItems) {
|
SystemStorageAttachmentDO attachment = new SystemStorageAttachmentDO();
|
String app = (item.getApplication() != null && !item.getApplication().trim().isEmpty())
|
? item.getApplication() : application.getType();
|
attachment.setApplication(app);
|
attachment.setRecordType(recordType.getType());
|
attachment.setRecordId(recordId);
|
attachment.setStorageBlobId(item.getBlobId());
|
attachments.add(attachment);
|
}
|
storageAttachmentMapper.insertBatch(attachments);
|
}
|
|
/**
|
* 按 recordType + recordId 保存附件关系,application 可指定也可从每个文件对象里读取
|
*/
|
public void saveStorageAttachmentByRecordTypeAndRecordId(String application, StorageRecordTypeEnum recordType,
|
Long recordId, List<SystemStorageAttachmentSaveReqVO.BlobItem> blobItems) {
|
if (recordType == null) {
|
throw new IllegalArgumentException("关联记录类型不能为空");
|
}
|
if (recordId == null) {
|
throw new IllegalArgumentException("关联记录id不能为空");
|
}
|
|
// 删除旧附件信息
|
if (application == null || application.trim().isEmpty()) {
|
for (SystemStorageAttachmentSaveReqVO.BlobItem item : blobItems) {
|
String itemApp = item.getApplication();
|
if (itemApp == null || itemApp.trim().isEmpty()) {
|
throw new IllegalArgumentException("文件用途不能为空");
|
}
|
deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(itemApp, recordType.getType(), recordId);
|
}
|
} else {
|
deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType.getType(), recordId);
|
}
|
|
if (CollUtil.isEmpty(blobItems)) {
|
deleteStorageAttachmentsByRecordTypeAndRecordId(recordType.getType(), recordId);
|
return;
|
}
|
|
List<SystemStorageAttachmentDO> attachments = new ArrayList<>();
|
for (SystemStorageAttachmentSaveReqVO.BlobItem item : blobItems) {
|
SystemStorageAttachmentDO attachment = new SystemStorageAttachmentDO();
|
String app = (application != null && !application.trim().isEmpty())
|
? application : item.getApplication();
|
attachment.setApplication(StorageApplicationTypeEnum.getByType(app).getType());
|
attachment.setRecordType(recordType.getType());
|
attachment.setRecordId(recordId);
|
attachment.setStorageBlobId(item.getBlobId());
|
attachments.add(attachment);
|
}
|
storageAttachmentMapper.insertBatch(attachments);
|
}
|
|
// ==================== 删除文件主表 ====================
|
|
public void deleteStorageBlobs(List<Long> storageBlobIds) {
|
if (CollUtil.isEmpty(storageBlobIds)) {
|
return;
|
}
|
// 同时删除磁盘文件
|
List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds);
|
for (SystemStorageBlobDO blob : blobs) {
|
deleteBlobFile(blob);
|
}
|
storageBlobMapper.deleteByIds(storageBlobIds);
|
}
|
|
public void deleteStorageBlobsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
|
if (CollUtil.isEmpty(storageAttachmentIds)) {
|
return;
|
}
|
List<SystemStorageAttachmentDO> attachments = storageAttachmentMapper.selectByIds(storageAttachmentIds);
|
List<Long> storageBlobIds = attachments.stream()
|
.map(SystemStorageAttachmentDO::getStorageBlobId)
|
.collect(Collectors.toList());
|
deleteStorageBlobs(storageBlobIds);
|
}
|
|
public void deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(
|
String application, String recordType, Collection<Long> recordIds) {
|
if (recordIds == null || recordIds.isEmpty()) {
|
throw new IllegalArgumentException("关联记录id不能为空");
|
}
|
List<SystemStorageAttachmentDO> attachments = storageAttachmentMapper
|
.selectListByApplicationAndRecordTypeAndRecordIds(application, recordType, recordIds);
|
if (CollUtil.isNotEmpty(attachments)) {
|
List<Long> blobIds = attachments.stream()
|
.map(SystemStorageAttachmentDO::getStorageBlobId)
|
.collect(Collectors.toList());
|
deleteStorageBlobs(blobIds);
|
}
|
}
|
|
public void deleteStorageBlobsByRecordTypeAndRecordId(String recordType, Long recordId) {
|
if (recordId == null || recordId <= 0) {
|
throw new IllegalArgumentException("关联记录id不能为空");
|
}
|
List<SystemStorageAttachmentDO> attachments = storageAttachmentMapper
|
.selectListByRecordTypeAndRecordId(recordType, recordId);
|
if (CollUtil.isNotEmpty(attachments)) {
|
List<Long> blobIds = attachments.stream()
|
.map(SystemStorageAttachmentDO::getStorageBlobId)
|
.collect(Collectors.toList());
|
deleteStorageBlobs(blobIds);
|
}
|
}
|
|
// ==================== 删除附件关系 ====================
|
|
public void deleteStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
|
if (CollUtil.isEmpty(storageAttachmentIds)) {
|
return;
|
}
|
deleteStorageBlobsByStorageAttachmentIds(storageAttachmentIds);
|
storageAttachmentMapper.deleteByIds(storageAttachmentIds);
|
}
|
|
public void deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(
|
String application, String recordType, Long recordId) {
|
if (recordId == null || recordId <= 0) {
|
throw new IllegalArgumentException("关联记录id不能为空");
|
}
|
deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(application, recordType, Collections.singletonList(recordId));
|
storageAttachmentMapper.deleteByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
|
}
|
|
public void deleteStorageAttachmentsByRecordTypeAndRecordId(String recordType, Long recordId) {
|
if (recordId == null || recordId <= 0) {
|
throw new IllegalArgumentException("关联记录id不能为空");
|
}
|
deleteStorageBlobsByRecordTypeAndRecordId(recordType, recordId);
|
storageAttachmentMapper.deleteByRecordTypeAndRecordId(recordType, recordId);
|
}
|
|
public void deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(
|
String application, String recordType, List<Long> recordIds) {
|
if (recordIds == null || recordIds.isEmpty()) {
|
throw new IllegalArgumentException("关联记录id不能为空");
|
}
|
deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(application, recordType, recordIds);
|
storageAttachmentMapper.deleteByApplicationAndRecordTypeAndRecordIds(application, recordType, recordIds);
|
}
|
|
// ==================== 查询附件关系 ====================
|
|
public List<SystemStorageAttachmentDO> getStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
|
if (CollUtil.isEmpty(storageAttachmentIds)) {
|
throw new IllegalArgumentException("附件ID不能为空");
|
}
|
return storageAttachmentMapper.selectByIds(storageAttachmentIds);
|
}
|
|
public List<SystemStorageAttachmentDO> getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(
|
String application, String recordType, Long recordId) {
|
return storageAttachmentMapper.selectListByApplicationAndRecordTypeAndRecordId(application, recordType, recordId);
|
}
|
|
public List<SystemStorageAttachmentDO> getStorageAttachmentsByRecordTypeAndRecordId(
|
String recordType, Long recordId) {
|
return storageAttachmentMapper.selectListByRecordTypeAndRecordId(recordType, recordId);
|
}
|
|
// ==================== 查询文件信息 ====================
|
|
/**
|
* 根据附件关系id查询文件列表(自动构建previewURL、downloadURL、storageAttachmentId)
|
*/
|
public List<SystemStorageBlobRespVO> getStorageBlobVOsByStorageAttachmentIds(List<Long> storageAttachmentIds) {
|
List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByStorageAttachmentIds(storageAttachmentIds);
|
if (CollUtil.isEmpty(attachments)) {
|
return null;
|
}
|
Map<Long, Long> blobIdToAttachmentIdMap = attachments.stream()
|
.collect(Collectors.toMap(SystemStorageAttachmentDO::getStorageBlobId, SystemStorageAttachmentDO::getId));
|
|
List<Long> storageBlobIds = attachments.stream()
|
.map(SystemStorageAttachmentDO::getStorageBlobId)
|
.collect(Collectors.toList());
|
List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds);
|
List<SystemStorageBlobRespVO> result = new ArrayList<>();
|
for (SystemStorageBlobDO blob : blobs) {
|
SystemStorageBlobRespVO vo = buildStorageBlobRespVO(blob);
|
vo.setStorageAttachmentId(blobIdToAttachmentIdMap.get(blob.getId()));
|
result.add(vo);
|
}
|
return result;
|
}
|
|
/**
|
* 按用途、业务类型、业务id查询文件列表
|
*/
|
public List<SystemStorageBlobRespVO> getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(
|
String application, String recordType, Long recordId) {
|
List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(
|
application, recordType, recordId);
|
if (CollUtil.isEmpty(attachments)) {
|
return null;
|
}
|
Map<Long, Long> blobIdToAttachmentIdMap = attachments.stream()
|
.collect(Collectors.toMap(SystemStorageAttachmentDO::getStorageBlobId, SystemStorageAttachmentDO::getId));
|
|
List<Long> storageBlobIds = attachments.stream()
|
.map(SystemStorageAttachmentDO::getStorageBlobId)
|
.collect(Collectors.toList());
|
List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds);
|
List<SystemStorageBlobRespVO> result = new ArrayList<>();
|
for (SystemStorageBlobDO blob : blobs) {
|
SystemStorageBlobRespVO vo = buildStorageBlobRespVO(blob);
|
vo.setStorageAttachmentId(blobIdToAttachmentIdMap.get(blob.getId()));
|
result.add(vo);
|
}
|
return result;
|
}
|
|
/**
|
* 按业务类型、业务id查询文件列表(不区分用途)
|
*/
|
public List<SystemStorageBlobRespVO> getStorageBlobVOsByRecordTypeAndRecordId(String recordType, Long recordId) {
|
List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByRecordTypeAndRecordId(recordType, recordId);
|
if (CollUtil.isEmpty(attachments)) {
|
return null;
|
}
|
List<Long> storageBlobIds = attachments.stream()
|
.map(SystemStorageAttachmentDO::getStorageBlobId)
|
.collect(Collectors.toList());
|
List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds);
|
List<SystemStorageBlobRespVO> result = new ArrayList<>();
|
for (SystemStorageBlobDO blob : blobs) {
|
result.add(buildStorageBlobRespVO(blob));
|
}
|
return result;
|
}
|
|
/**
|
* 按用途、业务类型、业务id查询文件列表,自定义过期时间(分钟)
|
*/
|
public List<SystemStorageBlobRespVO> getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(
|
String application, String recordType, Long recordId, BigDecimal expired) {
|
List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(
|
application, recordType, recordId);
|
if (CollUtil.isEmpty(attachments)) {
|
return null;
|
}
|
Map<Long, Long> blobIdToAttachmentIdMap = attachments.stream()
|
.collect(Collectors.toMap(SystemStorageAttachmentDO::getStorageBlobId, SystemStorageAttachmentDO::getId));
|
|
List<Long> storageBlobIds = attachments.stream()
|
.map(SystemStorageAttachmentDO::getStorageBlobId)
|
.collect(Collectors.toList());
|
List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds);
|
List<SystemStorageBlobRespVO> result = new ArrayList<>();
|
for (SystemStorageBlobDO blob : blobs) {
|
SystemStorageBlobRespVO vo = BeanUtils.toBean(blob, SystemStorageBlobRespVO.class);
|
vo.setPreviewURL(buildSignedUrl(blob, "/preview/", expired));
|
vo.setUrl(buildSignedUrl(blob, "/preview/", expired));
|
vo.setName(blob.getOriginalFilename());
|
vo.setDownloadURL(buildSignedUrl(blob, "/download/", expired));
|
vo.setStorageAttachmentId(blobIdToAttachmentIdMap.get(blob.getId()));
|
result.add(vo);
|
}
|
return result;
|
}
|
|
// ==================== 查询附件视图 ====================
|
|
/**
|
* 按用途、业务类型、业务id查询附件视图
|
*/
|
public List<SystemStorageAttachmentRespVO> getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId(
|
String application, String recordType, Long recordId) {
|
List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(
|
application, recordType, recordId);
|
if (CollUtil.isEmpty(attachments)) {
|
return new ArrayList<>();
|
}
|
List<SystemStorageAttachmentRespVO> result = new ArrayList<>();
|
for (SystemStorageAttachmentDO attachment : attachments) {
|
SystemStorageAttachmentRespVO vo = BeanUtils.toBean(attachment, SystemStorageAttachmentRespVO.class);
|
List<SystemStorageBlobRespVO> blobVOs = getStorageBlobVOsByStorageAttachmentIds(
|
Collections.singletonList(attachment.getId()));
|
vo.setBlobList(CollUtil.isEmpty(blobVOs) ? new ArrayList<>() : blobVOs);
|
result.add(vo);
|
}
|
return result;
|
}
|
|
// ==================== 仅获取预览/下载地址 ====================
|
|
public List<String> getFilePreviewURLsByApplicationAndRecordTypeAndRecordId(
|
String application, String recordType, Long recordId) {
|
List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(
|
application, recordType, recordId);
|
if (CollUtil.isEmpty(attachments)) {
|
return new ArrayList<>();
|
}
|
List<Long> blobIds = attachments.stream()
|
.map(SystemStorageAttachmentDO::getStorageBlobId)
|
.collect(Collectors.toList());
|
List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(blobIds);
|
return blobs.stream()
|
.map(this::buildSignedPreviewUrl)
|
.collect(Collectors.toList());
|
}
|
|
public List<String> getFileDownloadURLsByApplicationAndRecordTypeAndRecordId(
|
String application, String recordType, Long recordId) {
|
List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(
|
application, recordType, recordId);
|
if (CollUtil.isEmpty(attachments)) {
|
return new ArrayList<>();
|
}
|
List<Long> blobIds = attachments.stream()
|
.map(SystemStorageAttachmentDO::getStorageBlobId)
|
.collect(Collectors.toList());
|
List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(blobIds);
|
return blobs.stream()
|
.map(this::buildSignedDownloadUrl)
|
.collect(Collectors.toList());
|
}
|
|
// ==================== 构建 VO ====================
|
|
private SystemStorageBlobRespVO buildStorageBlobRespVO(SystemStorageBlobDO blob) {
|
SystemStorageBlobRespVO vo = BeanUtils.toBean(blob, SystemStorageBlobRespVO.class);
|
vo.setPreviewURL(buildSignedPreviewUrl(blob));
|
vo.setUrl(buildSignedPreviewUrl(blob));
|
vo.setName(blob.getOriginalFilename());
|
vo.setDownloadURL(buildSignedDownloadUrl(blob));
|
return vo;
|
}
|
|
// ==================== 构建签名URL ====================
|
|
public String buildSignedPreviewUrl(SystemStorageBlobDO blob) {
|
return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), "/preview/", properties.getExpired());
|
}
|
|
public String buildSignedDownloadUrl(SystemStorageBlobDO blob) {
|
return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), "/download/", properties.getExpired());
|
}
|
|
public String buildSignedPreviewUrl(SystemStorageBlobRespVO blob) {
|
return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), "/preview/", properties.getExpired());
|
}
|
|
public String buildSignedDownloadUrl(SystemStorageBlobRespVO blob) {
|
return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), "/download/", properties.getExpired());
|
}
|
|
public String buildSignedUrl(SystemStorageBlobDO blob, String actionPath, BigDecimal expired) {
|
return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), actionPath, expired);
|
}
|
|
public String buildSignedUrl(SystemStorageBlobRespVO blob, String actionPath, BigDecimal expired) {
|
return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), actionPath, expired);
|
}
|
|
/**
|
* 构建统一的带签名预览/下载地址
|
*
|
* @param uidFilename 唯一文件名
|
* @param path 文件相对路径
|
* @param resourceKey 资源唯一标识
|
* @param actionPath 操作路径 "/preview/" 或 "/download/"
|
* @param expired 过期时间(分钟),-1表示永久有效(走publicKey)
|
* @return 带签名的URL
|
*/
|
private String buildSignedUrlFromFields(String uidFilename, String path, String resourceKey,
|
String actionPath, BigDecimal expired) {
|
if (!Arrays.asList("/preview/", "/download/").contains(actionPath)) {
|
throw new IllegalArgumentException("操作路径参数错误");
|
}
|
if (!StringUtils.hasText(uidFilename)) {
|
throw new IllegalArgumentException("文件信息不完整");
|
}
|
String domain = StringUtils.trimTrailingCharacter(properties.getDomain(), '/');
|
String prefix = properties.getUrlPrefix().startsWith("/") ? properties.getUrlPrefix() : "/" + properties.getUrlPrefix();
|
String baseUrl = domain + prefix + actionPath + uidFilename;
|
|
// -1 表示永久有效,不生成 token,改为 publicKey 组合校验
|
if (expired != null && BigDecimal.valueOf(-1L).compareTo(expired) == 0) {
|
if (!StringUtils.hasText(resourceKey)) {
|
throw new IllegalArgumentException("公开链接缺少publicKey");
|
}
|
return baseUrl + "?publicKey=" + resourceKey;
|
}
|
|
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(uidFilename)
|
.issuedAt(issuedAt)
|
.expiration(expiration)
|
.claim("path", path)
|
.claim("resourceKey", resourceKey)
|
.signWith(key)
|
.compact();
|
cacheTokenUsage(token, expiredMillis);
|
return baseUrl + "?token=" + 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;
|
}
|
|
/**
|
* 校验token是否还能继续使用
|
*/
|
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();
|
}
|
|
// ==================== 路径与压缩 ====================
|
|
/**
|
* 生成文件存储相对路径,格式:yyyy/MMdd(如 2026/0430)
|
*/
|
public String buildRelativePath() {
|
LocalDate now = LocalDate.now();
|
return now.format(YEAR_PATH_FORMATTER) + "/" + now.format(MONTH_DAY_PATH_FORMATTER);
|
}
|
|
/**
|
* 对图片进行压缩,非图片或不满足条件时返回原文件
|
*/
|
public File compressFile(File file) {
|
if (Boolean.TRUE.equals(properties.getCompress())
|
&& isImage(file.getName())
|
&& file.length() > properties.getNeedCompressSize().toBytes()) {
|
try {
|
File compressedFile = new File(file.getParent(), "thumb_" + file.getName());
|
if (compressedFile.exists()) {
|
return compressedFile;
|
}
|
Thumbnails.of(file)
|
.scale(1.0f)
|
.outputQuality(properties.getCompressQuality())
|
.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);
|
}
|
|
// ==================== 磁盘文件操作 ====================
|
|
/**
|
* 删除 blob 对应的磁盘文件(含压缩文件)
|
*/
|
private void deleteBlobFile(SystemStorageBlobDO blob) {
|
File originalFile = resolveBlobFile(blob);
|
safeDelete(originalFile);
|
if (originalFile != null && originalFile.getParentFile() != null) {
|
File compressedFile = new File(originalFile.getParentFile(), "thumb_" + originalFile.getName());
|
safeDelete(compressedFile);
|
}
|
}
|
|
private File resolveBlobFile(SystemStorageBlobDO blob) {
|
String basePath = properties.getPath();
|
if (!StringUtils.hasText(blob.getPath())) {
|
return new File(basePath, blob.getUidFilename());
|
}
|
return new File(new File(basePath, blob.getPath()), blob.getUidFilename());
|
}
|
|
private boolean safeDelete(File file) {
|
if (file == null || !file.exists() || !file.isFile()) {
|
return false;
|
}
|
return file.delete();
|
}
|
|
}
|