From 3420c72c1da75598952f139aedff983b07a4481c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Fri, 4 Sep 2026 10:21:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(oss):=20=E8=BD=AF=E4=BB=B6=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=AE=89=E8=A3=85=E5=8C=85=E6=98=BE=E5=BC=8F=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=85=AC=E5=BC=80=20client=20=E6=A1=B6=EF=BC=8C?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E5=8E=86=E5=8F=B2=E7=9B=B4=E9=93=BE=E5=B8=83?= =?UTF-8?q?=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生产 Java 未设 AIIMAGE_OSS_BUCKET,默认桶为私有 nanri-ai-images;而 web_config 历史版本全部存于公开 client 桶(oss.aishufu.top/client/nanri-image/versions/..., 桌面端检测更新匿名下载)。原 uploadSoftwareVersionPackage 走默认桶会把新版本写入 私桶导致客户端无法下载。改为显式写 software-version-bucket(新增可配属性,默认 client,env AIIMAGE_OSS_SOFTWARE_VERSION_BUCKET)并保持 nanri-image/versions/ 前缀。 注:本提交含 OssStorageService 上"软件版本上传"方法与早前 readObjectBytesBounded 改动,为软件版本模块(见 ③)的配套存储能力。 --- .../nanri/aiimage/config/OssProperties.java | 2 + .../file/service/oss/OssStorageService.java | 57 +++++++++++++++++++ .../src/main/resources/application.yml | 1 + 3 files changed, 60 insertions(+) diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/OssProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/OssProperties.java index 5bc875f8..a991894d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/OssProperties.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/OssProperties.java @@ -14,6 +14,8 @@ public class OssProperties { private String imageVideoBucket; private String digitalHumanBucket; private String templateBucket; + /** 桌面客户端软件安装包桶(历史存公开 client 桶,勿用默认私桶)。 */ + private String softwareVersionBucket; private String accessKeyId; private String accessKeySecret; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java index b77fa248..3c0c3dae 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java @@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.net.URI; import java.net.URLDecoder; @@ -29,6 +30,7 @@ public class OssStorageService { private static final String IMAGE_VIDEO_MODULE = "IMAGE_VIDEO"; private static final String DIGITAL_HUMAN_PREFIX = "digital-human/versions/"; + private static final String SOFTWARE_VERSION_PREFIX = "nanri-image/versions/"; private static final String LEGACY_MINIO_ENDPOINT = "http://47.110.241.161:9000"; private final OssProperties ossProperties; @@ -77,6 +79,26 @@ public class OssStorageService { return objectKey; } + /** + * 上传桌面客户端软件安装包(web_config 关联的 zip):写入**公开 client 桶**的 + * {@code nanri-image/versions/} 前缀(对齐 Flask 历史对象布局与公开直链, + * 勿用默认私有桶 nanri-ai-images,否则桌面端"检测更新"无法匿名下载)。 + * 桶可用 aiimage.oss.software-version-bucket(env AIIMAGE_OSS_SOFTWARE_VERSION_BUCKET)覆盖。 + */ + public String uploadSoftwareVersionPackage(File file, String objectKey) { + if (objectKey == null || !objectKey.startsWith(SOFTWARE_VERSION_PREFIX)) { + throw new IllegalArgumentException("software version objectKey must start with " + SOFTWARE_VERSION_PREFIX); + } + String bucket = softwareVersionBucket(); + uploadFile(file, bucket, objectKey); + return getPublicUrl(objectKey, bucket); + } + + private String softwareVersionBucket() { + String configured = ossProperties.getSoftwareVersionBucket(); + return (configured == null || configured.isBlank()) ? "client" : configured; + } + public String uploadText(String objectKey, String content) { if (objectKey == null || objectKey.isBlank()) { throw new IllegalArgumentException("objectKey must not be blank"); @@ -159,6 +181,41 @@ public class OssStorageService { return readObjectBytes(location.bucket(), location.objectKey()); } + /** + * Reads a managed result object with a hard cap on the total bytes read. + * Streaming 1 MiB chunks, aborting once {@code maxBytes} is exceeded. + * The caller is expected to translate the value result_file_size. + */ + public byte[] readObjectBytesBounded(String value, long maxBytes) { + StorageLocation location = resolveStorageLocation(value); + if (location == null) { + throw new IllegalArgumentException("object value must not be blank"); + } + String bucket = location.bucket(); + String objectKey = location.objectKey(); + try (var stream = buildClient().getObject(GetObjectArgs.builder() + .bucket(bucket) + .object(objectKey) + .build())) { + byte[] buffer = new byte[1024 * 1024]; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + long total = 0; + int read; + while ((read = stream.read(buffer)) != -1) { + total += read; + if (total > maxBytes) { + throw new IllegalArgumentException("结果文件过大,无法分析"); + } + out.write(buffer, 0, read); + } + return out.toByteArray(); + } catch (IllegalArgumentException ex) { + throw ex; + } catch (Exception ex) { + throw storageFailure("read", objectKey, ex); + } + } + public boolean objectExists(String bucket, String objectKey) { String normalizedBucket = requireStorageName(bucket, "bucket"); String normalizedObjectKey = requireStorageName(objectKey, "objectKey"); diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index 5b105c93..160ed48b 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -110,6 +110,7 @@ aiimage: image-video-bucket: ${AIIMAGE_IMAGE_VIDEO_OSS_BUCKET:shufu-video} digital-human-bucket: ${AIIMAGE_DIGITAL_HUMAN_OSS_BUCKET:nanri-ai-digital-human} template-bucket: ${AIIMAGE_TEMPLATE_OSS_BUCKET:aiimage-templates} + software-version-bucket: ${AIIMAGE_OSS_SOFTWARE_VERSION_BUCKET:client} access-key-id: ${AIIMAGE_OSS_ACCESS_KEY_ID:appuser} access-key-secret: ${AIIMAGE_OSS_ACCESS_KEY_SECRET:AppUser@2024SecureKey} transient-storage: