清理原有的无人车位置和状态接口。

This commit is contained in:
Tian jianyong 2025-09-24 16:26:46 +08:00
parent 243d5734f1
commit c7a52469dd
20 changed files with 907 additions and 3655 deletions

View File

@ -272,7 +272,8 @@ curl -X GET "https://api.example.com/api/v1/vehicles/AV-001/status?fields=vehicl
"missionType": "CARGO_TRANSPORT",
"startTime": 1736175000000,
"estimatedEndTime": 1736178600000,
"progress": 65.5
"progress": 65.5,
"totalMileage": 1250.8
},
"waypoints": [
{
@ -527,6 +528,7 @@ curl -X GET "https://api.example.com/api/v1/vehicles/AV-001/status?fields=vehicl
- **startTime**: 开始时间戳 [可选]
- **estimatedEndTime**: 预计结束时间戳 [可选]
- **progress**: 任务进度百分比 [可选]
- **totalMileage**: 累计行驶里程 (米) [可选]
#### 路径点 (waypoints) [可选]
- **waypointId**: 路径点ID [可选]

View File

@ -95,6 +95,20 @@
- wifiStatus: CONNECTED | DISCONNECTED | FAULT
- cloudConnectivity: ONLINE | OFFLINE | FAULT
10) missionContext.currentMission
- missionId: string
- missionType: string
- startTime: number (ms, UTC)
- estimatedEndTime: number (ms, UTC)
- progress: number (0-100)
- totalMileage: number (m)
11) missionContext.waypoints
- waypointId: string
- latitude: number
- longitude: number
- status: PENDING | COMPLETED | SKIPPED
---
## 3. 统一要求
@ -213,6 +227,30 @@ curl -sS -X GET "${BASE_URL}/api/v1/vehicles/${VEHICLE_ID}/status" \
"cellularSignalStrength": -65,
"wifiStatus": "CONNECTED",
"cloudConnectivity": "ONLINE"
},
"missionContext": {
"currentMission": {
"missionId": "MISSION_001",
"missionType": "CARGO_TRANSPORT",
"startTime": 1736175000000,
"estimatedEndTime": 1736178600000,
"progress": 65.5,
"totalMileage": 1250.8
},
"waypoints": [
{
"waypointId": "WP_001",
"latitude": 36.354068,
"longitude": 120.083410,
"status": "COMPLETED"
},
{
"waypointId": "WP_002",
"latitude": 36.355123,
"longitude": 120.084567,
"status": "PENDING"
}
]
}
}
}
@ -346,6 +384,46 @@ curl -sS -X GET "${BASE_URL}/api/v1/vehicles/${VEHICLE_ID}/status" \
- communicationStatus.cloudConnectivity
- 类型enum(ONLINE|OFFLINE|FAULT);必填:是
- missionContext.currentMission.missionId
- 类型string必填
- 说明:当前任务的唯一标识符
- missionContext.currentMission.missionType
- 类型string必填
- 说明:任务类型,如 CARGO_TRANSPORT、PATROL_TRANSPORT 等
- missionContext.currentMission.startTime
- 类型number(ms,UTC);必填:是
- 说明:任务开始时间
- missionContext.currentMission.estimatedEndTime
- 类型number(ms,UTC);必填:是
- 说明:预计任务结束时间
- missionContext.currentMission.progress
- 类型number必填范围0~100%
- 说明:当前任务执行进度百分比
- missionContext.currentMission.totalMileage
- 类型number必填单位m
- 说明:累计行驶里程(米)
- missionContext.waypoints.waypointId
- 类型string必填
- 说明:路径点唯一标识符
- missionContext.waypoints.latitude
- 类型number必填单位WGS84 度
- 说明:路径点纬度
- missionContext.waypoints.longitude
- 类型number必填单位WGS84 度
- 说明:路径点经度
- missionContext.waypoints.status
- 类型enum(PENDING|COMPLETED|SKIPPED);必填:是
- 说明路径点状态PENDING=待到达COMPLETED=已完成SKIPPED=已跳过
---
## 8. 兼容性与扩展

View File

@ -192,11 +192,10 @@ data:
username: dianxin
password: dianxin@123
vehicle-api:
base-url: http://localhost:8090
base-url: http://localhost:8091
endpoints:
vehicle-location: /api/VehicleLocationInfo
vehicle-state: /api/VehicleStateInfo
vehicle-command: /api/VehicleCommandInfo
universal-status: /api/v1/vehicles/{vehicleId}/status
timeout: 1000
retry-attempts: 3
unmanned-vehicle:

View File

@ -221,8 +221,6 @@ data:
vehicle-api:
base-url: http://localhost:8091
endpoints:
vehicle-location: /api/VehicleLocationInfo
vehicle-state: /api/VehicleStateInfo
vehicle-command: /api/VehicleCommandInfo
# 通用车辆状态API端点符合universal_autonomous_vehicle_api规范
universal-status: /api/v1/vehicles/{vehicleId}/status

View File

@ -45,6 +45,36 @@ public class UnmannedVehicle extends MovingObject {
* 目标位置
*/
private Point targetPosition;
/**
* 任务类型
*/
private String missionType;
/**
* 任务开始时间戳
*/
private Long missionStartTime;
/**
* 预计任务结束时间戳
*/
private Long estimatedEndTime;
/**
* 任务进度百分比 (0-100)
*/
private Double progress;
/**
* 累计行驶里程
*/
private Double totalMileage;
/**
* 路径点列表
*/
private java.util.List<WaypointInfo> waypoints;
/**
* 无人车运行状态枚举
@ -69,6 +99,29 @@ public class UnmannedVehicle extends MovingObject {
PAUSED, // 暂停
CANCELLED // 已取消
}
/**
* 路径点状态枚举
*/
public enum WaypointStatus {
PENDING, // 待到达
COMPLETED, // 已完成
SKIPPED // 已跳过
}
/**
* 路径点信息
*/
@Data
@lombok.Builder
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
public static class WaypointInfo {
private String waypointId;
private Double latitude;
private Double longitude;
private WaypointStatus status;
}
/**
* 是否可以接收新指令

View File

@ -1,10 +1,7 @@
package com.qaup.collision.controller;
import com.qaup.collision.common.model.dto.Response;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.datacollector.model.dto.VehicleCommand;
import com.qaup.collision.datacollector.model.dto.VehicleStateInfo;
import com.qaup.collision.datacollector.model.dto.VehicleStateRequest;
import com.qaup.collision.datacollector.service.UnmannedVehicleControlService;
import org.slf4j.Logger;
@ -15,7 +12,6 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
/**
* 无人车控制接口控制器
@ -65,68 +61,4 @@ public class UnmannedVehicleController {
.body(Response.error("控制指令执行失败: " + e.getMessage()));
}
}
/**
* 无人车位置上报接口
* 查询指定车辆或所有无人车的当前位置信息
*
* @param vehicleId 车辆ID可选为空时返回所有无人车位置
* @return 车辆位置信息列表
*/
@GetMapping("/VehicleLocationInfo")
public ResponseEntity<Response<List<VehicleLocation>>> getVehicleLocationInfo(
@RequestParam(required = false) String vehicleId) {
logger.info("查询无人车位置信息: vehicleId={}", vehicleId);
try {
List<VehicleLocation> locations = unmannedVehicleControlService.getVehicleLocations(vehicleId);
logger.info("查询无人车位置信息成功: vehicleId={}, count={}", vehicleId, locations.size());
return ResponseEntity.ok(Response.success("位置信息查询成功", locations));
} catch (Exception e) {
logger.error("查询无人车位置信息失败: vehicleId={}, error={}", vehicleId, e.getMessage(), e);
return ResponseEntity.badRequest()
.body(Response.error("位置信息查询失败: " + e.getMessage()));
}
}
/**
* 无人车状态查询接口
* 查询指定车辆的状态信息
*
* @param request 状态查询请求对象
* @return 车辆状态信息
*/
@PostMapping("/VehicleStateInfo")
public ResponseEntity<Response<VehicleStateInfo>> getVehicleStateInfo(
@Valid @RequestBody VehicleStateRequest request) {
logger.info("查询无人车状态信息: vehicleId={}", request.getVehicleId());
try {
// 将Long类型的vehicleId转换为String类型
VehicleStateInfo vehicleState = unmannedVehicleControlService.getVehicleState(String.valueOf(request.getVehicleId()));
if (vehicleState != null) {
logger.info("查询无人车状态信息成功: vehicleId={}", request.getVehicleId());
return ResponseEntity.ok(Response.success("状态信息查询成功", vehicleState));
} else {
logger.warn("未找到指定车辆的状态信息: vehicleId={}", request.getVehicleId());
return ResponseEntity.badRequest()
.body(Response.error("未找到指定车辆的状态信息"));
}
} catch (Exception e) {
logger.error("查询无人车状态信息失败: vehicleId={}, error={}", request.getVehicleId(), e.getMessage(), e);
return ResponseEntity.badRequest()
.body(Response.error("状态信息查询失败: " + e.getMessage()));
}
}
}

View File

