package com.aida.seller.util; import com.aida.seller.common.constants.MinioFileConstants; import com.aida.seller.common.exception.MinioException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.minio.*; import io.minio.http.Method; import io.minio.messages.DeleteError; import io.minio.messages.DeleteObject; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URL; import java.util.*; import java.util.concurrent.TimeUnit; @Slf4j @Component @RequiredArgsConstructor public class MinioUtil { @Autowired private MinioClient minioClient; @Autowired private RedisUtil redisUtil; private static final String REDIS_MINIO_URL_PREFIX = "minio:url:"; private static final long URL_CACHE_EXPIRE_SECONDS = 24 * 60 * 60; @Value("${minio.default-bucket}") private String defaultBucketName; @Value("${minio.endpoint}") private String endpoint; private final ObjectMapper objectMapper = new ObjectMapper(); public String uploadImage(MultipartFile file, String bucketName, String userId) { try { if (bucketName == null || bucketName.isEmpty()) { bucketName = defaultBucketName; } if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } String originalFilename = file.getOriginalFilename(); String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString() + fileExtension; String filePath = (userId != null && !userId.isEmpty()) ? userId + "/" + fileName : fileName; minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(filePath) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); log.info("文件上传成功,桶名: {}, 文件路径: {}", bucketName, filePath); return bucketName + "/" + filePath; } catch (Exception e) { log.error("文件上传失败: {}", e.getMessage(), e); throw new MinioException("minio.upload.failed", e); } } public String uploadImage(MultipartFile file, String userId) { return uploadImage(file, null, userId); } public String uploadImage(MultipartFile file) { return uploadImage(file, null, null); } public String getImageUrl(String path, int expires) { if (!path.contains("/")) { } int index = path.indexOf("/"); String bucketName = path.substring(0, index); String fileName = path.substring(index + 1); return getImageUrl(bucketName, fileName, expires); } public String getImageUrl(String bucketName, String filePath, int expires) { String cacheKey = REDIS_MINIO_URL_PREFIX + bucketName + "/" + filePath; Object cachedUrl = redisUtil.get(cacheKey); if (cachedUrl != null) { return cachedUrl.toString(); } try { String url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) .object(filePath) .expiry(expires) .build()); redisUtil.setWithExpire(cacheKey, url, URL_CACHE_EXPIRE_SECONDS); return url; } catch (Exception e) { log.error("获取临时访问地址失败: {}", e.getMessage(), e); throw new MinioException("minio.get.presigned.url.failed", e); } } public String getImageUrl(String bucketName, String filePath) { return getImageUrl(bucketName, filePath, 7 * 24 * 60 * 60); } public void deleteImage(String objectPath) { try { int index = objectPath.indexOf("/"); if (index == -1) { throw new MinioException("minio.invalid.object.path"); } String bucketName = objectPath.substring(0, index); String filePath = objectPath.substring(index + 1); minioClient.removeObject(RemoveObjectArgs.builder() .bucket(bucketName) .object(filePath) .build()); log.info("文件删除成功,桶名: {}, 文件路径: {}", bucketName, filePath); } catch (Exception e) { log.error("文件删除失败: {}", e.getMessage(), e); throw new MinioException("minio.delete.failed", e); } } public void deleteImages(List objectPaths) { if (objectPaths == null || objectPaths.isEmpty()) { return; } try { String firstPath = objectPaths.get(0); int index = firstPath.indexOf("/"); if (index == -1) { throw new MinioException("minio.invalid.object.path"); } String bucketName = firstPath.substring(0, index); List deleteObjects = new ArrayList<>(); for (String objectPath : objectPaths) { int sepIndex = objectPath.indexOf("/"); if (sepIndex != -1) { String filePath = objectPath.substring(sepIndex + 1); deleteObjects.add(new DeleteObject(filePath)); } } Iterable> results = minioClient.removeObjects(RemoveObjectsArgs.builder() .bucket(bucketName) .objects(deleteObjects) .build()); for (Result result : results) { DeleteError error = result.get(); log.error("文件删除失败,桶名: {}, 文件路径: {}, 错误信息: {}", bucketName, error.objectName(), error.message()); } log.info("批量删除文件成功,桶名: {}, 文件数量: {}", bucketName, objectPaths.size()); } catch (Exception e) { log.error("批量删除文件失败: {}", e.getMessage(), e); throw new MinioException("minio.batch.delete.failed", e); } } public String uploadBase64Image(String base64Image, String bucketName, String filePath) { try { String[] base64Parts = base64Image.split(","); String imageData = base64Parts[1]; String contentType = base64Parts[0].split(":")[1].split(";")[0]; byte[] imageBytes = java.util.Base64.getDecoder().decode(imageData); if (bucketName == null || bucketName.isEmpty()) { bucketName = defaultBucketName; } if (filePath == null || filePath.isEmpty()) { String fileExtension = contentType.split("/")[1]; filePath = UUID.randomUUID().toString() + "." + fileExtension; } return uploadImage(imageBytes, bucketName, filePath, contentType); } catch (Exception e) { log.error("base64图片上传失败: {}", e.getMessage(), e); throw new MinioException("minio.base64.upload.failed", e); } } public String uploadBase64Image(String base64Image, String bucketName) { return uploadBase64Image(base64Image, bucketName, null); } public String uploadBase64Image(String base64Image) { return uploadBase64Image(base64Image, null, null); } private String uploadImage(byte[] bytes, String bucketName, String filePath, String contentType) { try { if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(filePath) .stream(new ByteArrayInputStream(bytes), bytes.length, -1) .contentType(contentType) .build()); return bucketName + "/" + filePath; } catch (Exception e) { log.error("文件上传失败: {}", e.getMessage(), e); throw new MinioException("minio.upload.failed", e); } } public String uploadBytes(byte[] bytes, MinioFileConstants.FileType fileType, String contentType, String bucketName) { String objectName = MinioFileConstants.generateObjectNameByType(fileType); return uploadBytes(bytes, objectName, contentType, bucketName); } public String uploadBytes(byte[] bytes, String objectName, String contentType, String bucketName) { if (bytes == null || bytes.length == 0) { throw new MinioException("minio.file.content.empty"); } try { minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(new ByteArrayInputStream(bytes), bytes.length, -1) .contentType(contentType) .build() ); log.info("字节数组上传成功: {}/{}", bucketName, objectName); return bucketName + "/" + objectName; } catch (Exception e) { log.error("字节数组上传失败: {}", e.getMessage(), e); throw new MinioException("minio.bytes.upload.failed", e); } } public InputStream downloadFile(String logicalPath) { int index = logicalPath.indexOf("/"); if (index <= 0) { throw new MinioException("minio.logical.path.format.error"); } String bucketName = logicalPath.substring(0, index); String objectName = logicalPath.substring(index + 1); try { boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); if (!bucketExists) { throw new MinioException("minio.bucket.not.exists"); } } catch (Exception e) { log.error("验证桶存在性失败: {}", e.getMessage(), e); throw new MinioException("minio.verify.bucket.failed"); } try { return minioClient.getObject( GetObjectArgs.builder() .bucket(bucketName) .object(objectName) .build() ); } catch (Exception e) { log.error("文件下载失败: {}", e.getMessage(), e); throw new MinioException("minio.download.failed", e); } } public String getLogicalPathFromPresignedUrl(String presignedUrl) { try { URL url = new URL(presignedUrl); String path = url.getPath(); if (path.startsWith("/")) { path = path.substring(1); } int firstSlashIndex = path.indexOf("/"); if (firstSlashIndex <= 0) { throw new MinioException("minio.presigned.url.format.invalid"); } String bucketName = path.substring(0, firstSlashIndex); String objectName = path.substring(firstSlashIndex + 1); return bucketName + "/" + objectName; } catch (Exception e) { log.error("预签名URL解析失败: {}", e.getMessage(), e); throw new MinioException("minio.presigned.url.parse.failed", e); } } public boolean isPresignedUrl(String str) { if (str == null || str.isEmpty()) { return false; } try { URL url = new URL(str); String host = url.getHost(); String endpointHost = endpoint; if (endpointHost.startsWith("http://")) { endpointHost = endpointHost.substring(7); } else if (endpointHost.startsWith("https://")) { endpointHost = endpointHost.substring(8); } if (endpointHost.contains(":")) { endpointHost = endpointHost.substring(0, endpointHost.indexOf(":")); } return host.equals(endpointHost); } catch (Exception e) { return false; } } public boolean isMinioLogicalPath(String str) { if (str == null || str.isEmpty()) { return false; } if (!(str instanceof String)) { return false; } String trimStr = str.trim(); if (trimStr.startsWith("http://") || trimStr.startsWith("https://")) { return false; } if (!trimStr.contains("/")) { return false; } if (trimStr.contains(" ") || trimStr.contains("\n") || trimStr.contains("\t")) { return false; } return true; } public boolean isMinioResource(String str) { return isPresignedUrl(str) || isMinioLogicalPath(str); } public String processJsonPresignedUrls(String jsonString, int expires) { if (jsonString == null || jsonString.isEmpty()) { return jsonString; } try { JsonNode rootNode = objectMapper.readTree(jsonString); JsonNode processedNode = processNode(rootNode, expires); return objectMapper.writeValueAsString(processedNode); } catch (Exception e) { log.error("处理JSON中的预签名URL失败: {}", e.getMessage(), e); return jsonString; } } private JsonNode processNode(JsonNode node, int expires) { if (node.isObject()) { ObjectNode objectNode = (ObjectNode) node; Iterator> fields = objectNode.fields(); while (fields.hasNext()) { Map.Entry field = fields.next(); JsonNode value = field.getValue(); if (value.isTextual()) { String text = value.asText(); if (isMinioResource(text)) { String newUrl = processMinioResource(text, expires); objectNode.put(field.getKey(), newUrl); } } else if (!value.isNull()) { JsonNode processedValue = processNode(value, expires); objectNode.set(field.getKey(), processedValue); } } return objectNode; } else if (node.isArray()) { ArrayNode arrayNode = (ArrayNode) node; for (int i = 0; i < arrayNode.size(); i++) { JsonNode element = arrayNode.get(i); if (element.isTextual()) { String text = element.asText(); if (isMinioResource(text)) { String newUrl = processMinioResource(text, expires); arrayNode.set(i, newUrl); } } else if (!element.isNull()) { JsonNode processedElement = processNode(element, expires); arrayNode.set(i, processedElement); } } return arrayNode; } else { return node; } } public String processMinioResource(String resource, int expires) { try { String logicalPath; if (isPresignedUrl(resource)) { logicalPath = getLogicalPathFromPresignedUrl(resource); } else if (isMinioLogicalPath(resource)) { logicalPath = resource.trim(); } else { log.warn("未识别的MinIO资源格式: {}", resource); return resource; } return getImageUrl(logicalPath, expires); } catch (Exception e) { log.error("处理MinIO资源失败: {}, error: {}", resource, e.getMessage(), e); return resource; } } public String convertToLogicalPath(String url) { if (url == null || url.isEmpty()) { throw new MinioException("minio.url.cannot.be.empty"); } if (isMinioLogicalPath(url)) { return url.trim(); } else if (isPresignedUrl(url)) { return getLogicalPathFromPresignedUrl(url); } else { throw new MinioException("minio.resource.format.unrecognized"); } } public void deleteImagesByUrls(Collection urls) { if (urls == null || urls.isEmpty()) { return; } for (String url : urls) { String logicalPath = convertToLogicalPath(url); deleteImage(logicalPath); } } }