diff --git a/pom.xml b/pom.xml index 1de518e..2b46905 100644 --- a/pom.xml +++ b/pom.xml @@ -166,6 +166,31 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + + 60 + + kill + + 2 + + false + 1 + false + + 5 + + + false + org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration + + test + + + diff --git a/src/main/java/com/dongni/collisionavoidance/area/model/AreaType.java b/src/main/java/com/dongni/collisionavoidance/area/model/AreaType.java deleted file mode 100644 index 4aed532..0000000 --- a/src/main/java/com/dongni/collisionavoidance/area/model/AreaType.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.dongni.collisionavoidance.area.model; - -/** - * 机场区域类型枚举 - */ -public enum AreaType { - RUNWAY, // 跑道 - TAXIWAY, // 滑行道 - APRON, // 停机坪 - SERVICE_AREA, // 服务区 - CARGO_AREA, // 货运区 - TERMINAL_AREA, // 航站楼区域 - MAINTENANCE, // 维修区 - RESTRICTED, // 限制区 - PROTECTION // 保护区 -} \ No newline at end of file diff --git a/src/main/java/com/dongni/collisionavoidance/area/service/AirportAreaService.java b/src/main/java/com/dongni/collisionavoidance/area/service/AirportAreaService.java deleted file mode 100644 index 0c9eb1a..0000000 --- a/src/main/java/com/dongni/collisionavoidance/area/service/AirportAreaService.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.dongni.collisionavoidance.area.service; - -import com.dongni.collisionavoidance.config.properties.AirportAreasProperties; -import com.dongni.collisionavoidance.config.properties.AreaProperties; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Optional; - -@Service -public class AirportAreaService { - private final List areas; - - public AirportAreaService(AirportAreasProperties properties) { - this.areas = properties.getAreas(); - } - - public List getAllAreas() { - return areas; - } - - public Optional getAreaById(String id) { - return areas.stream() - .filter(area -> area.getId().equals(id)) - .findFirst(); - } - - public List getAreasByType(String type) { - return areas.stream() - .filter(area -> area.getType().equals(type)) - .toList(); - } -} \ No newline at end of file diff --git a/src/main/java/com/dongni/collisionavoidance/areas/model/AreaInfo.java b/src/main/java/com/dongni/collisionavoidance/areas/model/AreaInfo.java new file mode 100644 index 0000000..2dd3f69 --- /dev/null +++ b/src/main/java/com/dongni/collisionavoidance/areas/model/AreaInfo.java @@ -0,0 +1,25 @@ +package com.dongni.collisionavoidance.areas.model; + +import lombok.Builder; +import lombok.Value; +import org.locationtech.jts.geom.Polygon; +import java.time.ZonedDateTime; +import java.util.List; + +@Value +@Builder +public class AreaInfo { + String id; // 区域唯一标识 + String name; // 区域名称 + AreaType type; // 区域类型(跑道、机坪等) + Double speedLimitKph; // 限速(公里/小时) + String description; // 区域用途描述 + boolean restricted; // 是否限制进入 + List allowedVehicleTypes; // 允许的车辆类型 + List allowedAircraftTypes; // 允许的航空器类型 + Double maxHeight; // 最大高度限制(米) + Double maxWeight; // 最大重量限制(吨) + Polygon boundary; // JTS 多边形边界 + ZonedDateTime activeTime; // 生效时间(用于临时区域) + ZonedDateTime expiryTime; // 失效时间(用于临时区域) +} \ No newline at end of file diff --git a/src/main/java/com/dongni/collisionavoidance/areas/model/AreaType.java b/src/main/java/com/dongni/collisionavoidance/areas/model/AreaType.java new file mode 100644 index 0000000..70a742b --- /dev/null +++ b/src/main/java/com/dongni/collisionavoidance/areas/model/AreaType.java @@ -0,0 +1,16 @@ +package com.dongni.collisionavoidance.areas.model; + +/** + * 机场区域类型枚举 + */ +public enum AreaType { + RUNWAY, // 跑道 + TAXIWAY, // 滑行道 + APRON, // 停机坪 + SERVICE_AREA, // 服务区 + CARGO_AREA, // 货运区 + TERMINAL_AREA, // 航站楼区域 + MAINTENANCE, // 维修区 + RESTRICTED, // 限制区 + PROTECTION // 保护区 +} \ No newline at end of file diff --git a/src/main/java/com/dongni/collisionavoidance/areas/service/AirportAreaService.java b/src/main/java/com/dongni/collisionavoidance/areas/service/AirportAreaService.java new file mode 100644 index 0000000..cc5f518 --- /dev/null +++ b/src/main/java/com/dongni/collisionavoidance/areas/service/AirportAreaService.java @@ -0,0 +1,176 @@ +package com.dongni.collisionavoidance.areas.service; + +import com.dongni.collisionavoidance.areas.model.AreaInfo; +import com.dongni.collisionavoidance.areas.model.AreaType; +import com.dongni.collisionavoidance.common.model.GeoPosition; +import com.dongni.collisionavoidance.config.properties.AirportAreasProperties; +import com.dongni.collisionavoidance.config.properties.AreaProperties; +import com.dongni.collisionavoidance.config.properties.GeometryProperties; +import lombok.extern.slf4j.Slf4j; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.index.strtree.STRtree; +import org.springframework.stereotype.Service; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class AirportAreaService { + private final List areas; + private final STRtree spatialIndex; + private final GeometryFactory geometryFactory; + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_DATE_TIME; + + public AirportAreaService(AirportAreasProperties properties) { + this.geometryFactory = new GeometryFactory(); + this.areas = convertToAreaInfo(properties.getAreas()); + this.spatialIndex = buildSpatialIndex(); + } + + private List convertToAreaInfo(List properties) { + return properties.stream() + .map(this::convertToAreaInfo) + .collect(Collectors.toList()); + } + + private AreaInfo convertToAreaInfo(AreaProperties properties) { + return AreaInfo.builder() + .id(properties.getId()) + .name(properties.getName()) + .type(AreaType.valueOf(properties.getType())) + .speedLimitKph(properties.getSpeedLimit()) + .description(properties.getPurpose()) + .restricted(properties.getRestrictions() != null && !properties.getRestrictions().isEmpty()) + .allowedVehicleTypes(properties.getAllowedVehicleTypes()) + .allowedAircraftTypes(properties.getAllowedAircraftTypes()) + .maxHeight(properties.getMaxHeight()) + .maxWeight(properties.getMaxWeight()) + .boundary(convertToPolygon(properties.getGeometry())) + .activeTime(parseDateTime(properties.getActiveTime())) + .expiryTime(parseDateTime(properties.getExpiryTime())) + .build(); + } + + private Polygon convertToPolygon(GeometryProperties geometry) { + if (geometry == null || geometry.getCoordinates() == null || geometry.getCoordinates().isEmpty()) { + return null; + } + + try { + List coordinates = geometry.getCoordinates().stream() + .map(coord -> { + double lon = coord.get(0); + double lat = coord.get(1); + log.info("转换坐标: [{},{}] -> JTS坐标", lon, lat); + return new Coordinate(lon, lat); + }) + .collect(Collectors.toList()); + + // 确保多边形是闭合的 + if (!coordinates.get(0).equals(coordinates.get(coordinates.size() - 1))) { + log.info("多边形未闭合,添加闭合点"); + coordinates.add(coordinates.get(0)); + } + + Polygon polygon = geometryFactory.createPolygon(coordinates.toArray(new Coordinate[0])); + log.info("创建多边形: {}", polygon); + return polygon; + } catch (Exception e) { + log.error("转换多边形失败: {}", e.getMessage(), e); + return null; + } + } + + private ZonedDateTime parseDateTime(String dateTimeStr) { + if (dateTimeStr == null || dateTimeStr.isEmpty()) { + return null; + } + try { + return ZonedDateTime.parse(dateTimeStr, DATE_TIME_FORMATTER); + } catch (Exception e) { + log.warn("Failed to parse date time: {}", dateTimeStr, e); + return null; + } + } + + private STRtree buildSpatialIndex() { + STRtree index = new STRtree(); + for (AreaInfo area : areas) { + if (area.getBoundary() != null) { + index.insert(area.getBoundary().getEnvelopeInternal(), area); + } + } + index.build(); + return index; + } + + public List getAllAreas() { + return new ArrayList<>(areas); + } + + public Optional getAreaById(String id) { + return areas.stream() + .filter(area -> area.getId().equals(id)) + .findFirst(); + } + + public List getAreasByType(AreaType type) { + return areas.stream() + .filter(area -> area.getType() == type) + .collect(Collectors.toList()); + } + + public List findAreasContainingPoint(GeoPosition position) { + log.info("查询包含点的区域: lat={}, lon={}", position.getLatitude(), position.getLongitude()); + Point point = geometryFactory.createPoint(new Coordinate(position.getLongitude(), position.getLatitude())); + log.info("创建JTS点: {}", point); + @SuppressWarnings("unchecked") + List candidates = spatialIndex.query(point.getEnvelopeInternal()); + log.info("空间索引查询结果数量: {}", candidates.size()); + return candidates.stream() + .filter(area -> { + boolean contains = area.getBoundary() != null && area.getBoundary().contains(point); + log.info("区域 {} ({}): boundary={}, contains={}", + area.getId(), area.getName(), + area.getBoundary() != null ? area.getBoundary().toString() : "null", + contains); + return contains; + }) + .collect(Collectors.toList()); + } + + public Optional findDominantAreaAt(GeoPosition position) { + List containingAreas = findAreasContainingPoint(position); + if (containingAreas.isEmpty()) { + return Optional.empty(); + } + // 按照区域优先级返回最优先的区域 + return containingAreas.stream() + .max((a1, a2) -> a2.getType().ordinal() - a1.getType().ordinal()); + } + + public Double getSpeedLimitKphAt(GeoPosition position) { + return findDominantAreaAt(position) + .map(AreaInfo::getSpeedLimitKph) + .orElse(null); + } + + public boolean isPositionInRestrictedArea(GeoPosition position) { + return findAreasContainingPoint(position).stream() + .anyMatch(AreaInfo::isRestricted); + } + + public boolean isAreaActive(AreaInfo area) { + ZonedDateTime now = ZonedDateTime.now(); + return (area.getActiveTime() == null || !now.isBefore(area.getActiveTime())) && + (area.getExpiryTime() == null || !now.isAfter(area.getExpiryTime())); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 61af7b3..cb5d190 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,7 +20,6 @@ spring: value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer properties: spring.json.trusted.packages: "com.airport.common.model" - # Redis配置 redis: host: localhost @@ -35,6 +34,8 @@ spring: min-idle: 0 key-serialization: org.springframework.data.redis.serialization.StringRedisSerializer value-serialization: org.springframework.data.redis.serialization.Jackson2JsonRedisSerializer + main: + allow-bean-definition-overriding: true # 数据采集配置 data: diff --git a/src/test/java/com/dongni/collisionavoidance/areas/service/AirportAreaServiceIntegrationTest.java b/src/test/java/com/dongni/collisionavoidance/areas/service/AirportAreaServiceIntegrationTest.java index fa9e581..15b7711 100644 --- a/src/test/java/com/dongni/collisionavoidance/areas/service/AirportAreaServiceIntegrationTest.java +++ b/src/test/java/com/dongni/collisionavoidance/areas/service/AirportAreaServiceIntegrationTest.java @@ -1,16 +1,22 @@ package com.dongni.collisionavoidance.areas.service; +import com.dongni.collisionavoidance.areas.model.AreaInfo; +import com.dongni.collisionavoidance.areas.model.AreaType; +import com.dongni.collisionavoidance.common.model.GeoPosition; import com.dongni.collisionavoidance.config.AirportAreaConfig; -import com.dongni.collisionavoidance.config.properties.AreaProperties; -import com.dongni.collisionavoidance.area.service.AirportAreaService; +import com.dongni.collisionavoidance.config.TestConfig; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; import java.util.List; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -18,9 +24,12 @@ import static org.assertj.core.api.Assertions.assertThat; * 机场区域服务的集成测试类 * 确保区域配置正确加载,并且服务方法按预期工作 */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ExtendWith(MockitoExtension.class) -@Import(AirportAreaConfig.class) +@Import({AirportAreaConfig.class, TestConfig.class}) +@TestPropertySource(locations = "classpath:config/airport_areas.yaml") +@ActiveProfiles("test") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) class AirportAreaServiceIntegrationTest { @Autowired @@ -34,12 +43,12 @@ class AirportAreaServiceIntegrationTest { @Test void getAllAreas_shouldReturnAllAreas() { - List areas = airportAreaService.getAllAreas(); + List areas = airportAreaService.getAllAreas(); assertThat(areas) .isNotNull() .isNotEmpty() - .hasSize(2); // 配置文件中有两个区域:跑道区域和停机坪区域 + .hasSize(2); // 配置文件中有两个区域:跑道区域和滑行道区域 System.out.println("获取到 " + areas.size() + " 个区域"); } @@ -47,18 +56,17 @@ class AirportAreaServiceIntegrationTest { @Test void getAreaById_shouldReturnArea_whenIdExists() { String areaId = "1"; // 跑道区域的ID - AreaProperties area = airportAreaService.getAreaById(areaId).orElse(null); + AreaInfo area = airportAreaService.getAreaById(areaId).orElse(null); assertThat(area) .isNotNull() .satisfies(a -> { assertThat(a.getId()).isEqualTo("1"); assertThat(a.getName()).isEqualTo("跑道区域"); - assertThat(a.getType()).isEqualTo("RUNWAY"); - assertThat(a.getSpeedLimit()).isEqualTo(0.0); - assertThat(a.getPurpose()).isEqualTo("用于航空器起降的主要跑道"); - assertThat(a.getRestrictions()) - .containsExactly("禁止停车", "禁止通行"); + assertThat(a.getType()).isEqualTo(AreaType.RUNWAY); + assertThat(a.getSpeedLimitKph()).isEqualTo(0.0); + assertThat(a.getDescription()).isEqualTo("用于航空器起降的主要跑道"); + assertThat(a.isRestricted()).isTrue(); assertThat(a.getAllowedVehicleTypes()) .containsExactly("AIRCRAFT"); assertThat(a.getAllowedAircraftTypes()) @@ -71,39 +79,116 @@ class AirportAreaServiceIntegrationTest { @Test void getAreaById_shouldReturnNull_whenIdDoesNotExist() { String nonExistentId = "non-existent-area-id"; - AreaProperties area = airportAreaService.getAreaById(nonExistentId).orElse(null); + AreaInfo area = airportAreaService.getAreaById(nonExistentId).orElse(null); assertThat(area).isNull(); } @Test void getAreasByType_shouldReturnAreas_whenTypeExists() { - String type = "RUNWAY"; - List areas = airportAreaService.getAreasByType(type); + AreaType type = AreaType.RUNWAY; + List areas = airportAreaService.getAreasByType(type); assertThat(areas) .isNotNull() .isNotEmpty() .hasSize(1) - .allMatch(area -> type.equals(area.getType())); + .allMatch(area -> type == area.getType()); // 验证返回的是跑道区域 - AreaProperties runwayArea = areas.get(0); + AreaInfo runwayArea = areas.get(0); assertThat(runwayArea) .satisfies(area -> { assertThat(area.getName()).isEqualTo("跑道区域"); - assertThat(area.getSpeedLimit()).isEqualTo(0.0); - assertThat(area.getPurpose()).isEqualTo("用于航空器起降的主要跑道"); + assertThat(area.getSpeedLimitKph()).isEqualTo(0.0); + assertThat(area.getDescription()).isEqualTo("用于航空器起降的主要跑道"); }); } @Test void getAreasByType_shouldReturnEmpty_whenTypeDoesNotExist() { - String nonExistentType = "non-existent-type"; - List areas = airportAreaService.getAreasByType(nonExistentType); + AreaType nonExistentType = AreaType.RESTRICTED; + List areas = airportAreaService.getAreasByType(nonExistentType); assertThat(areas) .isNotNull() .isEmpty(); } + + @Test + void findAreasContainingPoint_shouldReturnAllMatchingAreas_whenPointIsIn() { + // 测试点在跑道和滑行道重叠区域 + GeoPosition position = new GeoPosition(39.123500, 116.123600, 0.0); + List areas = airportAreaService.findAreasContainingPoint(position); + + assertThat(areas).isNotEmpty() + .extracting(AreaInfo::getType) + .contains(AreaType.RUNWAY, AreaType.TAXIWAY); + + // 测试点在区域外 + position = new GeoPosition(39.124000, 116.124000, 0.0); + areas = airportAreaService.findAreasContainingPoint(position); + + assertThat(areas).isEmpty(); + } + + @Test + void findDominantAreaAt_shouldReturnHighestPriorityArea_whenPointIsInMultipleAreas() { + // 测试点在跑道和滑行道重叠区域,跑道应该是优先级最高的 + GeoPosition position = new GeoPosition(39.123500, 116.123600, 0.0); + Optional dominantArea = airportAreaService.findDominantAreaAt(position); + + assertThat(dominantArea) + .isPresent() + .map(AreaInfo::getType) + .hasValue(AreaType.RUNWAY); + + // 测试点在区域外 + position = new GeoPosition(39.124000, 116.124000, 0.0); + dominantArea = airportAreaService.findDominantAreaAt(position); + + assertThat(dominantArea).isEmpty(); + } + + @Test + void getSpeedLimitKphAt_shouldReturnSpeedLimit_whenPointIsInArea() { + // 测试点在跑道和滑行道重叠区域,根据优先级应返回跑道的速度限制 + GeoPosition position = new GeoPosition(39.123500, 116.123600, 0.0); + Double speedLimit = airportAreaService.getSpeedLimitKphAt(position); + + assertThat(speedLimit) + .isNotNull() + .isEqualTo(0.0); + + // 测试点在区域外 + position = new GeoPosition(39.124000, 116.124000, 0.0); + speedLimit = airportAreaService.getSpeedLimitKphAt(position); + + assertThat(speedLimit).isNull(); + } + + @Test + void isPositionInRestrictedArea_shouldReturnTrue_whenPointIsInRestrictedArea() { + // 测试点在跑道内 + GeoPosition position = new GeoPosition(39.123600, 116.123600, 0.0); + boolean isRestricted = airportAreaService.isPositionInRestrictedArea(position); + + assertThat(isRestricted).isTrue(); + + // 测试点在滑行道内 + position = new GeoPosition(39.123500, 116.123600, 0.0); + isRestricted = airportAreaService.isPositionInRestrictedArea(position); + + assertThat(isRestricted).isTrue(); + } + + @Test + void isAreaActive_shouldReturnTrue_whenAreaIsActive() { + Optional area = airportAreaService.getAreaById("1"); + + assertThat(area) + .isPresent() + .hasValueSatisfying(a -> + assertThat(airportAreaService.isAreaActive(a)).isTrue()); + } } \ No newline at end of file diff --git a/src/test/java/com/dongni/collisionavoidance/config/TestConfig.java b/src/test/java/com/dongni/collisionavoidance/config/TestConfig.java new file mode 100644 index 0000000..e66da6a --- /dev/null +++ b/src/test/java/com/dongni/collisionavoidance/config/TestConfig.java @@ -0,0 +1,151 @@ +package com.dongni.collisionavoidance.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import com.dongni.collisionavoidance.common.model.MovingObject; +import com.dongni.collisionavoidance.common.model.MovingObjectType; +import com.dongni.collisionavoidance.common.model.repository.MovingObjectRepository; +import com.dongni.collisionavoidance.dataCollector.service.AuthService; +import com.dongni.collisionavoidance.dataCollector.service.DataCollectorService; +import com.dongni.collisionavoidance.dataProcessing.service.CoordinateSystemService; +import com.dongni.collisionavoidance.dataProcessing.service.DataProcessor; +import com.dongni.collisionavoidance.dataProcessing.service.SpeedCalculationService; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; +import org.springframework.web.client.RestTemplate; + +/** + * 测试专用配置类,用于禁用后台线程和外部连接 + */ +@Configuration +@Profile("test") +public class TestConfig { + + /** + * 提供一个测试专用的线程池,会立即关闭而不会等待任务执行完成 + */ + @Bean(name = "processingExecutor") + @Primary + public Executor testProcessingExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(1); + executor.setMaxPoolSize(1); + executor.setQueueCapacity(1); + executor.setWaitForTasksToCompleteOnShutdown(false); + executor.setAwaitTerminationSeconds(1); + executor.setThreadNamePrefix("test-proc-"); + return executor; + } + + /** + * 提供一个用于调度的线程池,会立即关闭而不会等待任务执行完成 + */ + @Bean + @Primary + public ThreadPoolTaskScheduler testTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(1); + scheduler.setThreadNamePrefix("test-sched-"); + scheduler.setWaitForTasksToCompleteOnShutdown(false); + scheduler.setAwaitTerminationSeconds(1); + return scheduler; + } + + /** + * 提供一个不执行任何操作的数据处理器 + */ + @Bean + @Primary + public DataProcessor noOpDataProcessor(MovingObjectRepository movingObjectRepository, + CoordinateSystemService coordinateSystemService, + SpeedCalculationService speedCalculationService, + Executor processingExecutor) { + return new TestDataProcessor(movingObjectRepository, coordinateSystemService, + speedCalculationService, processingExecutor); + } + + /** + * 提供一个不进行HTTP请求的测试用认证服务 + */ + @Bean + @Primary + public AuthService testAuthService(RestTemplate restTemplate) { + return new TestAuthService(restTemplate); + } + + /** + * 提供一个不调用外部API的数据采集服务 + */ + @Bean + @Primary + public DataCollectorService testDataCollectorService() { + return new TestDataCollectorService(); + } + + /** + * 测试用数据处理器实现,不会启动实际的处理线程 + */ + static class TestDataProcessor extends DataProcessor { + public TestDataProcessor(MovingObjectRepository movingObjectRepository, + CoordinateSystemService coordinateSystemService, + SpeedCalculationService speedCalculationService, + Executor processingExecutor) { + super(); + // 注入依赖,但不启动后台线程 + } + + @Override + public void init() { + // 不启动处理线程 + } + } + + /** + * 测试用认证服务实现,返回固定的测试令牌 + */ + static class TestAuthService extends AuthService { + public TestAuthService(RestTemplate restTemplate) { + super(restTemplate); + } + + @Override + public String loginAndGetToken() { + return "test-token"; + } + + @Override + public String refreshToken() { + return "test-token"; + } + + @Override + public String getToken() { + return "test-token"; + } + } + + /** + * 测试用数据采集服务,不执行实际的数据采集 + */ + static class TestDataCollectorService extends DataCollectorService { + // 覆盖所有定时任务方法,不执行实际操作 + @Override + public void collectAircraftData() { + // 测试环境下不执行实际数据采集 + } + + @Override + public void collectVehicleData() { + // 测试环境下不执行实际数据采集 + } + } +} \ No newline at end of file diff --git a/src/test/java/com/dongni/collisionavoidance/roads/service/RoadNetworkServiceIntegrationTest.java b/src/test/java/com/dongni/collisionavoidance/roads/service/RoadNetworkServiceIntegrationTest.java index f6ea27e..500a600 100644 --- a/src/test/java/com/dongni/collisionavoidance/roads/service/RoadNetworkServiceIntegrationTest.java +++ b/src/test/java/com/dongni/collisionavoidance/roads/service/RoadNetworkServiceIntegrationTest.java @@ -4,13 +4,16 @@ import com.dongni.collisionavoidance.common.model.GeoPosition; import com.dongni.collisionavoidance.dataCollector.service.DataCollectorService; import com.dongni.collisionavoidance.dataProcessing.service.DataProcessor; import com.dongni.collisionavoidance.roads.model.RoadInfo; +import com.dongni.collisionavoidance.config.TestConfig; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; // Optional: if you need specific test profile +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import java.util.List; import java.util.Optional; @@ -26,12 +29,17 @@ import static org.junit.jupiter.api.Assertions.*; */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) // Load context without web server @ExtendWith(MockitoExtension.class) -// @ActiveProfiles("test") // Activate a specific test profile if needed (e.g., for application-test.yml) +@ActiveProfiles("test") // 激活测试配置文件 +@Import(TestConfig.class) // 导入测试配置 +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) // 确保测试后销毁上下文 class RoadNetworkServiceIntegrationTest { @Autowired private RoadNetworkService roadNetworkService; + // 这些Mock对象在Spring上下文中不会生效,只会在测试类内部生效 + // 由于我们现在使用TestConfig,这些Mock对象实际上是不必要的 + // 但我们暂时保留它们以避免大量代码变更 @Mock private DataCollectorService dataCollectorService; diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..4358de6 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,51 @@ +spring: + # 禁用MongoDB自动配置 + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration + - org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration + + # 禁用数据服务 + data: + mongodb: + auto-index-creation: false + redis: + repositories: + enabled: false + + # 禁用Kafka + kafka: + bootstrap-servers: + producer: + bootstrap-servers: + consumer: + bootstrap-servers: + auto-startup: false + + # 禁用调度和异步任务 + task: + scheduling: + enabled: false + execution: + enabled: false + +# 测试模式标记 +test-mode: true + +# 数据采集器配置 +data: + collector: + disabled: true + airport-api: + base-url: http://localhost:8090 + auth: + username: test + password: test + data-refresh-interval-ms: 0 + +# 日志配置 +logging: + level: + root: INFO + com.dongni.collisionavoidance: DEBUG + org.locationtech.jts: INFO \ No newline at end of file diff --git a/src/test/resources/config/airport_areas.yaml b/src/test/resources/config/airport_areas.yaml index 2e24330..b370903 100644 --- a/src/test/resources/config/airport_areas.yaml +++ b/src/test/resources/config/airport_areas.yaml @@ -19,33 +19,33 @@ airport: geometry: type: "Polygon" coordinates: [ - [1.0, 1.0], - [1.0, 2.0], - [2.0, 2.0] + [116.123456, 39.123456], + [116.123789, 39.123456], + [116.123789, 39.123789], + [116.123456, 39.123789], + [116.123456, 39.123456] ] - id: "2" - name: "停机坪区域" - type: "APRON" + name: "滑行道区域" + type: "TAXIWAY" speedLimit: 30 # 单位:km/h - purpose: "用于航空器停放和地面服务" - restrictions: - - "限速30公里/小时" + purpose: "连接跑道和停机坪" + restrictions: [] allowedVehicleTypes: - - "AIRCRAFT" - - "TUG" + - "FOLLOW_ME" + - "TOW_TRUCK" - "FUEL_TRUCK" - - "BAGGAGE_CART" allowedAircraftTypes: - - "A320" - - "B737" - - "A330" - maxHeight: 15.0 # 单位:米 - maxWeight: 200.0 # 单位:吨 + - "AIRCRAFT" + maxHeight: 50.0 # 单位:米 + maxWeight: 500.0 # 单位:吨 geometry: type: "Polygon" coordinates: [ - [2.0, 2.0], - [2.0, 3.0], - [3.0, 3.0] + [116.123456, 39.123456], + [116.123789, 39.123456], + [116.123789, 39.123567], + [116.123456, 39.123567], + [116.123456, 39.123456] ] \ No newline at end of file