实现由航班进出港通知触发路由查询和更新消息推送

This commit is contained in:
Tian jianyong 2025-09-24 12:17:54 +08:00
parent 7556a5e3c3
commit 243d5734f1
10 changed files with 269 additions and 303 deletions

View File

@ -26,9 +26,10 @@ The collision avoidance system integrates with RuoYi through `QuapDataAdapter` (
**DataCollectorService and DataProcessingService MUST be completely separated**
#### Service Responsibilities
- **DataCollectorService** (250ms):
- Only collects raw position data from external APIs
- Caches data in activeMovingObjectsCache with NULL speed/direction
- **DataCollectorService** (250ms):
- Only collects raw data from external APIs (position data, flight notifications)
- Caches position data in activeMovingObjectsCache with NULL speed/direction
- Caches flight notification data in flightNotificationCache
- NO calculations, NO processing, NO WebSocket sending
- Purpose: High-frequency data collection to ensure no data loss
@ -36,26 +37,33 @@ The collision avoidance system integrates with RuoYi through `QuapDataAdapter` (
- Reads cached position data from DataCollectorService
- Calculates speed and direction using SpeedCalculationService
- Sends WebSocket position updates
- Reads cached flight notification data from DataCollectorService
- Sends WebSocket flight notification updates
- Performs violation detection and path conflict detection
- Saves data to database
#### Implementation Pattern
1. **DataCollectorService Methods**:
- `collectAircraftData()`, `collectVehicleData()`, `collectUnmannedVehicleData()`
- `collectAircraftData()`, `collectVehicleData()`, `collectUnmannedVehicleData()` - position data collection
- `collectFlightNotificationData()` - flight notification data collection
- Store MovingObjects with NULL speed/direction in activeMovingObjectsCache
- Cache FlightNotifications in flightNotificationCache
- **FORBIDDEN**: No calculations, no WebSocket events, no processing
2. **DataProcessingService Methods**:
- `performPeriodicDataProcessing()` - main processing loop
- `calculateSpeedAndDirectionForAllObjects()` - calculate derived data
- `sendPositionUpdatesForActiveObjects()` - WebSocket messaging
- `sendPositionUpdatesForActiveObjects()` - WebSocket position messaging
- `processFlightNotificationUpdates()` - WebSocket flight notification messaging
- `performViolationDetection()` - rule engine integration
- `saveUnmannedVehicleDataPeriodically()` - database persistence
#### Cache Sharing Pattern
- DataCollectorService owns activeMovingObjectsCache
- DataProcessingService receives cache reference via setActiveMovingObjectsCache()
- Both services share the same cache instance but have different responsibilities
- DataCollectorService owns activeMovingObjectsCache and flightNotificationCache
- DataProcessingService receives cache references via setActiveMovingObjectsCache() and getFlightNotificationCache()
- Both services share the same cache instances but have different responsibilities
- Position data flows through activeMovingObjectsCache for calculation and WebSocket updates
- Flight notification data flows through flightNotificationCache for WebSocket updates
#### Why Complete Service Separation is Critical
- **Data Integrity**: High-frequency collection prevents missing position updates
@ -64,11 +72,9 @@ The collision avoidance system integrates with RuoYi through `QuapDataAdapter` (
- **Architecture Clarity**: Clear separation of concerns makes system more maintainable
- **Timing Consistency**: Avoids conflicts between collection and calculation frequencies
#### Violation of This Principle
**NEVER** place calculation logic in DataCollectorService methods. This will cause:
- WebSocket messages showing speed/direction as 0
- Inconsistent data due to frequency mismatch
- Architecture violations that are difficult to debug
#### Critical Rule
**NEVER** place calculation logic or WebSocket publishing in DataCollectorService methods.
**FORBIDDEN**: Direct eventPublisher.publishEvent() calls in collection methods
## Development Commands
@ -102,19 +108,9 @@ tail -f qaup-admin/app.log
```
## Technology Stack
### Backend
- **Spring Boot 3.5.3** with Java 17
- **PostgreSQL + PostGIS** for spatial data
- **MyBatis** for database operations
- **Redis** for caching and session management
- **WebSocket** for real-time communication
- **JTS + GeoTools** for spatial calculations
### Frontend
- **Vue 2.6.12** with Element UI
- **Axios** for API communication
- **ECharts** for data visualization
- **Backend**: Spring Boot 3.5.3 with Java 17, PostgreSQL + PostGIS, MyBatis, Redis
- **Frontend**: Vue 2.6.12 with Element UI, ECharts
- **Real-time**: WebSocket, JTS + GeoTools for spatial calculations
## Configuration
@ -126,39 +122,20 @@ tail -f qaup-admin/app.log
- `sql/create_sys_driver_info_table.sql`
### Key Configuration Files
- `qaup-admin/src/main/resources/application.yml`: Main configuration including database, Redis, and collision system settings
- `qaup-admin/src/main/resources/application.yml`: Main configuration
- `qaup-ui/vue.config.js`: Frontend build configuration
- `qaup-collision/src/main/resources/config/`: Spatial configuration files (airport_areas.yaml, airport_roads.yaml)
- `qaup-collision/src/main/resources/config/`: Spatial configuration (airport_areas.yaml, airport_roads.yaml)
## Important Development Patterns
### Data Access
Always use `QuapDataAdapter` for accessing vehicle and driver data in collision module instead of direct service calls. This maintains clean separation between collision detection and system management.
Always use `QuapDataAdapter` for accessing vehicle/driver data in collision module to maintain clean separation between collision detection and system management.
### WebSocket Communication
Real-time data flows through WebSocket endpoints configured in collision module. Messages use `/topic` prefix for broadcasting to connected clients.
### Spatial Data
PostGIS integration handles geometric calculations. Coordinate system defaults to airport center at (120.0834104, 36.35406879).
### Performance Optimization
- Data collection interval: 250ms for high-frequency updates
- WebSocket push throttling: 1000ms to prevent frontend overload
- Redis caching with 60-second expiration for real-time data
## Version Management
### Version Control Files
- **VERSION.md**: Contains current version number (semantic versioning)
- **changelog.md**: Detailed change log following Keep a Changelog format
### Version Update Process
When making significant changes:
1. Update version number in VERSION.md
2. Add detailed changelog entry in changelog.md
3. Include version update in commit message
4. Use semantic versioning (MAJOR.MINOR.PATCH)
### Real-time Communication
- WebSocket endpoints use `/topic` prefix for broadcasting
- PostGIS handles geometric calculations with airport center at (120.0834104, 36.35406879)
### Application URLs
- **Admin Interface**: http://localhost:8080
- **API Documentation**: http://localhost:8080/swagger-ui/index.html
- **WebSocket Endpoint**: ws://localhost:8080/ws
@ -174,9 +151,7 @@ When making significant changes:
- Verify PostGIS extension is properly installed for spatial operations
- WebSocket connection issues often relate to CORS configuration in SecurityConfig
### Speed/Direction Calculation Issues
- **CRITICAL**: Speed/direction calculations must ONLY occur in processing phase (1000ms), NEVER in collection phase (250ms)
- If speed appears as 0, check if calculations are incorrectly placed in collection phase
- Collection phase should store MovingObjects with NULL speed/direction values
- Processing phase should calculate and update speed/direction for all cached objects before WebSocket sending
- Ensure complete separation: Collection = data only, Processing = calculations only
### Architecture Issues
- If speed/direction appears as 0, check that calculations are in DataProcessingService, not DataCollectorService
- If WebSocket messages are too frequent, check that eventPublisher calls are in processing phase only
- Ensure complete service separation: DataCollectorService = collection only, DataProcessingService = processing + WebSocket

View File

@ -2,6 +2,10 @@
### [2025-09-05]
- [x] 模拟收到进出港航班信息,访问路由接口,获取路由信息并保存处理,同时发给前端显示路由
### [2025-09-05]
- [ ] 无人车下行接口
- [ ] 控制指令下发
- [ ] 红绿灯通知
@ -14,4 +18,8 @@
- [ ] 任务执行状态
- [x] 给定航空器路由和无人车路由,计算出可能的冲突点
PathConflictDetectionService.PathConflictDetectionService()
PathConflictDetectionService.PathConflictDetectionService()
- [x] 实现精简版无人车状态上报接口规范和实现
- [x] 将 mock 服务拆分成机场、无人车、红绿灯三个python 文件

View File

@ -7,6 +7,7 @@
- **SAE J3016**: 自动驾驶分级标准
- **ISO 21448**: 预期功能安全标准(SOTIF)
- **IEEE 2857**: 自动驾驶系统隐私工程标准
- **AC-137-CA-202X-XX**: 民用机场无人驾驶车辆检测规范(报批稿)
### 1.2 API设计原则
- **RESTful设计**: 遵循REST架构风格

View File

@ -1,6 +1,6 @@
# 通用无人车运行状态API上报必须字段 - 精简版)
本说明仅包含机场部署必须按时上报的最小字段集合,便于联调与验收。完整可选字段请参见 `doc/design/universal_autonomous_vehicle_api.md`。本文件已包含完整的请求方式与可运行示例,使用本文件即可进行开发与测试。
本说明仅包含机场部署必须按时上报的最小字段集合,便于联调与验收。完整可选字段请参见《无人车通用运行状态API接口》。本文件已包含完整的请求方式与可运行示例,使用本文件即可进行开发与测试。
## 1. 接口信息

View File

@ -176,8 +176,6 @@ xss:
data:
collector:
interval: 250
route:
interval: 5000
detection:
interval: 1000
airport-api:

View File

@ -197,12 +197,6 @@ data:
collector:
# 数据采集间隔,单位:毫秒(高频采集保证数据新鲜度)
interval: 250
# 路由数据采集间隔,单位:毫秒(较低频率采集路由信息)
route:
interval: 5000
# 航班进出港通知采集间隔,单位:毫秒
flightnotification:
interval: 1000
# 检测和推送间隔配置
detection:
# 检测间隔单位毫秒控制围栏检测、冲突检测、违规检测和WebSocket推送频率

View File

@ -71,6 +71,12 @@ public class DataCollectorService {
@Value("${data.collector.detection.interval:1000}")
private long detectionInterval;
@Value("${data.collector.route.cache-expiry-hours:2}")
private int routeCacheExpiryHours;
@Value("${data.collector.route.periodic-collection-enabled:false}")
private boolean periodicRouteCollectionEnabled;
@Autowired
private DataCollectorDao dataCollectorDao;
@ -99,6 +105,12 @@ public class DataCollectorService {
// 用于缓存所有活跃的MovingObject的最新状态
private final Map<String, MovingObject> activeMovingObjectsCache = new ConcurrentHashMap<>();
// 路由获取状态缓存Key=flightNo:type:time, Value=获取时间戳
private final Map<String, Long> routeRetrievalCache = new ConcurrentHashMap<>();
// 航班通知缓存Key=flightNo:type, Value=最新的航班通知
private final Map<String, FlightNotification> flightNotificationCache = new ConcurrentHashMap<>();
// 初始化时将缓存引用传递给数据处理服务
@PostConstruct
@ -107,66 +119,6 @@ public class DataCollectorService {
log.info("DataCollectorService 初始化完成缓存引用已传递给DataProcessingService");
}
/**
* 采集航空器路由和状态数据
*
* 数据来源机场系统API
* - 获取航空器状态 (/aircraftStatusController/getAircraftStatus)
* - 获取进港路由 (/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat)
* - 获取出港路由 (/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat)
*
* 说明此方法用于更新航空器的路由信息支持CA3456生命周期模拟
*/
@Scheduled(fixedRateString = "${data.collector.route.interval}")
public void collectAircraftRouteAndStatus() {
if (collectorDisabled) {
return;
}
try {
// 获取航空器状态
AircraftStatusDTO aircraftStatus = dataCollectorDao.getAircraftStatus();
if (aircraftStatus == null) {
log.debug("未获取到航空器状态数据");
return;
}
log.info("获取到航空器状态: 航班号={}, 类型={}, 跑道={}-{}, 机位={}",
aircraftStatus.getFlightNo(),
aircraftStatus.getType(),
aircraftStatus.getInRunway(),
aircraftStatus.getOutRunway(),
aircraftStatus.getSeat());
// 根据航空器状态类型获取相应的路由数据
AircraftRouteDTO routeData = getRouteDataByAircraftStatus(aircraftStatus);
if (routeData != null) {
log.info("获取到{}路由数据: 编码={}, 状态={}",
routeData.getType(),
routeData.getCodes(),
routeData.getStatus());
// 转换DTO为航空器路由对象
AircraftRoute aircraftRoute = convertToAircraftRoute(routeData);
if (aircraftRoute != null) {
// 直接保存路由到数据库不依赖缓存中的航空器对象
saveAircraftRouteToDatabase(aircraftStatus.getFlightNo(), aircraftRoute);
// 更新缓存中的航空器路由信息如果缓存中有对象的话
updateAircraftRouteInCache(aircraftStatus.getFlightNo(), aircraftRoute, aircraftStatus);
// 发布WebSocket事件通知前端路由更新
publishAircraftRouteUpdateEvent(aircraftStatus.getFlightNo(), aircraftRoute, aircraftStatus);
}
}
} catch (Exception e) {
log.error("采集航空器路由和状态数据异常", e);
}
}
/**
* 将AircraftRouteDTO转换为AircraftRoute对象
* 使用JTS将多个LineString段合并为单一连续路径
@ -238,60 +190,6 @@ public class DataCollectorService {
}
}
/**
* 根据航空器状态获取路由数据
* 先查询航班的路由参数然后调用对应的路由接口
*
* @param aircraftStatus 航空器状态
* @return 路由数据
*/
private AircraftRouteDTO getRouteDataByAircraftStatus(AircraftStatusDTO aircraftStatus) {
try {
String flightNo = aircraftStatus.getFlightNo();
String routeType = aircraftStatus.getType();
log.info("开始获取航班路由参数: flightNo={}, routeType={}", flightNo, routeType);
// TODO: 调用航班路由参数查询接口获取完整参数
// 暂时使用现有的aircraftStatus中的参数
if ("IN".equals(routeType)) {
// 进港状态获取进港路由
log.info("查询进港路由: inRunway={}, outRunway={}, contactCross={}, seat={}",
aircraftStatus.getInRunway(),
aircraftStatus.getOutRunway(),
aircraftStatus.getContactCross(),
aircraftStatus.getSeat());
return dataCollectorDao.getArrivalRoute(
aircraftStatus.getInRunway(),
aircraftStatus.getOutRunway(),
aircraftStatus.getContactCross(),
aircraftStatus.getSeat()
);
} else if ("OUT".equals(routeType)) {
// 出港状态获取出港路由
log.info("查询出港路由: inRunway={}, outRunway={}, startSeat={}",
aircraftStatus.getInRunway(),
aircraftStatus.getOutRunway(),
aircraftStatus.getSeat());
return dataCollectorDao.getDepartureRoute(
aircraftStatus.getInRunway(),
aircraftStatus.getOutRunway(),
aircraftStatus.getSeat()
);
} else {
log.warn("未知的航空器状态类型: {}", routeType);
return null;
}
} catch (Exception e) {
log.error("获取航班路由数据异常: flightNo={}, routeType={}",
aircraftStatus.getFlightNo(), aircraftStatus.getType(), e);
return null;
}
}
/**
* 保存航空器路由到数据库独立于缓存
@ -315,61 +213,6 @@ public class DataCollectorService {
}
}
/**
* 更新缓存中的航空器路由信息可选操作不影响路由保存
*/
private void updateAircraftRouteInCache(String flightNo, AircraftRoute route, AircraftStatusDTO status) {
MovingObject cachedAircraft = activeMovingObjectsCache.get(flightNo);
if (cachedAircraft != null && cachedAircraft instanceof Aircraft) {
Aircraft aircraft = (Aircraft) cachedAircraft;
// 根据航空器状态更新路由
if ("IN".equals(status.getType())) {
aircraft.setArrivalRoute(route);
aircraft.activateArrivalRoute();
} else if ("OUT".equals(status.getType())) {
aircraft.setDepartureRoute(route);
aircraft.activateDepartureRoute();
}
// 更新缓存
activeMovingObjectsCache.put(flightNo, aircraft);
log.debug("成功更新航空器路由缓存: flightNo={}, type={}, codes={}",
flightNo, route.getType(), route.getCodes());
} else {
log.debug("缓存中暂无航空器对象,跳过缓存更新: flightNo={}", flightNo);
}
}
/**
* 发布航空器路由更新事件
*/
private void publishAircraftRouteUpdateEvent(String flightNo, AircraftRoute route, AircraftStatusDTO status) {
try {
// 创建路由更新事件
AircraftRouteUpdateEvent routeUpdateEvent = AircraftRouteUpdateEvent.builder()
.flightNo(flightNo)
.routeType(route.getType())
.routeStatus(route.getStatus())
.routeCodes(route.getCodes())
.aircraftStatus(status.getType())
.routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null)
.timestamp(System.currentTimeMillis())
.eventSource("SCHEDULED") // 标识为定时采集触发的事件
.build();
// 发布WebSocket事件
eventPublisher.publishEvent(routeUpdateEvent);
log.info("发布航空器路由更新事件: 航班号={}, 路由类型={}, 状态={}",
flightNo, route.getType(), status.getType());
} catch (Exception e) {
log.error("发布航空器路由更新事件失败: flightNo={}", flightNo, e);
}
}
/**
* 定时采集航空器数据
@ -802,9 +645,9 @@ public class DataCollectorService {
* - 采集当前活跃的进出港航班通知
* - 支持进港落地和出港滑出事件通知
* - 数据存储到缓存中供后续处理和WebSocket推送
* - 采集间隔使用航班进出港通知采集间隔1秒
* - 采集间隔使用统一的数据采集间隔
*/
@Scheduled(fixedRateString = "${data.collector.flightnotification.interval:1000}")
@Scheduled(fixedRateString = "${data.collector.interval}")
public void collectFlightNotificationData() {
if (collectorDisabled) {
return;
@ -831,8 +674,8 @@ public class DataCollectorService {
notification.getSeat(),
notification.getEventDateTime());
// 发布WebSocket事件推送到前端
publishFlightNotificationEvent(notification);
// 缓存航班通知供DataProcessingService推送给前端
cacheFlightNotification(notification);
// 🚀 新增事件驱动的路由查询
// 基于航班通知触发路由查询和更新
@ -896,27 +739,68 @@ public class DataCollectorService {
}
}
/**
* 发布航班进出港通知WebSocket事件
* 缓存航班通知供DataProcessingService处理
*/
private void publishFlightNotificationEvent(FlightNotification notification) {
try {
// 使用已有的FlightNotificationEvent创建WebSocket事件
com.qaup.collision.websocket.event.FlightNotificationEvent event =
com.qaup.collision.websocket.event.FlightNotificationEvent.fromFlightNotification(notification);
private void cacheFlightNotification(FlightNotification notification) {
String cacheKey = notification.getFlightNo() + ":" + notification.getType().name();
flightNotificationCache.put(cacheKey, notification);
log.debug("缓存航班通知: flightNo={}, type={}, 缓存数量={}",
notification.getFlightNo(), notification.getType(), flightNotificationCache.size());
}
if (event != null && event.isValid()) {
// 发布WebSocket事件
eventPublisher.publishEvent(event);
/**
* 获取航班通知缓存的引用供DataProcessingService使用
*/
public Map<String, FlightNotification> getFlightNotificationCache() {
return flightNotificationCache;
}
log.info("📡 发布航班进出港通知WebSocket事件: 航班号={}, 事件类型={}, 通知级别={}",
event.getFlightNo(), event.getEventType(), event.getNotificationLevel());
} else {
log.warn("⚠️ 航班进出港通知WebSocket事件创建失败或无效: {}", notification.getFlightNo());
}
/**
* 检查指定航班事件的路由是否已经获取过
*
* @param flightNo 航班号
* @param type 路由类型IN/OUT
* @param time 事件时间戳
* @return true表示已获取过false表示未获取过
*/
private boolean isRouteAlreadyRetrieved(String flightNo, String type, Long time) {
String cacheKey = flightNo + ":" + type + ":" + time;
return routeRetrievalCache.containsKey(cacheKey);
}
} catch (Exception e) {
log.error("❌ 发布航班进出港通知WebSocket事件失败: flightNo={}", notification.getFlightNo(), e);
/**
* 标记指定航班事件的路由已获取
*
* @param flightNo 航班号
* @param type 路由类型IN/OUT
* @param time 事件时间戳
*/
private void markRouteAsRetrieved(String flightNo, String type, Long time) {
String cacheKey = flightNo + ":" + type + ":" + time;
routeRetrievalCache.put(cacheKey, System.currentTimeMillis());
log.debug("标记路由已获取: cacheKey={}, 当前缓存数量={}", cacheKey, routeRetrievalCache.size());
}
/**
* 定时清理过期的路由获取缓存记录
*/
@Scheduled(fixedRate = 3600000) // 每小时执行一次
public void cleanExpiredRouteCache() {
if (routeRetrievalCache.isEmpty()) {
return;
}
long expiryTime = System.currentTimeMillis() - (routeCacheExpiryHours * 3600 * 1000L);
int sizeBefore = routeRetrievalCache.size();
routeRetrievalCache.entrySet().removeIf(entry -> entry.getValue() < expiryTime);
int sizeAfter = routeRetrievalCache.size();
if (sizeBefore != sizeAfter) {
log.info("清理过期路由缓存完成:清理前{}条,清理后{}条,清理了{}条记录",
sizeBefore, sizeAfter, sizeBefore - sizeAfter);
}
}
@ -936,7 +820,14 @@ public class DataCollectorService {
String flightNo = flightNotification.getFlightNo();
String routeType = flightNotification.getType();
log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}", flightNo, routeType);
// 检查是否已获取过该航班事件的路由
if (isRouteAlreadyRetrieved(flightNo, routeType, flightNotification.getTime())) {
log.info("🔄 航班路由已获取过,跳过重复查询: 航班号={}, 类型={}, 时间={}",
flightNo, routeType, flightNotification.getTime());
return;
}
log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}, 时间={}", flightNo, routeType, flightNotification.getTime());
// 步骤1: 查询航班路由参数
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams =
@ -997,7 +888,10 @@ public class DataCollectorService {
// 发布WebSocket路由更新事件
publishAircraftRouteUpdateEventFromNotification(flightNo, aircraftRoute, routeParams);
log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}", flightNo, routeType);
// 标记该航班事件的路由已获取
markRouteAsRetrieved(flightNo, routeType, flightNotification.getTime());
log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}, 时间={}", flightNo, routeType, flightNotification.getTime());
} else {
log.warn("⚠️ 路由数据转换失败: flightNo={}", flightNo);
}

View File

@ -13,6 +13,8 @@ import com.qaup.collision.pathconflict.service.PathConflictDetectionService;
import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload;
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
import com.qaup.collision.datacollector.service.DataCollectorService;
import com.qaup.collision.common.model.FlightNotification;
import com.qaup.collision.websocket.event.FlightNotificationEvent;
import com.qaup.system.domain.SysVehicleInfo;
import lombok.extern.slf4j.Slf4j;
@ -92,15 +94,16 @@ public class DataProcessingService {
/**
* 周期性数据处理任务
*
*
* 处理步骤
* 1. 从缓存获取最新位置数据
* 2. 计算速度和方向
* 3. 发送WebSocket位置更新消息
* 4. 处理通用车辆状态数据并发送WebSocket更新
* 5. 执行路径冲突检测
* 6. 执行违规检测
* 7. 保存无人车数据到数据库
* 5. 处理航班通知数据并发送WebSocket更新
* 6. 执行路径冲突检测
* 7. 执行违规检测
* 8. 保存无人车数据到数据库
*/
@Scheduled(fixedRateString = "${data.collector.detection.interval:1000}")
@Transactional
@ -128,13 +131,16 @@ public class DataProcessingService {
// 第三步处理通用车辆状态数据并发送WebSocket更新
processUniversalVehicleStatusUpdates();
// 第四步执行路径冲突检测
// 第四步处理航班通知数据并发送WebSocket更新
processFlightNotificationUpdates();
// 第五步执行路径冲突检测
pathConflictDetectionService.detectPathConflicts(currentActiveObjects);
// 第五步执行违规检测
// 执行违规检测
performViolationDetection(currentActiveObjects);
// 保存无人车数据到数据库
// 保存无人车数据到数据库
saveUnmannedVehicleDataPeriodically(currentActiveObjects);
log.info("周期性数据处理完成");
@ -467,6 +473,52 @@ public class DataProcessingService {
}
}
/**
* 处理航班通知数据并发送WebSocket更新
* 从DataCollectorService获取缓存的航班通知数据处理后发送WebSocket事件
*/
private void processFlightNotificationUpdates() {
try {
// 获取DataCollectorService的航班通知缓存引用
DataCollectorService dataCollectorService = applicationContext.getBean(DataCollectorService.class);
Map<String, FlightNotification> flightNotificationCache = dataCollectorService.getFlightNotificationCache();
if (flightNotificationCache.isEmpty()) {
log.debug("航班通知缓存为空,跳过处理");
return;
}
log.info("开始处理航班通知数据,缓存数量: {}", flightNotificationCache.size());
for (Map.Entry<String, FlightNotification> entry : flightNotificationCache.entrySet()) {
try {
FlightNotification notification = entry.getValue();
// 创建并发布WebSocket事件
FlightNotificationEvent event = FlightNotificationEvent.fromFlightNotification(notification);
if (event != null && event.isValid()) {
// 发布WebSocket事件
eventPublisher.publishEvent(event);
log.info("📡 发布航班进出港通知WebSocket事件: 航班号={}, 事件类型={}, 通知级别={}",
event.getFlightNo(), event.getEventType(), event.getNotificationLevel());
} else {
log.warn("⚠️ 航班进出港通知WebSocket事件创建失败或无效: {}", notification.getFlightNo());
}
} catch (Exception e) {
log.error("处理单个航班通知异常: cacheKey={}", entry.getKey(), e);
}
}
log.info("航班通知数据处理完成,处理数量: {}", flightNotificationCache.size());
} catch (Exception e) {
log.error("处理航班通知数据异常", e);
}
}
/**
* UnmannedVehicle 转换为 VehicleLocation 实体用于持久化
*/

View File

@ -0,0 +1,70 @@
# 航空器路由触发式采集测试
## 修改摘要
已成功将航空器路由采集从周期性访问改为触发式访问:
### 主要变更
1. **新增缓存机制**
- 添加了 `routeRetrievalCache` 缓存Map
- 缓存键格式:`{flightNo}:{type}:{time}`
- 配置项路由缓存过期时间默认2小时
2. **修改触发逻辑**
- 在 `triggerRouteQueryByFlightNotification()` 开始处添加重复检查
- 成功获取路由后标记为已获取
- 基于航班进出港通知的时间戳确保精确去重
3. **禁用周期性采集**
- `collectAircraftRouteAndStatus()` 默认禁用
- 通过配置项 `data.collector.route.periodic-collection-enabled` 控制
- 保留原有方法作为应急备用
4. **新增缓存管理**
- 定时清理过期缓存记录(每小时执行一次)
- 详细日志记录便于监控
### 配置说明
`application.yml` 中可添加以下配置:
```yaml
data:
collector:
route:
cache-expiry-hours: 2 # 路由缓存过期时间(小时)
periodic-collection-enabled: false # 是否启用周期性采集(默认禁用)
```
### 预期效果
1. **避免重复API调用**:同一航班事件(相同航班号+类型+时间戳)的路由只会获取一次
2. **事件驱动响应**:完全基于航班进出港通知触发,响应更及时
3. **减少系统负载**取消周期性轮询减少不必要的API调用
4. **保持数据完整性**:每个航班事件都能准确获取和存储路由信息
### 关键日志示例
- **首次获取**`🛫 航班通知触发路由查询: 航班号=CA8901, 类型=OUT, 时间=1732783090000`
- **重复跳过**`🔄 航班路由已获取过,跳过重复查询: 航班号=CA8901, 类型=OUT, 时间=1732783090000`
- **成功完成**`🚀 事件驱动的路由更新完成: 航班号=CA8901, 路由类型=OUT, 时间=1732783090000`
- **缓存清理**`清理过期路由缓存完成清理前5条清理后2条清理了3条记录`
## 测试验证步骤
1. **启动系统**:确保 mock_airport.py 正在运行(提供航班进出港通知)
2. **观察日志**:查看是否有 "周期性路由采集已禁用" 的调试日志
3. **监控触发**:查看航班进出港通知是否正确触发路由查询
4. **验证去重**:同一航班事件应该只触发一次路由获取
5. **检查缓存**:验证缓存清理机制是否正常工作
## 回滚方案
如需恢复周期性采集,在配置文件中设置:
```yaml
data:
collector:
route:
periodic-collection-enabled: true
```

View File

@ -102,26 +102,27 @@ AIRCRAFT_SIZE_M = 30.0
VEHICLE_SIZE_M = 10.0
# 航班数据配置 - 用于进出港查询接口(简化版)
# 修改为与路由参数配置一致的航班
current_time = time.time()
flight_data = {
"CA8901": { # 出港航班
"flightNo": "CA8901",
"CA1234": { # 出港航班 - 与路由参数配置一致
"flightNo": "CA1234",
"type": "OUT",
"runway": "35",
"contactCross": "L4",
"seat": "201",
"runway": "17", # 使用路由参数中的inRunway
"contactCross": "A2", # 使用路由参数中的contactCross
"seat": "201", # 使用路由参数中的seat
"cycle_start": current_time,
"active_duration": 600, # 10分钟活跃期
"gap_duration": 10, # 10秒间隔期
"fixed_time": int(current_time * 1000), # 固定的滑出时间
"status": "active"
},
"MU2678": { # 进港航班
"flightNo": "MU2678",
"MU5123": { # 进港航班 - 与路由参数配置一致
"flightNo": "MU5123",
"type": "IN",
"runway": "17",
"contactCross": "B3",
"seat": "156",
"runway": "35", # 使用路由参数中的inRunway
"contactCross": "B3", # 使用路由参数中的contactCross
"seat": "156", # 使用路由参数中的seat
"cycle_start": current_time,
"active_duration": 900, # 15分钟活跃期
"gap_duration": 10, # 10秒间隔期
@ -170,33 +171,6 @@ aircraft_route_params = {
"outRunway": "17",
"startSeat": "156"
}
},
# 新增:航班进出港通知中出现的航班配置
"CA8901": { # 出港航班
"arrival": {
"inRunway": "35",
"outRunway": "17",
"contactCross": "L4",
"seat": "201"
},
"departure": { # 出港参数
"inRunway": "35",
"outRunway": "17",
"startSeat": "201"
}
},
"MU2678": { # 进港航班
"arrival": { # 进港参数
"inRunway": "17",
"outRunway": "35",
"contactCross": "B3",
"seat": "156"
},
"departure": {
"inRunway": "17",
"outRunway": "35",
"startSeat": "156"
}
}
}