增加了航空器进出港通知接口和无人车状态上报接口

This commit is contained in:
Tian jianyong 2025-09-19 11:05:55 +08:00
parent c8dadc2f30
commit 589a212877
23 changed files with 2818 additions and 859 deletions

View File

@ -128,7 +128,7 @@ curl -G "https://api.example.com/api/v1/vehicles/AV-001/status" \
--data-urlencode "fields=vehicleInfo,operationalStatus,controlStatus,motionStatus,safetyStatus,sensorStatus,batteryStatus,communicationStatus"
```
### 4.2 Postman/HTTPie 示例
### 4.2 Postman/HTTP 示例
HTTPie
```bash
http GET https://api.example.com/api/v1/vehicles/AV-001/status \

View File

@ -230,6 +230,8 @@ data:
vehicle-location: /api/VehicleLocationInfo
vehicle-state: /api/VehicleStateInfo
vehicle-command: /api/VehicleCommandInfo
# 通用车辆状态API端点符合universal_autonomous_vehicle_api规范
universal-status: /api/v1/vehicles/{vehicleId}/status
timeout: 1000
retry-attempts: 3
# 无人车数据持久化配置

View File

@ -0,0 +1,233 @@
package com.qaup.collision.controller;
import com.qaup.collision.datacollector.model.dto.UniversalApiResponse;
import com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO;
import com.qaup.collision.datacollector.dao.DataCollectorDao;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* 通用车辆API控制器
* 符合universal_autonomous_vehicle_api规范
*
* 提供标准化的车辆状态查询接口支持8字段组完整状态数据
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/vehicles")
@Tag(name = "通用车辆API", description = "符合universal_autonomous_vehicle_api规范的车辆状态接口")
public class UniversalVehicleApiController {
@Autowired
private DataCollectorDao dataCollectorDao;
/**
* 获取车辆状态信息
* 符合universal_autonomous_vehicle_api规范的完整8字段组格式
*
* @param vehicleId 车辆唯一标识
* @return 车辆状态数据
*/
@GetMapping("/{vehicleId}/status")
@Operation(
summary = "获取车辆状态",
description = "获取指定车辆的完整状态信息包含8个必需字段组vehicleInfo, operationalStatus, controlStatus, motionStatus, safetyStatus, sensorStatus, batteryStatus, communicationStatus"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功获取车辆状态"),
@ApiResponse(responseCode = "404", description = "车辆不存在"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public ResponseEntity<UniversalApiResponse<UniversalVehicleStatusDTO>> getVehicleStatus(
@Parameter(description = "车辆唯一标识", required = true, example = "UV001")
@PathVariable String vehicleId) {
try {
log.info("接收到车辆状态查询请求: vehicleId={}", vehicleId);
// 调用DAO层获取车辆状态数据
UniversalVehicleStatusDTO statusData = dataCollectorDao.getUniversalVehicleStatus(vehicleId);
if (statusData != null) {
// 构建成功响应
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
.code(200)
.message("成功")
.timestamp(System.currentTimeMillis())
.data(statusData)
.build();
log.info("成功返回车辆状态: vehicleId={}", vehicleId);
return ResponseEntity.ok(response);
} else {
// 构建车辆不存在响应
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
.code(404)
.message("车辆不存在或状态数据不可用")
.timestamp(System.currentTimeMillis())
.data(null)
.build();
log.warn("车辆不存在或状态数据不可用: vehicleId={}", vehicleId);
return ResponseEntity.status(404).body(response);
}
} catch (Exception e) {
log.error("获取车辆状态异常: vehicleId={}", vehicleId, e);
// 构建错误响应
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
.code(500)
.message("服务器内部错误: " + e.getMessage())
.timestamp(System.currentTimeMillis())
.data(null)
.error(UniversalApiResponse.ErrorDetails.builder()
.type("INTERNAL_SERVER_ERROR")
.details(e.getMessage())
.build())
.build();
return ResponseEntity.status(500).body(response);
}
}
/**
* 获取车辆状态信息简化版本仅返回核心字段
*
* @param vehicleId 车辆唯一标识
* @param fields 指定返回的字段组多个字段用逗号分隔
* @return 车辆状态数据
*/
@GetMapping("/{vehicleId}/status/simple")
@Operation(
summary = "获取车辆状态(简化版)",
description = "获取指定车辆的核心状态信息可通过fields参数指定返回的字段组"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功获取车辆状态"),
@ApiResponse(responseCode = "404", description = "车辆不存在"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public ResponseEntity<UniversalApiResponse<UniversalVehicleStatusDTO>> getVehicleStatusSimple(
@Parameter(description = "车辆唯一标识", required = true, example = "UV001")
@PathVariable String vehicleId,
@Parameter(description = "指定返回的字段组,多个字段用逗号分隔", example = "vehicleInfo,operationalStatus,batteryStatus")
@RequestParam(required = false, defaultValue = "vehicleInfo,operationalStatus,motionStatus,batteryStatus") String fields) {
try {
log.info("接收到车辆状态简化查询请求: vehicleId={}, fields={}", vehicleId, fields);
// 调用DAO层获取完整车辆状态数据
UniversalVehicleStatusDTO fullStatusData = dataCollectorDao.getUniversalVehicleStatus(vehicleId);
if (fullStatusData != null) {
// 根据fields参数过滤字段
UniversalVehicleStatusDTO filteredStatusData = filterStatusDataByFields(fullStatusData, fields);
// 构建成功响应
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
.code(200)
.message("成功")
.timestamp(System.currentTimeMillis())
.data(filteredStatusData)
.build();
log.info("成功返回车辆简化状态: vehicleId={}, fields={}", vehicleId, fields);
return ResponseEntity.ok(response);
} else {
// 构建车辆不存在响应
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
.code(404)
.message("车辆不存在或状态数据不可用")
.timestamp(System.currentTimeMillis())
.data(null)
.build();
log.warn("车辆不存在或状态数据不可用: vehicleId={}", vehicleId);
return ResponseEntity.status(404).body(response);
}
} catch (Exception e) {
log.error("获取车辆简化状态异常: vehicleId={}, fields={}", vehicleId, fields, e);
// 构建错误响应
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
.code(500)
.message("服务器内部错误: " + e.getMessage())
.timestamp(System.currentTimeMillis())
.data(null)
.error(UniversalApiResponse.ErrorDetails.builder()
.type("INTERNAL_SERVER_ERROR")
.details(e.getMessage())
.build())
.build();
return ResponseEntity.status(500).body(response);
}
}
/**
* 根据指定字段过滤状态数据
*
* @param fullData 完整状态数据
* @param fieldsParam 字段参数字符串
* @return 过滤后的状态数据
*/
private UniversalVehicleStatusDTO filterStatusDataByFields(UniversalVehicleStatusDTO fullData, String fieldsParam) {
if (fieldsParam == null || fieldsParam.trim().isEmpty()) {
return fullData;
}
String[] requestedFields = fieldsParam.toLowerCase().split(",");
UniversalVehicleStatusDTO.UniversalVehicleStatusDTOBuilder builder = UniversalVehicleStatusDTO.builder();
for (String field : requestedFields) {
field = field.trim();
switch (field) {
case "vehicleinfo":
builder.vehicleInfo(fullData.getVehicleInfo());
break;
case "operationalstatus":
builder.operationalStatus(fullData.getOperationalStatus());
break;
case "controlstatus":
builder.controlStatus(fullData.getControlStatus());
break;
case "motionstatus":
builder.motionStatus(fullData.getMotionStatus());
break;
case "safetystatus":
builder.safetyStatus(fullData.getSafetyStatus());
break;
case "sensorstatus":
builder.sensorStatus(fullData.getSensorStatus());
break;
case "batterystatus":
builder.batteryStatus(fullData.getBatteryStatus());
break;
case "communicationstatus":
builder.communicationStatus(fullData.getCommunicationStatus());
break;
default:
log.warn("未知的字段名: {}", field);
break;
}
}
return builder.build();
}
}

View File

@ -23,7 +23,12 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@ -476,4 +481,76 @@ public class DataCollectorDao {
return Collections.emptyList();
}
}
/**
* 获取通用车辆状态数据 (符合universal_autonomous_vehicle_api规范)
*
* @param vehicleId 车辆ID
* @return 通用车辆状态数据
*/
public com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO getUniversalVehicleStatus(String vehicleId) {
String url = vehicleBaseUrl + "/api/v1/vehicles/" + vehicleId + "/status";
try {
// 获取认证token
String token = authService.getToken();
if (token == null) {
log.warn("无法获取认证token跳过车辆 {} 的状态采集", vehicleId);
return null;
}
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token); // token已经包含"Bearer "前缀
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(headers);
log.debug("调用通用车辆状态API: {}", url);
// 调用API获取状态数据
ResponseEntity<com.qaup.collision.datacollector.model.dto.UniversalApiResponse<com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO>> response =
restTemplate.exchange(
url,
HttpMethod.GET,
entity,
new ParameterizedTypeReference<com.qaup.collision.datacollector.model.dto.UniversalApiResponse<com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO>>() {}
);
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) {
log.debug("成功获取车辆 {} 的通用状态数据", vehicleId);
return apiResponse.getData();
} else {
log.warn("API返回错误: 车辆={}, code={}, message={}",
vehicleId, apiResponse.getCode(), apiResponse.getMessage());
return null;
}
} else {
log.warn("通用车辆状态API调用失败: 车辆={}, status={}", vehicleId, response.getStatusCode());
return null;
}
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
log.debug("车辆 {} 不存在或不支持通用状态API", vehicleId);
} else {
log.warn("调用通用车辆状态API出现客户端错误: 车辆={}, status={}, error={}",
vehicleId, e.getStatusCode(), e.getMessage());
}
return null;
} catch (HttpServerErrorException e) {
log.error("调用通用车辆状态API出现服务器错误: 车辆={}, status={}, error={}",
vehicleId, e.getStatusCode(), e.getMessage());
return null;
} catch (ResourceAccessException e) {
log.error("调用通用车辆状态API网络连接异常: 车辆={}, error={}", vehicleId, e.getMessage());
return null;
} catch (Exception e) {
log.error("调用通用车辆状态API发生未知异常: 车辆={}", vehicleId, e);
return null;
}
}
}

