package com.xiaopeng.map.service.serviceimpl; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.HttpMethod; import com.aliyun.oss.model.GeneratePresignedUrlRequest; import com.aliyun.oss.model.ListObjectsRequest; import com.aliyun.oss.model.OSSObjectSummary; import com.aliyun.oss.model.ObjectListing; import com.aliyun.oss.model.ObjectMetadata; import com.xiaopeng.map.config.OssProperties; import com.xiaopeng.map.service.RobotaxiOssService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * ROBOTAXI OSS 服务实现,使用阿里云官方 OSS SDK 上传 ODD 图层文件。 * 路径格式:ROBOTAXI_ODD_layer/{环境}/{车辆uuid}/{fileName} */ @Slf4j @Service @RequiredArgsConstructor @ConditionalOnProperty(name = "aliyun.oss.enabled", havingValue = "true") public class RobotaxiOssServiceImpl implements RobotaxiOssService { private final OssProperties ossProperties; @Override public String uploadOddLayerFile(byte[] data, String fileName, String vehicleUuid) { if (data == null) { throw new IllegalArgumentException("data cannot be null"); } return uploadOddLayerFile(new ByteArrayInputStream(data), data.length, fileName, vehicleUuid); } @Override public String uploadOddLayerFile(InputStream inputStream, long contentLength, String fileName, String vehicleUuid) { validateParams(fileName, vehicleUuid); String objectKey = buildObjectKey(vehicleUuid, fileName); String bucketName = ossProperties.getBucketName(); OSS ossClient = null; try { ossClient = createOssClient(); ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentLength(contentLength); ossClient.putObject(bucketName, objectKey, inputStream, metadata); log.debug("Uploaded ODD layer file to OSS: {}", objectKey); return objectKey; } finally { if (ossClient != null) { ossClient.shutdown(); } } } @Override public boolean deleteByObjectKey(String objectKey) { if (!StringUtils.hasText(objectKey)) { throw new IllegalArgumentException("objectKey cannot be empty"); } String bucketName = ossProperties.getBucketName(); OSS ossClient = null; try { ossClient = createOssClient(); ossClient.deleteObject(bucketName, objectKey); log.debug("Deleted OSS object: {}", objectKey); return true; } catch (Exception e) { log.warn("Failed to delete OSS object: {}, error: {}", objectKey, e.getMessage()); return false; } finally { if (ossClient != null) { ossClient.shutdown(); } } } @Override public List deleteByObjectKeys(List objectKeys) { if (objectKeys == null || objectKeys.isEmpty()) { return new ArrayList<>(); } String bucketName = ossProperties.getBucketName(); OSS ossClient = null; List deleted = new ArrayList<>(); try { ossClient = createOssClient(); for (String key : objectKeys) { if (!StringUtils.hasText(key)) { continue; } try { ossClient.deleteObject(bucketName, key); deleted.add(key); log.debug("Deleted OSS object: {}", key); } catch (Exception e) { log.warn("Failed to delete OSS object: {}, error: {}", key, e.getMessage()); } } return deleted; } finally { if (ossClient != null) { ossClient.shutdown(); } } } @Override public List deleteOddLayerFilesByPrefix(String vehicleUuid) { validateParamsForPrefix(vehicleUuid); String prefix = buildObjectKeyPrefix(vehicleUuid); String bucketName = ossProperties.getBucketName(); OSS ossClient = null; try { ossClient = createOssClient(); List objectKeys = new ArrayList<>(); String nextMarker = null; do { ListObjectsRequest request = new ListObjectsRequest(bucketName).withPrefix(prefix).withMarker(nextMarker); ObjectListing listing = ossClient.listObjects(request); for (OSSObjectSummary summary : listing.getObjectSummaries()) { objectKeys.add(summary.getKey()); } nextMarker = listing.isTruncated() ? listing.getNextMarker() : null; } while (nextMarker != null); if (objectKeys.isEmpty()) { return new ArrayList<>(); } return deleteByObjectKeys(objectKeys); } finally { if (ossClient != null) { ossClient.shutdown(); } } } @Override public String generatePresignedDownloadUrl(String objectKey, int expireSeconds) { if (!StringUtils.hasText(objectKey)) { throw new IllegalArgumentException("objectKey cannot be empty"); } if (expireSeconds <= 0) { throw new IllegalArgumentException("expireSeconds must be positive"); } String bucketName = ossProperties.getBucketName(); OSS ossClient = null; try { ossClient = createOssClient(); GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectKey, HttpMethod.GET); request.setExpiration(new Date(System.currentTimeMillis() + expireSeconds * 1000L)); URL url = ossClient.generatePresignedUrl(request); return url != null ? url.toString() : null; } finally { if (ossClient != null) { ossClient.shutdown(); } } } private void validateParamsForPrefix(String vehicleUuid) { if (!StringUtils.hasText(vehicleUuid)) { throw new IllegalArgumentException("vehicleUuid cannot be empty"); } } private String buildObjectKeyPrefix(String vehicleUuid) { String pathPrefix = ossProperties.getPathPrefix(); if (!StringUtils.hasText(pathPrefix)) { pathPrefix = "ROBOTAXI_ODD_layer"; } String env = StringUtils.hasText(ossProperties.getEnv()) ? ossProperties.getEnv() : "test"; return String.format("%s/%s/%s/", pathPrefix, env, vehicleUuid); } private void validateParams(String fileName, String vehicleUuid) { if (!StringUtils.hasText(fileName)) { throw new IllegalArgumentException("fileName cannot be empty"); } if (!StringUtils.hasText(vehicleUuid)) { throw new IllegalArgumentException("vehicleUuid cannot be empty"); } } private String buildObjectKey(String vehicleUuid, String fileName) { String pathPrefix = ossProperties.getPathPrefix(); if (!StringUtils.hasText(pathPrefix)) { pathPrefix = "ROBOTAXI_ODD_layer"; } String env = StringUtils.hasText(ossProperties.getEnv()) ? ossProperties.getEnv() : "test"; return String.format("%s/%s/%s/%s", pathPrefix, env, vehicleUuid, fileName); } private OSS createOssClient() { String endpoint = ossProperties.getEndpoint(); String accessKeyId = ossProperties.getAccessKeyId(); String accessKeySecret = ossProperties.getAccessKeySecret(); if (!StringUtils.hasText(endpoint) || !StringUtils.hasText(accessKeyId) || !StringUtils.hasText(accessKeySecret)) { throw new IllegalStateException("OSS config incomplete: endpoint, accessKeyId, accessKeySecret must be set"); } return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); } /** * 构建 OSS 对象访问 URL:https://{bucket}.{endpoint}/{objectKey} */ private String buildObjectUrl(String bucketName, String objectKey) { String endpoint = ossProperties.getEndpoint(); if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) { int schemeEnd = endpoint.indexOf("://") + 3; endpoint = endpoint.substring(schemeEnd); } return String.format("https://%s.%s/%s", bucketName, endpoint, objectKey); } }