This commit is contained in:
lxp
2026-07-07 15:47:14 +08:00
parent 55d74653da
commit 234a166924
34 changed files with 1566 additions and 18 deletions
@@ -0,0 +1,145 @@
package com.cadlabel.service.client;
import com.cadlabel.common.exception.BizException;
import com.cadlabel.config.CosProperties;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.region.Region;
import com.tencent.cloud.CosStsClient;
import com.tencent.cloud.Response;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.util.TreeMap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Slf4j
@Component
public class TencentCosObjectStorageClient implements CosObjectStorageClient, DisposableBean {
private static final String[] ALLOWED_ACTIONS = new String[]{
"name/cos:PutObject",
"name/cos:InitiateMultipartUpload",
"name/cos:ListMultipartUploads",
"name/cos:ListParts",
"name/cos:UploadPart",
"name/cos:CompleteMultipartUpload",
"name/cos:AbortMultipartUpload"
};
private final CosProperties cosProperties;
private final COSClient cosClient;
public TencentCosObjectStorageClient(CosProperties cosProperties) {
this.cosProperties = cosProperties;
this.cosClient = createCosClient(cosProperties);
}
@Override
public TemporaryCredentials issueTemporaryCredentials(String keyPrefix, Duration duration) {
validateConfiguration();
int durationSeconds = validateDuration(duration);
TreeMap<String, Object> config = new TreeMap<>();
config.put("secretId", cosProperties.secretId());
config.put("secretKey", cosProperties.secretKey());
config.put("bucket", cosProperties.bucket());
config.put("region", cosProperties.region());
config.put("durationSeconds", durationSeconds);
config.put("allowPrefixes", new String[]{normalizeKeyPrefix(keyPrefix) + "*"});
config.put("allowActions", ALLOWED_ACTIONS);
try {
Response response = CosStsClient.getCredential(config);
if (response == null || response.credentials == null
|| !StringUtils.hasText(response.credentials.tmpSecretId)
|| !StringUtils.hasText(response.credentials.tmpSecretKey)
|| !StringUtils.hasText(response.credentials.sessionToken)) {
throw new BizException(HttpStatus.BAD_GATEWAY.value(), "COS_UPLOAD_CREDENTIALS_FAILED", "COS 未返回完整的临时上传凭证");
}
return new TemporaryCredentials(
response.credentials.tmpSecretId,
response.credentials.tmpSecretKey,
response.credentials.sessionToken,
response.startTime,
response.expiredTime
);
} catch (IOException exception) {
log.error("Failed to issue COS upload credentials. keyPrefix={}", keyPrefix, exception);
throw new BizException(HttpStatus.BAD_GATEWAY.value(), "COS_UPLOAD_CREDENTIALS_FAILED",
"申请 COS 临时上传凭证失败: " + exception.getMessage());
}
}
@Override
public void putObject(String objectKey, InputStream inputStream, long contentLength, String contentType) {
validateConfiguration();
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(contentLength);
if (StringUtils.hasText(contentType)) {
metadata.setContentType(contentType);
}
try {
cosClient.putObject(new PutObjectRequest(cosProperties.bucket(), normalizeKeyPrefix(objectKey), inputStream, metadata));
} catch (CosClientException exception) {
log.error("Failed to upload COS object. objectKey={}", objectKey, exception);
throw new BizException(HttpStatus.BAD_GATEWAY.value(), "COS_OBJECT_UPLOAD_FAILED",
"上传 COS 对象失败: " + exception.getMessage());
}
}
@Override
public void destroy() {
cosClient.shutdown();
}
private static COSClient createCosClient(CosProperties cosProperties) {
String region = StringUtils.hasText(cosProperties.region()) ? cosProperties.region().trim() : "ap-beijing";
return new COSClient(
new BasicCOSCredentials(defaultText(cosProperties.secretId()), defaultText(cosProperties.secretKey())),
new ClientConfig(new Region(region))
);
}
private void validateConfiguration() {
requireText(cosProperties.secretId(), "APP_COS_SECRET_ID");
requireText(cosProperties.secretKey(), "APP_COS_SECRET_KEY");
requireText(cosProperties.bucket(), "APP_COS_BUCKET");
requireText(cosProperties.region(), "APP_COS_REGION");
}
private int validateDuration(Duration duration) {
if (duration == null || duration.isZero() || duration.isNegative()) {
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "COS_CONFIG_ERROR", "COS 上传会话有效期配置非法");
}
long seconds = duration.toSeconds();
if (seconds > Integer.MAX_VALUE) {
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "COS_CONFIG_ERROR", "COS 上传会话有效期配置过大");
}
return (int) seconds;
}
private String normalizeKeyPrefix(String keyPrefix) {
String normalized = defaultText(keyPrefix).trim().replace("\\", "/");
while (normalized.startsWith("/")) {
normalized = normalized.substring(1);
}
return normalized;
}
private void requireText(String value, String envName) {
if (!StringUtils.hasText(value)) {
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "COS_CONFIG_ERROR", "COS 配置缺失: " + envName);
}
}
private static String defaultText(String value) {
return value == null ? "" : value;
}
}