@ -3,8 +3,6 @@ package com.qaup.collision.datacollector.dao;
import com.qaup.collision.common.model.Aircraft;
import com.qaup.collision.common.model.AirportVehicle;
import com.qaup.collision.common.model.UnmannedVehicle;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.dto.Response;
import com.qaup.collision.datacollector.service.AuthService;
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
@ -53,10 +51,6 @@ public class DataCollectorDao {
@Value("${data.collector.vehicle-api.base-url}")
private String vehicleBaseUrl;
@Value("${data.collector.vehicle-api.endpoints.vehicle-location}")
private String vehicleLocationEndpoint;
private final RestTemplate restTemplate;
private final AuthService authService;
private final GeometryFactory geometryFactory;
@ -163,61 +157,6 @@ public class DataCollectorDao {
return Collections.emptyList();
}
/**
* 获取无人车位置信息
* 数据来源第2章 无人车位置上报 (/api/VehicleLocationInfo)
* 说明此接口专门用于获取无人车的实时位置数据用于数据持久化
*
* @return 无人车位置信息列表
*/
public List<UnmannedVehicle> getVehicleLocationInfo() {
try {
String url = UriComponentsBuilder
.fromUriString(vehicleBaseUrl)
.path(vehicleLocationEndpoint)
.toUriString();
log.debug("正在从 {} 获取无人车位置信息", url);
HttpHeaders headers = new HttpHeaders();
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
ResponseEntity<List<ExternalUnmannedVehicleData>> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
new ParameterizedTypeReference<List<ExternalUnmannedVehicleData>>() {}
);
if (response.getStatusCode().is2xxSuccessful()) {
List<ExternalUnmannedVehicleData> rawDataList = response.getBody();
if (rawDataList != null) {
List<UnmannedVehicle> processedUnmannedVehicles = rawDataList.stream().map(rawData -> {
if (rawData.getVehicleID() == null || rawData.getLongitude() == null || rawData.getLatitude() == null) {
log.warn("原始无人车数据缺失必要字段 (vehicleID, longitude, latitude),跳过处理: {}", rawData);
return null;
}
UnmannedVehicle unmannedVehicle = new UnmannedVehicle();
unmannedVehicle.setObjectId(rawData.getVehicleID());
unmannedVehicle.setObjectName(rawData.getVehicleID()); // 使用 vehicleID 作为 objectName
unmannedVehicle.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude())));
unmannedVehicle.setCurrentSpeed(rawData.getSpeed() != null ? rawData.getSpeed() * 3.6 : 0.0); // m/s to km/h
unmannedVehicle.setCurrentHeading(rawData.getDirection() != null ? Math.toDegrees(rawData.getDirection()) : 0.0); // 弧度 to 角度
unmannedVehicle.setAltitude(0.0); // 假设默认高度为0如果API有提供可以设置
unmannedVehicle.setObjectType(MovingObjectType.UNMANNED_VEHICLE); // 显式设置对象类型
return unmannedVehicle;
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
log.info("成功获取无人车定位信息,数量: {}", processedUnmannedVehicles.size());
return processedUnmannedVehicles;
}
}
} catch (Exception e) {
log.error("获取无人车定位信息时发生异常", e);
return Collections.emptyList();
}
return Collections.emptyList();
}
// 内部 DTOs精确匹配外部 API 接口返回的 JSON 结构
@ -252,26 +191,6 @@ public class DataCollectorDao {
private Long time;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
private static class ExternalUnmannedVehicleData {
@JsonProperty("transId")
private String transId;
@JsonProperty("timestamp")
private Long timestamp;
@JsonProperty("vehicleID")
private String vehicleID;
@JsonProperty("latitude")
private Double latitude;
@JsonProperty("longitude")
private Double longitude;
@JsonProperty("speed")
private Double speed;
@JsonProperty("direction")
private Double direction;
}
// 新增航空器路由和状态数据采集方法
@ -576,12 +495,15 @@ public class DataCollectorDao {
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
com.qaup.collision.datacollector.model.dto.UniversalApiResponse<com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO> apiResponse = response.getBody();
if (apiResponse.getCode() != null && apiResponse.getCode() == 200 && apiResponse.getData() != null) {
// 检查 apiResponse 是否为 null避免空指针异常
if (apiResponse != null && apiResponse.getCode() != null && apiResponse.getCode() == 200 && apiResponse.getData() != null) {
log.debug("成功获取车辆 {} 的通用状态数据", vehicleId);
return apiResponse.getData();
} else {
log.warn("API返回错误: 车辆={}, code={}, message={}",
vehicleId, apiResponse.getCode(), apiResponse.getMessage());
vehicleId,
apiResponse != null ? apiResponse.getCode() : null,
apiResponse != null ? apiResponse.getMessage() : "响应为空");
return null;
}
} else {

View File

@ -0,0 +1,100 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import java.util.List;
/**
* 任务上下文DTO
* 符合universal_autonomous_vehicle_api规范的任务上下文数据结构
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MissionContextDTO {
/**
* 当前任务信息
*/
private CurrentMissionDTO currentMission;
/**
* 路径点列表
*/
private List<WaypointDTO> waypoints;
/**
* 当前任务DTO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class CurrentMissionDTO {
/**
* 任务ID
*/
private String missionId;
/**
* 任务类型
*/
private String missionType;
/**
* 开始时间戳 (ms, UTC)
*/
private Long startTime;
/**
* 预计结束时间戳 (ms, UTC)
*/
private Long estimatedEndTime;
/**
* 任务进度百分比 (0-100)
*/
private Double progress;
/**
* 累计行驶里程
*/
private Double totalMileage;
}
/**
* 路径点DTO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class WaypointDTO {
/**
* 路径点ID
*/
private String waypointId;
/**
* 纬度
*/
private Double latitude;
/**
* 经度
*/
private Double longitude;
/**
* 状态 (PENDING, COMPLETED, SKIPPED)
*/
private String status;
}
}

View File

@ -68,4 +68,11 @@ public class UniversalVehicleStatusDTO {
* 包含v2xStatus, cellularSignalStrength, wifiStatus, cloudConnectivity
*/
private CommunicationStatusDTO communicationStatus;
/**
* 任务上下文字段组
* 包含currentMission (missionId, missionType, startTime, estimatedEndTime, progress, totalMileage),
* waypoints (waypointId, latitude, longitude, status)
*/
private MissionContextDTO missionContext;
}

View File