View File

@ -0,0 +1,62 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 电池状态DTO
* 符合universal_autonomous_vehicle_api规范 - batteryStatus字段组
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BatteryStatusDTO {
/**
* 主电池状态
*/
private MainBatteryDTO mainBattery;
/**
* 主电池状态内部类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class MainBatteryDTO {
/**
* 电池电量百分比0~100
*/
private Double chargeLevel;
/**
* 电压V
*/
private Double voltage;
/**
* 电流A
* 正值=充电负值=放电
*/
private Double current;
/**
* 温度°C
*/
private Double temperature;
/**
* 充电状态
* 枚举值CHARGING | DISCHARGING | IDLE | FAULT
*/
private String chargingStatus;
}
}

View File

@ -0,0 +1,44 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 通信状态DTO
* 符合universal_autonomous_vehicle_api规范 - communicationStatus字段组
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CommunicationStatusDTO {
/**
* V2X通信状态
* 枚举值CONNECTED | DISCONNECTED | FAULT
*/
private String v2xStatus;
/**
* 蜂窝信号强度dBm
*/
private Integer cellularSignalStrength;
/**
* WiFi连接状态
* 枚举值CONNECTED | DISCONNECTED | FAULT
*/
private String wifiStatus;
/**
* 云连接状态
* 枚举值ONLINE | OFFLINE | FAULT
*/
private String cloudConnectivity;
}

View File

@ -0,0 +1,38 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 控制状态DTO
* 符合universal_autonomous_vehicle_api规范 - controlStatus字段组
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ControlStatusDTO {
/**
* 控制模式
* 枚举值MANUAL | AUTONOMOUS | REMOTE | HYBRID
*/
private String controlMode;
/**
* 控制权限
* 枚举值DRIVER | SYSTEM | REMOTE_OPERATOR
*/
private String controlAuthority;
/**
* 是否处于远程控制激活态
*/
private Boolean remoteControlActive;
}

View File

@ -0,0 +1,69 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 运动状态DTO
* 符合universal_autonomous_vehicle_api规范 - motionStatus字段组
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MotionStatusDTO {
/**
* 位置信息
*/
private PositionDTO position;
/**
* 速度信息
*/
private VelocityDTO velocity;
/**
* 位置信息内部类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class PositionDTO {
/**
* 纬度WGS84
*/
private Double latitude;
/**
* 经度WGS84
*/
private Double longitude;
}
/**
* 速度信息内部类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class VelocityDTO {
/**
* 速度m/s
*/
private Double speed;
/**
* 方向radians
*/
private Double direction;
}
}

View File

@ -0,0 +1,52 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 运行状态DTO
* 符合universal_autonomous_vehicle_api规范 - operationalStatus字段组
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OperationalStatusDTO {
/**
* 供电与上电状态
* 枚举值ON | OFF | STANDBY
*/
private String powerStatus;
/**
* 系统健康概览
* 枚举值HEALTHY | DEGRADED | CRITICAL | FAULT
* DEGRADED/CRITICAL 用于运营判定
*/
private String systemHealth;
/**
* 当前控制模式
* 枚举值MANUAL | ASSISTED | AUTONOMOUS | REMOTE
*/
private String operationalMode;
/**
* 紧急状态等级
* 枚举值NORMAL | WARNING | EMERGENCY | CRITICAL
*/
private String emergencyStatus;
/**
* 设备心跳时间毫秒级 UTC
* 用于在线监控存活判定
*/
private Long lastHeartbeat;
}

View File

