| | |
| | | package com.ruoyi.basic.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.basic.dto.StorageBlobDTO; |
| | | import com.ruoyi.basic.mapper.StorageAttachmentMapper; |
| | | import com.ruoyi.basic.dto.StorageBlobVO; |
| | | import com.ruoyi.basic.mapper.StorageBlobMapper; |
| | | import com.ruoyi.basic.pojo.StorageAttachment; |
| | | import com.ruoyi.basic.pojo.StorageBlob; |
| | | import com.ruoyi.basic.service.StorageBlobService; |
| | | import com.ruoyi.common.exception.file.InvalidExtensionException; |
| | | import com.ruoyi.common.utils.DateUtils; |
| | | import com.ruoyi.common.utils.MinioUtils; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.common.utils.uuid.IdUtils; |
| | | import com.ruoyi.framework.web.domain.MinioResult; |
| | | import com.ruoyi.basic.utils.FileUtil; |
| | | import com.ruoyi.common.config.FileProperties; |
| | | import io.jsonwebtoken.Claims; |
| | | import io.jsonwebtoken.Jwts; |
| | | import io.jsonwebtoken.security.Keys; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.CollectionUtils; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.crypto.SecretKey; |
| | | import java.io.File; |
| | | import java.io.IOException; |
| | | import java.math.BigDecimal; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.stream.Collectors; |
| | | import java.util.UUID; |
| | | |
| | | /** |
| | | * <p> |
| | | * 通用文件上传的附件信息 服务实现类 |
| | | * </p> |
| | | * |
| | | * @author ruoyi |
| | | * @since 2025-05-29 |
| | | */ |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class StorageBlobServiceImpl extends ServiceImpl<StorageBlobMapper, StorageBlob> implements StorageBlobService { |
| | | @Autowired |
| | | private StorageAttachmentMapper storageAttachmentMapper; |
| | | |
| | | @Autowired |
| | | private StorageBlobMapper storageBlobMapper; |
| | | |
| | | @Autowired |
| | | private MinioUtils minioUtils; |
| | | private final FileProperties properties; |
| | | private final StorageBlobMapper storageBlobMapper; |
| | | private final FileUtil fileUtil; |
| | | |
| | | @Override |
| | | public List<StorageBlobDTO> updateStorageBlobs(List<MultipartFile> files, String bucketName) { |
| | | |
| | | // 若没传入bucketName,则使用默认bucketName |
| | | if (StringUtils.isEmpty(bucketName)) { |
| | | bucketName = minioUtils.getDefaultBucket(); |
| | | public List<StorageBlobVO> upload(List<MultipartFile> files, Boolean isPublic) { |
| | | if (CollectionUtils.isEmpty(files)) { |
| | | throw new IllegalArgumentException("文件不能为空"); |
| | | } |
| | | |
| | | List<StorageBlobDTO> storageBlobDTOs = new ArrayList<>(); |
| | | List<StorageBlobVO> storageBlobVOS = new ArrayList<>(); |
| | | |
| | | for (MultipartFile file : files) { |
| | | try { |
| | | MinioResult res = minioUtils.upload(bucketName, file, false); |
| | | StorageBlobDTO dto = new StorageBlobDTO(); |
| | | dto.setContentType(file.getContentType()); |
| | | dto.setBucketFilename(res.getBucketFileName()); |
| | | dto.setOriginalFilename(res.getOriginalName()); |
| | | dto.setByteSize(file.getSize()); |
| | | dto.setKey(IdUtils.simpleUUID()); |
| | | dto.setBucketName(bucketName); |
| | | dto.setCreateTime(DateUtils.getNowDate()); |
| | | dto.setUrl(minioUtils.getPreviewUrl(res.getBucketFileName(), bucketName, false)); |
| | | // 插入数据库 |
| | | storageBlobMapper.insert(dto); |
| | | |
| | | storageBlobDTOs.add(dto); |
| | | } catch (InvalidExtensionException e) { |
| | | throw new RuntimeException("minio文件上传异常:" + e); |
| | | if (file == null || file.isEmpty()) { |
| | | throw new IllegalArgumentException("文件不能为空"); |
| | | } |
| | | |
| | | String originalFileName = StringUtils.hasText(file.getOriginalFilename()) |
| | | ? StringUtils.cleanPath(file.getOriginalFilename()) |
| | | : UUID.randomUUID().toString(); |
| | | String fileName = UUID.randomUUID() + "_" + originalFileName; |
| | | String relativePath = fileUtil.buildRelativePath(); |
| | | File targetDirectory = new File(properties.getPath(), relativePath); |
| | | if (!targetDirectory.exists() && !targetDirectory.mkdirs()) { |
| | | throw new RuntimeException("创建上传目录失败"); |
| | | } |
| | | File dest = new File(targetDirectory, fileName); |
| | | |
| | | StorageBlobVO storageBlob; |
| | | try { |
| | | file.transferTo(dest); |
| | | storageBlob = getStorageBlob(file, originalFileName, fileName, relativePath, isPublic); |
| | | if (storageBlob == null || storageBlob.getId() == null) { |
| | | throw new RuntimeException("文件元数据保存失败"); |
| | | } |
| | | } catch (RuntimeException e) { |
| | | if (dest.exists()) { |
| | | dest.delete(); |
| | | } |
| | | throw e; |
| | | } catch (IOException e) { |
| | | throw new RuntimeException("文件保存失败", e); |
| | | } |
| | | |
| | | storageBlobVOS.add(storageBlob); |
| | | } |
| | | |
| | | |
| | | return storageBlobDTOs; |
| | | return storageBlobVOS; |
| | | } |
| | | |
| | | @Override |
| | | public int deleteStorageBlobs(StorageAttachment attachment) { |
| | | List<StorageAttachment> attachments = storageAttachmentMapper.selectList(new LambdaQueryWrapper<StorageAttachment>() |
| | | .eq(StorageAttachment::getRecordId, attachment.getRecordId()) |
| | | .eq(StorageAttachment::getRecordType, attachment.getRecordType()) |
| | | .eq(StorageAttachment::getName, attachment.getName())); |
| | | List<Long> ids = attachments.stream().map(StorageAttachment::getStorageBlobId).collect(Collectors.toList()); |
| | | List<StorageBlob> storageBlobs = storageBlobMapper.selectList(new LambdaQueryWrapper<StorageBlob>() |
| | | .in(StorageBlob::getId, ids)); |
| | | if (!storageBlobs.isEmpty()) { |
| | | for (StorageBlob storageBlob : storageBlobs) { |
| | | // 移除桶内文件 |
| | | minioUtils.removeObjectsResult(storageBlob.getBucketName(), storageBlob.getBucketName()); |
| | | } |
| | | public File getFileByToken(String fileName, String token) { |
| | | if (!StringUtils.hasText(token)) { |
| | | throw new IllegalArgumentException("token不能为空"); |
| | | } |
| | | |
| | | if (!ids.isEmpty()) { |
| | | return storageBlobMapper.delete(new QueryWrapper<StorageBlob>().lambda().in(StorageBlob::getId, ids)); |
| | | } |
| | | String secretStr = properties.getJwtSecret(); |
| | | |
| | | return 0; |
| | | SecretKey key = Keys.hmacShaKeyFor(secretStr.getBytes(StandardCharsets.UTF_8)); |
| | | Claims claims = Jwts.parser() |
| | | .verifyWith(key) // 代替旧版的 setSigningKey |
| | | .build() // 必须先构建解析器 |
| | | .parseSignedClaims(token) // 代替旧版的 parseClaimsJws |
| | | .getPayload(); // 代替旧版的 getBody() |
| | | if (!fileName.equals(claims.getSubject())) { |
| | | throw new IllegalArgumentException("token与文件不匹配"); |
| | | } |
| | | fileUtil.validateTokenUsage(token); |
| | | StorageBlob storageBlob = findStorageBlob(fileName); |
| | | String path = storageBlob == null ? claims.get("path", String.class) : storageBlob.getPath(); |
| | | if (!StringUtils.hasText(path)) { |
| | | return new File(properties.getPath(), fileName); |
| | | } |
| | | return new File(new File(properties.getPath(), path), fileName); |
| | | } |
| | | |
| | | @Override |
| | | public File getPublicFile(String fileName, String publicKey) { |
| | | if (!StringUtils.hasText(fileName)) { |
| | | throw new IllegalArgumentException("文件名不能为空"); |
| | | } |
| | | if (!StringUtils.hasText(publicKey)) { |
| | | throw new IllegalArgumentException("publicKey不能为空"); |
| | | } |
| | | StorageBlob storageBlob = storageBlobMapper.selectOne(new LambdaQueryWrapper<StorageBlob>() |
| | | .eq(StorageBlob::getUidFilename, fileName) |
| | | .eq(StorageBlob::getResourceKey, publicKey) |
| | | .last("limit 1")); |
| | | if (storageBlob == null) { |
| | | throw new IllegalArgumentException("公开文件不存在或publicKey不匹配"); |
| | | } |
| | | String path = storageBlob.getPath(); |
| | | if (!StringUtils.hasText(path)) { |
| | | return new File(properties.getPath(), fileName); |
| | | } |
| | | return new File(new File(properties.getPath(), path), fileName); |
| | | } |
| | | |
| | | private StorageBlob findStorageBlob(String fileName) { |
| | | return storageBlobMapper.selectOne(new LambdaQueryWrapper<StorageBlob>() |
| | | .eq(StorageBlob::getUidFilename, fileName) |
| | | .last("limit 1")); |
| | | } |
| | | |
| | | private StorageBlobVO getStorageBlob(MultipartFile file, String originalFileName, String fileName, String relativePath, Boolean isPublic) { |
| | | StorageBlobVO storageBlob = new StorageBlobVO(); |
| | | storageBlob.setResourceKey(UUID.randomUUID().toString().replace("-", "")); |
| | | storageBlob.setContentType(file.getContentType()); |
| | | storageBlob.setOriginalFilename(originalFileName); |
| | | storageBlob.setUidFilename(fileName); |
| | | storageBlob.setByteSize(file.getSize()); |
| | | storageBlob.setPath(relativePath); |
| | | if (isPublic) { |
| | | storageBlob.setPreviewURL(fileUtil.buildSignedUrl(storageBlob, "/preview/", BigDecimal.valueOf(-1))); |
| | | storageBlob.setDownloadURL(fileUtil.buildSignedUrl(storageBlob, "/download/", BigDecimal.valueOf(-1))); |
| | | } else { |
| | | storageBlob.setPreviewURL(fileUtil.buildSignedPreviewUrl(storageBlob)); |
| | | storageBlob.setDownloadURL(fileUtil.buildSignedDownloadUrl(storageBlob)); |
| | | } |
| | | int affectedRows = storageBlobMapper.insert(storageBlob); |
| | | if (affectedRows <= 0) { |
| | | throw new RuntimeException("文件元数据保存失败"); |
| | | } |
| | | return storageBlob; |
| | | } |
| | | |
| | | @Override |
| | | public String getDownloadFileName(String fileName) { |
| | | StorageBlob storageBlob = findStorageBlob(fileName); |
| | | if (storageBlob == null || !StringUtils.hasText(storageBlob.getOriginalFilename())) { |
| | | return fileName; |
| | | } |
| | | return storageBlob.getOriginalFilename(); |
| | | } |
| | | } |