✨ 接入账单 OSS 存储并区分生产测试目录
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user