@ -5,15 +5,12 @@ import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.Aircraft;
import com.qaup.collision.common.model.AirportVehicle;
import com.qaup.collision.common.model.UnmannedVehicle;
import com.qaup.collision.common.model.AircraftRoute;
import com.qaup.collision.common.service.VehicleLocationService;
import com.qaup.collision.datacollector.dao.DataCollectorDao;
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
import com.qaup.collision.datacollector.dto.AircraftStatusDTO;
import com.qaup.collision.datacollector.dto.FlightNotificationDTO;
import com.qaup.collision.common.model.FlightNotification;
import com.qaup.collision.datacollector.model.dto.MissionContextDTO;
import com.qaup.collision.datacollector.filter.VehicleLocationFilter;
import com.qaup.collision.websocket.event.AircraftRouteUpdateEvent;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@ -23,12 +20,10 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationEventPublisher;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -36,7 +31,6 @@ import java.util.concurrent.ConcurrentHashMap;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.PrecisionModel;
import com.qaup.collision.datacollector.util.RouteGeometryProcessor;
/**
* 数据采集服务 - 重构版本
@ -71,8 +65,6 @@ 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;
@ -83,18 +75,9 @@ public class DataCollectorService {
@Autowired
private VehicleLocationService vehicleLocationService;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private com.qaup.collision.dataprocessing.service.DataProcessingService dataProcessingService; // 注入数据处理服务
@Autowired
private RouteGeometryProcessor routeGeometryProcessor; // 注入路由几何处理器
@Autowired
private RoutePersistenceService routePersistenceService; // 注入路由持久化服务
@Autowired
private VehicleLocationFilter vehicleLocationFilter; // 注入车辆位置过滤器
@ -106,8 +89,6 @@ 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<>();
@ -119,99 +100,6 @@ public class DataCollectorService {
log.info("DataCollectorService 初始化完成缓存引用已传递给DataProcessingService");
}
/**
* 将AircraftRouteDTO转换为AircraftRoute对象
* 使用JTS将多个LineString段合并为单一连续路径
*/
private AircraftRoute convertToAircraftRoute(AircraftRouteDTO routeDTO) {
if (routeDTO == null || routeDTO.getGeoPath() == null) {
return null;
}
try {
// 创建路由段列表
List<AircraftRoute.RouteSegment> routeSegments = new ArrayList<>();
List<org.locationtech.jts.geom.LineString> lineStringSegments = new ArrayList<>();
if (routeDTO.getGeoPath().getFeatures() != null) {
for (AircraftRouteDTO.Feature feature : routeDTO.getGeoPath().getFeatures()) {
if (feature.getGeometry() != null && feature.getGeometry().getCoordinates() != null) {
// 转换坐标格式从List<List<Double>>到List<Point>
List<org.locationtech.jts.geom.Point> points = feature.getGeometry().getCoordinates().stream()
.map(coord -> geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(coord.get(0), coord.get(1))))
.collect(java.util.stream.Collectors.toList());
// 创建路由段
AircraftRoute.RouteSegment segment = AircraftRoute.RouteSegment.builder()
.code(feature.getProperties() != null ? feature.getProperties().getCode() : "")
.coordinates(points)
.build();
routeSegments.add(segment);
// 创建LineString段用于合并
if (points.size() >= 2) {
org.locationtech.jts.geom.Coordinate[] coords = points.stream()
.map(point -> point.getCoordinate())
.toArray(org.locationtech.jts.geom.Coordinate[]::new);
org.locationtech.jts.geom.LineString lineString = geometryFactory.createLineString(coords);
lineStringSegments.add(lineString);
}
}
}
}
// 使用JTS将多个LineString段合并为单一连续路径
org.locationtech.jts.geom.LineString mergedGeometry = null;
if (!lineStringSegments.isEmpty()) {
mergedGeometry = routeGeometryProcessor.mergeLineStrings(lineStringSegments);
if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) {
// 可选简化路径以减少冗余点容差1米
mergedGeometry = routeGeometryProcessor.simplifyLineString(mergedGeometry, 1.0);
log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点",
lineStringSegments.size(), mergedGeometry.getNumPoints());
} else {
log.warn("路由段合并失败或结果无效");
mergedGeometry = null;
}
}
return AircraftRoute.builder()
.type(routeDTO.getType())
.status(routeDTO.getStatus())
.codes(routeDTO.getCodes())
.geometry(mergedGeometry) // 使用合并后的单一LineString
.routeSegments(routeSegments)
.build();
} catch (Exception e) {
log.error("转换航空器路由数据失败", e);
return null;
}
}
/**
* 保存航空器路由到数据库独立于缓存
*/
private void saveAircraftRouteToDatabase(String flightNo, AircraftRoute route) {
try {
log.info("开始保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType());
boolean saveSuccess = routePersistenceService.saveAircraftRoute(flightNo, route);
if (saveSuccess) {
log.info("成功保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType());
// TODO: 保存到路由分配表
// saveRouteAssignment(flightNo, route);
} else {
log.warn("保存航空器路由到数据库失败: flightNo={}, type={}", flightNo, route.getType());
}
} catch (Exception e) {
log.error("保存航空器路由到数据库异常: flightNo={}, type={}", flightNo, route.getType(), e);
}
}
/**
@ -233,10 +121,6 @@ public class DataCollectorService {
log.info("采集到 {} 条航空器数据,用于实时处理", newAircrafts.size());
// List<MovingObject> activeMovingObjects = new ArrayList<>(); // 移除此行
// 航空器数据仅用于实时处理不存储到数据库
// 数据处理完成后发布WebSocket事件进行实时推送
for (Aircraft aircraft : newAircrafts) {
try {
if (aircraft.getCurrentPosition() == null) {
@ -259,7 +143,6 @@ public class DataCollectorService {
.altitude(aircraft.getAltitude())
.build();
// 将最新数据更新到缓存不发送WebSocket消息统一在周期性检测中发送
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
log.debug("处理航空器数据并更新缓存: (航班号: {}, 位置: {}, {}, 速度: {})",
@ -272,24 +155,6 @@ public class DataCollectorService {
log.error("处理航空器数据异常: objectId={}", aircraft.getObjectId(), e); // Changed from flightNo
}
}
// 执行路径冲突检测 (临时移除将在新的定时任务中统一处理)
// if (!activeMovingObjects.isEmpty()) {
// pathConflictDetectionService.detectPathConflicts(activeMovingObjects);
// }
// 执行实时违规检测 (临时移除将在新的定时任务中统一处理)
// for (MovingObject movingObject : activeMovingObjects) {
// VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject);
// if (tempVehicleLocation != null) {
// List<RuleViolationEvent> violations = realTimeViolationDetector.detectViolations(tempVehicleLocation);
// if (!violations.isEmpty()) {
// log.warn("检测到航空器违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size());
// // 如果需要可以在这里进一步处理这些违规事件例如持久化或发送告警
// // realTimeViolationDetector.processBatchViolationEvents(violations);
// }
// }
// }
log.info("航空器数据处理和事件发布完成,处理数量: {}", newAircrafts.size());
@ -376,24 +241,6 @@ public class DataCollectorService {
log.error("处理机场车辆数据异常: objectId={}", vehicle.getObjectId(), e);
}
}
// 执行路径冲突检测 (临时移除将在新的定时任务中统一处理)
// if (!activeMovingObjects.isEmpty()) {
// pathConflictDetectionService.detectPathConflicts(activeMovingObjects);
// }
// 执行实时违规检测 (临时移除将在新的定时任务中统一处理)
// for (MovingObject movingObject : activeMovingObjects) {
// VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject);
// if (tempVehicleLocation != null) {
// List<RuleViolationEvent> violations = realTimeViolationDetector.detectViolations(tempVehicleLocation);
// if (!violations.isEmpty()) {
// log.warn("检测到机场车辆违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size());
// // 如果需要可以在这里进一步处理这些违规事件例如持久化或发送告警
// // realTimeViolationDetector.processBatchViolationEvents(violations);
// }
// }
// }
log.info("机场车辆数据处理和事件发布完成,处理数量: {}", filteredVehicles.size());
@ -404,13 +251,13 @@ public class DataCollectorService {
/**
* 定时采集无人车数据 (外部API)
*
* 数据来源第2章 无人车位置上报 (/api/VehicleLocationInfo)
* 说明仅传递目前无人车平台已接入的无人车位置数据
*
* 数据来源通用无人车状态API (/api/v1/vehicles/{vehicleId}/status)
* 说明采集完整的无人车状态数据包含位置和任务上下文信息
* 重构说明
* - 无人车数据仅用于实时处理不存储到数据库
* - 数据采集后直接用于碰撞检测等实时计算
* - 不进行数据持久化
* - 使用通用状态API替代基础位置API获取更完整的数据
* - 包含missionContext任务信息支持里程和路径点数据
* - 数据仅用于实时处理不存储到数据库
*/
@Scheduled(fixedRateString = "${data.collector.interval}")
@Async // 异步执行
@ -418,88 +265,150 @@ public class DataCollectorService {
if (collectorDisabled) {
return;
}
try {
List<UnmannedVehicle> unmannedVehicles = dataCollectorDao.getVehicleLocationInfo();
if (unmannedVehicles.isEmpty()) {
log.debug("未获取到无人车数据");
// 获取当前活跃的无人车列表从之前的缓存或配置中获取
java.util.Set<String> knownVehicleIds = getKnownUnmannedVehicleIds();
if (knownVehicleIds.isEmpty()) {
log.debug("当前没有已知的无人车ID跳过数据采集");
return;
}
log.info("采集到 {} 条无人车数据", unmannedVehicles.size());
// 将采集到的无人车数据转换为MovingObject并添加到缓存
// List<VehicleLocation> unmannedVehicleLocations = new ArrayList<>(); // 移除此行
for (UnmannedVehicle unmannedVehicle : unmannedVehicles) {
log.info("开始采集 {} 辆无人车的完整状态数据", knownVehicleIds.size());
int successCount = 0;
for (String vehicleId : knownVehicleIds) {
try {
if (unmannedVehicle.getCurrentPosition() == null) {
log.warn("无人车 {} 位置信息缺失,跳过处理。", unmannedVehicle.getObjectId());
continue; // 跳过此无人车
// 调用通用状态API获取完整数据
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData =
dataCollectorDao.getUniversalVehicleStatus(vehicleId);
if (statusData == null || statusData.getMotionStatus() == null) {
log.debug("无人车 {} 未返回有效状态数据,跳过处理", vehicleId);
continue;
}
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(
unmannedVehicle.getCurrentPosition().getX(),
unmannedVehicle.getCurrentPosition().getY()
));
// 提取位置信息
if (statusData.getMotionStatus().getPosition() == null) {
log.warn("无人车 {} 位置信息缺失,跳过处理", vehicleId);
continue;
}
// 数据采集阶段只采集和缓存位置数据不进行任何计算
MovingObject movingObject = MovingObject.builder()
.objectId(unmannedVehicle.getObjectId())
Double latitude = statusData.getMotionStatus().getPosition().getLatitude();
Double longitude = statusData.getMotionStatus().getPosition().getLongitude();
if (latitude == null || longitude == null) {
log.warn("无人车 {} 经纬度信息缺失,跳过处理", vehicleId);
continue;
}
Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(longitude, latitude));
// 创建增强的UnmannedVehicle对象
var vehicleBuilder = UnmannedVehicle.builder()
.objectId(vehicleId)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName(unmannedVehicle.getObjectName())
.objectName(vehicleId)
.currentPosition(currentPosition)
.currentSpeed(null) // 不在采集阶段计算速度
.currentHeading(null) // 不在采集阶段计算方向
.altitude(unmannedVehicle.getAltitude())
.build();
.altitude(0.0); // 默认高度
log.debug("在DataCollectorService中MovingObject的速度: {}", movingObject.getCurrentSpeed());
// 提取任务上下文信息
if (statusData.getMissionContext() != null) {
MissionContextDTO missionContext = statusData.getMissionContext();
// 注意电子围栏检测已移至周期性检测方法中以降低检测频率
// 仅在此处更新缓存实际检测在performPeriodicViolationDetection()中执行
if (missionContext.getCurrentMission() != null) {
MissionContextDTO.CurrentMissionDTO currentMission = missionContext.getCurrentMission();
vehicleBuilder
.missionId(currentMission.getMissionId())
.missionType(currentMission.getMissionType())
.missionStartTime(currentMission.getStartTime())
.estimatedEndTime(currentMission.getEstimatedEndTime())
.progress(currentMission.getProgress())
.totalMileage(currentMission.getTotalMileage());
}
// 将最新数据更新到缓存不保存到数据库统一在周期性检测中保存
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
log.debug("处理无人车数据并更新缓存: (车牌号: {}, 位置: {}, {}, 速度: {})",
unmannedVehicle.getObjectId(),
// 提取路径点信息
if (missionContext.getWaypoints() != null) {
java.util.List<UnmannedVehicle.WaypointInfo> waypoints = missionContext.getWaypoints().stream()
.map(wp -> UnmannedVehicle.WaypointInfo.builder()
.waypointId(wp.getWaypointId())
.latitude(wp.getLatitude())
.longitude(wp.getLongitude())
.status(UnmannedVehicle.WaypointStatus.valueOf(wp.getStatus()))
.build())
.collect(java.util.stream.Collectors.toList());
vehicleBuilder.waypoints(waypoints);
}
}
// 提取电池信息
if (statusData.getBatteryStatus() != null && statusData.getBatteryStatus().getMainBattery() != null) {
Double chargeLevel = statusData.getBatteryStatus().getMainBattery().getChargeLevel();
if (chargeLevel != null) {
vehicleBuilder.batteryLevel(chargeLevel.intValue());
}
}
// 设置车辆运行状态
if (statusData.getOperationalStatus() != null) {
String systemHealth = statusData.getOperationalStatus().getSystemHealth();
if ("HEALTHY".equals(systemHealth)) {
vehicleBuilder.vehicleStatus(UnmannedVehicle.VehicleStatus.WORKING);
} else {
vehicleBuilder.vehicleStatus(UnmannedVehicle.VehicleStatus.ERROR);
}
}
UnmannedVehicle enhancedUnmannedVehicle = vehicleBuilder.build();
// 将最新数据更新到缓存
activeMovingObjectsCache.put(enhancedUnmannedVehicle.getObjectId(), enhancedUnmannedVehicle);
successCount++;
log.debug("处理无人车完整状态数据并更新缓存: (车辆ID: {}, 位置: {}, {}, 任务ID: {}, 里程: {}米, 电量: {}%)",
vehicleId,
currentPosition.getX(),
currentPosition.getY(),
movingObject.getCurrentSpeed());
enhancedUnmannedVehicle.getMissionId(),
enhancedUnmannedVehicle.getTotalMileage(),
enhancedUnmannedVehicle.getBatteryLevel());
} catch (Exception e) {
log.error("处理无人车数据异常: objectId={}", unmannedVehicle.getObjectId(), e);
log.error("处理无人车状态数据异常: vehicleId={}", vehicleId, e);
}
}
// 执行路径冲突检测 (临时移除将在新的定时任务中统一处理)
// if (!unmannedVehicleLocations.isEmpty()) {
// pathConflictDetectionService.detectPathConflicts(unmannedVehicleLocations.stream()
// .map(v -> MovingObject.builder()
// .objectId(String.valueOf(v.getVehicleId()))
// .objectType(v.getVehicleType())
// .objectName(v.getVehicleLicense())
// .currentPosition(v.getLocation())
// .currentSpeed(v.getSpeed())
// .currentHeading(v.getHeading())
// .altitude(v.getAltitude())
// .build())
// .collect(Collectors.toList()));
// }
// 执行实时违规检测 (临时移除将在新的定时任务中统一处理)
// if (!unmannedVehicleLocations.isEmpty()) {
// realTimeViolationDetector.detectBatchViolations(unmannedVehicleLocations);
// }
log.info("无人车数据处理和事件发布完成,处理数量: {}", unmannedVehicles.size());
log.info("无人车完整状态数据采集完成,成功处理: {}/{}", successCount, knownVehicleIds.size());
} catch (Exception e) {
log.error("采集无人车数据异常", e);
}
}
/**
* 获取已知的无人车ID列表
* 可以从配置缓存或其他数据源获取
*/
private java.util.Set<String> getKnownUnmannedVehicleIds() {
java.util.Set<String> vehicleIds = new java.util.HashSet<>();
// 从当前缓存中获取已存在的无人车ID
activeMovingObjectsCache.values().stream()
.filter(obj -> obj.getObjectType() == MovingObjectType.UNMANNED_VEHICLE)
.forEach(obj -> vehicleIds.add(obj.getObjectId()));
// 如果缓存为空使用默认的无人车ID与mock服务对应
if (vehicleIds.isEmpty()) {
vehicleIds.add("鲁B567");
vehicleIds.add("鲁B579");
}
return vehicleIds;
}
/**
* 定时采集通用车辆状态数据 (符合universal_autonomous_vehicle_api规范)
*
@ -567,14 +476,14 @@ public class DataCollectorService {
}
/**
* 缓存通用状态数据
* 缓存通用无人车状态数据
* 将状态数据存储到专门的缓存中供DataProcessingService处理
* 同时将missionContext信息更新到activeMovingObjectsCache中的无人车对象
*/
private void cacheUniversalVehicleStatus(String vehicleId, com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData) {
// 使用专门的缓存存储通用状态数据
// 注意这里不影响activeMovingObjectsCache中的位置数据两者独立存储
String cacheKey = "universal_status_" + vehicleId;
// 创建带时间戳的状态数据包装器
UniversalVehicleStatusCacheEntry cacheEntry = UniversalVehicleStatusCacheEntry.builder()
.vehicleId(vehicleId)
@ -582,12 +491,63 @@ public class DataCollectorService {
.timestamp(System.currentTimeMillis())
.dataSource("COLLECTOR")
.build();
// 存储到缓存中这里简化为使用activeMovingObjectsCache的扩展实际可以使用Redis等
// 临时解决方案使用一个Map来存储状态数据
// 存储到通用状态缓存中
getUniversalStatusCache().put(cacheKey, cacheEntry);
log.debug("缓存通用车辆状态数据: vehicleId={}, cacheKey={}", vehicleId, cacheKey);
// 如果存在missionContext数据更新到activeMovingObjectsCache中的无人车对象
if (statusData.getMissionContext() != null) {
updateUnmannedVehicleMissionContext(vehicleId, statusData.getMissionContext());
}
log.debug("缓存通用车辆状态数据: vehicleId={}, cacheKey={}, 包含任务上下文: {}",
vehicleId, cacheKey, statusData.getMissionContext() != null);
}
/**
* 更新无人车的任务上下文信息
*/
private void updateUnmannedVehicleMissionContext(String vehicleId, MissionContextDTO missionContext) {
MovingObject existingObject = activeMovingObjectsCache.get(vehicleId);
if (existingObject instanceof UnmannedVehicle) {
UnmannedVehicle unmannedVehicle = (UnmannedVehicle) existingObject;
// 更新任务上下文信息
if (missionContext.getCurrentMission() != null) {
MissionContextDTO.CurrentMissionDTO currentMission = missionContext.getCurrentMission();
unmannedVehicle.setMissionId(currentMission.getMissionId());
unmannedVehicle.setMissionType(currentMission.getMissionType());
unmannedVehicle.setMissionStartTime(currentMission.getStartTime());
unmannedVehicle.setEstimatedEndTime(currentMission.getEstimatedEndTime());
unmannedVehicle.setProgress(currentMission.getProgress());
unmannedVehicle.setTotalMileage(currentMission.getTotalMileage());
}
// 更新路径点信息
if (missionContext.getWaypoints() != null) {
java.util.List<UnmannedVehicle.WaypointInfo> waypoints = missionContext.getWaypoints().stream()
.map(wp -> UnmannedVehicle.WaypointInfo.builder()
.waypointId(wp.getWaypointId())
.latitude(wp.getLatitude())
.longitude(wp.getLongitude())
.status(UnmannedVehicle.WaypointStatus.valueOf(wp.getStatus()))
.build())
.collect(java.util.stream.Collectors.toList());
unmannedVehicle.setWaypoints(waypoints);
}
// 更新缓存
activeMovingObjectsCache.put(vehicleId, unmannedVehicle);
log.debug("更新无人车任务上下文: vehicleId={}, 任务ID={}, 里程={}米, 路径点数量={}",
vehicleId,
unmannedVehicle.getMissionId(),
unmannedVehicle.getTotalMileage(),
unmannedVehicle.getWaypoints() != null ? unmannedVehicle.getWaypoints().size() : 0);
} else {
log.debug("缓存中未找到无人车对象或类型不匹配,跳过任务上下文更新: vehicleId={}", vehicleId);
}
}
// 用于存储通用车辆状态数据的缓存
@ -614,8 +574,6 @@ public class DataCollectorService {
private String dataSource;
}
// 移除周期性处理方法 - 现在由DataProcessingService处理
/**
* 统计当前数据采集服务的健康状态和关键指标
* @return 统计信息字符串
@ -674,13 +632,9 @@ public class DataCollectorService {
notification.getSeat(),
notification.getEventDateTime());
// 缓存航班通知供DataProcessingService推送给前端
// 缓存航班通知供DataProcessingService处理
cacheFlightNotification(notification);
// 🚀 新增事件驱动的路由查询
// 基于航班通知触发路由查询和更新
triggerRouteQueryByFlightNotification(dto);
} else {
log.warn("⚠️ 航班进出港通知数据无效,跳过处理: {}", dto);
}
@ -757,211 +711,6 @@ public class DataCollectorService {
return flightNotificationCache;
}
/**
* 检查指定航班事件的路由是否已经获取过
*
* @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);
}
/**
* 标记指定航班事件的路由已获取
*
* @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);
}
}
/**
* 基于航班通知触发路由查询
*
* 实现事件驱动的路由更新流程
* 1. 收到航班进出港通知
* 2. 使用航班号和类型查询路由参数
* 3. 调用路由查询接口获取完整路由数据
* 4. 发布WebSocket路由更新事件
*
* @param flightNotification 航班进出港通知
*/
private void triggerRouteQueryByFlightNotification(FlightNotificationDTO flightNotification) {
try {
String flightNo = flightNotification.getFlightNo();
String routeType = flightNotification.getType();
// 检查是否已获取过该航班事件的路由
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 =
dataCollectorDao.getAircraftRouteParams(flightNo, routeType);
if (routeParams == null || !routeParams.isValid()) {
log.warn("⚠️ 未能获取有效的航班路由参数: flightNo={}, routeType={}", flightNo, routeType);
return;
}
log.info("✅ 成功获取航班路由参数: flightNo={}, inRunway={}, outRunway={}, contactCross={}, seat/startSeat={}",
routeParams.getFlightNo(),
routeParams.getInRunway(),
routeParams.getOutRunway(),
routeParams.getContactCross(),
routeParams.isArrivalRoute() ? routeParams.getSeat() : routeParams.getStartSeat());
// 步骤2: 基于路由参数调用相应的路由查询接口
AircraftRouteDTO routeData = null;
if (routeParams.isArrivalRoute()) {
// 进港路由查询
routeData = dataCollectorDao.getArrivalRoute(
routeParams.getInRunway(),
routeParams.getOutRunway(),
routeParams.getContactCross(),
routeParams.getSeat()
);
log.info("🛬 查询进港路由: inRunway={}, outRunway={}, contactCross={}, seat={}",
routeParams.getInRunway(), routeParams.getOutRunway(),
routeParams.getContactCross(), routeParams.getSeat());
} else if (routeParams.isDepartureRoute()) {
// 出港路由查询
routeData = dataCollectorDao.getDepartureRoute(
routeParams.getInRunway(),
routeParams.getOutRunway(),
routeParams.getStartSeat()
);
log.info("🛫 查询出港路由: inRunway={}, outRunway={}, startSeat={}",
routeParams.getInRunway(), routeParams.getOutRunway(), routeParams.getStartSeat());
}
// 步骤3: 处理路由查询结果
if (routeData != null) {
log.info("🎯 成功获取{}路由数据: 编码={}, 状态={}",
routeData.getType(), routeData.getCodes(), routeData.getStatus());
// 转换DTO为航空器路由对象
AircraftRoute aircraftRoute = convertToAircraftRoute(routeData);
if (aircraftRoute != null) {
// 保存路由到数据库
saveAircraftRouteToDatabase(flightNo, aircraftRoute);
// 更新缓存中的航空器路由信息
updateAircraftRouteInCacheFromNotification(flightNo, aircraftRoute, routeParams);
// 发布WebSocket路由更新事件
publishAircraftRouteUpdateEventFromNotification(flightNo, aircraftRoute, routeParams);
// 标记该航班事件的路由已获取
markRouteAsRetrieved(flightNo, routeType, flightNotification.getTime());
log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}, 时间={}", flightNo, routeType, flightNotification.getTime());
} else {
log.warn("⚠️ 路由数据转换失败: flightNo={}", flightNo);
}
} else {
log.warn("⚠️ 未获取到路由数据: flightNo={}, routeType={}", flightNo, routeType);
}
} catch (Exception e) {
log.error("❌ 航班通知触发路由查询异常: flightNo={}",
flightNotification.getFlightNo(), e);
}
}
/**
* 基于航班通知更新缓存中的航空器路由信息
*/
private void updateAircraftRouteInCacheFromNotification(String flightNo, AircraftRoute route,
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) {
MovingObject cachedAircraft = activeMovingObjectsCache.get(flightNo);
if (cachedAircraft != null && cachedAircraft instanceof Aircraft) {
Aircraft aircraft = (Aircraft) cachedAircraft;
// 根据路由类型更新路由
if (routeParams.isArrivalRoute()) {
aircraft.setArrivalRoute(route);
aircraft.activateArrivalRoute();
} else if (routeParams.isDepartureRoute()) {
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 publishAircraftRouteUpdateEventFromNotification(String flightNo, AircraftRoute route,
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) {
try {
// 创建路由更新事件
AircraftRouteUpdateEvent routeUpdateEvent = AircraftRouteUpdateEvent.builder()
.flightNo(flightNo)
.routeType(route.getType())
.routeStatus(route.getStatus())
.routeCodes(route.getCodes())
.aircraftStatus(routeParams.getRouteType()) // 使用路由参数中的类型
.routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null)
.timestamp(System.currentTimeMillis())
.eventSource("FLIGHT_NOTIFICATION") // 标识事件来源
.build();
// 发布WebSocket事件
eventPublisher.publishEvent(routeUpdateEvent);
log.info("📡 发布航空器路由更新事件 (事件驱动): 航班号={}, 路由类型={}, 事件来源=航班通知",
flightNo, route.getType());
} catch (Exception e) {
log.error("❌ 发布航空器路由更新事件失败 (事件驱动): flightNo={}", flightNo, e);
}
}
@PreDestroy
public void shutdown() {

View File

@ -4,7 +4,6 @@ import com.qaup.collision.common.model.AircraftRoute;
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.LineString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -234,11 +233,4 @@ public class RoutePersistenceService {
objectName, routeType, routeId, e);
}
}
/**
* 验证LineString的有效性
*/
private boolean isValidLineString(LineString lineString) {
return lineString != null && lineString.isValid() && lineString.getNumPoints() >= 2;
}
}

View File

@ -3,7 +3,6 @@ package com.qaup.collision.datacollector.service;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.datacollector.model.converter.VehicleCommandConverter;
import com.qaup.collision.datacollector.model.dto.VehicleCommand;
import com.qaup.collision.datacollector.model.dto.VehicleStateInfo;
import com.qaup.collision.datacollector.model.entity.VehicleCommandEntity;
import com.qaup.collision.datacollector.repository.VehicleCommandRepository;
import com.qaup.collision.common.model.repository.VehicleLocationRepository;
@ -22,7 +21,6 @@ import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* 无人车控制服务类
@ -59,11 +57,7 @@ public class UnmannedVehicleControlService {
@Value("${data.collector.vehicle-api.endpoints.vehicle-command:#{null}}")
private String vehicleCommandEndpoint;
@Value("${data.collector.vehicle-api.endpoints.vehicle-location}")
private String vehicleLocationEndpoint;
@Value("${data.collector.vehicle-api.endpoints.vehicle-state:#{null}}")
private String vehicleStateEndpoint;
/**
* 处理无人车控制指令
@ -155,29 +149,90 @@ public class UnmannedVehicleControlService {
}
/**
* 获取无人车状态信息
*
* 获取完整的无人车状态信息 (符合universal_autonomous_vehicle_api规范)
*
* @param vehicleId 车辆ID
* @return 车辆状态信息
* @return 完整的无人车状态数据
*/
public VehicleStateInfo getVehicleState(String vehicleId) {
logger.info("查询无人车状态信息: vehicleId={}", vehicleId);
@Transactional(readOnly = true)
public com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO getCompleteVehicleStatus(String vehicleId) {
logger.info("查询无人车完整状态信息: vehicleId={}", vehicleId);
try {
// 如果配置了外部状态查询API则调用外部系统
if (vehicleStateEndpoint != null && !vehicleStateEndpoint.trim().isEmpty()) {
return queryVehicleStateFromExternalSystem(vehicleId);
// 直接调用DataCollectorDao的通用状态API
com.qaup.collision.datacollector.dao.DataCollectorDao dataCollectorDao =
applicationContext.getBean(com.qaup.collision.datacollector.dao.DataCollectorDao.class);
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData =
dataCollectorDao.getUniversalVehicleStatus(vehicleId);
if (statusData != null) {
logger.info("成功获取无人车完整状态: vehicleId={}, 电池电量={}%",
vehicleId,
statusData.getBatteryStatus() != null && statusData.getBatteryStatus().getMainBattery() != null
? statusData.getBatteryStatus().getMainBattery().getChargeLevel() : "未知");
return statusData;
} else {
// 否则基于本地数据构造状态信息
return buildVehicleStateFromLocalData(vehicleId);
logger.warn("未能获取无人车完整状态: vehicleId={}", vehicleId);
return null;
}
} catch (Exception e) {
logger.error("查询无人车状态信息失败: vehicleId={}, error={}", vehicleId, e.getMessage(), e);
throw new RuntimeException("状态信息查询失败: " + e.getMessage(), e);
logger.error("查询无人车完整状态信息失败: vehicleId={}, error={}", vehicleId, e.getMessage(), e);
throw new RuntimeException("完整状态信息查询失败: " + e.getMessage(), e);
}
}
/**
* 批量获取无人车完整状态信息
*
* @param vehicleIds 车辆ID列表
* @return 车辆状态映射表 (vehicleId -> 状态数据)
*/
@Transactional(readOnly = true)
public java.util.Map<String, com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO> getBatchVehicleStatus(List<String> vehicleIds) {
logger.info("批量查询无人车完整状态信息: vehicleIds={}", vehicleIds);
java.util.Map<String, com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO> statusMap = new java.util.HashMap<>();
if (vehicleIds == null || vehicleIds.isEmpty()) {
logger.warn("车辆ID列表为空");
return statusMap;
}
try {
com.qaup.collision.datacollector.dao.DataCollectorDao dataCollectorDao =
applicationContext.getBean(com.qaup.collision.datacollector.dao.DataCollectorDao.class);
int successCount = 0;
for (String vehicleId : vehicleIds) {
try {
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData =
dataCollectorDao.getUniversalVehicleStatus(vehicleId);
if (statusData != null) {
statusMap.put(vehicleId, statusData);
successCount++;
}
} catch (Exception e) {
logger.warn("获取单个车辆状态失败: vehicleId={}, error={}", vehicleId, e.getMessage());
}
}
logger.info("批量查询无人车状态完成: 总数={}, 成功={}, 失败={}",
vehicleIds.size(), successCount, vehicleIds.size() - successCount);
return statusMap;
} catch (Exception e) {
logger.error("批量查询无人车完整状态信息失败: vehicleIds={}, error={}", vehicleIds, e.getMessage(), e);
throw new RuntimeException("批量状态信息查询失败: " + e.getMessage(), e);
}
}
@Autowired
private org.springframework.context.ApplicationContext applicationContext;
/**
* 验证控制指令参数
*/
@ -238,107 +293,5 @@ public class UnmannedVehicleControlService {
}
}
/**
* 从外部系统查询车辆状态
*/
private VehicleStateInfo queryVehicleStateFromExternalSystem(String vehicleId) {
try {
String url = vehicleApiBaseUrl + vehicleStateEndpoint;
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
// 构造查询请求参数
VehicleStateQueryRequest queryRequest = new VehicleStateQueryRequest();
queryRequest.setTransId(UUID.randomUUID().toString());
queryRequest.setTimestamp(System.currentTimeMillis());
queryRequest.setVehicleId(vehicleId);
queryRequest.setSingle(true);
HttpEntity<VehicleStateQueryRequest> requestEntity = new HttpEntity<>(queryRequest, headers);
ResponseEntity<VehicleStateInfo[]> response = restTemplate.exchange(
url, HttpMethod.POST, requestEntity, VehicleStateInfo[].class);
if (response != null && response.getStatusCode().is2xxSuccessful()) {
VehicleStateInfo[] body = response.getBody();
if (body != null && body.length > 0) {
VehicleStateInfo stateInfo = body[0];
logger.info("从外部系统获取车辆状态成功: vehicleId={}", vehicleId);
return stateInfo;
} else {
logger.warn("外部系统未返回车辆状态: vehicleId={}", vehicleId);
return null;
}
} else {
logger.warn("外部系统调用失败: vehicleId={}, status={}", vehicleId, response != null ? response.getStatusCode() : "无响应");
return null;
}
} catch (Exception e) {
logger.error("从外部系统查询车辆状态异常: vehicleId={}, error={}", vehicleId, e.getMessage(), e);
return null;
}
}
/**
* 基于本地数据构造车辆状态信息
*/
private VehicleStateInfo buildVehicleStateFromLocalData(String vehicleId) {
// 查询最近的位置信息判断车辆是否在线
try {
Long vehicleIdLong = Long.parseLong(vehicleId);
Optional<VehicleLocation> recentLocation = vehicleLocationRepository.findLatestByVehicleId(vehicleIdLong);
boolean isOnline = recentLocation.isPresent();
// 构造基本状态信息
VehicleStateInfo stateInfo = new VehicleStateInfo();
stateInfo.setTransId(UUID.randomUUID().toString());
stateInfo.setTimestamp(System.currentTimeMillis());
stateInfo.setVehicleId(vehicleIdLong); // 使用Long类型
stateInfo.setLoginStatus(isOnline);
stateInfo.setFaultInfo(List.of()); // 空故障列表
stateInfo.setActiveSafety(false);
stateInfo.setRc(false);
stateInfo.setCommand(0); // 0表示恢复状态
stateInfo.setAirportInfo(List.of());
stateInfo.setVehicleMode(isOnline ? 2 : 5); // 2:自动, 5:故障等待
stateInfo.setGearState(2); // 2:D档
stateInfo.setChassisReady(isOnline);
stateInfo.setCollisionStatus(false);
stateInfo.setClearance(0);
stateInfo.setTurnSignalStatus(0);
stateInfo.setPointCloud(List.of());
logger.info("基于本地数据构造车辆状态: vehicleId={}, isOnline={}", vehicleId, isOnline);
return stateInfo;
} catch (NumberFormatException e) {
logger.warn("无效的vehicleId格式: {}", vehicleId);
return null;
}
}
/**
* 车辆状态查询请求内部类
*/
private static class VehicleStateQueryRequest {
private String transId;
private long timestamp;
private String vehicleId;
private boolean single;
public String getTransId() { return transId; }
public void setTransId(String transId) { this.transId = transId; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
public String getVehicleId() { return vehicleId; }
public void setVehicleId(String vehicleId) { this.vehicleId = vehicleId; }
public boolean isSingle() { return single; }
public void setSingle(boolean single) { this.single = single; }
}
}

View File

@ -32,8 +32,13 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import com.qaup.collision.datacollector.dao.DataCollectorDao;
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
import com.qaup.collision.common.model.AircraftRoute;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.PrecisionModel;
@ -503,6 +508,9 @@ public class DataProcessingService {
log.info("📡 发布航班进出港通知WebSocket事件: 航班号={}, 事件类型={}, 通知级别={}",
event.getFlightNo(), event.getEventType(), event.getNotificationLevel());
// 触发基于航班通知的路由查询和更新处理
triggerRouteQueryByFlightNotification(notification);
} else {
log.warn("⚠️ 航班进出港通知WebSocket事件创建失败或无效: {}", notification.getFlightNo());
}
@ -977,4 +985,287 @@ public class DataProcessingService {
log.error("🚦 发布红绿灯状态事件失败: intersectionId={}", payload.getIntersectionId(), e);
}
}
// ==================== 航班路由处理相关方法 ====================
// 路由获取状态缓存Key=flightNo:type:time, Value=获取时间戳
private final Map<String, Long> routeRetrievalCache = new ConcurrentHashMap<>();
/**
* 基于航班通知触发路由查询
*
* 实现事件驱动的路由更新流程
* 1. 收到航班进出港通知
* 2. 使用航班号和类型查询路由参数
* 3. 调用路由查询接口获取完整路由数据
* 4. 发布WebSocket路由更新事件
*
* @param flightNotification 航班进出港通知
*/
private void triggerRouteQueryByFlightNotification(FlightNotification flightNotification) {
try {
String flightNo = flightNotification.getFlightNo();
String routeType = flightNotification.getType().name();
// 检查是否已获取过该航班事件的路由
if (isRouteAlreadyRetrieved(flightNo, routeType, flightNotification.getEventTime())) {
log.info("🔄 航班路由已获取过,跳过重复查询: 航班号={}, 类型={}, 时间={}",
flightNo, routeType, flightNotification.getEventTime());
return;
}
log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}, 时间={}", flightNo, routeType, flightNotification.getEventTime());
// 获取DataCollectorDao进行路由数据查询
DataCollectorDao dataCollectorDao = applicationContext.getBean(DataCollectorDao.class);
// 步骤1: 查询航班路由参数
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams =
dataCollectorDao.getAircraftRouteParams(flightNo, routeType);
if (routeParams == null || !routeParams.isValid()) {
log.warn("⚠️ 未能获取有效的航班路由参数: flightNo={}, routeType={}", flightNo, routeType);
return;
}
log.info("✅ 成功获取航班路由参数: flightNo={}, inRunway={}, outRunway={}, contactCross={}, seat/startSeat={}",
routeParams.getFlightNo(),
routeParams.getInRunway(),
routeParams.getOutRunway(),
routeParams.getContactCross(),
routeParams.isArrivalRoute() ? routeParams.getSeat() : routeParams.getStartSeat());
// 步骤2: 基于路由参数调用相应的路由查询接口
AircraftRouteDTO routeData = null;
if (routeParams.isArrivalRoute()) {
// 进港路由查询
routeData = dataCollectorDao.getArrivalRoute(
routeParams.getInRunway(),
routeParams.getOutRunway(),
routeParams.getContactCross(),
routeParams.getSeat()
);
log.info("🛬 查询进港路由: inRunway={}, outRunway={}, contactCross={}, seat={}",
routeParams.getInRunway(), routeParams.getOutRunway(),
routeParams.getContactCross(), routeParams.getSeat());
} else if (routeParams.isDepartureRoute()) {
// 出港路由查询
routeData = dataCollectorDao.getDepartureRoute(
routeParams.getInRunway(),
routeParams.getOutRunway(),
routeParams.getStartSeat()
);
log.info("🛫 查询出港路由: inRunway={}, outRunway={}, startSeat={}",
routeParams.getInRunway(), routeParams.getOutRunway(), routeParams.getStartSeat());
}
// 步骤3: 处理路由查询结果
if (routeData != null) {
log.info("🎯 成功获取{}路由数据: 编码={}, 状态={}",
routeData.getType(), routeData.getCodes(), routeData.getStatus());
// 转换DTO为航空器路由对象
AircraftRoute aircraftRoute = convertToAircraftRoute(routeData);
if (aircraftRoute != null) {
// 保存路由到数据库
saveAircraftRouteToDatabase(flightNo, aircraftRoute);
// 更新缓存中的航空器路由信息
updateAircraftRouteInCache(flightNo, aircraftRoute, routeParams);
// 发布WebSocket路由更新事件
publishAircraftRouteUpdateEvent(flightNo, aircraftRoute, routeParams);
// 标记该航班事件的路由已获取
markRouteAsRetrieved(flightNo, routeType, flightNotification.getEventTime());
log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}, 时间={}", flightNo, routeType, flightNotification.getEventTime());
} else {
log.warn("⚠️ 路由数据转换失败: flightNo={}", flightNo);
}
} else {
log.warn("⚠️ 未获取到路由数据: flightNo={}, routeType={}", flightNo, routeType);
}
} catch (Exception e) {
log.error("❌ 航班通知触发路由查询异常: flightNo={}",
flightNotification.getFlightNo(), e);
}
}
/**
* 检查指定航班事件的路由是否已经获取过
*/
private boolean isRouteAlreadyRetrieved(String flightNo, String type, Long time) {
String cacheKey = flightNo + ":" + type + ":" + time;
return routeRetrievalCache.containsKey(cacheKey);
}
/**
* 标记指定航班事件的路由已获取
*/
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());
}
/**
* 将AircraftRouteDTO转换为AircraftRoute对象
* 使用JTS将多个LineString段合并为单一连续路径
*/
private AircraftRoute convertToAircraftRoute(AircraftRouteDTO routeDTO) {
if (routeDTO == null || routeDTO.getGeoPath() == null) {
return null;
}
try {
// 获取路由几何处理器
com.qaup.collision.datacollector.util.RouteGeometryProcessor routeGeometryProcessor =
applicationContext.getBean(com.qaup.collision.datacollector.util.RouteGeometryProcessor.class);
// 创建路由段列表
List<AircraftRoute.RouteSegment> routeSegments = new ArrayList<>();
List<org.locationtech.jts.geom.LineString> lineStringSegments = new ArrayList<>();
if (routeDTO.getGeoPath().getFeatures() != null) {
for (AircraftRouteDTO.Feature feature : routeDTO.getGeoPath().getFeatures()) {
if (feature.getGeometry() != null && feature.getGeometry().getCoordinates() != null) {
// 转换坐标格式从List<List<Double>>到List<Point>
List<org.locationtech.jts.geom.Point> points = feature.getGeometry().getCoordinates().stream()
.map(coord -> geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(coord.get(0), coord.get(1))))
.collect(java.util.stream.Collectors.toList());
// 创建路由段
AircraftRoute.RouteSegment segment = AircraftRoute.RouteSegment.builder()
.code(feature.getProperties() != null ? feature.getProperties().getCode() : "")
.coordinates(points)
.build();
routeSegments.add(segment);
// 创建LineString段用于合并
if (points.size() >= 2) {
org.locationtech.jts.geom.Coordinate[] coords = points.stream()
.map(point -> point.getCoordinate())
.toArray(org.locationtech.jts.geom.Coordinate[]::new);
org.locationtech.jts.geom.LineString lineString = geometryFactory.createLineString(coords);
lineStringSegments.add(lineString);
}
}
}
}
// 使用JTS将多个LineString段合并为单一连续路径
org.locationtech.jts.geom.LineString mergedGeometry = null;
if (!lineStringSegments.isEmpty()) {
mergedGeometry = routeGeometryProcessor.mergeLineStrings(lineStringSegments);
if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) {
// 可选简化路径以减少冗余点容差1米
mergedGeometry = routeGeometryProcessor.simplifyLineString(mergedGeometry, 1.0);
log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点",
lineStringSegments.size(), mergedGeometry.getNumPoints());
} else {
log.warn("路由段合并失败或结果无效");
mergedGeometry = null;
}
}
return AircraftRoute.builder()
.type(routeDTO.getType())
.status(routeDTO.getStatus())
.codes(routeDTO.getCodes())
.geometry(mergedGeometry) // 使用合并后的单一LineString
.routeSegments(routeSegments)
.build();
} catch (Exception e) {
log.error("转换航空器路由数据失败", e);
return null;
}
}
/**
* 保存航空器路由到数据库独立于缓存
*/
private void saveAircraftRouteToDatabase(String flightNo, AircraftRoute route) {
try {
log.info("开始保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType());
// 获取路由持久化服务
com.qaup.collision.datacollector.service.RoutePersistenceService routePersistenceService =
applicationContext.getBean(com.qaup.collision.datacollector.service.RoutePersistenceService.class);
boolean saveSuccess = routePersistenceService.saveAircraftRoute(flightNo, route);
if (saveSuccess) {
log.info("成功保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType());
} else {
log.warn("保存航空器路由到数据库失败: flightNo={}, type={}", flightNo, route.getType());
}
} catch (Exception e) {
log.error("保存航空器路由到数据库异常: flightNo={}, type={}", flightNo, route.getType(), e);
}
}
/**
* 更新缓存中的航空器路由信息
*/
private void updateAircraftRouteInCache(String flightNo, AircraftRoute route,
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) {
MovingObject cachedAircraft = activeMovingObjectsCache.get(flightNo);
if (cachedAircraft != null && cachedAircraft instanceof com.qaup.collision.common.model.Aircraft) {
com.qaup.collision.common.model.Aircraft aircraft = (com.qaup.collision.common.model.Aircraft) cachedAircraft;
// 根据路由类型更新路由
if (routeParams.isArrivalRoute()) {
aircraft.setArrivalRoute(route);
aircraft.activateArrivalRoute();
} else if (routeParams.isDepartureRoute()) {
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,
com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) {
try {
// 创建路由更新事件
com.qaup.collision.websocket.event.AircraftRouteUpdateEvent routeUpdateEvent =
com.qaup.collision.websocket.event.AircraftRouteUpdateEvent.builder()
.flightNo(flightNo)
.routeType(route.getType())
.routeStatus(route.getStatus())
.routeCodes(route.getCodes())
.aircraftStatus(routeParams.getRouteType()) // 使用路由参数中的类型
.routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null)
.timestamp(System.currentTimeMillis())
.eventSource("FLIGHT_NOTIFICATION") // 标识事件来源
.build();
// 发布WebSocket事件
eventPublisher.publishEvent(routeUpdateEvent);
log.info("📡 发布航空器路由更新事件: 航班号={}, 路由类型={}, 事件来源=航班通知",
flightNo, route.getType());
} catch (Exception e) {
log.error("❌ 发布航空器路由更新事件失败: flightNo={}", flightNo, e);
}
}
}

View File

@ -427,17 +427,6 @@ public class LocationRuleQueryServiceImpl implements LocationRuleQueryService {
return path;
}
/**
* 检查复杂时间模式匹配
* @param rule 规则
* @param timestamp 时间戳
* @return 如果匹配返回true
*/
private boolean matchesComplexTimePatterns(SpatialRule rule, LocalDateTime timestamp) {
// 使用TimeWindowMatchingService处理复杂时间模式匹配
return timeWindowMatchingService.matchesComplexTimePatterns(rule.getTimePatterns(), timestamp);
}
/**
* 从区域检查获取几何形状
* @param areaId 区域ID

View File

@ -1083,26 +1083,6 @@ public class SpatialRuleServiceImpl implements SpatialRuleService {
return true;
}
/**
* 检查星期名称匹配 "MON,WED,FRI"
*/
private boolean matchesWeekdayNames(String weekdays, LocalDateTime timestamp) {
try {
String[] dayNames = weekdays.split(",");
String currentDay = timestamp.getDayOfWeek().name().substring(0, 3); // MON, TUE, etc.
for (String dayName : dayNames) {
if (dayName.trim().equalsIgnoreCase(currentDay)) {
return true;
}
}
return false;
} catch (Exception e) {
logger.warn("检查星期名称失败: {}", weekdays, e);
return true;
}
}
private JsonNode parseJsonString(String jsonString) {
try {
if (jsonString == null || jsonString.trim().isEmpty()) {

View File

@ -8,7 +8,6 @@ import com.qaup.collision.websocket.event.RuleStateChangeWebSocketEvent;
import com.qaup.collision.websocket.event.RuleViolationWebSocketEvent;
import com.qaup.collision.websocket.event.GeofenceAlertWebSocketEvent;
import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent;
import com.qaup.collision.websocket.event.TrafficLightStatusEvent;
import com.qaup.collision.websocket.event.VehicleCommandEvent;
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
import com.qaup.collision.websocket.message.CollisionWarningPayload;
@ -18,7 +17,6 @@ import com.qaup.collision.websocket.message.PositionUpdatePayload;
import com.qaup.collision.websocket.message.RuleExecutionStatusPayload;
import com.qaup.collision.websocket.message.RuleStateChangePayload;
import com.qaup.collision.websocket.message.RuleViolationPayload;
import com.qaup.collision.websocket.message.TrafficLightStatusPayload;
import com.qaup.collision.websocket.message.VehicleCommandPayload;
import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload;
import com.qaup.collision.websocket.message.UniversalMessage;
@ -119,21 +117,6 @@ public class WebSocketMessageBroadcaster {
broadcastMessageInternal(message);
}
/**
* 处理红绿灯状态事件
* @param event 红绿灯状态事件
*/
@EventListener
public void handleTrafficLightStatus(TrafficLightStatusEvent event) {
UniversalMessage<TrafficLightStatusPayload> message = UniversalMessage.<TrafficLightStatusPayload>builder()
.type(MessageTypeConstants.TRAFFIC_LIGHT_STATUS)
.timestamp(event.getTimestamp())
.messageId(generateMessageId())
.payload((TrafficLightStatusPayload) event.getPayload())
.build();
broadcastMessageInternal(message);
}
/**
* 处理碰撞预警事件

File diff suppressed because it is too large Load Diff

View File

@ -181,6 +181,12 @@ def update_position_with_vector(
speed_mps = float(speed) * 1000.0 / 3600.0
distance = speed_mps * float(elapsed_time)
# 累计里程到总里程中
if "total_mileage" not in obj:
obj["total_mileage"] = 0.0
obj["total_mileage"] = to_float_safe(obj.get("total_mileage", 0.0)) or 0.0
obj["total_mileage"] += distance
# 安全读取并转换当前位置与目标点/起点经纬度为 float
cur_lat = to_float_safe(obj.get("latitude"))
cur_lon = to_float_safe(obj.get("longitude"))
@ -287,28 +293,39 @@ def update_vehicle_position(vehicle: dict[str, Any], elapsed_time: float) -> Non
update_object_position(vehicle, elapsed_time)
# 无人车数据配置
current_timestamp = time.time()
unmanned_vehicle_data = [
{
"vehicleNo": "鲁B567", # 无人车A
"longitude": UNMANNED_A_START["longitude"],
"latitude": UNMANNED_A_START["latitude"],
"time": int(time.time() * 1000),
"time": int(current_timestamp * 1000),
"direction": 90.0, # 90度
"speed": 25.0, # 25km/h
"start_point": UNMANNED_A_START, # 起点
"end_point": UNMANNED_A_END, # 终点
"moving_to_end": True # 当前是否向终点移动
"moving_to_end": True, # 当前是否向终点移动
# 任务和里程相关字段
"total_mileage": 0.0, # 总里程(米)
"mission_start_time": int(current_timestamp * 1000), # 任务开始时间
"mission_id": "MISSION_A_001", # 任务ID
"mission_type": "PATROL_TRANSPORT" # 任务类型:巡逻运输
},
{
"vehicleNo": "鲁B579", # 无人车B
"longitude": UNMANNED_B_START["longitude"],
"latitude": UNMANNED_B_START["latitude"],
"time": int(time.time() * 1000),
"time": int(current_timestamp * 1000),
"direction": 270.0, # 270度
"speed": 25.0, # 25km/h
"start_point": UNMANNED_B_START, # 起点
"end_point": UNMANNED_B_END, # 终点
"moving_to_end": True # 当前是否向终点移动
"moving_to_end": True, # 当前是否向终点移动
# 任务和里程相关字段
"total_mileage": 0.0, # 总里程(米)
"mission_start_time": int(current_timestamp * 1000), # 任务开始时间
"mission_id": "MISSION_B_001", # 任务ID
"mission_type": "CARGO_TRANSPORT" # 任务类型:货物运输
}
]
@ -473,24 +490,6 @@ class RequestVehicleCommandPayload(TypedDict, total=False):
longitude: float
transId: str
# 无人车位置数据生成函数
def generate_unmanned_vehicle_location_data() -> list[dict[str, Any]]:
"""生成无人车位置数据符合官方API格式"""
unmanned_vehicles = []
for vehicle in unmanned_vehicle_data:
lon_val = to_float_safe(vehicle.get("longitude"))
lat_val = to_float_safe(vehicle.get("latitude"))
location_info = {
"transId": str(uuid.uuid4()),
"timestamp": int(time.time() * 1000),
"vehicleID": vehicle["vehicleNo"],
"longitude": round(lon_val, 6) if lon_val is not None else vehicle.get("longitude"),
"latitude": round(lat_val, 6) if lat_val is not None else vehicle.get("latitude")
}
unmanned_vehicles.append(location_info)
return unmanned_vehicles
# 生成符合API规范的完整车辆状态数据
def generate_comprehensive_vehicle_status(vehicle_id: str) -> dict[str, Any]:
@ -574,11 +573,102 @@ def generate_comprehensive_vehicle_status(vehicle_id: str) -> dict[str, Any]:
"cellularSignalStrength": -65, # -65dBm
"wifiStatus": "CONNECTED",
"cloudConnectivity": "ONLINE"
},
"missionContext": {
"currentMission": {
"missionId": vehicle_data_item.get("mission_id", "UNKNOWN"),
"missionType": vehicle_data_item.get("mission_type", "UNKNOWN"),
"startTime": vehicle_data_item.get("mission_start_time", timestamp_ms),
"estimatedEndTime": vehicle_data_item.get("mission_start_time", timestamp_ms) + 3600000, # 1小时后
"progress": round(calculate_mission_progress(vehicle_data_item), 1),
"totalMileage": round(to_float_safe(vehicle_data_item.get("total_mileage", 0.0)) or 0.0, 1)
},
"waypoints": [
{
"waypointId": f"{vehicle_id}_START",
"latitude": round(to_float_safe(vehicle_data_item.get("start_point", {}).get("latitude", 0.0)) or 0.0, 6),
"longitude": round(to_float_safe(vehicle_data_item.get("start_point", {}).get("longitude", 0.0)) or 0.0, 6),
"status": "COMPLETED" if not vehicle_data_item.get("moving_to_end", True) else "PENDING"
},
{
"waypointId": f"{vehicle_id}_END",
"latitude": round(to_float_safe(vehicle_data_item.get("end_point", {}).get("latitude", 0.0)) or 0.0, 6),
"longitude": round(to_float_safe(vehicle_data_item.get("end_point", {}).get("longitude", 0.0)) or 0.0, 6),
"status": "COMPLETED" if vehicle_data_item.get("moving_to_end", True) else "PENDING"
}
]
}
}
return status_data
def calculate_mission_progress(vehicle_data: dict[str, Any]) -> float:
"""计算任务进度百分比"""
try:
# 获取车辆当前位置
cur_lat = to_float_safe(vehicle_data.get("latitude"))
cur_lon = to_float_safe(vehicle_data.get("longitude"))
# 获取起点和终点坐标
start_point = vehicle_data.get("start_point", {})
end_point = vehicle_data.get("end_point", {})
start_lat = to_float_safe(start_point.get("latitude"))
start_lon = to_float_safe(start_point.get("longitude"))
end_lat = to_float_safe(end_point.get("latitude"))
end_lon = to_float_safe(end_point.get("longitude"))
if any(v is None for v in [cur_lat, cur_lon, start_lat, start_lon, end_lat, end_lon]):
return 0.0
# 确保所有值都不为None使用断言来帮助类型检查器理解
assert cur_lat is not None and cur_lon is not None
assert start_lat is not None and start_lon is not None
assert end_lat is not None and end_lon is not None
# 现在可以安全地转换为float类型
start_lat_safe = float(start_lat)
start_lon_safe = float(start_lon)
end_lat_safe = float(end_lat)
end_lon_safe = float(end_lon)
cur_lat_safe = float(cur_lat)
cur_lon_safe = float(cur_lon)
# 计算起点到终点的总距离
total_distance = calculate_distance_to_target(
{"latitude": start_lat_safe, "longitude": start_lon_safe},
end_lat_safe, end_lon_safe
)
if total_distance == 0:
return 100.0
# 计算当前位置到起点的距离
distance_from_start = calculate_distance_to_target(
{"latitude": cur_lat_safe, "longitude": cur_lon_safe},
start_lat_safe, start_lon_safe
)
# 计算当前位置到终点的距离
distance_to_end = calculate_distance_to_target(
{"latitude": cur_lat_safe, "longitude": cur_lon_safe},
end_lat_safe, end_lon_safe
)
# 根据移动方向计算进度
if vehicle_data.get("moving_to_end", True):
# 向终点移动:进度 = (已走距离 / 总距离) * 100
progress = (distance_from_start / total_distance) * 100.0
else:
# 向起点移动:进度 = (1 - 距离起点的距离 / 总距离) * 100 + 100
progress = 100.0 + ((total_distance - distance_from_start) / total_distance) * 100.0
# 确保进度在合理范围内
return max(0.0, min(200.0, progress))
except Exception:
return 0.0
def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None) -> dict[str, Any]:
"""根据fields参数过滤车辆状态字段"""
if not fields:
@ -600,59 +690,6 @@ def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None
return filtered_data
# 无人车状态数据生成函数
def generate_unmanned_vehicle_state_data(vehicle_id: str | None = None, is_single: bool = True) -> list[dict[str, Any]]:
"""生成无人车状态数据符合官方API格式"""
vehicle_states_data = []
if is_single and vehicle_id:
# 单个车辆状态查询
vehicle_state = vehicle_states.get(vehicle_id)
if vehicle_state:
state_info = {
"transId": str(uuid.uuid4()),
"timestamp": int(time.time() * 1000),
"vehicleID": vehicle_id,
"loginState": True,
"faultInfo": [],
"activeSafety": not vehicle_state.is_running,
"RC": False,
"Command": 1 if vehicle_state.current_command == "ALERT" else 0,
"airportInfo": [],
"vehicleMode": 2, # 自动模式
"gearState": 2, # D档
"chassisReady": vehicle_state.is_running,
"collisionStatus": False,
"clearance": 1 if vehicle_state.is_running else 0,
"turnSignalStstus": 0,
"pointCloud": []
}
vehicle_states_data.append(state_info)
else:
# 所有车辆状态查询
for vehicle in unmanned_vehicle_data:
vehicle_state = vehicle_states.get(vehicle["vehicleNo"])
state_info = {
"transId": str(uuid.uuid4()),
"timestamp": int(time.time() * 1000),
"vehicleID": vehicle["vehicleNo"],
"loginState": True,
"faultInfo": [],
"activeSafety": not vehicle_state.is_running if vehicle_state else False,
"RC": False,
"Command": 1 if vehicle_state and vehicle_state.current_command == "ALERT" else 0,
"airportInfo": [],
"vehicleMode": 2,
"gearState": 2,
"chassisReady": vehicle_state.is_running if vehicle_state else True,
"collisionStatus": False,
"clearance": 1 if vehicle_state and vehicle_state.is_running else 0,
"turnSignalStstus": 0,
"pointCloud": []
}
vehicle_states_data.append(state_info)
return vehicle_states_data
# API 端点实现
@ -805,51 +842,7 @@ def handle_vehicle_command():
"timestamp": int(time.time() * 1000)
}), 500
@app.route('/api/VehicleLocationInfo', methods=['GET', 'OPTIONS'])
def get_unmanned_vehicle_location():
"""无人车位置上报接口"""
if request.method == 'OPTIONS':
return '', 204
try:
current_time = time.time()
global last_unmanned_vehicle_update_time
elapsed_time = current_time - last_unmanned_vehicle_update_time
if elapsed_time >= UPDATE_INTERVAL:
for vehicle in unmanned_vehicle_data:
update_vehicle_position(vehicle, UPDATE_INTERVAL)
vehicle["time"] = int(current_time * 1000)
last_unmanned_vehicle_update_time = current_time
# 生成无人车位置数据
location_data = generate_unmanned_vehicle_location_data()
logging.info(f"📍 返回无人车位置数据,车辆数量: {len(location_data)}")
return jsonify(location_data)
except Exception as e:
logging.error(f"Error in get_unmanned_vehicle_location: {str(e)}")
return jsonify([]), 500
@app.route('/api/VehicleStateInfo', methods=['POST', 'OPTIONS'])
def get_unmanned_vehicle_state():
"""无人车状态查询接口"""
if request.method == 'OPTIONS':
return '', 204
try:
data = request.json
vehicle_id = data.get("vehicleID") if data else None
is_single = data.get("isSingle", True) if data else False
logging.info(f"📊 收到无人车状态查询请求: vehicle_id={vehicle_id}, is_single={is_single}")
# 生成无人车状态数据
state_data = generate_unmanned_vehicle_state_data(vehicle_id, is_single)
logging.info(f"返回无人车状态数据,数量: {len(state_data)}")
return jsonify(state_data)
except Exception as e:
logging.error(f"Error in get_unmanned_vehicle_state: {str(e)}")
return jsonify([]), 500
@app.route('/api/v1/vehicles/<vehicle_id>/status', methods=['GET', 'OPTIONS'])
def get_vehicle_status_universal(vehicle_id):
@ -944,43 +937,6 @@ def get_vehicle_status_universal(vehicle_id):
}
}), 500
@app.route('/openApi/getVehicleStatus', methods=['GET', 'OPTIONS'])
def get_vehicle_status():
"""获取无人车状态信息"""
if request.method == 'OPTIONS':
return '', 204
vehicle_id = request.args.get('vehicleId')
if not vehicle_id:
return jsonify({
"status": 400,
"msg": "缺少 vehicleId 参数",
"data": None
}), 400
# 查找对应的车辆
vehicle_state = vehicle_states.get(vehicle_id)
if not vehicle_state:
return jsonify({
"status": 404,
"msg": f"未找到无人车 {vehicle_id}",
"data": None
}), 404
# 返回车辆状态
logging.info(f"📋 返回无人车状态: vehicleId={vehicle_id}")
return jsonify({
"status": 200,
"msg": "获取无人车状态成功",
"data": {
"vehicleId": vehicle_id,
"status": "NORMAL" if vehicle_state.is_running else "STOPPED",
"command": vehicle_state.current_command,
"commandPriority": vehicle_state.command_priority,
"trafficLightState": vehicle_state.traffic_light_state,
"timestamp": int(time.time() * 1000)
}
})
# 设置CORS
@app.after_request
@ -995,8 +951,6 @@ if __name__ == '__main__':
print("🚗 无人车厂商平台模拟服务启动中...")
print("📍 服务端点:")
print(" - 车辆控制指令: POST /api/VehicleCommandInfo")
print(" - 车辆位置上报: GET /api/VehicleLocationInfo")
print(" - 车辆状态查询: POST /api/VehicleStateInfo")
print(" - 通用状态接口: GET /api/v1/vehicles/<vehicle_id>/status")
print(" - 车辆状态获取: GET /openApi/getVehicleStatus")
print(f"🌐 服务地址: http://localhost:8091")

View File

@ -121,7 +121,6 @@
<div><strong>路口红绿灯:</strong> <span id="intersectionTrafficLightCount">0</span></div>
<div><strong>航空器路由:</strong> <span id="aircraftRouteCount">0</span></div>
<div><strong>事件驱动路由:</strong> <span id="eventDrivenRouteCount">0</span></div>
<div><strong>定时采集路由:</strong> <span id="scheduledRouteCount">0</span></div>
<div><strong>航班进出港:</strong> <span id="flightNotificationCount">0</span></div>
<div><strong>路径冲突:</strong> <span id="pathConflictCount">0</span></div>
<div><strong>车辆指令:</strong> <span id="vehicleCommandCount">0</span></div>
@ -189,7 +188,6 @@
intersection_traffic_light_status: 0,
aircraftRouteUpdate: 0,
eventDrivenRoute: 0, // 事件驱动的路由更新
scheduledRoute: 0, // 定时采集的路由更新
flight_notification: 0,
path_conflict_alert: 0,
vehicle_command: 0,
@ -257,7 +255,6 @@
document.getElementById('intersectionTrafficLightCount').textContent = messageStats.intersection_traffic_light_status;
document.getElementById('aircraftRouteCount').textContent = messageStats.aircraftRouteUpdate;
document.getElementById('eventDrivenRouteCount').textContent = messageStats.eventDrivenRoute;
document.getElementById('scheduledRouteCount').textContent = messageStats.scheduledRoute;
document.getElementById('flightNotificationCount').textContent = messageStats.flight_notification;
document.getElementById('pathConflictCount').textContent = messageStats.path_conflict_alert;
document.getElementById('vehicleCommandCount').textContent = messageStats.vehicle_command;
@ -421,8 +418,6 @@
// 根据事件来源更新分类统计
if (eventSource === 'FLIGHT_NOTIFICATION') {
messageStats.eventDrivenRoute++;
} else if (eventSource === 'SCHEDULED') {
messageStats.scheduledRoute++;
}
// 根据事件来源设置不同的日志类型和图标
@ -431,8 +426,6 @@
if (eventSource === 'FLIGHT_NOTIFICATION') {
logType = 'success';
icon = '🚀'; // 事件驱动的路由更新用火箭图标
} else if (eventSource === 'SCHEDULED') {
icon = '🕐'; // 定时采集的路由更新用时钟图标
}
log('collisionLog', `${icon} 航空器路由更新: ${flightNo} | 路由:${routeType} 状态:${routeStatus} | 航空器:${aircraftStatus} | 编码:${routeCodes} | 来源:${eventSource} | ${routeGeometry}`, logType);
@ -598,7 +591,6 @@
document.getElementById('intersectionTrafficLightCount').textContent = '0';
document.getElementById('aircraftRouteCount').textContent = '0';
document.getElementById('eventDrivenRouteCount').textContent = '0';
document.getElementById('scheduledRouteCount').textContent = '0';
document.getElementById('flightNotificationCount').textContent = '0';
document.getElementById('pathConflictCount').textContent = '0';
document.getElementById('vehicleCommandCount').textContent = '0';