@ -0,0 +1,49 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 安全状态DTO
* 符合universal_autonomous_vehicle_api规范 - safetyStatus字段组
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SafetyStatusDTO {
/**
* 防撞系统是否激活
*/
private Boolean collisionAvoidanceActive;
/**
* 紧急制动是否就绪
*/
private Boolean emergencyBrakingReady;
/**
* 路径规划状态
* 枚举值ACTIVE | INACTIVE | FAULT
*/
private String pathPlanningStatus;
/**
* 障碍检测状态
* 枚举值ACTIVE | INACTIVE | FAULT
*/
private String obstacleDetectionStatus;
/**
* MRM最小风险机动是否被触发
* 仅标识不含细节
*/
private Boolean minimumRiskManeuverTriggered;
}

View File

@ -0,0 +1,51 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 传感器状态DTO
* 符合universal_autonomous_vehicle_api规范 - sensorStatus字段组
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SensorStatusDTO {
/**
* GPS传感器状态
*/
private GpsStatusDTO gps;
/**
* GPS传感器状态内部类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class GpsStatusDTO {
/**
* GPS状态
* 枚举值ACTIVE | INACTIVE | FAULT
*/
private String status;
/**
* GPS精度
*/
private Double accuracy;
/**
* 最后更新时间毫秒级 UTC
*/
private Long lastUpdate;
}
}

View File

@ -0,0 +1,157 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 通用API响应格式
* 符合universal_autonomous_vehicle_api规范
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UniversalApiResponse<T> {
/**
* 响应状态码
* 200: 成功
* 400: 请求参数错误
* 401: 未授权
* 403: 禁止访问
* 404: 资源不存在
* 429: 请求过多
* 500: 服务器内部错误
* 503: 服务不可用
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 响应时间戳UTC毫秒
*/
private Long timestamp;
/**
* 响应数据
*/
private T data;
/**
* 错误详情可选仅在出错时包含
*/
private ErrorDetails error;
/**
* 错误详情内部类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class ErrorDetails {
/**
* 错误类型
*/
private String type;
/**
* 错误详情描述
*/
private String details;
/**
* 出错字段可选
*/
private String field;
}
/**
* 创建成功响应
*
* @param data 响应数据
* @param <T> 数据类型
* @return 成功响应对象
*/
public static <T> UniversalApiResponse<T> success(T data) {
return UniversalApiResponse.<T>builder()
.code(200)
.message("success")
.timestamp(System.currentTimeMillis())
.data(data)
.build();
}
/**
* 创建成功响应带自定义消息
*
* @param message 响应消息
* @param data 响应数据
* @param <T> 数据类型
* @return 成功响应对象
*/
public static <T> UniversalApiResponse<T> success(String message, T data) {
return UniversalApiResponse.<T>builder()
.code(200)
.message(message)
.timestamp(System.currentTimeMillis())
.data(data)
.build();
}
/**
* 创建错误响应
*
* @param code 错误码
* @param message 错误消息
* @param errorType 错误类型
* @param errorDetails 错误详情
* @param <T> 数据类型
* @return 错误响应对象
*/
public static <T> UniversalApiResponse<T> error(int code, String message, String errorType, String errorDetails) {
return UniversalApiResponse.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.error(ErrorDetails.builder()
.type(errorType)
.details(errorDetails)
.build())
.build();
}
/**
* 创建错误响应带字段信息
*
* @param code 错误码
* @param message 错误消息
* @param errorType 错误类型
* @param errorDetails 错误详情
* @param field 出错字段
* @param <T> 数据类型
* @return 错误响应对象
*/
public static <T> UniversalApiResponse<T> error(int code, String message, String errorType, String errorDetails, String field) {
return UniversalApiResponse.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.error(ErrorDetails.builder()
.type(errorType)
.details(errorDetails)
.field(field)
.build())
.build();
}
}

View File

@ -0,0 +1,71 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 通用车辆状态DTO
* 符合universal_autonomous_vehicle_api规范的完整状态数据结构
* 包含所有8个必需字段组
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UniversalVehicleStatusDTO {
/**
* 车辆信息字段组
* 包含vehicleId
*/
private VehicleInfoDTO vehicleInfo;
/**
* 运行状态字段组
* 包含powerStatus, systemHealth, operationalMode, emergencyStatus, lastHeartbeat
*/
private OperationalStatusDTO operationalStatus;
/**
* 控制状态字段组
* 包含controlMode, controlAuthority, remoteControlActive
*/
private ControlStatusDTO controlStatus;
/**
* 运动状态字段组
* 包含position (latitude, longitude), velocity (speed, direction)
*/
private MotionStatusDTO motionStatus;
/**
* 安全状态字段组
* 包含collisionAvoidanceActive, emergencyBrakingReady, pathPlanningStatus,
* obstacleDetectionStatus, minimumRiskManeuverTriggered
*/
private SafetyStatusDTO safetyStatus;
/**
* 传感器状态字段组
* 包含gps (status, accuracy, lastUpdate)
*/
private SensorStatusDTO sensorStatus;
/**
* 电池状态字段组
* 包含mainBattery (chargeLevel, voltage, current, temperature, chargingStatus)
*/
private BatteryStatusDTO batteryStatus;
/**
* 通信状态字段组
* 包含v2xStatus, cellularSignalStrength, wifiStatus, cloudConnectivity
*/
private CommunicationStatusDTO communicationStatus;
}

View File

@ -0,0 +1,27 @@
package com.qaup.collision.datacollector.model.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 车辆信息DTO
* 符合universal_autonomous_vehicle_api规范 - vehicleInfo字段组
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VehicleInfoDTO {
/**
* 车辆唯一标识
* 字母数字 3~20 示例AV-001
*/
private String vehicleId;
}

View File

