Bladeren bron

feat(service): 添加工作流结果存储服务实现

- 实现了图片URL批量转存功能,支持将远程图片转存至Google Storage
- 实现了视频URL转存功能,支持将远程视频转存至Google Storage
- 集成了Google Storage工具类,实现了文件上传功能
- 添加了HTTP客户端配置,设置了30秒连接超时和5分钟请求超时
- 实现了内容类型识别和文件扩展名解析逻辑
- 添加了UUID生成和日期目录结构,确保文件存储路径唯一性
- 实现了异常处理和日志记录,便于问题排查和监控
ChenYu 7 maanden geleden
bovenliggende
commit
dc1d48a5c5
1 gewijzigde bestanden met toevoegingen van 140 en 0 verwijderingen
  1. 140 0
      src/main/java/com/aitori/service/WorkflowResultStorageService.java

+ 140 - 0
src/main/java/com/aitori/service/WorkflowResultStorageService.java

@@ -0,0 +1,140 @@
+package com.aitori.service;
+
+import com.aitori.utils.GoogleStorageUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+@Slf4j
+@Service
+public class WorkflowResultStorageService {
+
+    private static final String GOOGLE_STORAGE_BASE_URL = "https://storage.googleapis.com/";
+    private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
+            .connectTimeout(Duration.ofSeconds(30))
+            .followRedirects(HttpClient.Redirect.NORMAL)
+            .build();
+
+    public List<String> transferImageUrls(List<String> imageUrls) {
+        if (imageUrls == null || imageUrls.isEmpty()) {
+            return imageUrls;
+        }
+
+        List<String> storedUrls = new ArrayList<>(imageUrls.size());
+        for (String imageUrl : imageUrls) {
+            storedUrls.add(transferUrl(imageUrl, "workflow/images"));
+        }
+        return storedUrls;
+    }
+
+    public String transferVideoUrl(String videoUrl) {
+        return transferUrl(videoUrl, "workflow/videos");
+    }
+
+    private String transferUrl(String sourceUrl, String prefix) {
+        if (sourceUrl == null || sourceUrl.isBlank()) {
+            return sourceUrl;
+        }
+        if (isGoogleStorageUrl(sourceUrl)) {
+            return sourceUrl;
+        }
+
+        try {
+            HttpRequest request = HttpRequest.newBuilder()
+                    .uri(URI.create(sourceUrl))
+                    .timeout(Duration.ofMinutes(5))
+                    .GET()
+                    .build();
+            HttpResponse<byte[]> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray());
+            if (response.statusCode() < 200 || response.statusCode() >= 300) {
+                log.warn("转存远程文件失败, statusCode={}, sourceUrl={}", response.statusCode(), sourceUrl);
+                return sourceUrl;
+            }
+
+            String contentType = normalizeContentType(response.headers().firstValue("Content-Type").orElse(null));
+            String extension = resolveExtension(sourceUrl, contentType);
+            String objectName = buildObjectName(prefix, extension);
+            String uploadedUrl = GoogleStorageUtil.upload(response.body(), objectName, contentType);
+            if (uploadedUrl == null) {
+                log.warn("上传到 Google Storage 失败, sourceUrl={}", sourceUrl);
+                return sourceUrl;
+            }
+
+            log.info("远程文件转存成功, sourceUrl={}, uploadedUrl={}", sourceUrl, uploadedUrl);
+            return uploadedUrl;
+        } catch (Exception e) {
+            log.warn("转存远程文件异常, sourceUrl={}", sourceUrl, e);
+            return sourceUrl;
+        }
+    }
+
+    private boolean isGoogleStorageUrl(String url) {
+        return url.startsWith(GOOGLE_STORAGE_BASE_URL);
+    }
+
+    private String buildObjectName(String prefix, String extension) {
+        return prefix + "/" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "/"
+                + UUID.randomUUID() + "." + extension;
+    }
+
+    private String normalizeContentType(String contentType) {
+        if (contentType == null || contentType.isBlank()) {
+            return null;
+        }
+        int separatorIndex = contentType.indexOf(';');
+        String normalized = separatorIndex >= 0 ? contentType.substring(0, separatorIndex) : contentType;
+        return normalized.trim().toLowerCase(Locale.ROOT);
+    }
+
+    private String resolveExtension(String sourceUrl, String contentType) {
+        String extensionFromUrl = extractExtensionFromUrl(sourceUrl);
+        if (extensionFromUrl != null) {
+            return extensionFromUrl;
+        }
+        if (contentType == null) {
+            return "bin";
+        }
+        return switch (contentType) {
+            case "image/jpeg" -> "jpg";
+            case "image/png" -> "png";
+            case "image/webp" -> "webp";
+            case "image/gif" -> "gif";
+            case "video/mp4" -> "mp4";
+            case "video/quicktime" -> "mov";
+            case "video/webm" -> "webm";
+            default -> "bin";
+        };
+    }
+
+    private String extractExtensionFromUrl(String sourceUrl) {
+        try {
+            String path = URI.create(sourceUrl).getPath();
+            if (path == null || path.isBlank()) {
+                return null;
+            }
+            int dotIndex = path.lastIndexOf('.');
+            if (dotIndex < 0 || dotIndex == path.length() - 1) {
+                return null;
+            }
+            String extension = path.substring(dotIndex + 1).toLowerCase(Locale.ROOT);
+            if (!extension.matches("[a-z0-9]{1,10}")) {
+                return null;
+            }
+            return extension;
+        } catch (Exception e) {
+            log.debug("解析 URL 扩展名失败, sourceUrl={}", sourceUrl, e);
+            return null;
+        }
+    }
+}