feat: vin转uuid、地图文件上传逻辑补充
| | |
| | | <artifactId>xp-common-log</artifactId> |
| | | </dependency> |
| | | |
| | | <!-- xiaopeng Common Redis(XpMapController、XpElectronicFenceAreaServiceImpl、HolidayUtil 等使用 RedisService) --> |
| | | <dependency> |
| | | <groupId>com.xiaopeng</groupId> |
| | | <artifactId>xp-common-redis</artifactId> |
| | | </dependency> |
| | | |
| | | <dependency> |
| | | <groupId>com.xiaopeng</groupId> |
| | | <artifactId>xp-common-swagger</artifactId> |
| | |
| | | <artifactId>snakeyaml</artifactId> |
| | | <version>1.33</version> |
| | | </dependency> |
| | | |
| | | <!-- FlatBuffers Java runtime for map area RTree fbs 构建 --> |
| | | <dependency> |
| | | <groupId>com.google.flatbuffers</groupId> |
| | | <artifactId>flatbuffers-java</artifactId> |
| | | <version>23.5.26</version> |
| | | </dependency> |
| | | |
| | | <!-- Protobuf for map area data proto 序列化 --> |
| | | <dependency> |
| | | <groupId>com.google.protobuf</groupId> |
| | | <artifactId>protobuf-java</artifactId> |
| | | <version>3.19.4</version> |
| | | </dependency> |
| | | |
| | | <!-- 阿里云 OSS 官方 SDK(ROBOTAXI ODD 图层上传) --> |
| | | <dependency> |
| | | <groupId>com.aliyun.oss</groupId> |
| | | <artifactId>aliyun-sdk-oss</artifactId> |
| | | <version>3.18.5</version> |
| | | </dependency> |
| | | </dependencies> |
| | | |
| | | <build> |
| New file |
| | |
| | | package com.xiaopeng.map.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | /** |
| | | * 阿里云 OSS 配置属性(ROBOTAXI ODD 图层上传) |
| | | */ |
| | | @Data |
| | | @Component |
| | | @ConfigurationProperties(prefix = "aliyun.oss") |
| | | public class OssProperties { |
| | | |
| | | /** |
| | | * 是否启用 OSS |
| | | */ |
| | | private boolean enabled = true; |
| | | |
| | | /** |
| | | * OSS Endpoint,如 oss-cn-beijing.aliyuncs.com |
| | | */ |
| | | private String endpoint; |
| | | |
| | | /** |
| | | * AccessKey ID |
| | | */ |
| | | private String accessKeyId; |
| | | |
| | | /** |
| | | * AccessKey Secret |
| | | */ |
| | | private String accessKeySecret; |
| | | |
| | | /** |
| | | * Bucket 名称 |
| | | */ |
| | | private String bucketName = "cngp-crowdsourcing"; |
| | | |
| | | /** |
| | | * 路径前缀,如 ROBOTAXI_ODD_layer |
| | | */ |
| | | private String pathPrefix = "ROBOTAXI_ODD_layer"; |
| | | |
| | | /** |
| | | * OSS 路径中的环境:test(测试)、staging(预发)、prod(生产) |
| | | * 支持占位符:${spring.profiles.active} 或环境变量 ${OSS_ENV:test} |
| | | */ |
| | | private String env = "test"; |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.config; |
| | | |
| | | import lombok.Data; |
| | | import org.springframework.boot.context.properties.ConfigurationProperties; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | /** |
| | | * VIN 转 UUID 盐值配置,用于 VehicleCodec.vinToUUID |
| | | */ |
| | | @Data |
| | | @Component |
| | | @ConfigurationProperties(prefix = "adc.vin") |
| | | public class VinSaltProperties { |
| | | |
| | | /** |
| | | * VIN 转 UUID 盐值1 |
| | | */ |
| | | private String salt1; |
| | | |
| | | /** |
| | | * VIN 转 UUID 盐值2 |
| | | */ |
| | | private String salt2; |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.dto; |
| | | |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Builder; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 对应 map_area_data.proto 中的 MapAreaData。 |
| | | */ |
| | | @Data |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Builder |
| | | public class MapAreaDataDto { |
| | | private int version; |
| | | private List<OddAreaDto> oddAreas; |
| | | |
| | | public List<OddAreaDto> getOddAreas() { |
| | | if (oddAreas == null) { |
| | | oddAreas = new ArrayList<>(); |
| | | } |
| | | return oddAreas; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.dto; |
| | | |
| | | import com.google.flatbuffers.FlatBufferBuilder; |
| | | import com.google.flatbuffers.Table; |
| | | |
| | | /** |
| | | * 对应 map_area_rtree.fbs 中的 Node 表。 |
| | | * 只实现构建 FlatBuffer 所需的最小方法集合。 |
| | | */ |
| | | public final class Node extends Table { |
| | | |
| | | private Node() { |
| | | } |
| | | |
| | | public static int createNode(FlatBufferBuilder builder, |
| | | int type, |
| | | int rangeOffset, |
| | | int pointsVectorOffset, |
| | | int id, |
| | | boolean isBlacklist) { |
| | | builder.startTable(5); |
| | | addType(builder, type); |
| | | addRange(builder, rangeOffset); |
| | | addPoints(builder, pointsVectorOffset); |
| | | addId(builder, id); |
| | | addIsBlacklist(builder, isBlacklist); |
| | | return endNode(builder); |
| | | } |
| | | |
| | | public static void startNode(FlatBufferBuilder builder) { |
| | | builder.startTable(5); |
| | | } |
| | | |
| | | public static void addType(FlatBufferBuilder builder, int type) { |
| | | builder.addInt(0, type, 0); |
| | | } |
| | | |
| | | public static void addRange(FlatBufferBuilder builder, int rangeOffset) { |
| | | builder.addOffset(1, rangeOffset, 0); |
| | | } |
| | | |
| | | public static void addPoints(FlatBufferBuilder builder, int pointsOffset) { |
| | | builder.addOffset(2, pointsOffset, 0); |
| | | } |
| | | |
| | | public static void addId(FlatBufferBuilder builder, int id) { |
| | | builder.addInt(3, id, 0); |
| | | } |
| | | |
| | | public static void addIsBlacklist(FlatBufferBuilder builder, boolean isBlacklist) { |
| | | builder.addBoolean(4, isBlacklist, false); |
| | | } |
| | | |
| | | /** |
| | | * 构建 points 向量。 |
| | | */ |
| | | public static int createPointsVector(FlatBufferBuilder builder, int[] data) { |
| | | builder.startVector(4, data.length, 4); |
| | | for (int i = data.length - 1; i >= 0; i--) { |
| | | builder.addOffset(data[i]); |
| | | } |
| | | return builder.endVector(); |
| | | } |
| | | |
| | | public static void startPointsVector(FlatBufferBuilder builder, int numElems) { |
| | | builder.startVector(4, numElems, 4); |
| | | } |
| | | |
| | | public static int endNode(FlatBufferBuilder builder) { |
| | | return builder.endTable(); |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | package com.xiaopeng.map.dto; |
| | | |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Builder; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 对应 map_area_data.proto 中的 OddArea。 |
| | | */ |
| | | @Data |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Builder |
| | | public class OddAreaDto { |
| | | private int oddId; |
| | | private List<OddPointDto> oddPoints; |
| | | private boolean isBlacklist; |
| | | |
| | | public List<OddPointDto> getOddPoints() { |
| | | if (oddPoints == null) { |
| | | oddPoints = new ArrayList<>(); |
| | | } |
| | | return oddPoints; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.dto; |
| | | |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Builder; |
| | | |
| | | /** |
| | | * 对应 map_area_data.proto 中的 OddPoint。 |
| | | */ |
| | | @Data |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | @Builder |
| | | public class OddPointDto { |
| | | private double latitude; |
| | | private double longitude; |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.dto; |
| | | |
| | | import com.google.flatbuffers.FlatBufferBuilder; |
| | | import com.google.flatbuffers.Table; |
| | | |
| | | /** |
| | | * 对应 map_area_rtree.fbs 中的 Point2D 表。 |
| | | * 只实现构建 FlatBuffer 所需的最小方法集合。 |
| | | */ |
| | | public final class Point2D extends Table { |
| | | |
| | | private Point2D() { |
| | | } |
| | | |
| | | public static int createPoint2D(FlatBufferBuilder builder, float x, float y) { |
| | | builder.startTable(2); |
| | | addX(builder, x); |
| | | addY(builder, y); |
| | | return endPoint2D(builder); |
| | | } |
| | | |
| | | public static void startPoint2D(FlatBufferBuilder builder) { |
| | | builder.startTable(2); |
| | | } |
| | | |
| | | public static void addX(FlatBufferBuilder builder, float x) { |
| | | builder.addFloat(0, x, 0.0f); |
| | | } |
| | | |
| | | public static void addY(FlatBufferBuilder builder, float y) { |
| | | builder.addFloat(1, y, 0.0f); |
| | | } |
| | | |
| | | public static int endPoint2D(FlatBufferBuilder builder) { |
| | | return builder.endTable(); |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | package com.xiaopeng.map.dto; |
| | | |
| | | import com.google.flatbuffers.FlatBufferBuilder; |
| | | import com.google.flatbuffers.Table; |
| | | |
| | | /** |
| | | * 对应 map_area_rtree.fbs 中的 RTree 表。 |
| | | * 只实现构建 FlatBuffer 所需的最小方法集合。 |
| | | */ |
| | | public final class RTree extends Table { |
| | | |
| | | private RTree() { |
| | | } |
| | | |
| | | public static int createRTree(FlatBufferBuilder builder, int nodesVectorOffset) { |
| | | builder.startTable(1); |
| | | addNodes(builder, nodesVectorOffset); |
| | | return endRTree(builder); |
| | | } |
| | | |
| | | public static void startRTree(FlatBufferBuilder builder) { |
| | | builder.startTable(1); |
| | | } |
| | | |
| | | public static void addNodes(FlatBufferBuilder builder, int nodesOffset) { |
| | | builder.addOffset(0, nodesOffset, 0); |
| | | } |
| | | |
| | | /** |
| | | * 构建 nodes 向量。 |
| | | */ |
| | | public static int createNodesVector(FlatBufferBuilder builder, int[] data) { |
| | | builder.startVector(4, data.length, 4); |
| | | for (int i = data.length - 1; i >= 0; i--) { |
| | | builder.addOffset(data[i]); |
| | | } |
| | | return builder.endVector(); |
| | | } |
| | | |
| | | public static void startNodesVector(FlatBufferBuilder builder, int numElems) { |
| | | builder.startVector(4, numElems, 4); |
| | | } |
| | | |
| | | public static int endRTree(FlatBufferBuilder builder) { |
| | | return builder.endTable(); |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | package com.xiaopeng.map.dto; |
| | | |
| | | import com.google.flatbuffers.FlatBufferBuilder; |
| | | import com.google.flatbuffers.Table; |
| | | |
| | | /** |
| | | * 对应 map_area_rtree.fbs 中的 Rect 表。 |
| | | * 只实现构建 FlatBuffer 所需的最小方法集合。 |
| | | */ |
| | | public final class Rect extends Table { |
| | | |
| | | private Rect() { |
| | | } |
| | | |
| | | public static int createRect(FlatBufferBuilder builder, int leftDownOffset, int rightUpOffset) { |
| | | builder.startTable(2); |
| | | addLeftDown(builder, leftDownOffset); |
| | | addRightUp(builder, rightUpOffset); |
| | | return endRect(builder); |
| | | } |
| | | |
| | | public static void startRect(FlatBufferBuilder builder) { |
| | | builder.startTable(2); |
| | | } |
| | | |
| | | public static void addLeftDown(FlatBufferBuilder builder, int leftDownOffset) { |
| | | builder.addOffset(0, leftDownOffset, 0); |
| | | } |
| | | |
| | | public static void addRightUp(FlatBufferBuilder builder, int rightUpOffset) { |
| | | builder.addOffset(1, rightUpOffset, 0); |
| | | } |
| | | |
| | | public static int endRect(FlatBufferBuilder builder) { |
| | | return builder.endTable(); |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | package com.xiaopeng.map.service; |
| | | |
| | | import com.xiaopeng.map.utils.XngpAreaRTreeFbsUtil; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * ADC OSS 处理服务,根据坐标点生成 RTree fbs 并上传至 OSS。 |
| | | */ |
| | | public interface AdcOssHandleService { |
| | | |
| | | /** |
| | | * 根据坐标点生成 RTree fbs 并上传至 OSS |
| | | * |
| | | * @param points 多边形顶点(使用 XngpAreaRTreeFbsUtil.LonLat) |
| | | * @param areaId 区域 areaId |
| | | * @param vin 车辆 VIN 码 |
| | | * @param areaCode 区域编码 |
| | | * @return 上传后的 objectKey,失败抛异常 |
| | | */ |
| | | String uploadOddLayerFile(List<XngpAreaRTreeFbsUtil.LonLat> points, int areaId, String vin, String areaCode); |
| | | |
| | | /** |
| | | * 删除指定车辆在指定区域下的 ODD 图层文件 |
| | | * |
| | | * @param vin 车辆 VIN 码 |
| | | * @param areaCode 区域编码 |
| | | * @return 成功删除的 objectKey 列表 |
| | | */ |
| | | List<String> deleteOddLayerFiles(String vin, String areaCode); |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.service; |
| | | |
| | | import java.io.InputStream; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * ROBOTAXI OSS 服务,用于上传 ODD 图层文件到阿里云 OSS。 |
| | | * 路径格式:ROBOTAXI_ODD_layer/{区域编码}/{环境}/{车辆uuid}/{fileName} |
| | | */ |
| | | public interface RobotaxiOssService { |
| | | |
| | | /** |
| | | * 上传 ODD 图层文件到 OSS |
| | | * 路径:{pathPrefix}/{areaCode}/{env}/{vehicleUuid}/{fileName} |
| | | * |
| | | * @param data 文件内容 |
| | | * @param fileName 文件名(如 map_area_data.bin、map_area_rtree.fbs) |
| | | * @param areaCode 区域编码(动态) |
| | | * @param vehicleUuid 车辆 UUID(动态) |
| | | * @return 上传成功后的 objectKey,失败时抛异常 |
| | | */ |
| | | String uploadOddLayerFile(byte[] data, String fileName, String areaCode, String vehicleUuid); |
| | | |
| | | /** |
| | | * 上传 ODD 图层文件到 OSS(InputStream 重载) |
| | | * |
| | | * @param inputStream 文件输入流 |
| | | * @param contentLength 文件大小 |
| | | * @param fileName 文件名 |
| | | * @param areaCode 区域编码 |
| | | * @param vehicleUuid 车辆 UUID |
| | | * @return 上传成功后的 objectKey,失败时抛异常 |
| | | */ |
| | | String uploadOddLayerFile(InputStream inputStream, long contentLength, String fileName, |
| | | String areaCode, String vehicleUuid); |
| | | |
| | | /** |
| | | * 根据 OSS objectKey 删除单个文件 |
| | | * |
| | | * @param objectKey OSS 对象键,如 ROBOTAXI_ODD_layer/440100/test/vehicle-001/map_area_rtree.fbs |
| | | * @return 是否删除成功 |
| | | */ |
| | | boolean deleteByObjectKey(String objectKey); |
| | | |
| | | /** |
| | | * 根据 OSS objectKey 批量删除文件 |
| | | * |
| | | * @param objectKeys OSS 对象键列表 |
| | | * @return 成功删除的对象键列表 |
| | | */ |
| | | List<String> deleteByObjectKeys(List<String> objectKeys); |
| | | |
| | | /** |
| | | * 按前缀删除 ODD 路径下所有文件 |
| | | * 路径前缀:{pathPrefix}/{areaCode}/{env}/{vehicleUuid}/ |
| | | * |
| | | * @param areaCode 区域编码 |
| | | * @param vehicleUuid 车辆 UUID |
| | | * @return 成功删除的 objectKey 列表 |
| | | */ |
| | | List<String> deleteOddLayerFilesByPrefix(String areaCode, String vehicleUuid); |
| | | |
| | | /** |
| | | * 根据 objectKey 生成文件下载临时链接(Presigned URL) |
| | | * |
| | | * @param objectKey OSS 对象键 |
| | | * @param expireSeconds 有效期(秒) |
| | | * @return 带签名的临时下载 URL |
| | | */ |
| | | String generatePresignedDownloadUrl(String objectKey, int expireSeconds); |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.service.serviceimpl; |
| | | |
| | | import com.xiaopeng.map.config.VinSaltProperties; |
| | | import com.xiaopeng.map.service.AdcOssHandleService; |
| | | import com.xiaopeng.map.service.RobotaxiOssService; |
| | | import com.xiaopeng.map.utils.VehicleCodec; |
| | | import com.xiaopeng.map.utils.XngpAreaRTreeFbsUtil; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.CollectionUtils; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * ADC OSS 处理服务实现,根据坐标点生成 RTree fbs 并上传至 OSS。 |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | @ConditionalOnBean(RobotaxiOssService.class) |
| | | public class AdcOssHandleServiceImpl implements AdcOssHandleService { |
| | | |
| | | private static final String FILE_NAME_FORMAT = "yyyyMMddHHmmss"; |
| | | |
| | | private final RobotaxiOssService robotaxiOssService; |
| | | private final VinSaltProperties vinSaltProperties; |
| | | |
| | | @Override |
| | | public String uploadOddLayerFile(List<XngpAreaRTreeFbsUtil.LonLat> points, int areaId, String vin, String areaCode) { |
| | | validateParams(points, vin, areaCode); |
| | | |
| | | String vehicleUuid = VehicleCodec.vinToUUID(vin, vinSaltProperties.getSalt1(), vinSaltProperties.getSalt2()); |
| | | |
| | | byte[] fbsBytes = XngpAreaRTreeFbsUtil.buildRTreeBytesFromPoints(points, areaId, false); |
| | | if (fbsBytes == null || fbsBytes.length == 0) { |
| | | throw new IllegalStateException("Failed to build RTree fbs from points"); |
| | | } |
| | | |
| | | String fileName = new SimpleDateFormat(FILE_NAME_FORMAT).format(new Date()) + "_xngp_area_data.bin"; |
| | | |
| | | String objectKey = robotaxiOssService.uploadOddLayerFile(fbsBytes, fileName, areaCode, vehicleUuid); |
| | | log.debug("AdcOssHandle uploaded ODD layer file, areaId={}, areaCode={}, objectKey={}", areaId, areaCode, objectKey); |
| | | return objectKey; |
| | | } |
| | | |
| | | @Override |
| | | public List<String> deleteOddLayerFiles(String vin, String areaCode) { |
| | | if (!StringUtils.hasText(vin)) { |
| | | throw new IllegalArgumentException("vin cannot be empty"); |
| | | } |
| | | if (!StringUtils.hasText(areaCode)) { |
| | | throw new IllegalArgumentException("areaCode cannot be empty"); |
| | | } |
| | | String vehicleUuid = VehicleCodec.vinToUUID(vin, vinSaltProperties.getSalt1(), vinSaltProperties.getSalt2()); |
| | | List<String> deleted = robotaxiOssService.deleteOddLayerFilesByPrefix(areaCode, vehicleUuid); |
| | | log.debug("AdcOssHandle deleted ODD layer files, vin={}, areaCode={}, count={}", vin, areaCode, deleted.size()); |
| | | return deleted; |
| | | } |
| | | |
| | | private void validateParams(List<XngpAreaRTreeFbsUtil.LonLat> points, String vin, String areaCode) { |
| | | if (CollectionUtils.isEmpty(points)) { |
| | | throw new IllegalArgumentException("points cannot be empty"); |
| | | } |
| | | if (!StringUtils.hasText(vin)) { |
| | | throw new IllegalArgumentException("vin cannot be empty"); |
| | | } |
| | | if (!StringUtils.hasText(areaCode)) { |
| | | throw new IllegalArgumentException("areaCode cannot be empty"); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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 areaCode, String vehicleUuid) { |
| | | if (data == null) { |
| | | throw new IllegalArgumentException("data cannot be null"); |
| | | } |
| | | return uploadOddLayerFile(new ByteArrayInputStream(data), data.length, fileName, areaCode, vehicleUuid); |
| | | } |
| | | |
| | | @Override |
| | | public String uploadOddLayerFile(InputStream inputStream, long contentLength, String fileName, |
| | | String areaCode, String vehicleUuid) { |
| | | validateParams(fileName, areaCode, vehicleUuid); |
| | | |
| | | String objectKey = buildObjectKey(areaCode, 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<String> deleteByObjectKeys(List<String> objectKeys) { |
| | | if (objectKeys == null || objectKeys.isEmpty()) { |
| | | return new ArrayList<>(); |
| | | } |
| | | String bucketName = ossProperties.getBucketName(); |
| | | OSS ossClient = null; |
| | | List<String> 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<String> deleteOddLayerFilesByPrefix(String areaCode, String vehicleUuid) { |
| | | validateParamsForPrefix(areaCode, vehicleUuid); |
| | | |
| | | String prefix = buildObjectKeyPrefix(areaCode, vehicleUuid); |
| | | String bucketName = ossProperties.getBucketName(); |
| | | |
| | | OSS ossClient = null; |
| | | try { |
| | | ossClient = createOssClient(); |
| | | List<String> 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 areaCode, String vehicleUuid) { |
| | | if (!StringUtils.hasText(areaCode)) { |
| | | throw new IllegalArgumentException("areaCode cannot be empty"); |
| | | } |
| | | if (!StringUtils.hasText(vehicleUuid)) { |
| | | throw new IllegalArgumentException("vehicleUuid cannot be empty"); |
| | | } |
| | | } |
| | | |
| | | private String buildObjectKeyPrefix(String areaCode, 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/%s/", pathPrefix, areaCode, env, vehicleUuid); |
| | | } |
| | | |
| | | private void validateParams(String fileName, String areaCode, String vehicleUuid) { |
| | | if (!StringUtils.hasText(fileName)) { |
| | | throw new IllegalArgumentException("fileName cannot be empty"); |
| | | } |
| | | if (!StringUtils.hasText(areaCode)) { |
| | | throw new IllegalArgumentException("areaCode cannot be empty"); |
| | | } |
| | | if (!StringUtils.hasText(vehicleUuid)) { |
| | | throw new IllegalArgumentException("vehicleUuid cannot be empty"); |
| | | } |
| | | } |
| | | |
| | | private String buildObjectKey(String areaCode, 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/%s", pathPrefix, areaCode, 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); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.utils; |
| | | |
| | | import com.google.protobuf.CodedOutputStream; |
| | | import com.xiaopeng.map.dto.MapAreaDataDto; |
| | | import com.xiaopeng.map.dto.OddAreaDto; |
| | | import com.xiaopeng.map.dto.OddPointDto; |
| | | |
| | | import java.io.FileOutputStream; |
| | | import java.io.IOException; |
| | | import java.io.OutputStream; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 将 MapAreaDataDto 序列化为与 map_area_data.proto 兼容的 Protobuf 二进制格式。 |
| | | */ |
| | | public final class MapAreaDataProtoWriter { |
| | | |
| | | private MapAreaDataProtoWriter() { |
| | | } |
| | | |
| | | public static void writeTo(MapAreaDataDto data, OutputStream out) throws IOException { |
| | | byte[] bytes = toByteArray(data); |
| | | out.write(bytes); |
| | | } |
| | | |
| | | public static byte[] toByteArray(MapAreaDataDto data) throws IOException { |
| | | int size = computeSize(data); |
| | | byte[] buf = new byte[size]; |
| | | CodedOutputStream cos = CodedOutputStream.newInstance(buf); |
| | | writeMapAreaData(cos, data); |
| | | cos.checkNoSpaceLeft(); |
| | | return buf; |
| | | } |
| | | |
| | | private static int computeSize(MapAreaDataDto data) { |
| | | int size = 0; |
| | | if (data.getVersion() != 0) { |
| | | size += CodedOutputStream.computeUInt32Size(1, data.getVersion()); |
| | | } |
| | | for (OddAreaDto area : data.getOddAreas()) { |
| | | int areaSize = computeOddAreaSize(area); |
| | | size += CodedOutputStream.computeTagSize(2) + CodedOutputStream.computeRawVarint32Size(areaSize) + areaSize; |
| | | } |
| | | return size; |
| | | } |
| | | |
| | | private static int computeOddAreaSize(OddAreaDto area) { |
| | | int size = 0; |
| | | if (area.getOddId() != 0) { |
| | | size += CodedOutputStream.computeUInt32Size(1, area.getOddId()); |
| | | } |
| | | for (OddPointDto pt : area.getOddPoints()) { |
| | | int ptSize = computeOddPointSize(pt); |
| | | size += CodedOutputStream.computeTagSize(2) + CodedOutputStream.computeRawVarint32Size(ptSize) + ptSize; |
| | | } |
| | | if (area.isBlacklist()) { |
| | | size += CodedOutputStream.computeBoolSize(3, true); |
| | | } |
| | | return size; |
| | | } |
| | | |
| | | private static int computeOddPointSize(OddPointDto pt) { |
| | | int size = 0; |
| | | if (pt.getLatitude() != 0.0) { |
| | | size += CodedOutputStream.computeDoubleSize(1, pt.getLatitude()); |
| | | } |
| | | if (pt.getLongitude() != 0.0) { |
| | | size += CodedOutputStream.computeDoubleSize(2, pt.getLongitude()); |
| | | } |
| | | return size; |
| | | } |
| | | |
| | | private static void writeMapAreaData(CodedOutputStream cos, MapAreaDataDto data) throws IOException { |
| | | cos.writeUInt32(1, data.getVersion()); |
| | | for (OddAreaDto area : data.getOddAreas()) { |
| | | cos.writeTag(2, 2); // field 2, wire type length-delimited |
| | | int areaSize = computeOddAreaSize(area); |
| | | cos.writeRawVarint32(areaSize); |
| | | writeOddArea(cos, area); |
| | | } |
| | | } |
| | | |
| | | private static void writeOddArea(CodedOutputStream cos, OddAreaDto area) throws IOException { |
| | | cos.writeUInt32(1, area.getOddId()); |
| | | List<OddPointDto> points = area.getOddPoints(); |
| | | for (OddPointDto pt : points) { |
| | | cos.writeTag(2, 2); |
| | | int ptSize = computeOddPointSize(pt); |
| | | cos.writeRawVarint32(ptSize); |
| | | writeOddPoint(cos, pt); |
| | | } |
| | | if (area.isBlacklist()) { |
| | | cos.writeBool(3, true); |
| | | } |
| | | } |
| | | |
| | | private static void writeOddPoint(CodedOutputStream cos, OddPointDto pt) throws IOException { |
| | | cos.writeDouble(1, pt.getLatitude()); |
| | | cos.writeDouble(2, pt.getLongitude()); |
| | | } |
| | | |
| | | /** |
| | | * 测试入口:使用 XngpAreaRTreeFbsUtil 的示例 GCL 数据生成 map_area_data 文件。 |
| | | */ |
| | | public static void main(String[] args) { |
| | | String gcl = "104.06249657558965_30.583221674855963," + |
| | | "104.06244328689024_30.58049965528362," + |
| | | "104.06640444191817_30.580606702069797," + |
| | | "104.06496563619123_30.58420035984882," + |
| | | "104.06249657558965_30.583221674855963"; |
| | | |
| | | List<XngpAreaRTreeFbsUtil.LonLat> points = XngpAreaRTreeFbsUtil.parseGcl(gcl); |
| | | if (points.isEmpty()) { |
| | | System.err.println("parseGcl failed: no points"); |
| | | return; |
| | | } |
| | | |
| | | OddAreaDto oddArea = OddAreaDto.builder() |
| | | .oddId(1) |
| | | .isBlacklist(false) |
| | | .build(); |
| | | for (XngpAreaRTreeFbsUtil.LonLat p : points) { |
| | | oddArea.getOddPoints().add(OddPointDto.builder() |
| | | .latitude(p.getLat()) |
| | | .longitude(p.getLon()) |
| | | .build()); |
| | | } |
| | | |
| | | MapAreaDataDto data = MapAreaDataDto.builder() |
| | | .version(1) |
| | | .oddAreas(java.util.Collections.singletonList(oddArea)) |
| | | .build(); |
| | | |
| | | String outFile = "map_area_data_test.bin"; |
| | | try (FileOutputStream fos = new FileOutputStream(outFile)) { |
| | | MapAreaDataProtoWriter.writeTo(data, fos); |
| | | System.out.println("MapAreaDataProtoWriter success, outFile=" + outFile); |
| | | } catch (IOException e) { |
| | | System.err.println("MapAreaDataProtoWriter failed: " + e.getMessage()); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.utils; |
| | | |
| | | import com.google.flatbuffers.FlatBufferBuilder; |
| | | import com.xiaopeng.map.dto.Node; |
| | | import com.xiaopeng.map.dto.Point2D; |
| | | import com.xiaopeng.map.dto.RTree; |
| | | import com.xiaopeng.map.dto.Rect; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Comparator; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 等价于 C++ PbRTreeCreater<Node>,将叶节点列表构建成完整的 R-Tree 数组后写入 FlatBuffer。 |
| | | * |
| | | * R-Tree 结构:完全二叉树,以数组形式存储(与 C++ 一致) |
| | | * - 左子节点下标:2 * parent + 1 |
| | | * - 右子节点下标:2 * parent + 2 |
| | | * - 总节点数:2^(ceil(log2(n))+1) - 1 |
| | | * |
| | | * 节点类型(对应 map_area_rtree.proto 中 Node.type): |
| | | * - 0: INVALID(空占位节点) |
| | | * - 1: LEAF(叶节点,存有实际区域数据) |
| | | * - 2: BRANCH(内部节点,存有子树的合并包围框) |
| | | */ |
| | | public final class PbRTreeBuilder { |
| | | |
| | | public static final int NODE_TYPE_INVALID = 0; |
| | | public static final int NODE_TYPE_LEAF = 1; |
| | | public static final int NODE_TYPE_BRANCH = 2; |
| | | |
| | | private PbRTreeBuilder() { |
| | | } |
| | | |
| | | /** |
| | | * R-Tree 中间节点结构,对应 C++ 中 map_area_rtree.proto 的 Node。 |
| | | */ |
| | | public static class RTreeNode { |
| | | public int type = NODE_TYPE_INVALID; |
| | | /** 包围框(range),单位与 FlatBuffers Point2D.x/y 一致(float) */ |
| | | public float minX, minY, maxX, maxY; |
| | | /** 仅 LEAF 节点有效,每项 float[]{x, y} 对应一个多边形顶点 */ |
| | | public List<float[]> points; |
| | | /** 业务 ID,对应 odd_id */ |
| | | public int id; |
| | | /** 是否黑名单 */ |
| | | public boolean isBlacklist; |
| | | } |
| | | |
| | | /** |
| | | * 从叶节点列表构建完整 R-Tree 数组(等价于 PbRTreeCreater.CreatePbRTree)。 |
| | | * |
| | | * @param leafNodes 原始叶节点,每个对应一个 OddArea(已算好 minX/Y, maxX/Y) |
| | | * @return 按数组下标顺序排列的完整 R-Tree 节点列表(包含 INVALID 占位节点) |
| | | */ |
| | | public static List<RTreeNode> createRTree(List<RTreeNode> leafNodes) { |
| | | int n = leafNodes.size(); |
| | | // tree_height = ceil(log2(n)),n<=1 时 log2(1)=0 |
| | | int treeHeight = (n <= 1) ? 0 : (int) Math.ceil(Math.log(n) / Math.log(2)); |
| | | int nodeSize = (int) Math.pow(2, treeHeight + 1) - 1; |
| | | |
| | | List<RTreeNode> rtreeNodes = new ArrayList<>(nodeSize); |
| | | for (int i = 0; i < nodeSize; i++) { |
| | | rtreeNodes.add(new RTreeNode()); // type = INVALID |
| | | } |
| | | |
| | | buildRecursive(new ArrayList<>(leafNodes), 0, leafNodes.size(), 0, rtreeNodes); |
| | | return rtreeNodes; |
| | | } |
| | | |
| | | private static void buildRecursive(List<RTreeNode> objects, int begin, int end, |
| | | int index, List<RTreeNode> rtreeNodes) { |
| | | int size = end - begin; |
| | | if (size == 0) { |
| | | return; |
| | | } |
| | | RTreeNode node = rtreeNodes.get(index); |
| | | if (size == 1) { |
| | | RTreeNode src = objects.get(begin); |
| | | node.type = NODE_TYPE_LEAF; |
| | | node.minX = src.minX; |
| | | node.minY = src.minY; |
| | | node.maxX = src.maxX; |
| | | node.maxY = src.maxY; |
| | | node.points = src.points; |
| | | node.id = src.id; |
| | | node.isBlacklist = src.isBlacklist; |
| | | return; |
| | | } |
| | | |
| | | node.type = NODE_TYPE_BRANCH; |
| | | combineRect(objects, begin, end, node); |
| | | sort(objects, begin, end); |
| | | |
| | | int mid = begin + size / 2; |
| | | buildRecursive(objects, begin, mid, 2 * index + 1, rtreeNodes); |
| | | buildRecursive(objects, mid, end, 2 * index + 2, rtreeNodes); |
| | | } |
| | | |
| | | private static void combineRect(List<RTreeNode> objects, int begin, int end, RTreeNode out) { |
| | | float minX = objects.get(begin).minX; |
| | | float minY = objects.get(begin).minY; |
| | | float maxX = objects.get(begin).maxX; |
| | | float maxY = objects.get(begin).maxY; |
| | | for (int i = begin + 1; i < end; i++) { |
| | | RTreeNode n = objects.get(i); |
| | | minX = Math.min(minX, n.minX); |
| | | minY = Math.min(minY, n.minY); |
| | | maxX = Math.max(maxX, n.maxX); |
| | | maxY = Math.max(maxY, n.maxY); |
| | | } |
| | | out.minX = minX; |
| | | out.minY = minY; |
| | | out.maxX = maxX; |
| | | out.maxY = maxY; |
| | | } |
| | | |
| | | private static void sort(List<RTreeNode> objects, int begin, int end) { |
| | | // 找当前子集的总包围框,以较长轴排序(与 C++ Sort 逻辑完全一致) |
| | | float minX = objects.get(begin).minX, minY = objects.get(begin).minY; |
| | | float maxX = objects.get(begin).maxX, maxY = objects.get(begin).maxY; |
| | | for (int i = begin + 1; i < end; i++) { |
| | | RTreeNode n = objects.get(i); |
| | | minX = Math.min(minX, n.minX); |
| | | minY = Math.min(minY, n.minY); |
| | | maxX = Math.max(maxX, n.maxX); |
| | | maxY = Math.max(maxY, n.maxY); |
| | | } |
| | | boolean sortByX = (maxX - minX) > (maxY - minY); |
| | | |
| | | List<RTreeNode> sub = objects.subList(begin, end); |
| | | if (sortByX) { |
| | | sub.sort(Comparator.comparingDouble(n -> n.maxX)); |
| | | } else { |
| | | sub.sort(Comparator.comparingDouble(n -> n.maxY)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 将 R-Tree 节点列表序列化为 FlatBuffer,返回 RTree table 的 offset。 |
| | | * 与 C++ PbRTreeToFlatBuffer 的 FlatBuffer 写入逻辑对齐。 |
| | | */ |
| | | public static int writeFlatBuffer(FlatBufferBuilder builder, List<RTreeNode> rtreeNodes) { |
| | | int[] nodeOffsets = new int[rtreeNodes.size()]; |
| | | for (int i = 0; i < rtreeNodes.size(); i++) { |
| | | RTreeNode n = rtreeNodes.get(i); |
| | | |
| | | // points vector |
| | | int pointsVector; |
| | | if (n.points != null && !n.points.isEmpty()) { |
| | | int[] ptOffsets = new int[n.points.size()]; |
| | | for (int j = 0; j < n.points.size(); j++) { |
| | | float[] pt = n.points.get(j); |
| | | ptOffsets[j] = Point2D.createPoint2D(builder, pt[0], pt[1]); |
| | | } |
| | | pointsVector = Node.createPointsVector(builder, ptOffsets); |
| | | } else { |
| | | pointsVector = Node.createPointsVector(builder, new int[0]); |
| | | } |
| | | |
| | | // range |
| | | int leftDown = Point2D.createPoint2D(builder, n.minX, n.minY); |
| | | int rightUp = Point2D.createPoint2D(builder, n.maxX, n.maxY); |
| | | int rangeOffset = Rect.createRect(builder, leftDown, rightUp); |
| | | |
| | | nodeOffsets[i] = Node.createNode(builder, n.type, rangeOffset, pointsVector, n.id, n.isBlacklist); |
| | | } |
| | | |
| | | int nodesVector = RTree.createNodesVector(builder, nodeOffsets); |
| | | return RTree.createRTree(builder, nodesVector); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.utils; |
| | | |
| | | import org.apache.commons.codec.binary.Base64; |
| | | import org.apache.commons.codec.binary.Hex; |
| | | import org.apache.commons.lang.RandomStringUtils; |
| | | import org.bouncycastle.openssl.PEMKeyPair; |
| | | import org.bouncycastle.openssl.PEMParser; |
| | | import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; |
| | | import org.bouncycastle.util.io.pem.PemReader; |
| | | |
| | | import java.io.*; |
| | | import java.math.BigInteger; |
| | | import java.security.*; |
| | | import java.security.interfaces.ECPrivateKey; |
| | | import java.security.interfaces.ECPublicKey; |
| | | import java.security.spec.*; |
| | | import java.util.UUID; |
| | | |
| | | /** |
| | | * @author chenjl |
| | | * @date 2020/6/3 |
| | | */ |
| | | public class VehicleCodec { |
| | | static { |
| | | Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); |
| | | } |
| | | |
| | | /** |
| | | * 获得一个UUID |
| | | * |
| | | * @return String UUID |
| | | */ |
| | | public static String getUUID() { |
| | | String s = UUID.randomUUID().toString(); |
| | | // 去掉“-”符号 |
| | | return s.substring(0, 8) + s.substring(9, 13) + s.substring(14, 18) + s.substring(19, 23) + s.substring(24); |
| | | } |
| | | |
| | | /** |
| | | * 生成序列号 |
| | | * @return |
| | | */ |
| | | public static String generateSn() { |
| | | return String.join("-", |
| | | RandomStringUtils.randomAlphanumeric(4), |
| | | RandomStringUtils.randomAlphanumeric(4), |
| | | RandomStringUtils.randomAlphanumeric(4), |
| | | RandomStringUtils.randomAlphanumeric(4)).toUpperCase(); |
| | | } |
| | | |
| | | /** |
| | | * Encode binary data to base64 |
| | | * |
| | | * @param value |
| | | * The binary data |
| | | * @return The base64 encoded String |
| | | */ |
| | | public static String encodeBASE64(byte[] value) { |
| | | return new String(Base64.encodeBase64(value)); |
| | | } |
| | | |
| | | /** |
| | | * Decode a base64 value |
| | | * |
| | | * @param value |
| | | * The base64 encoded String |
| | | * @return decoded binary data |
| | | * @throws UnsupportedEncodingException |
| | | */ |
| | | public static byte[] decodeBASE64(String value) { |
| | | try { |
| | | return Base64.decodeBase64(value.getBytes("utf-8")); |
| | | } catch (UnsupportedEncodingException ex) { |
| | | throw new IllegalArgumentException(ex); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * ECDSA签名 |
| | | * @param data 明文 |
| | | * @param ecPrivateKey EC私钥 |
| | | * @return 签名byte[] |
| | | * @throws Exception |
| | | */ |
| | | public static String sign(byte[] data, ECPrivateKey ecPrivateKey) throws Exception { |
| | | Signature signature = Signature.getInstance("SHA256withECDSA", "BC"); |
| | | signature.initSign(ecPrivateKey); |
| | | signature.update(data); |
| | | return encodeBASE64(signature.sign()); |
| | | } |
| | | |
| | | /** |
| | | * ECDSA 验签 |
| | | * @param data 明文 |
| | | * @param sign 签名byte[] |
| | | * @param ecPublicKey EC公钥 |
| | | * @return 通过-true,失败-false |
| | | * @throws Exception |
| | | */ |
| | | public static boolean verify(byte[] data, String sign, ECPublicKey ecPublicKey) throws Exception { |
| | | Signature signature = Signature.getInstance("SHA256withECDSA", "BC"); |
| | | signature.initVerify(ecPublicKey); |
| | | signature.update(data); |
| | | return signature.verify(decodeBASE64(sign)); |
| | | } |
| | | |
| | | /** |
| | | * 根据X和Y,得到EC公钥 |
| | | * @param publicKeyXBase64 |
| | | * @param publicKeyYBase64 |
| | | * @return |
| | | * @throws NoSuchAlgorithmException |
| | | * @throws InvalidParameterSpecException |
| | | * @throws InvalidKeySpecException |
| | | */ |
| | | public static ECPublicKey getECPublicKeyFromPoint(String publicKeyXBase64, String publicKeyYBase64) throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException { |
| | | byte[] xkeyBytes = decodeBASE64(publicKeyXBase64); |
| | | byte[] ykeyBytes = decodeBASE64(publicKeyYBase64); |
| | | |
| | | KeyFactory keyFactory = KeyFactory.getInstance("ECDSA"); |
| | | |
| | | ECPoint pubPoint = new ECPoint(new BigInteger(1, xkeyBytes), new BigInteger(1, ykeyBytes)); |
| | | AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); |
| | | parameters.init(new ECGenParameterSpec("secp256k1")); |
| | | ECParameterSpec ecParameters = parameters.getParameterSpec(ECParameterSpec.class); |
| | | ECPublicKeySpec pubSpec = new ECPublicKeySpec(pubPoint, ecParameters); |
| | | |
| | | return (ECPublicKey) keyFactory.generatePublic(pubSpec); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 获取ECPrivateKey对象 |
| | | * @param privateKeyBase64 |
| | | * @return |
| | | * @throws NoSuchAlgorithmException |
| | | * @throws InvalidKeySpecException |
| | | */ |
| | | public static ECPrivateKey getECPrivateKey(String privateKeyBase64) throws NoSuchAlgorithmException, InvalidKeySpecException { |
| | | byte[] keyBytes = decodeBASE64(privateKeyBase64); |
| | | PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); |
| | | KeyFactory keyFactory = KeyFactory.getInstance("EC"); |
| | | |
| | | return (ECPrivateKey) keyFactory |
| | | .generatePrivate(pkcs8KeySpec); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 从KEY文件解析KeyPair |
| | | * @param pemFile |
| | | * @return |
| | | * @throws IOException |
| | | */ |
| | | public static KeyPair parsePEMFile(File pemFile) throws IOException { |
| | | if (!pemFile.isFile() || !pemFile.exists()) { |
| | | throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath())); |
| | | } |
| | | PemReader reader = new PemReader(new FileReader(pemFile)); |
| | | PEMKeyPair pemKeyPair; |
| | | try (PEMParser pemParser = new PEMParser(reader)) { |
| | | reader.readPemObject(); |
| | | pemKeyPair = (PEMKeyPair) pemParser.readObject(); |
| | | return new JcaPEMKeyConverter().getKeyPair(pemKeyPair); |
| | | } catch (Exception e) { |
| | | //ignore |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * Write a byte array as hexadecimal String. |
| | | */ |
| | | public static String byteToHexString(byte[] bytes) { |
| | | return String.valueOf(Hex.encodeHex(bytes)); |
| | | } |
| | | |
| | | /** |
| | | * Build an hexadecimal MD5 hash for a String |
| | | * |
| | | * @param value |
| | | * The String to hash |
| | | * @return An hexadecimal Hash |
| | | * @throws Exception |
| | | */ |
| | | public static String hexMD5(String value) { |
| | | try { |
| | | MessageDigest messageDigest = MessageDigest.getInstance("MD5"); |
| | | messageDigest.reset(); |
| | | messageDigest.update(value.getBytes("utf-8")); |
| | | byte[] digest = messageDigest.digest(); |
| | | return byteToHexString(digest); |
| | | } catch (Exception ex) { |
| | | throw new IllegalArgumentException(ex); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * VIN 转UUID |
| | | * @param vin 车架号 |
| | | * @param salt1 盐1 |
| | | * @param salt2 盐2 |
| | | * @return |
| | | */ |
| | | public static String vinToUUID(String vin, String salt1, String salt2) { |
| | | return hexMD5(hexMD5(vin + salt1) + salt2).toUpperCase(); |
| | | } |
| | | |
| | | /** |
| | | * 将字节数组转换成16进制字符串 |
| | | */ |
| | | public static String encodeHexStr(final byte[] bytes){ |
| | | if (bytes == null) { |
| | | return null; |
| | | } |
| | | char[] digital = "0123456789ABCDEF".toCharArray(); |
| | | char[] result = new char[bytes.length * 2]; |
| | | for (int i = 0; i < bytes.length; i++) { |
| | | result[i * 2] = digital[(bytes[i] & 0xf0) >> 4]; |
| | | result[i * 2 + 1] = digital[bytes[i] & 0x0f]; |
| | | } |
| | | return new String(result); |
| | | } |
| | | |
| | | /** |
| | | * 检查是否VIN |
| | | * @param vin |
| | | * @return |
| | | */ |
| | | public static boolean isVin(String vin) { |
| | | return vin.length() == 17 && vin.charAt(0) == 'L'; |
| | | } |
| | | |
| | | |
| | | } |
| New file |
| | |
| | | package com.xiaopeng.map.utils; |
| | | |
| | | import com.google.flatbuffers.FlatBufferBuilder; |
| | | import com.xiaopeng.map.dto.MapAreaDataDto; |
| | | import com.xiaopeng.map.dto.OddAreaDto; |
| | | import com.xiaopeng.map.dto.OddPointDto; |
| | | |
| | | import java.io.FileOutputStream; |
| | | import java.io.IOException; |
| | | import java.nio.ByteBuffer; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 基于坐标生成 XNGP 区域相关数据文件的工具类,包括: |
| | | * - MapAreaData proto 源数据(map_area_data.proto) |
| | | * - RTree flatbuffer 索引数据(map_area_rtree.fbs) |
| | | */ |
| | | public class XngpAreaRTreeFbsUtil { |
| | | |
| | | /** |
| | | * 简单经纬度点结构。 |
| | | */ |
| | | public static class LonLat { |
| | | private final double lon; |
| | | private final double lat; |
| | | |
| | | public LonLat(double lon, double lat) { |
| | | this.lon = lon; |
| | | this.lat = lat; |
| | | } |
| | | |
| | | public double getLon() { |
| | | return lon; |
| | | } |
| | | |
| | | public double getLat() { |
| | | return lat; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 将形如 \"lon_lat,lon_lat,...\" 的字符串解析为坐标点列表。 |
| | | * 例如:104.06249_30.58322,104.06244_30.58049, ... |
| | | */ |
| | | public static List<LonLat> parseGcl(String gcl) { |
| | | if (gcl == null || gcl.isEmpty()) { |
| | | return java.util.Collections.emptyList(); |
| | | } |
| | | String[] pairs = gcl.split(","); |
| | | java.util.List<LonLat> result = new java.util.ArrayList<>(pairs.length); |
| | | for (String pair : pairs) { |
| | | if (pair == null || pair.isEmpty()) { |
| | | continue; |
| | | } |
| | | String[] parts = pair.split("_"); |
| | | if (parts.length != 2) { |
| | | continue; |
| | | } |
| | | double lon = Double.parseDouble(parts[0]); |
| | | double lat = Double.parseDouble(parts[1]); |
| | | result.add(new LonLat(lon, lat)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 基于单个 GCL 字符串构造 MapAreaData,并写入指定文件。 |
| | | * |
| | | * @param gcl GCL 字符串:形如 "lon_lat,lon_lat,..." |
| | | * @param oddId 区域 ID,对应 OddArea.odd_id |
| | | * @param isBlacklist 是否黑名单区域,对应 OddArea.is_blacklist |
| | | * @param version MapAreaData 的版本号,对应 MapAreaData.version |
| | | * @param outFile 输出的 MapAreaData 二进制文件路径 |
| | | * @return 写入是否成功 |
| | | */ |
| | | public static boolean buildMapAreaDataFromGcl(String gcl, |
| | | int oddId, |
| | | boolean isBlacklist, |
| | | int version, |
| | | String outFile) { |
| | | List<LonLat> points = parseGcl(gcl); |
| | | if (points == null || points.isEmpty()) { |
| | | return false; |
| | | } |
| | | |
| | | // 构造 OddAreaDto |
| | | OddAreaDto oddArea = OddAreaDto.builder() |
| | | .oddId(oddId) |
| | | .isBlacklist(isBlacklist) |
| | | .build(); |
| | | for (LonLat p : points) { |
| | | oddArea.getOddPoints().add(OddPointDto.builder() |
| | | .latitude(p.getLat()) |
| | | .longitude(p.getLon()) |
| | | .build()); |
| | | } |
| | | |
| | | // 构造 MapAreaDataDto |
| | | MapAreaDataDto data = MapAreaDataDto.builder() |
| | | .version(version) |
| | | .oddAreas(java.util.Collections.singletonList(oddArea)) |
| | | .build(); |
| | | |
| | | try (FileOutputStream fos = new FileOutputStream(outFile)) { |
| | | MapAreaDataProtoWriter.writeTo(data, fos); |
| | | } catch (IOException e) { |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 从一组多边形坐标点生成 RTree fbs 文件(单区域版本)。 |
| | | * 等价于 C++ 流程:单个 OddArea → PbRTreeCreater → PbRTreeToFlatBuffer。 |
| | | * Node.type 由 R-Tree 算法自动赋值(1=LEAF),不再由调用方指定。 |
| | | * |
| | | * @param points 多边形顶点(经度、纬度),顺序与原始区域一致 |
| | | * @param id 对应 proto 里的 odd_id |
| | | * @param isBlacklist 是否黑名单区域 |
| | | * @param outFile 输出的 fbs 二进制文件路径 |
| | | * @return 生成是否成功 |
| | | */ |
| | | public static boolean buildRTreeFromPoints(List<LonLat> points, |
| | | int id, |
| | | boolean isBlacklist, |
| | | String outFile) { |
| | | java.util.List<PbRTreeBuilder.RTreeNode> leafNodes = new java.util.ArrayList<>(); |
| | | PbRTreeBuilder.RTreeNode leaf = buildLeafNode(points, id, isBlacklist); |
| | | if (leaf == null) { |
| | | return false; |
| | | } |
| | | leafNodes.add(leaf); |
| | | return writeRTreeFbs(leafNodes, outFile); |
| | | } |
| | | |
| | | /** |
| | | * 从坐标点生成 RTree fbs 字节数组(内存版,供上传等场景使用)。 |
| | | * |
| | | * @param points 多边形顶点 |
| | | * @param areaId 区域 odd_id |
| | | * @param isBlacklist 是否黑名单 |
| | | * @return fbs 二进制,失败返回 null |
| | | */ |
| | | public static byte[] buildRTreeBytesFromPoints(List<LonLat> points, int areaId, boolean isBlacklist) { |
| | | java.util.List<PbRTreeBuilder.RTreeNode> leafNodes = new java.util.ArrayList<>(); |
| | | PbRTreeBuilder.RTreeNode leaf = buildLeafNode(points, areaId, isBlacklist); |
| | | if (leaf == null) { |
| | | return null; |
| | | } |
| | | leafNodes.add(leaf); |
| | | return buildRTreeBytes(leafNodes); |
| | | } |
| | | |
| | | /** |
| | | * 从多个区域的坐标点列表生成 RTree fbs 文件(多区域版本,与 C++ 完全对齐)。 |
| | | * |
| | | * @param areaPoints 每个区域对应的坐标点列表 |
| | | * @param ids 每个区域的 odd_id |
| | | * @param isBlacklists 每个区域是否黑名单 |
| | | * @param outFile 输出的 fbs 二进制文件路径 |
| | | * @return 生成是否成功 |
| | | */ |
| | | public static boolean buildRTreeFromMultiAreas(java.util.List<List<LonLat>> areaPoints, |
| | | java.util.List<Integer> ids, |
| | | java.util.List<Boolean> isBlacklists, |
| | | String outFile) { |
| | | if (areaPoints == null || areaPoints.isEmpty()) { |
| | | return false; |
| | | } |
| | | java.util.List<PbRTreeBuilder.RTreeNode> leafNodes = new java.util.ArrayList<>(); |
| | | for (int i = 0; i < areaPoints.size(); i++) { |
| | | PbRTreeBuilder.RTreeNode leaf = buildLeafNode( |
| | | areaPoints.get(i), |
| | | ids.get(i), |
| | | isBlacklists.get(i) |
| | | ); |
| | | if (leaf != null) { |
| | | leafNodes.add(leaf); |
| | | } |
| | | } |
| | | return writeRTreeFbs(leafNodes, outFile); |
| | | } |
| | | |
| | | private static PbRTreeBuilder.RTreeNode buildLeafNode(List<LonLat> points, int id, boolean isBlacklist) { |
| | | if (points == null || points.isEmpty()) { |
| | | return null; |
| | | } |
| | | float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; |
| | | float maxX = -Float.MAX_VALUE, maxY = -Float.MAX_VALUE; |
| | | |
| | | java.util.List<float[]> ptList = new java.util.ArrayList<>(points.size()); |
| | | for (LonLat p : points) { |
| | | float x = (float) p.getLon(); // lon → x |
| | | float y = (float) p.getLat(); // lat → y |
| | | ptList.add(new float[]{x, y}); |
| | | minX = Math.min(minX, x); |
| | | minY = Math.min(minY, y); |
| | | maxX = Math.max(maxX, x); |
| | | maxY = Math.max(maxY, y); |
| | | } |
| | | |
| | | PbRTreeBuilder.RTreeNode leaf = new PbRTreeBuilder.RTreeNode(); |
| | | leaf.minX = minX; |
| | | leaf.minY = minY; |
| | | leaf.maxX = maxX; |
| | | leaf.maxY = maxY; |
| | | leaf.points = ptList; |
| | | leaf.id = id; |
| | | leaf.isBlacklist = isBlacklist; |
| | | return leaf; |
| | | } |
| | | |
| | | private static byte[] buildRTreeBytes(java.util.List<PbRTreeBuilder.RTreeNode> leafNodes) { |
| | | java.util.List<PbRTreeBuilder.RTreeNode> rtreeNodes = PbRTreeBuilder.createRTree(leafNodes); |
| | | |
| | | FlatBufferBuilder builder = new FlatBufferBuilder(1024); |
| | | int rtreeOffset = PbRTreeBuilder.writeFlatBuffer(builder, rtreeNodes); |
| | | builder.finish(rtreeOffset); |
| | | |
| | | ByteBuffer buffer = builder.dataBuffer(); |
| | | byte[] bytes = new byte[buffer.remaining()]; |
| | | buffer.get(bytes); |
| | | return bytes; |
| | | } |
| | | |
| | | private static boolean writeRTreeFbs(java.util.List<PbRTreeBuilder.RTreeNode> leafNodes, String outFile) { |
| | | byte[] bytes = buildRTreeBytes(leafNodes); |
| | | try (FileOutputStream fos = new FileOutputStream(outFile)) { |
| | | fos.write(bytes); |
| | | } catch (IOException e) { |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 简单命令行测试入口。 |
| | | * 示例:构造一个矩形区域的四个顶点,生成 RTree fbs 文件到指定目录。 |
| | | */ |
| | | public static void main(String[] args) { |
| | | // 示例 GCL 字符串:lon_lat,lon_lat,... |
| | | String gcl; |
| | | if (args != null && args.length > 0) { |
| | | // 如果通过命令行传入,则优先使用参数中的 GCL 字符串 |
| | | gcl = args[0]; |
| | | } else { |
| | | // 默认示例,来源于用户提供的坐标 |
| | | gcl = "104.06249657558965_30.583221674855963," + |
| | | "104.06244328689024_30.58049965528362," + |
| | | "104.06640444191817_30.580606702069797," + |
| | | "104.06496563619123_30.58420035984882," + |
| | | "104.06249657558965_30.583221674855963"; |
| | | } |
| | | |
| | | List<LonLat> pts = parseGcl(gcl); |
| | | |
| | | int id = 1; |
| | | boolean isBlacklist = false; |
| | | |
| | | String outFile; |
| | | if (args != null && args.length > 1) { |
| | | // 如果有第二个参数,则视为输出文件路径 |
| | | outFile = args[1]; |
| | | } else { |
| | | outFile = "xngp_area_data_test.bin"; |
| | | } |
| | | |
| | | boolean ok = buildRTreeFromPoints(pts, id, isBlacklist, outFile); |
| | | System.out.println("buildRTreeFromPoints result=" + ok + ", outFile=" + outFile); |
| | | } |
| | | |
| | | |
| | | |
| | | } |
| | | |
| | |
| | | config: |
| | | activate: |
| | | on-profile: local |
| | | redis: |
| | | database: 99 |
| | | host: r-bp1ip8die60uuo3ovypd.redis.rds.aliyuncs.com |
| | | password: syAktm2x9uDusoF_ |
| | | port: 6379 |
| | | autoconfigure: |
| | | exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure |
| | | |
| | |
| | | |
| | | seata: |
| | | enabled: false |
| | | |
| | | # 阿里云 OSS 配置(ROBOTAXI ODD 图层上传) |
| | | aliyun: |
| | | oss: |
| | | enabled: true |
| | | endpoint: http://oss-cn-wulanchabu.aliyuncs.com |
| | | access-key-id: ${ALIYUN_OSS_ACCESS_KEY_ID:} |
| | | access-key-secret: ${ALIYUN_OSS_ACCESS_KEY_SECRET:} |
| | | bucket-name: cngp-crowdsourcing |
| | | path-prefix: ROBOTAXI_ODD_layer |
| | | # 环境:test(测试)、staging(预发)、prod(生产),支持 ${OSS_ENV:test} 或直接写死 |
| | | env: ${OSS_ENV:test} |
| | | |
| | | # ADC VIN 转 UUID 盐值配置 |
| | | adc: |
| | | vin: |
| | | salt1: ${VIN_SALT1:} |
| | | salt2: ${VIN_SALT2:} |
| New file |
| | |
| | | package com.xiaopeng.map.service; |
| | | |
| | | import com.xiaopeng.map.utils.XngpAreaRTreeFbsUtil; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.junit.jupiter.api.Assumptions; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.boot.test.autoconfigure.actuate.metrics.AutoConfigureMetrics; |
| | | import org.springframework.boot.test.context.SpringBootTest; |
| | | import org.springframework.test.context.ActiveProfiles; |
| | | |
| | | import java.util.List; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | |
| | | /** |
| | | * AdcOssHandleService 集成测试,直接调用真实 OSS 服务。 |
| | | * 运行前需配置环境变量:ALIYUN_OSS_ACCESS_KEY_ID、ALIYUN_OSS_ACCESS_KEY_SECRET、VIN_SALT1、VIN_SALT2 |
| | | */ |
| | | @Slf4j |
| | | @SpringBootTest(classes = com.xiaopeng.map.MapApplication.class) |
| | | @ActiveProfiles("local") |
| | | @AutoConfigureMetrics |
| | | class AdcOssHandleServiceTest { |
| | | |
| | | @Autowired(required = false) |
| | | private AdcOssHandleService adcOssHandleService; |
| | | |
| | | @Autowired(required = false) |
| | | private RobotaxiOssService robotaxiOssService; |
| | | |
| | | @Test |
| | | void uploadOddLayerFile() { |
| | | if (adcOssHandleService == null || robotaxiOssService == null) { |
| | | log.warn("OSS 未启用,测试跳过。请配置 ALIYUN_OSS_ACCESS_KEY_ID、ALIYUN_OSS_ACCESS_KEY_SECRET、VIN_SALT1、VIN_SALT2"); |
| | | Assumptions.assumeTrue(false, "OSS 未启用,请配置环境变量"); |
| | | } |
| | | // 构造测试坐标点:成都某区域多边形 |
| | | String gcl = "104.06249_30.58322,104.06244_30.58049,104.06500_30.58100,104.06600_30.58400"; |
| | | List<XngpAreaRTreeFbsUtil.LonLat> points = XngpAreaRTreeFbsUtil.parseGcl(gcl); |
| | | assertNotNull(points); |
| | | assertTrue(points.size() >= 3, "多边形至少需要 3 个点"); |
| | | |
| | | int areaId = 1; |
| | | String vin = "21312323333333312"; // 替换为实际测试 VIN |
| | | String areaCode = "510100"; |
| | | |
| | | String objectKey = adcOssHandleService.uploadOddLayerFile(points, areaId, vin, areaCode); |
| | | log.info("uploadOddLayerFile objectKey={}", objectKey); |
| | | |
| | | assertNotNull(objectKey); |
| | | assertTrue(objectKey.contains("ROBOTAXI_ODD_layer") && objectKey.contains(areaCode), |
| | | "返回 objectKey 应包含路径前缀和区域编码"); |
| | | |
| | | // 获取下载临时链接(Presigned URL) |
| | | String downloadUrl = robotaxiOssService.generatePresignedDownloadUrl(objectKey, 3600); |
| | | log.info("generatePresignedDownloadUrl downloadUrl={}", downloadUrl); |
| | | assertNotNull(downloadUrl); |
| | | assertTrue(downloadUrl.contains("Expires") || downloadUrl.contains("Signature"), |
| | | "下载临时链接应包含签名参数(Expires 或 Signature)"); |
| | | } |
| | | } |
| New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <configuration> |
| | | <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
| | | <encoder> |
| | | <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> |
| | | </encoder> |
| | | </appender> |
| | | <root level="INFO"> |
| | | <appender-ref ref="STDOUT"/> |
| | | </root> |
| | | <logger name="com.xiaopeng.map" level="DEBUG"/> |
| | | </configuration> |