@ -656,6 +656,120 @@ public class DataCollectorService {
}
}
/**
* 定时采集通用车辆状态数据 (符合universal_autonomous_vehicle_api规范)
*
* 数据来源无人车厂商数据源配置中的通用状态API端点 (/api/v1/vehicles/{vehicleId}/status)
* 说明采集符合universal_autonomous_vehicle_api规范的完整8字段组状态数据
* 重构说明
* - 通用车辆状态数据仅用于实时处理不存储到数据库
* - 数据采集后直接用于WebSocket推送和状态监控
* - 不进行数据持久化
*/
@Scheduled(fixedRateString = "${data.collector.interval}")
@Async // 异步执行
public void collectUniversalVehicleStatus() {
if (collectorDisabled) {
return;
}
try {
// 获取当前缓存中的所有无人车ID列表用于状态查询
List<String> activeUnmannedVehicleIds = activeMovingObjectsCache.values().stream()
.filter(obj -> obj.getObjectType() == MovingObjectType.UNMANNED_VEHICLE)
.map(MovingObject::getObjectId)
.collect(java.util.stream.Collectors.toList());
if (activeUnmannedVehicleIds.isEmpty()) {
log.debug("当前没有活跃的无人车,跳过通用状态数据采集");
return;
}
log.info("开始采集 {} 辆无人车的通用状态数据", activeUnmannedVehicleIds.size());
// 为每个活跃的无人车采集状态数据
for (String vehicleId : activeUnmannedVehicleIds) {
try {
// 调用DAO层获取通用车辆状态数据
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData =
dataCollectorDao.getUniversalVehicleStatus(vehicleId);
if (statusData != null) {
// 数据采集阶段只采集和缓存状态数据不进行任何计算
// 将状态数据存储到专门的缓存中供后续处理
cacheUniversalVehicleStatus(vehicleId, statusData);
log.debug("成功采集无人车通用状态数据: 车辆ID={}, 电池电量={}%, 运行模式={}, 控制模式={}",
vehicleId,
statusData.getBatteryStatus() != null && statusData.getBatteryStatus().getMainBattery() != null
? statusData.getBatteryStatus().getMainBattery().getChargeLevel() : "未知",
statusData.getOperationalStatus() != null ? statusData.getOperationalStatus().getOperationalMode() : "未知",
statusData.getControlStatus() != null ? statusData.getControlStatus().getControlMode() : "未知");
} else {
log.debug("无人车 {} 未返回通用状态数据", vehicleId);
}
} catch (Exception e) {
log.error("采集无人车通用状态数据异常: vehicleId={}", vehicleId, e);
}
}
log.info("通用车辆状态数据采集完成,处理车辆数量: {}", activeUnmannedVehicleIds.size());
} catch (Exception e) {
log.error("采集通用车辆状态数据异常", e);
}
}
/**
* 缓存通用车辆状态数据
* 将状态数据存储到专门的缓存中供DataProcessingService处理
*/
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)
.statusData(statusData)
.timestamp(System.currentTimeMillis())
.dataSource("COLLECTOR")
.build();
// 存储到缓存中这里简化为使用activeMovingObjectsCache的扩展实际可以使用Redis等
// 临时解决方案使用一个Map来存储状态数据
getUniversalStatusCache().put(cacheKey, cacheEntry);
log.debug("缓存通用车辆状态数据: vehicleId={}, cacheKey={}", vehicleId, cacheKey);
}
// 用于存储通用车辆状态数据的缓存
private final Map<String, UniversalVehicleStatusCacheEntry> universalStatusCache = new ConcurrentHashMap<>();
/**
* 获取通用状态缓存的引用
*/
public Map<String, UniversalVehicleStatusCacheEntry> getUniversalStatusCache() {
return universalStatusCache;
}
/**
* 通用车辆状态缓存条目
*/
@lombok.Data
@lombok.Builder
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
public static class UniversalVehicleStatusCacheEntry {
private String vehicleId;
private com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData;
private Long timestamp;
private String dataSource;
}
// 移除周期性处理方法 - 现在由DataProcessingService处理
/**

View File

@ -10,6 +10,7 @@ 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;
import com.qaup.collision.websocket.message.MessageTypeConstants;
import com.qaup.collision.websocket.message.GeofenceAlertPayload;
@ -19,6 +20,7 @@ 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;
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
@ -27,15 +29,18 @@ import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
/**
* WebSocket统一消息广播器
* 监听所有WebSocket事件使用原生WebSocket统一推送到控制台前端
*/
@Slf4j
@Component
public class WebSocketMessageBroadcaster {
@ -65,6 +70,38 @@ public class WebSocketMessageBroadcaster {
broadcastMessageInternal(message);
}
/**
* 处理车辆状态更新事件
* 符合universal_autonomous_vehicle_api规范
*/
@EventListener
@Async
public void handleVehicleStatusUpdate(VehicleStatusUpdateEvent event) {
try {
if (event.getPayload() == null) {
log.warn("车辆状态更新事件负载为空,跳过处理");
return;
}
VehicleStatusUpdatePayload payload = event.getPayload();
UniversalMessage<VehicleStatusUpdatePayload> message = UniversalMessage.<VehicleStatusUpdatePayload>builder()
.type(MessageTypeConstants.VEHICLE_STATUS_UPDATE)
.payload(payload)
.timestamp(event.getTimestamp())
.messageId(generateMessageId())
.build();
broadcastMessage(message);
log.debug("发送车辆状态更新WebSocket消息: vehicleId={}, 消息ID={}",
payload.getVehicleId(), message.getMessageId());
} catch (Exception e) {
log.error("处理车辆状态更新事件异常", e);
}
}
/**
* 处理车辆指令事件

View File

@ -0,0 +1,49 @@
package com.qaup.collision.websocket.event;
import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload;
import lombok.Getter;
import java.util.UUID;
/**
* 车辆状态更新WebSocket事件
* 符合universal_autonomous_vehicle_api规范
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Getter
public class VehicleStatusUpdateEvent implements WebSocketEvent {
private final String eventId;
private final long timestamp;
private final String eventType;
private final VehicleStatusUpdatePayload payload;
/**
* 构造车辆状态更新事件
*
* @param source 事件源对象
* @param payload 车辆状态更新载荷
*/
public VehicleStatusUpdateEvent(Object source, VehicleStatusUpdatePayload payload) {
this.eventId = UUID.randomUUID().toString();
this.timestamp = System.currentTimeMillis();
this.eventType = "vehicle_status_update";
this.payload = payload;
}
/**
* 构造带时间戳的车辆状态更新事件
*
* @param source 事件源对象
* @param payload 车辆状态更新载荷
* @param timestamp 事件时间戳
*/
public VehicleStatusUpdateEvent(Object source, VehicleStatusUpdatePayload payload, long timestamp) {
this.eventId = UUID.randomUUID().toString();
this.timestamp = timestamp;
this.eventType = "vehicle_status_update";
this.payload = payload;
}
}

View File

