✨ 接入账单 OSS 存储并区分生产测试目录

This commit is contained in:
caojia
2026-10-16 16:44:51 +08:00
parent 9824e05919
commit b0719d2ae9
9 changed files with 406 additions and 36 deletions
+7 -1
View File
@@ -17,6 +17,12 @@
<dependencies>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.18.3</version>
</dependency>
<!-- Spring框架基本的核心工具 -->
<dependency>
<groupId>org.springframework</groupId>
@@ -120,4 +126,4 @@
</dependencies>
</project>
</project>
@@ -0,0 +1,30 @@
package com.bms.common.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/** OSS 连接配置;密钥由环境变量或外部配置提供。 */
@Component
@ConfigurationProperties(prefix = "oss")
public class OssProperties {
private String endpoint;
private String bucket;
private String accessKeyId;
private String accessKeySecret;
private boolean cname = true;
/** 仅用于新上传对象;历史文件按数据库中的完整存储标识读取。 */
private String pathPrefix = "bms/test";
public String getEndpoint() { return endpoint; }
public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
public String getBucket() { return bucket; }
public void setBucket(String bucket) { this.bucket = bucket; }
public String getAccessKeyId() { return accessKeyId; }
public void setAccessKeyId(String accessKeyId) { this.accessKeyId = accessKeyId; }
public String getAccessKeySecret() { return accessKeySecret; }
public void setAccessKeySecret(String accessKeySecret) { this.accessKeySecret = accessKeySecret; }
public boolean isCname() { return cname; }
public void setCname(boolean cname) { this.cname = cname; }
public String getPathPrefix() { return pathPrefix; }
public void setPathPrefix(String pathPrefix) { this.pathPrefix = pathPrefix; }
}
@@ -0,0 +1,105 @@
package com.bms.common.utils.file;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.CannedAccessControlList;
import com.aliyun.oss.model.OSSObject;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import com.bms.common.config.OssProperties;
import com.bms.common.exception.ServiceException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/** 通用 OSS 文件工具。数据库保存 oss://bucket/key,下载通过后端鉴权接口转发。 */
@Component
public class OssStorage {
private final OssProperties properties;
public OssStorage(OssProperties properties) {
this.properties = properties;
}
/** 使用文件流上传,避免把整个账单读入内存。返回可持久化的存储标识。 */
public String upload(Path file, String objectKey, String contentType) throws IOException {
validateObjectKey(objectKey);
String prefix = properties.getPathPrefix();
validateObjectKey(prefix);
if (prefix.endsWith("/")) {
throw new ServiceException("OSS 存储前缀不能以 / 结尾");
}
objectKey = prefix + "/" + objectKey;
OSS client = createClient();
try (InputStream input = Files.newInputStream(file)) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(Files.size(file));
metadata.setContentType(contentType);
metadata.setObjectAcl(CannedAccessControlList.Private);
client.putObject(new PutObjectRequest(properties.getBucket(), objectKey, input, metadata));
return "oss://" + properties.getBucket() + "/" + objectKey;
} finally {
client.shutdown();
}
}
/** 不关闭调用方的响应输出流。 */
public void download(String storageKey, OutputStream output) throws IOException {
String objectKey = resolveObjectKey(storageKey);
OSS client = createClient();
try (OSSObject object = client.getObject(properties.getBucket(), objectKey)) {
object.getObjectContent().transferTo(output);
} finally {
client.shutdown();
}
}
public void delete(String storageKey) {
String objectKey = resolveObjectKey(storageKey);
OSS client = createClient();
try {
client.deleteObject(properties.getBucket(), objectKey);
} finally {
client.shutdown();
}
}
private String resolveObjectKey(String storageKey) {
String prefix = "oss://" + properties.getBucket() + "/";
if (storageKey == null || !storageKey.startsWith(prefix)) {
throw new ServiceException("OSS 文件存储标识非法或 Bucket 不匹配");
}
String objectKey = storageKey.substring(prefix.length());
validateObjectKey(objectKey);
return objectKey;
}
private void validateObjectKey(String objectKey) {
if (!StringUtils.hasText(objectKey) || objectKey.startsWith("/")
|| objectKey.contains("\\") || objectKey.contains("..")) {
throw new ServiceException("OSS 对象路径非法");
}
}
// 延迟检查密钥,未配置 OSS 时不影响历史本地文件下载及其他业务启动。
private OSS createClient() {
if (!StringUtils.hasText(properties.getEndpoint()) || !StringUtils.hasText(properties.getBucket())
|| !StringUtils.hasText(properties.getAccessKeyId())
|| !StringUtils.hasText(properties.getAccessKeySecret())) {
throw new ServiceException("请配置 OSS endpoint、bucket、access-key-id 和 access-key-secret");
}
ClientBuilderConfiguration config = new ClientBuilderConfiguration();
config.setSupportCname(properties.isCname());
config.setConnectionTimeout(10000);
config.setConnectionRequestTimeout(10000);
config.setSocketTimeout(60000);
return new OSSClientBuilder().build(properties.getEndpoint(), properties.getAccessKeyId(),
properties.getAccessKeySecret(), config);
}
}