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