@ -72,7 +72,13 @@ public final class MessageTypeConstants {
* 电子围栏告警消息
*/
public static final String GEOFENCE_ALERT = "geofence_alert";
/**
* 车辆状态更新消息
* 符合universal_autonomous_vehicle_api规范
*/
public static final String VEHICLE_STATUS_UPDATE = "vehicle_status_update";
// 私有构造函数防止实例化
private MessageTypeConstants() {
throw new UnsupportedOperationException("常量类不允许实例化");

View File

@ -0,0 +1,50 @@
package com.qaup.collision.websocket.message;
import com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
/**
* 车辆状态更新WebSocket消息载荷
* 符合universal_autonomous_vehicle_api规范
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VehicleStatusUpdatePayload {
/**
* 车辆唯一标识
*/
private String vehicleId;
/**
* 完整的车辆状态数据
* 符合universal_autonomous_vehicle_api规范的8字段组格式
*/
private UniversalVehicleStatusDTO statusData;
/**
* 状态更新时间戳毫秒级 UTC
*/
private Long timestamp;
/**
* 数据源标识
* 标识数据来源COLLECTOR采集器CACHE缓存DATABASE数据库
*/
private String dataSource;
/**
* 更新类型
* 标识更新类型FULL完整更新PARTIAL部分更新HEARTBEAT心跳更新
*/
private String updateType;
}

View File

@ -0,0 +1,352 @@
package com.qaup.collision.datacollector.service;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.datacollector.dao.DataCollectorDao;
import com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO;
import com.qaup.collision.dataprocessing.service.DataProcessingService;
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.PrecisionModel;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* 车辆状态更新完整集成测试
* 测试从数据采集数据处理WebSocket事件发布的完整流程
* 如果任何环节有问题测试必须失败
*/
@ExtendWith(MockitoExtension.class)
class VehicleStatusUpdateIntegrationTest {
private DataCollectorService dataCollectorService;
private DataProcessingService dataProcessingService;
private DataCollectorDao dataCollectorDao;
private AuthService authService;
private GeometryFactory geometryFactory;
@Mock
private ApplicationEventPublisher eventPublisher;
@Mock
private ApplicationContext applicationContext;
@Captor
private ArgumentCaptor<VehicleStatusUpdateEvent> eventCaptor;
// 模拟的活跃移动对象缓存
private ConcurrentHashMap<String, MovingObject> activeMovingObjectsCache;
@BeforeEach
void setUp() {
// 创建几何工厂
geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
// 创建真实的服务实例连接真实API
RestTemplate restTemplate = new RestTemplate();
authService = new AuthService(restTemplate);
// 设置认证服务配置
ReflectionTestUtils.setField(authService, "username", "dianxin");
ReflectionTestUtils.setField(authService, "password", "dianxin@123");
ReflectionTestUtils.setField(authService, "baseUrl", "http://localhost:8090");
ReflectionTestUtils.setField(authService, "loginEndpoint", "/login");
ReflectionTestUtils.setField(authService, "refreshEndpoint", "/userInfoController/refreshToken");
// 创建DAO使用真实的AuthService
dataCollectorDao = new DataCollectorDao(restTemplate, authService);
// 设置DAO的车辆API配置
ReflectionTestUtils.setField(dataCollectorDao, "vehicleBaseUrl", "http://localhost:8090");
// 创建活跃移动对象缓存
activeMovingObjectsCache = new ConcurrentHashMap<>();
// 创建DataCollectorService实例
dataCollectorService = new DataCollectorService();
// 注入DataCollectorService依赖
ReflectionTestUtils.setField(dataCollectorService, "dataCollectorDao", dataCollectorDao);
ReflectionTestUtils.setField(dataCollectorService, "eventPublisher", eventPublisher);
ReflectionTestUtils.setField(dataCollectorService, "collectorDisabled", false);
ReflectionTestUtils.setField(dataCollectorService, "activeMovingObjectsCache", activeMovingObjectsCache);
// 创建DataProcessingService实例
dataProcessingService = new DataProcessingService();
// 设置ApplicationContext模拟让DataProcessingService可以获取DataCollectorService
when(applicationContext.getBean(DataCollectorService.class)).thenReturn(dataCollectorService);
// 注入DataProcessingService依赖
ReflectionTestUtils.setField(dataProcessingService, "eventPublisher", eventPublisher);
ReflectionTestUtils.setField(dataProcessingService, "applicationContext", applicationContext);
ReflectionTestUtils.setField(dataProcessingService, "collectorDisabled", false);
}
/**
* 测试完整的车辆状态更新处理流程
* 1. 模拟活跃无人车缓存数据
* 2. 执行状态数据采集
* 3. 执行状态数据处理
* 4. 验证WebSocket事件发布
* 5. 验证事件内容
*/
@Test
void testCompleteVehicleStatusUpdateProcessing() {
System.out.println("=== 车辆状态更新完整流程集成测试 ===");
System.out.println("1. 设置测试数据:添加活跃无人车到缓存...");
// 添加模拟的无人车到活跃对象缓存
String vehicleId1 = "鲁B567";
String vehicleId2 = "鲁B579";
Point position1 = geometryFactory.createPoint(new Coordinate(120.0834104, 36.35406879));
Point position2 = geometryFactory.createPoint(new Coordinate(120.0844104, 36.35506879));
MovingObject vehicle1 = MovingObject.builder()
.objectId(vehicleId1)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName(vehicleId1)
.currentPosition(position1)
.currentSpeed(null)
.currentHeading(null)
.altitude(0.0)
.build();
MovingObject vehicle2 = MovingObject.builder()
.objectId(vehicleId2)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName(vehicleId2)
.currentPosition(position2)
.currentSpeed(null)
.currentHeading(null)
.altitude(0.0)
.build();
activeMovingObjectsCache.put(vehicleId1, vehicle1);
activeMovingObjectsCache.put(vehicleId2, vehicle2);
System.out.println("✓ 添加了 " + activeMovingObjectsCache.size() + " 辆无人车到缓存");
System.out.println("2. 执行车辆状态数据采集...");
// 执行实际的数据采集方法这会调用真实API
dataCollectorService.collectUniversalVehicleStatus();
System.out.println("3. 检查状态缓存...");
// 验证状态数据是否被缓存
var statusCache = dataCollectorService.getUniversalStatusCache();
System.out.println("✓ 状态缓存中有 " + statusCache.size() + " 个条目");
System.out.println("4. 执行状态数据处理...");
// 打印状态缓存的内容
for (Map.Entry<String, ?> entry : statusCache.entrySet()) {
System.out.println(" 缓存条目: " + entry.getKey() + " -> " + entry.getValue().getClass().getSimpleName());
}
// 执行数据处理这会发布WebSocket事件
dataProcessingService.performPeriodicDataProcessing();
System.out.println("5. 验证事件发布...");
try {
// 验证事件发布
verify(eventPublisher, atLeastOnce()).publishEvent(eventCaptor.capture());
List<VehicleStatusUpdateEvent> capturedEvents = eventCaptor.getAllValues();
assertFalse(capturedEvents.isEmpty(), "如果有活跃车辆和状态数据,必须发布事件");
System.out.println("✓ 检测到事件发布,共发布了 " + capturedEvents.size() + " 个事件");
System.out.println("6. 验证事件内容...");
// 验证每个事件的完整性
for (int i = 0; i < capturedEvents.size(); i++) {
VehicleStatusUpdateEvent event = capturedEvents.get(i);
System.out.println(String.format(" 事件[%d]: 车辆ID=%s, 事件类型=%s, 时间戳=%d",
i+1, event.getPayload() != null ? event.getPayload().getVehicleId() : "未知",
event.getEventType(), event.getTimestamp()));
// 严格验证必要字段
assertNotNull(event.getEventId(), "事件ID不能为空");
assertNotNull(event.getEventType(), "事件类型不能为空");
assertEquals("vehicle_status_update", event.getEventType(), "事件类型必须是vehicle_status_update");
assertNotNull(event.getTimestamp(), "时间戳不能为空");
assertNotNull(event.getPayload(), "事件载荷不能为空");
// 验证载荷内容
var payload = event.getPayload();
assertNotNull(payload.getVehicleId(), "车辆ID不能为空");
assertFalse(payload.getVehicleId().trim().isEmpty(), "车辆ID不能为空字符串");
assertNotNull(payload.getUpdateType(), "更新类型不能为空");
assertNotNull(payload.getDataSource(), "数据源不能为空");
assertNotNull(payload.getStatusData(), "状态数据不能为空");
// 验证状态数据结构
var statusData = payload.getStatusData();
assertTrue(statusData.getVehicleInfo() != null ||
statusData.getOperationalStatus() != null ||
statusData.getControlStatus() != null ||
statusData.getMotionStatus() != null ||
statusData.getSafetyStatus() != null ||
statusData.getSensorStatus() != null ||
statusData.getBatteryStatus() != null ||
statusData.getCommunicationStatus() != null,
"状态数据必须包含至少一个字段组");
// 验证时间戳合理性
long now = System.currentTimeMillis();
long eventTime = event.getTimestamp();
assertTrue(Math.abs(now - eventTime) < 300000,
"事件时间戳应该在5分钟内实际差值: " + Math.abs(now - eventTime) + "ms");
}
System.out.println("✓ 事件内容验证通过");
System.out.println("🎉 车辆状态更新完整流程集成测试成功!");
} catch (Exception e) {
System.err.println("❌ 检测到问题:" + e.getMessage());
// 检查是否没有事件发布
try {
verify(eventPublisher, never()).publishEvent(any());
System.err.println(" 没有事件被发布");
System.err.println(" 可能原因:");
System.err.println(" 1. Mock服务器未启动http://localhost:8090");
System.err.println(" 2. 认证失败");
System.err.println(" 3. 车辆状态API连接问题");
System.err.println(" 4. 状态数据转换失败");
System.err.println(" 5. 缓存操作失败");
fail("车辆状态更新流程失败,应该有事件发布但实际没有");
} catch (Exception verifyException) {
System.err.println(" 事件发布验证失败: " + e.getMessage());
fail("车辆状态更新处理流程失败: " + e.getMessage());
}
}
}
/**
* 测试无活跃车辆情况的处理
* 如果没有活跃车辆应该跳过处理而不是报错
*/
@Test
void testNoActiveVehiclesHandling() {
System.out.println("=== 测试无活跃车辆处理 ===");
// 确保缓存为空
activeMovingObjectsCache.clear();
// 执行数据采集应该跳过
dataCollectorService.collectUniversalVehicleStatus();
// 执行数据处理应该跳过
dataProcessingService.performPeriodicDataProcessing();
// 验证没有事件发布
verify(eventPublisher, never()).publishEvent(any());
System.out.println("✓ 无活跃车辆处理测试完成(正确跳过处理)");
}
/**
* 测试单独的数据采集功能
* 验证数据采集是否能正常工作
*/
@Test
void testDataCollectionOnly() {
System.out.println("=== 测试单独数据采集 ===");
// 添加一辆测试车辆
String vehicleId = "鲁B567";
Point position = geometryFactory.createPoint(new Coordinate(120.0834104, 36.35406879));
MovingObject vehicle = MovingObject.builder()
.objectId(vehicleId)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName(vehicleId)
.currentPosition(position)
.build();
activeMovingObjectsCache.put(vehicleId, vehicle);
// 执行数据采集
try {
dataCollectorService.collectUniversalVehicleStatus();
System.out.println("✓ 数据采集执行完成,未抛出异常");
// 检查状态缓存
var statusCache = dataCollectorService.getUniversalStatusCache();
System.out.println("✓ 状态缓存条目数: " + statusCache.size());
} catch (Exception e) {
System.err.println("❌ 数据采集失败: " + e.getMessage());
// 这不一定是失败可能是Mock服务器没启动
System.out.println("⚠ 数据采集异常但这可能是由于Mock服务器未启动");
}
}
/**
* 测试配置和依赖注入
* 确保所有必要的组件都正确注入
*/
@Test
void testDependencyInjection() {
System.out.println("=== 测试依赖注入 ===");
assertNotNull(dataCollectorService, "DataCollectorService应该被正确注入");
assertNotNull(dataProcessingService, "DataProcessingService应该被正确注入");
assertNotNull(eventPublisher, "ApplicationEventPublisher应该被正确注入");
assertNotNull(activeMovingObjectsCache, "ActiveMovingObjectsCache应该被正确注入");
System.out.println("✓ 依赖注入验证通过");
System.out.println("✓ DataCollectorService实例: " + dataCollectorService.getClass().getSimpleName());
System.out.println("✓ DataProcessingService实例: " + dataProcessingService.getClass().getSimpleName());
System.out.println("✓ EventPublisher实例: " + eventPublisher.getClass().getSimpleName());
}
/**
* 测试缓存操作
* 验证状态缓存的读写操作
*/
@Test
void testCacheOperations() {
System.out.println("=== 测试缓存操作 ===");
// 获取状态缓存
var statusCache = dataCollectorService.getUniversalStatusCache();
assertNotNull(statusCache, "状态缓存不能为空");
int initialSize = statusCache.size();
System.out.println("✓ 初始缓存大小: " + initialSize);
// 测试缓存清理如果有数据的话
statusCache.clear();
assertEquals(0, statusCache.size(), "缓存清理后应该为空");
System.out.println("✓ 缓存操作验证通过");
}
}

View File

@ -1340,6 +1340,7 @@ class VehicleState:
self.target_lat = None # 目标纬度
self.target_lon = None # 目标经度
self.is_traffic_light_stop = False # 是否因红灯停车
self.remoteControlActive = False # 远程控制是否激活
def can_be_overridden_by(self, command_type: str) -> bool:
"""判断当前指令是否可以被新指令覆盖"""
@ -1847,22 +1848,17 @@ last_light_switch_time = time.time()
def check_auth() -> bool:
auth_header = request.headers.get('Authorization')
if not auth_header:
print("认证失败: 缺少Authorization头")
return False
# 检查是否包含Bearer前缀
if not auth_header.startswith('Bearer '):
print("认证失败: 不是Bearer格式")
return False
# 提取token部分
token = auth_header[7:] # 去掉 "Bearer " 前缀
expected_token = AUTH_TOKEN[7:] # 去掉 "Bearer " 前缀
result = token == expected_token
#print(f"认证结果: {result}")
# 直接比较完整的Authorization头
result = auth_header == AUTH_TOKEN
if not result:
print(f"认证失败: token不匹配")
print(f"收到的token: {auth_header}")
print(f"期望的token: {AUTH_TOKEN}")
return result
def to_float_safe(x: object) -> float | None:
@ -2166,6 +2162,114 @@ def generate_unmanned_vehicle_location_data() -> list[dict[str, Any]]:
return unmanned_vehicles
# 生成符合API规范的完整车辆状态数据
def generate_comprehensive_vehicle_status(vehicle_id: str) -> dict[str, Any]:
"""生成符合universal_autonomous_vehicle_api规范的完整车辆状态数据"""
current_time = time.time()
timestamp_ms = int(current_time * 1000)
# 获取车辆基础数据
vehicle_data_item = None
for v in unmanned_vehicle_data:
if v["vehicleNo"] == vehicle_id:
vehicle_data_item = v
break
if not vehicle_data_item:
return None
# 获取车辆状态
vehicle_state = vehicle_states.get(vehicle_id)
# 基础位置和运动数据
lat_val = to_float_safe(vehicle_data_item.get("latitude"))
lon_val = to_float_safe(vehicle_data_item.get("longitude"))
speed_val = to_float_safe(vehicle_data_item.get("speed", 0))
direction_val = to_float_safe(vehicle_data_item.get("direction", 0))
# 生成完整状态数据
status_data = {
"vehicleInfo": {
"vehicleId": vehicle_id
},
"operationalStatus": {
"powerStatus": "ON",
"systemHealth": "HEALTHY" if vehicle_state and vehicle_state.is_running else "DEGRADED",
"operationalMode": "AUTONOMOUS",
"emergencyStatus": "CRITICAL" if vehicle_state and vehicle_state.current_command == "ALERT"
else "WARNING" if vehicle_state and vehicle_state.current_command == "WARNING"
else "NORMAL",
"lastHeartbeat": timestamp_ms
},
"controlStatus": {
"controlMode": "AUTONOMOUS",
"controlAuthority": "SYSTEM",
"remoteControlActive": vehicle_state.remoteControlActive if vehicle_state else False
},
"motionStatus": {
"position": {
"latitude": round(lat_val, 6) if lat_val is not None else 0.0,
"longitude": round(lon_val, 6) if lon_val is not None else 0.0
},
"velocity": {
"speed": round(speed_val * 1000 / 3600, 2) if speed_val is not None else 0.0, # 转换为 m/s
"direction": round(math.radians(direction_val), 4) if direction_val is not None else 0.0 # 转换为弧度
}
},
"safetyStatus": {
"collisionAvoidanceActive": vehicle_state.is_running if vehicle_state else True,
"emergencyBrakingReady": True,
"pathPlanningStatus": "ACTIVE" if vehicle_state and vehicle_state.is_running else "INACTIVE",
"obstacleDetectionStatus": "ACTIVE" if vehicle_state and vehicle_state.is_running else "INACTIVE",
"minimumRiskManeuverTriggered": vehicle_state.current_command == "ALERT" if vehicle_state else False
},
"sensorStatus": {
"gps": {
"status": "ACTIVE",
"accuracy": 0.5, # 0.5米精度
"lastUpdate": timestamp_ms
}
},
"batteryStatus": {
"mainBattery": {
"chargeLevel": 85.5, # 85.5%
"voltage": 48.2, # 48.2V
"current": -15.3, # -15.3A (放电)
"temperature": 35.2, # 35.2°C
"chargingStatus": "DISCHARGING"
}
},
"communicationStatus": {
"v2xStatus": "CONNECTED",
"cellularSignalStrength": -65, # -65dBm
"wifiStatus": "CONNECTED",
"cloudConnectivity": "ONLINE"
}
}
return status_data
def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None) -> dict[str, Any]:
"""根据fields参数过滤车辆状态字段"""
if not fields:
return status_data
# 解析fields参数
requested_fields = [f.strip() for f in fields.split(',')]
filtered_data = {}
# 可用的字段组
available_fields = {
"vehicleInfo", "operationalStatus", "controlStatus", "motionStatus",
"safetyStatus", "sensorStatus", "batteryStatus", "communicationStatus"
}
for field in requested_fields:
if field in available_fields and field in status_data:
filtered_data[field] = status_data[field]
return filtered_data
# 无人车状态数据生成函数
def generate_unmanned_vehicle_state_data(vehicle_id: str | None = None, is_single: bool = True) -> list[dict[str, Any]]:
"""生成无人车状态数据符合官方API格式"""
@ -2512,5 +2616,107 @@ def get_aircraft_status():
}
})
@app.route('/api/v1/vehicles/<vehicle_id>/status', methods=['GET', 'OPTIONS'])
def get_vehicle_status_universal(vehicle_id):
"""通用无人车状态上报接口 - 符合universal_autonomous_vehicle_api规范"""
if request.method == 'OPTIONS':
return '', 204
logging.info(f"🔍 收到车辆状态查询请求: vehicleId={vehicle_id}")
# JWT认证检查
if not check_auth():
logging.warning(f"❌ 车辆状态查询认证失败: vehicleId={vehicle_id}")
return jsonify({
"code": 401,
"message": "认证失败",
"timestamp": int(time.time() * 1000),
"data": None
}), 401
# 检查vehicleId格式字母数字 3~20 位)
if not vehicle_id or len(vehicle_id) < 3 or len(vehicle_id) > 20:
return jsonify({
"code": 400,
"message": "Bad Request",
"timestamp": int(time.time() * 1000),
"error": {
"type": "VALIDATION_ERROR",
"details": "vehicleId must be 3-20 alphanumeric characters",
"field": "vehicleId"
}
}), 400
# 检查车辆是否存在
vehicle_exists = False
for v in unmanned_vehicle_data:
if v["vehicleNo"] == vehicle_id:
vehicle_exists = True
break
if not vehicle_exists:
return jsonify({
"code": 404,
"message": "Not Found",
"timestamp": int(time.time() * 1000),
"error": {
"type": "RESOURCE_NOT_FOUND",
"details": f"Vehicle {vehicle_id} not found",
"field": "vehicleId"
}
}), 404
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
# 生成完整的车辆状态数据
status_data = generate_comprehensive_vehicle_status(vehicle_id)
if not status_data:
return jsonify({
"code": 500,
"message": "Internal Server Error",
"timestamp": int(time.time() * 1000),
"error": {
"type": "DATA_ERROR",
"details": "Failed to generate vehicle status data",
"field": None
}
}), 500
# 处理fields查询参数
fields = request.args.get('fields')
if fields:
status_data = filter_vehicle_status_fields(status_data, fields)
# 返回成功响应
logging.info(f"🚗 返回车辆状态数据: vehicleId={vehicle_id}, 电量={status_data.get('batteryStatus', {}).get('mainBattery', {}).get('chargeLevel', '未知')}%")
return jsonify({
"code": 200,
"message": "success",
"timestamp": int(time.time() * 1000),
"data": status_data
})
except Exception as e:
logging.error(f"Error in get_vehicle_status_universal: {str(e)}")
return jsonify({
"code": 500,
"message": "Internal Server Error",
"timestamp": int(time.time() * 1000),
"error": {
"type": "SERVER_ERROR",
"details": str(e),
"field": None
}
}), 500
if __name__ == '__main__':
app.run(host='localhost', port=8090, debug=True)

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8">
<title>冲突检测WebSocket测试工具</title>
<title>冲突检测与车辆状态WebSocket测试工具</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
@ -125,6 +125,7 @@
<div><strong>车辆指令:</strong> <span id="vehicleCommandCount">0</span></div>
<div><strong>规则执行状态:</strong> <span id="ruleExecutionCount">0</span></div>
<div><strong>规则状态变更:</strong> <span id="ruleStateChangeCount">0</span></div>
<div><strong>车辆状态更新:</strong> <span id="vehicleStatusUpdateCount">0</span></div>
<div><strong>心跳响应:</strong> <span id="pongCount">0</span></div>
<div><strong>连接确认:</strong> <span id="connectionCount">0</span></div>
<div><strong>未知消息:</strong> <span id="unknownCount">0</span></div>
@ -134,7 +135,7 @@
<!-- 冲突检测WebSocket测试 -->
<div class="test-section">
<h3>🚗 冲突检测WebSocket</h3>
<h3>🚗 冲突检测与车辆状态WebSocket</h3>
<div class="control-group">
<label>服务器地址:</label>
@ -190,11 +191,15 @@
vehicle_command: 0,
rule_execution_status: 0,
rule_state_change: 0,
vehicle_status_update: 0,
pong: 0,
connection: 0,
unknown: 0
};
// 记录未知消息类型以便调试
let unknownMessageTypes = new Set();
// 通用日志函数
function log(containerId, message, type = 'info') {
const container = document.getElementById(containerId);
@ -216,10 +221,27 @@
// 更新消息统计
function updateMessageStats(messageType) {
messageStats.total++;
if (messageStats.hasOwnProperty(messageType)) {
messageStats[messageType]++;
// 统一转换为小写以匹配统计键
const normalizedType = messageType?.toLowerCase();
// 处理特殊映射
let statsKey = normalizedType;
if (normalizedType === 'vehicle_status_update' || normalizedType === 'VEHICLE_STATUS_UPDATE') {
statsKey = 'vehicle_status_update';
}
// 处理aircraftRouteUpdate的映射
if (normalizedType === 'aircraftrouteupdate') {
statsKey = 'aircraftRouteUpdate';
}
if (messageStats.hasOwnProperty(statsKey)) {
messageStats[statsKey]++;
} else {
messageStats.unknown++;
// 调试:记录未知的消息类型
unknownMessageTypes.add(messageType);
console.log('Unknown message type:', messageType, 'normalized:', normalizedType);
}
// 更新页面显示
@ -235,6 +257,7 @@
document.getElementById('vehicleCommandCount').textContent = messageStats.vehicle_command;
document.getElementById('ruleExecutionCount').textContent = messageStats.rule_execution_status;
document.getElementById('ruleStateChangeCount').textContent = messageStats.rule_state_change;
document.getElementById('vehicleStatusUpdateCount').textContent = messageStats.vehicle_status_update;
document.getElementById('pongCount').textContent = messageStats.pong;
document.getElementById('connectionCount').textContent = messageStats.connection;
document.getElementById('unknownCount').textContent = messageStats.unknown;
@ -318,11 +341,16 @@
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
const message = JSON.stringify({
type: 'subscribe',
topics: ['position_update', 'collision_warning', 'rule_violation'],
topics: [
'position_update',
'collision_warning',
'rule_violation',
'vehicle_status_update'
],
timestamp: Date.now()
});
collisionWebSocket.send(message);
log('collisionLog', '发送订阅请求', 'info');
log('collisionLog', '发送订阅请求(包含车辆状态更新)', 'info');
} else {
log('collisionLog', 'WebSocket未连接', 'error');
}
@ -403,11 +431,105 @@
const conflictLevel = message.payload?.alertLevel || '未知级别';
log('collisionLog', `路径冲突告警: ${object1} vs ${object2} (${conflictType}/${conflictLevel})`, 'error');
break;
case 'vehicle_status_update':
case 'VEHICLE_STATUS_UPDATE': // 可能是大写
handleVehicleStatusUpdate(message);
break;
default:
log('collisionLog', `未知消息类型: ${message.type}`, 'info');
// 打印完整消息内容以便调试
log('collisionLog', `未知消息类型: ${message.type} | 完整消息: ${JSON.stringify(message)}`, 'info');
}
}
// 处理车辆状态更新消息
function handleVehicleStatusUpdate(message) {
const payload = message.payload;
if (!payload) {
log('collisionLog', '🚗 车辆状态更新: 无载荷数据', 'warning');
return;
}
const vehicleId = payload.vehicleId || '未知车辆';
const timestamp = payload.timestamp || Date.now();
const updateType = payload.updateType || '未知';
const dataSource = payload.dataSource || '未知';
// 解析状态数据
const statusData = payload.statusData;
let statusInfo = [];
if (statusData) {
// 车辆信息
if (statusData.vehicleInfo) {
statusInfo.push(`ID:${statusData.vehicleInfo.vehicleId || '未知'}`);
}
// 运行状态
if (statusData.operationalStatus) {
const ops = statusData.operationalStatus;
statusInfo.push(`电源:${ops.powerStatus || '未知'}`);
statusInfo.push(`模式:${ops.operationalMode || '未知'}`);
statusInfo.push(`健康:${ops.systemHealth || '未知'}`);
}
// 控制状态
if (statusData.controlStatus) {
const ctrl = statusData.controlStatus;
statusInfo.push(`控制:${ctrl.controlMode || '未知'}`);
statusInfo.push(`权限:${ctrl.controlAuthority || '未知'}`);
if (ctrl.remoteControlActive !== undefined) {
statusInfo.push(`远程:${ctrl.remoteControlActive ? '是' : '否'}`);
}
}
// 运动状态
if (statusData.motionStatus) {
const motion = statusData.motionStatus;
if (motion.position) {
statusInfo.push(`位置:(${motion.position.latitude?.toFixed(6)},${motion.position.longitude?.toFixed(6)})`);
}
if (motion.velocity) {
statusInfo.push(`速度:${motion.velocity.speed?.toFixed(2)}km/h`);
statusInfo.push(`方向:${motion.velocity.direction?.toFixed(1)}°`);
}
}
// 安全状态
if (statusData.safetyStatus) {
const safety = statusData.safetyStatus;
statusInfo.push(`避障:${safety.collisionAvoidanceActive ? '启用' : '禁用'}`);
statusInfo.push(`急停:${safety.emergencyBrakingReady ? '就绪' : '未就绪'}`);
}
// 电池状态
if (statusData.batteryStatus?.mainBattery) {
const battery = statusData.batteryStatus.mainBattery;
statusInfo.push(`电量:${battery.chargeLevel}%`);
statusInfo.push(`电压:${battery.voltage}V`);
statusInfo.push(`充电:${battery.chargingStatus || '未知'}`);
}
// 传感器状态
if (statusData.sensorStatus?.gps) {
const gps = statusData.sensorStatus.gps;
statusInfo.push(`GPS:${gps.status}(精度:${gps.accuracy}m)`);
}
// 通信状态
if (statusData.communicationStatus) {
const comm = statusData.communicationStatus;
statusInfo.push(`V2X:${comm.v2xStatus || '未知'}`);
statusInfo.push(`信号:${comm.cellularSignalStrength || '未知'}`);
}
}
// 格式化日志消息
const statusStr = statusInfo.length > 0 ? statusInfo.join(' | ') : '无状态数据';
const timeStr = new Date(timestamp).toLocaleTimeString();
log('collisionLog', `🚗 车辆状态更新: ${vehicleId} [${updateType}/${dataSource}] @ ${timeStr} - ${statusStr}`, 'success');
}
// =================== 控制面板 ===================
function clearLogs() {
document.getElementById('collisionLog').innerHTML = '';
@ -452,6 +574,7 @@
document.getElementById('vehicleCommandCount').textContent = '0';
document.getElementById('ruleExecutionCount').textContent = '0';
document.getElementById('ruleStateChangeCount').textContent = '0';
document.getElementById('vehicleStatusUpdateCount').textContent = '0';
document.getElementById('pongCount').textContent = '0';
document.getElementById('connectionCount').textContent = '0';
document.getElementById('unknownCount').textContent = '0';