Align collision flow with platform vehicle registration
This commit is contained in:
parent
190d62cd3f
commit
fb649516ee
11
.codex/environments/environment.toml
Normal file
11
.codex/environments/environment.toml
Normal file
@ -0,0 +1,11 @@
|
||||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "QAUP_Management"
|
||||
|
||||
[setup]
|
||||
script = ""
|
||||
|
||||
[[actions]]
|
||||
name = "运行"
|
||||
icon = "run"
|
||||
command = "mvn -pl qaup-admin -am clean package -DskipTests"
|
||||
536
doc/frontend_platform_http_api_integration.md
Normal file
536
doc/frontend_platform_http_api_integration.md
Normal file
@ -0,0 +1,536 @@
|
||||
# 前端接入文档:新增平台 HTTP 接口
|
||||
|
||||
## 1. 背景
|
||||
|
||||
本次后端新增了一组 HTTP 接口,供前端在现有 WebSocket 交互之外,补充以下能力:
|
||||
|
||||
- 同步“当前关心的对象及其类型”
|
||||
- 运行时修改部分碰撞检测相关参数
|
||||
|
||||
## 2. 通用约定
|
||||
|
||||
- 所有接口均为 `POST`
|
||||
- 所有请求和响应均为 `application/json`
|
||||
- 当前接口无鉴权、无 token、无签名校验
|
||||
- 当前后端未补 CORS
|
||||
- 如果前端是浏览器并且跨域访问,后续可能还需要后端补 CORS
|
||||
- 配置修改仅在内存中生效
|
||||
- 服务重启后恢复默认配置
|
||||
- `VehicleRegistry` 是“增量更新”
|
||||
- 本次未传入的历史对象不会被清空
|
||||
|
||||
建议前端统一配置接口基础地址,例如:
|
||||
|
||||
```ts
|
||||
const API_BASE = 'http://<后端IP>:8080';
|
||||
```
|
||||
|
||||
## 3. 接口一:对象注册同步
|
||||
|
||||
### 3.1 接口地址
|
||||
|
||||
```http
|
||||
POST /api/VehicleRegistry
|
||||
```
|
||||
|
||||
### 3.2 用途
|
||||
|
||||
前端将“当前关心的对象及其类型”增量同步给后端。
|
||||
|
||||
该接口会直接影响:
|
||||
|
||||
- 哪些对象参与碰撞检测
|
||||
- 哪些车辆可能被系统下发控制命令
|
||||
|
||||
### 3.3 请求头
|
||||
|
||||
```http
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 3.4 请求体
|
||||
|
||||
```json
|
||||
[
|
||||
{ "vehicleID": "QN001", "vehicleType": "WUREN" },
|
||||
{ "vehicleID": "TQ001", "vehicleType": "TEQIN" },
|
||||
{ "vehicleID": "AC001", "vehicleType": "HANGKONG" }
|
||||
]
|
||||
```
|
||||
|
||||
### 3.5 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `vehicleID` | `string` | 是 | 对象唯一标识,不能为空 |
|
||||
| `vehicleType` | `string` | 是 | 对象类型,只允许固定枚举值 |
|
||||
|
||||
### 3.6 `vehicleType` 可选值
|
||||
|
||||
- `WUREN`
|
||||
- `TEQIN`
|
||||
- `HANGKONG`
|
||||
- `PUTONG`
|
||||
- `JIUYUAN`
|
||||
|
||||
### 3.7 业务语义
|
||||
|
||||
#### `WUREN`
|
||||
|
||||
- 加入“可控车辆集合”
|
||||
- 加入“受管车辆集合”
|
||||
|
||||
#### `TEQIN`
|
||||
|
||||
- 加入“受管车辆集合”
|
||||
- 不属于“可控车辆集合”
|
||||
|
||||
#### `HANGKONG`
|
||||
|
||||
- 加入“已选中航空器集合”
|
||||
- 用于参与航空器相关碰撞/安全区计算
|
||||
|
||||
#### `PUTONG`
|
||||
|
||||
- 仅记录类型
|
||||
- 不进入可控/受管/已选航空器集合
|
||||
|
||||
#### `JIUYUAN`
|
||||
|
||||
- 仅记录类型
|
||||
- 不进入可控/受管/已选航空器集合
|
||||
|
||||
### 3.8 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"updatedAt": 1742280000000,
|
||||
"updated": 3,
|
||||
"controllableCount": 1,
|
||||
"typesCount": {
|
||||
"WUREN": 1,
|
||||
"TEQIN": 1,
|
||||
"HANGKONG": 1
|
||||
},
|
||||
"controllableVehicleIDs": ["QN001"]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.9 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `status` | `string` | 固定为 `success` |
|
||||
| `updatedAt` | `number` | 毫秒时间戳 |
|
||||
| `updated` | `number` | 本次成功处理的条目数 |
|
||||
| `controllableCount` | `number` | 当前系统内全部 `WUREN` 总数 |
|
||||
| `typesCount` | `object` | 本次请求内各类型数量统计 |
|
||||
| `controllableVehicleIDs` | `string[]` | 当前系统内全部可控车辆 ID 列表,即所有 `WUREN` |
|
||||
|
||||
### 3.10 错误响应示例
|
||||
|
||||
#### 请求体不是数组
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Body must be an array"
|
||||
}
|
||||
```
|
||||
|
||||
#### 参数非法
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid request",
|
||||
"errors": [
|
||||
"item must be an object",
|
||||
"missing vehicleID",
|
||||
"missing vehicleType",
|
||||
"vehicleID must be string",
|
||||
"vehicleType must be string",
|
||||
"vehicleID must be non-empty",
|
||||
"invalid vehicleType=xxx"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 非法 JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid JSON",
|
||||
"detail": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 3.11 前端调用示例
|
||||
|
||||
```ts
|
||||
await fetch(`${API_BASE}/api/VehicleRegistry`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify([
|
||||
{ vehicleID: 'QN001', vehicleType: 'WUREN' },
|
||||
{ vehicleID: 'TQ001', vehicleType: 'TEQIN' },
|
||||
{ vehicleID: 'AC001', vehicleType: 'HANGKONG' }
|
||||
])
|
||||
});
|
||||
```
|
||||
|
||||
### 3.12 接入建议
|
||||
|
||||
- 页面首次拿到关注对象列表后调用一次
|
||||
- 当前关注对象发生变化时再次调用
|
||||
- 请求体必须是数组,即使只有一条数据也必须传数组
|
||||
- 后端是“增量更新”,不要假设未传对象会被清空
|
||||
|
||||
## 4. 接口二:跑道区域航空器预警区半径
|
||||
|
||||
### 4.1 接口地址
|
||||
|
||||
```http
|
||||
POST /config/runway/warning_zone_radius/aircraft
|
||||
```
|
||||
|
||||
### 4.2 用途
|
||||
|
||||
前端运行时修改跑道区域的“航空器预警区半径”。
|
||||
|
||||
### 4.3 请求头
|
||||
|
||||
```http
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 4.4 请求体
|
||||
|
||||
```json
|
||||
{ "value": 300 }
|
||||
```
|
||||
|
||||
### 4.5 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `value` | `number` | 是 | 必须是大于 0 的有限数值 |
|
||||
|
||||
### 4.6 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"area": "runway",
|
||||
"field": "warning_zone_radius.aircraft",
|
||||
"old": 200.0,
|
||||
"new": 300.0
|
||||
}
|
||||
```
|
||||
|
||||
### 4.7 错误响应示例
|
||||
|
||||
#### 缺少字段
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Missing field: value"
|
||||
}
|
||||
```
|
||||
|
||||
#### 值非法
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid value: must be a finite number greater than 0"
|
||||
}
|
||||
```
|
||||
|
||||
#### 非法 JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid JSON",
|
||||
"detail": "..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段类型错误
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid field type",
|
||||
"detail": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 4.8 前端调用示例
|
||||
|
||||
```ts
|
||||
await fetch(`${API_BASE}/config/runway/warning_zone_radius/aircraft`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ value: 300 })
|
||||
});
|
||||
```
|
||||
|
||||
## 5. 接口三:跑道区域航空器告警区半径
|
||||
|
||||
### 5.1 接口地址
|
||||
|
||||
```http
|
||||
POST /config/runway/alert_zone_radius/aircraft
|
||||
```
|
||||
|
||||
### 5.2 用途
|
||||
|
||||
前端运行时修改跑道区域的“航空器告警区半径”。
|
||||
|
||||
### 5.3 请求体
|
||||
|
||||
```json
|
||||
{ "value": 200 }
|
||||
```
|
||||
|
||||
### 5.4 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"area": "runway",
|
||||
"field": "alert_zone_radius.aircraft",
|
||||
"old": 150.0,
|
||||
"new": 200.0
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 前端调用示例
|
||||
|
||||
```ts
|
||||
await fetch(`${API_BASE}/config/runway/alert_zone_radius/aircraft`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ value: 200 })
|
||||
});
|
||||
```
|
||||
|
||||
### 5.6 说明
|
||||
|
||||
该接口的请求规则、错误规则与“预警区半径”接口一致,唯一差异是:
|
||||
|
||||
- 路径不同
|
||||
- 成功响应中的 `field` 固定为 `alert_zone_radius.aircraft`
|
||||
|
||||
## 6. 接口四:冲突解除距离阈值
|
||||
|
||||
### 6.1 接口地址
|
||||
|
||||
```http
|
||||
POST /config/collision/diverging_release_distance
|
||||
```
|
||||
|
||||
### 6.2 用途
|
||||
|
||||
前端运行时修改“冲突解除距离阈值”。
|
||||
|
||||
### 6.3 请求体
|
||||
|
||||
```json
|
||||
{ "value": 50 }
|
||||
```
|
||||
|
||||
### 6.4 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"area": "collision",
|
||||
"field": "collision.diverging_release_distance",
|
||||
"old": 40.0,
|
||||
"new": 50.0
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 前端调用示例
|
||||
|
||||
```ts
|
||||
await fetch(`${API_BASE}/config/collision/diverging_release_distance`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ value: 50 })
|
||||
});
|
||||
```
|
||||
|
||||
### 6.6 说明
|
||||
|
||||
该接口的请求规则、错误规则与前两个配置接口一致,唯一差异是:
|
||||
|
||||
- 路径不同
|
||||
- 成功响应中的:
|
||||
- `area` 固定为 `collision`
|
||||
- `field` 固定为 `collision.diverging_release_distance`
|
||||
|
||||
## 7. 推荐前端封装
|
||||
|
||||
### 7.1 通用 POST JSON 请求
|
||||
|
||||
```ts
|
||||
async function postJson<T>(url: string, body: unknown): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw data;
|
||||
}
|
||||
|
||||
return data as T;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 类型定义
|
||||
|
||||
```ts
|
||||
type VehicleType = 'WUREN' | 'TEQIN' | 'HANGKONG' | 'PUTONG' | 'JIUYUAN';
|
||||
|
||||
interface VehicleRegistryItem {
|
||||
vehicleID: string;
|
||||
vehicleType: VehicleType;
|
||||
}
|
||||
|
||||
interface VehicleRegistryResponse {
|
||||
status: 'success';
|
||||
updatedAt: number;
|
||||
updated: number;
|
||||
controllableCount: number;
|
||||
typesCount: Record<string, number>;
|
||||
controllableVehicleIDs: string[];
|
||||
}
|
||||
|
||||
interface ConfigUpdateResponse {
|
||||
status: 'success';
|
||||
area: string;
|
||||
field: string;
|
||||
old: number;
|
||||
new: number;
|
||||
}
|
||||
|
||||
interface ErrorResponse {
|
||||
status: 'error';
|
||||
message: string;
|
||||
detail?: string;
|
||||
errors?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 接口封装示例
|
||||
|
||||
```ts
|
||||
export async function syncVehicleRegistry(items: VehicleRegistryItem[]) {
|
||||
return postJson<VehicleRegistryResponse>(`${API_BASE}/api/VehicleRegistry`, items);
|
||||
}
|
||||
|
||||
export async function updateRunwayWarningRadius(value: number) {
|
||||
return postJson<ConfigUpdateResponse>(
|
||||
`${API_BASE}/config/runway/warning_zone_radius/aircraft`,
|
||||
{ value }
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateRunwayAlertRadius(value: number) {
|
||||
return postJson<ConfigUpdateResponse>(
|
||||
`${API_BASE}/config/runway/alert_zone_radius/aircraft`,
|
||||
{ value }
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateDivergingReleaseDistance(value: number) {
|
||||
return postJson<ConfigUpdateResponse>(
|
||||
`${API_BASE}/config/collision/diverging_release_distance`,
|
||||
{ value }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 前端联调注意事项
|
||||
|
||||
### 8.1 `VehicleRegistry`
|
||||
|
||||
- 请求体必须是数组
|
||||
- `vehicleID` 不能为空字符串
|
||||
- `vehicleType` 必须严格使用大写枚举值
|
||||
- 该接口是“增量更新”,未传对象不会被后端自动删除
|
||||
|
||||
### 8.2 配置接口
|
||||
|
||||
- `value` 必须是 `number`
|
||||
- 不要传字符串,例如 `"300"` 是错误的
|
||||
- 必须大于 `0`
|
||||
|
||||
### 8.3 跨域问题
|
||||
|
||||
- 当前后端未配置 CORS
|
||||
- 如果前端页面和后端接口不是同域同端口,浏览器可能拦截
|
||||
- 如出现跨域报错,需要后端后续补 CORS 配置
|
||||
|
||||
### 8.4 生效方式
|
||||
|
||||
- 这 3 个配置接口修改后立即在当前服务进程内生效
|
||||
- 服务重启后恢复默认值,不会持久化
|
||||
|
||||
## 9. 联调建议流程
|
||||
|
||||
### 9.1 第一步:先调对象注册接口
|
||||
|
||||
确认前端能成功将当前关注对象同步给后端。
|
||||
|
||||
### 9.2 第二步:再调配置接口
|
||||
|
||||
确认预警半径、告警半径、冲突解除距离可运行时修改。
|
||||
|
||||
### 9.3 第三步:观察业务联动
|
||||
|
||||
重点观察:
|
||||
|
||||
- 碰撞检测结果是否按新对象范围生效
|
||||
- 跑道区域预警/告警范围是否按新阈值变化
|
||||
- 冲突解除逻辑后续接入时是否读取当前运行时配置
|
||||
|
||||
## 10. 最小联调示例
|
||||
|
||||
```ts
|
||||
async function initPlatformObjects() {
|
||||
await syncVehicleRegistry([
|
||||
{ vehicleID: 'QN001', vehicleType: 'WUREN' },
|
||||
{ vehicleID: 'TQ001', vehicleType: 'TEQIN' },
|
||||
{ vehicleID: 'AC001', vehicleType: 'HANGKONG' }
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateRuntimeConfig() {
|
||||
await updateRunwayWarningRadius(300);
|
||||
await updateRunwayAlertRadius(200);
|
||||
await updateDivergingReleaseDistance(50);
|
||||
}
|
||||
```
|
||||
|
||||
## 11. 接口清单汇总
|
||||
|
||||
| 接口 | 方法 | 用途 |
|
||||
|---|---|---|
|
||||
| `/api/VehicleRegistry` | `POST` | 同步当前关心对象及类型 |
|
||||
| `/config/runway/warning_zone_radius/aircraft` | `POST` | 修改跑道区域航空器预警区半径 |
|
||||
| `/config/runway/alert_zone_radius/aircraft` | `POST` | 修改跑道区域航空器告警区半径 |
|
||||
| `/config/collision/diverging_release_distance` | `POST` | 修改冲突解除距离阈值 |
|
||||
@ -42,6 +42,13 @@
|
||||
<version>${qaup.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.qaup</groupId>
|
||||
<artifactId>qaup-framework</artifactId>
|
||||
<version>${qaup.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@ -0,0 +1,219 @@
|
||||
package com.qaup.collision.controller;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public class PlatformIntegrationController {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
public PlatformIntegrationController(
|
||||
ObjectMapper objectMapper,
|
||||
PlatformRuntimeStateService platformRuntimeStateService) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.platformRuntimeStateService = platformRuntimeStateService;
|
||||
}
|
||||
|
||||
@PostMapping(path = "/api/VehicleRegistry", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Map<String, Object>> registerVehicles(@RequestBody String requestBody) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(requestBody);
|
||||
if (!root.isArray()) {
|
||||
return error(HttpStatus.BAD_REQUEST, "Body must be an array");
|
||||
}
|
||||
|
||||
List<String> errors = new ArrayList<>();
|
||||
List<PlatformRuntimeStateService.VehicleRegistryEntry> entries = new ArrayList<>();
|
||||
for (JsonNode item : root) {
|
||||
validateVehicleRegistryItem(item, errors, entries);
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "error");
|
||||
payload.put("message", "Invalid request");
|
||||
payload.put("errors", errors);
|
||||
return ResponseEntity.badRequest().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||
}
|
||||
|
||||
PlatformRuntimeStateService.VehicleRegistryUpdateResult result =
|
||||
platformRuntimeStateService.updateVehicleRegistry(entries);
|
||||
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "success");
|
||||
payload.put("updatedAt", result.updatedAt());
|
||||
payload.put("updated", result.updated());
|
||||
payload.put("controllableCount", result.controllableCount());
|
||||
payload.put("typesCount", result.typesCount());
|
||||
payload.put("controllableVehicleIDs", result.controllableVehicleIDs());
|
||||
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||
} catch (JsonProcessingException e) {
|
||||
return invalidJson(e);
|
||||
} catch (Exception e) {
|
||||
return internalError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(path = "/config/runway/warning_zone_radius/aircraft", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Map<String, Object>> updateRunwayWarningZoneRadiusAircraft(@RequestBody String requestBody) {
|
||||
return updateNumericConfig(requestBody, "runway", "warning_zone_radius.aircraft",
|
||||
platformRuntimeStateService::updateRunwayWarningZoneRadiusAircraft);
|
||||
}
|
||||
|
||||
@PostMapping(path = "/config/runway/alert_zone_radius/aircraft", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Map<String, Object>> updateRunwayAlertZoneRadiusAircraft(@RequestBody String requestBody) {
|
||||
return updateNumericConfig(requestBody, "runway", "alert_zone_radius.aircraft",
|
||||
platformRuntimeStateService::updateRunwayAlertZoneRadiusAircraft);
|
||||
}
|
||||
|
||||
@PostMapping(path = "/config/collision/diverging_release_distance", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Map<String, Object>> updateCollisionDivergingReleaseDistance(@RequestBody String requestBody) {
|
||||
return updateNumericConfig(requestBody, "collision", "collision.diverging_release_distance",
|
||||
platformRuntimeStateService::updateCollisionDivergingReleaseDistance);
|
||||
}
|
||||
|
||||
private void validateVehicleRegistryItem(
|
||||
JsonNode item,
|
||||
List<String> errors,
|
||||
List<PlatformRuntimeStateService.VehicleRegistryEntry> entries) {
|
||||
|
||||
if (!item.isObject()) {
|
||||
errors.add("item must be an object");
|
||||
return;
|
||||
}
|
||||
|
||||
JsonNode vehicleIdNode = item.get("vehicleID");
|
||||
JsonNode vehicleTypeNode = item.get("vehicleType");
|
||||
|
||||
if (vehicleIdNode == null) {
|
||||
errors.add("missing vehicleID");
|
||||
}
|
||||
if (vehicleTypeNode == null) {
|
||||
errors.add("missing vehicleType");
|
||||
}
|
||||
if (vehicleIdNode == null || vehicleTypeNode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vehicleIdNode.isTextual()) {
|
||||
errors.add("vehicleID must be string");
|
||||
}
|
||||
if (!vehicleTypeNode.isTextual()) {
|
||||
errors.add("vehicleType must be string");
|
||||
}
|
||||
if (!vehicleIdNode.isTextual() || !vehicleTypeNode.isTextual()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String vehicleId = vehicleIdNode.asText();
|
||||
String vehicleType = vehicleTypeNode.asText();
|
||||
|
||||
if (vehicleId.trim().isEmpty()) {
|
||||
errors.add("vehicleID must be non-empty");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
PlatformRuntimeStateService.VehicleRegistryType parsedType =
|
||||
PlatformRuntimeStateService.VehicleRegistryType.valueOf(vehicleType);
|
||||
entries.add(new PlatformRuntimeStateService.VehicleRegistryEntry(vehicleId, parsedType));
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.add("invalid vehicleType=" + vehicleType);
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> updateNumericConfig(
|
||||
String requestBody,
|
||||
String area,
|
||||
String field,
|
||||
NumericUpdater updater) {
|
||||
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(requestBody);
|
||||
if (!root.isObject()) {
|
||||
return invalidFieldType("Request body must be a JSON object");
|
||||
}
|
||||
|
||||
JsonNode valueNode = root.get("value");
|
||||
if (valueNode == null) {
|
||||
return error(HttpStatus.BAD_REQUEST, "Missing field: value");
|
||||
}
|
||||
if (!valueNode.isNumber()) {
|
||||
return invalidFieldType("Field 'value' must be a number");
|
||||
}
|
||||
|
||||
double newValue = valueNode.asDouble();
|
||||
if (!Double.isFinite(newValue) || newValue <= 0) {
|
||||
return error(HttpStatus.BAD_REQUEST, "Invalid value: must be a finite number greater than 0");
|
||||
}
|
||||
|
||||
double oldValue = updater.update(newValue);
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "success");
|
||||
payload.put("area", area);
|
||||
payload.put("field", field);
|
||||
payload.put("old", oldValue);
|
||||
payload.put("new", newValue);
|
||||
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||
} catch (JsonProcessingException e) {
|
||||
return invalidJson(e);
|
||||
} catch (Exception e) {
|
||||
return internalError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> invalidJson(JsonProcessingException e) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "error");
|
||||
payload.put("message", "Invalid JSON");
|
||||
payload.put("detail", e.getOriginalMessage());
|
||||
return ResponseEntity.badRequest().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> invalidFieldType(String detail) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "error");
|
||||
payload.put("message", "Invalid field type");
|
||||
payload.put("detail", detail);
|
||||
return ResponseEntity.badRequest().contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> error(HttpStatus status, String message) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "error");
|
||||
payload.put("message", message);
|
||||
return ResponseEntity.status(status).contentType(MediaType.APPLICATION_JSON).body(payload);
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> internalError(Exception e) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("status", "error");
|
||||
payload.put("message", "Internal error");
|
||||
payload.put("detail", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(payload);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface NumericUpdater {
|
||||
double update(double newValue);
|
||||
}
|
||||
}
|
||||
@ -744,7 +744,8 @@ public class DataCollectorService {
|
||||
.missionStartTime(currentMission.getStartTime())
|
||||
.estimatedEndTime(currentMission.getEstimatedEndTime())
|
||||
.progress(currentMission.getProgress())
|
||||
.totalMileage(currentMission.getTotalMileage());
|
||||
.totalMileage(currentMission.getTotalMileage())
|
||||
.missionStatus(resolveMissionStatus(statusData, missionContext));
|
||||
}
|
||||
|
||||
// 提取路径点信息
|
||||
@ -984,6 +985,7 @@ public class DataCollectorService {
|
||||
unmannedVehicle.setEstimatedEndTime(currentMission.getEstimatedEndTime());
|
||||
unmannedVehicle.setProgress(currentMission.getProgress());
|
||||
unmannedVehicle.setTotalMileage(currentMission.getTotalMileage());
|
||||
unmannedVehicle.setMissionStatus(resolveMissionStatus(null, missionContext));
|
||||
}
|
||||
|
||||
// 更新路径点信息
|
||||
@ -1085,6 +1087,53 @@ public class DataCollectorService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private UnmannedVehicle.MissionStatus resolveMissionStatus(
|
||||
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData,
|
||||
MissionContextDTO missionContext) {
|
||||
|
||||
if (missionContext == null || missionContext.getCurrentMission() == null) {
|
||||
return UnmannedVehicle.MissionStatus.NONE;
|
||||
}
|
||||
|
||||
MissionContextDTO.CurrentMissionDTO currentMission = missionContext.getCurrentMission();
|
||||
if (currentMission.getMissionId() == null || currentMission.getMissionId().isBlank()) {
|
||||
return UnmannedVehicle.MissionStatus.NONE;
|
||||
}
|
||||
|
||||
if (currentMission.getProgress() != null && currentMission.getProgress() >= 100.0d) {
|
||||
return UnmannedVehicle.MissionStatus.COMPLETED;
|
||||
}
|
||||
|
||||
if (missionContext.getWaypoints() != null && !missionContext.getWaypoints().isEmpty()) {
|
||||
boolean allFinished = missionContext.getWaypoints().stream()
|
||||
.allMatch(waypoint -> {
|
||||
String status = waypoint.getStatus();
|
||||
return "COMPLETED".equalsIgnoreCase(status) || "SKIPPED".equalsIgnoreCase(status);
|
||||
});
|
||||
if (allFinished) {
|
||||
return UnmannedVehicle.MissionStatus.COMPLETED;
|
||||
}
|
||||
}
|
||||
|
||||
if (statusData != null && statusData.getOperationalStatus() != null) {
|
||||
String operationalMode = statusData.getOperationalStatus().getOperationalMode();
|
||||
if (operationalMode != null) {
|
||||
String normalized = operationalMode.trim().toUpperCase();
|
||||
if (normalized.contains("CANCEL")) {
|
||||
return UnmannedVehicle.MissionStatus.CANCELLED;
|
||||
}
|
||||
if (normalized.contains("PAUSE")) {
|
||||
return UnmannedVehicle.MissionStatus.PAUSED;
|
||||
}
|
||||
if (normalized.contains("IDLE")) {
|
||||
return UnmannedVehicle.MissionStatus.ASSIGNED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UnmannedVehicle.MissionStatus.IN_PROGRESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用车辆状态缓存条目
|
||||
*/
|
||||
|
||||
@ -0,0 +1,300 @@
|
||||
package com.qaup.collision.datacollector.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.common.model.UnmannedVehicle;
|
||||
import com.qaup.collision.datacollector.util.RouteGeometryProcessor;
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.PrecisionModel;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Prepares route inputs before conflict detection.
|
||||
* This service standardizes unmanned vehicle task waypoints into formal routes and assignments.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RoutePreparationService {
|
||||
|
||||
private static final String CREATED_BY = "RoutePreparationService";
|
||||
|
||||
private final TransportRouteRepository transportRouteRepository;
|
||||
private final ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||
private final RouteGeometryProcessor routeGeometryProcessor;
|
||||
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
|
||||
|
||||
@Transactional
|
||||
public void synchronizeRoutes(List<MovingObject> activeObjects) {
|
||||
if (activeObjects == null || activeObjects.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (MovingObject movingObject : activeObjects) {
|
||||
if (movingObject instanceof UnmannedVehicle unmannedVehicle) {
|
||||
synchronizeUnmannedVehicleRoute(unmannedVehicle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void synchronizeUnmannedVehicleRoute(UnmannedVehicle unmannedVehicle) {
|
||||
if (unmannedVehicle == null || unmannedVehicle.getObjectId() == null || unmannedVehicle.getObjectId().isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String objectName = normalizeObjectName(unmannedVehicle);
|
||||
if (!shouldParticipateInConflictDetection(unmannedVehicle)) {
|
||||
deactivatePreparedRouteIfPresent(objectName);
|
||||
return;
|
||||
}
|
||||
|
||||
LineString routeGeometry = buildUnmannedVehicleRouteGeometry(unmannedVehicle);
|
||||
|
||||
if (routeGeometry == null) {
|
||||
deactivatePreparedRouteIfPresent(objectName);
|
||||
return;
|
||||
}
|
||||
|
||||
String routeName = buildUnmannedVehicleRouteName(unmannedVehicle);
|
||||
Optional<TransportRoute> existingRouteOptional = transportRouteRepository
|
||||
.findByRouteNameAndRouteType(routeName, TransportRoute.RouteType.UNMANNED_VEHICLE);
|
||||
|
||||
TransportRoute savedRoute = existingRouteOptional
|
||||
.map(existing -> saveUpdatedRouteIfNeeded(existing, routeGeometry, unmannedVehicle))
|
||||
.orElseGet(() -> transportRouteRepository.save(createNewRoute(routeName, routeGeometry, unmannedVehicle)));
|
||||
|
||||
saveAssignmentIfChanged(objectName, savedRoute.getId());
|
||||
deactivateOtherPreparedRoutes(objectName, savedRoute.getId());
|
||||
}
|
||||
|
||||
private TransportRoute createNewRoute(String routeName, LineString routeGeometry, UnmannedVehicle unmannedVehicle) {
|
||||
return TransportRoute.builder()
|
||||
.routeName(routeName)
|
||||
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
|
||||
.description(buildRouteDescription(unmannedVehicle, routeGeometry))
|
||||
.routeGeometry(routeGeometry)
|
||||
.maxSpeedKph(30.0)
|
||||
.typicalSpeedKph(unmannedVehicle.getCurrentSpeed() != null ? unmannedVehicle.getCurrentSpeed() : 15.0)
|
||||
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||
.isBidirectional(false)
|
||||
.createdBy(CREATED_BY)
|
||||
.updatedBy(CREATED_BY)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
private TransportRoute updateExistingRoute(TransportRoute existing, LineString routeGeometry, UnmannedVehicle unmannedVehicle) {
|
||||
existing.setRouteGeometry(routeGeometry);
|
||||
existing.setDescription(buildRouteDescription(unmannedVehicle, routeGeometry));
|
||||
existing.setTypicalSpeedKph(unmannedVehicle.getCurrentSpeed() != null ? unmannedVehicle.getCurrentSpeed() : existing.getTypicalSpeedKph());
|
||||
existing.setStatus(TransportRoute.RouteStatus.ACTIVE);
|
||||
existing.setUpdatedBy(CREATED_BY);
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
return existing;
|
||||
}
|
||||
|
||||
private TransportRoute saveUpdatedRouteIfNeeded(TransportRoute existing, LineString routeGeometry, UnmannedVehicle unmannedVehicle) {
|
||||
String newDescription = buildRouteDescription(unmannedVehicle, routeGeometry);
|
||||
Double newTypicalSpeed = unmannedVehicle.getCurrentSpeed() != null ? unmannedVehicle.getCurrentSpeed() : existing.getTypicalSpeedKph();
|
||||
|
||||
boolean sameGeometry = sameLineString(existing.getRouteGeometry(), routeGeometry);
|
||||
boolean sameDescription = Objects.equals(existing.getDescription(), newDescription);
|
||||
boolean sameTypicalSpeed = Objects.equals(existing.getTypicalSpeedKph(), newTypicalSpeed);
|
||||
boolean alreadyActive = existing.getStatus() == TransportRoute.RouteStatus.ACTIVE;
|
||||
|
||||
if (sameGeometry && sameDescription && sameTypicalSpeed && alreadyActive) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
TransportRoute updatedRoute = updateExistingRoute(existing, routeGeometry, unmannedVehicle);
|
||||
return Optional.ofNullable(transportRouteRepository.save(updatedRoute)).orElse(existing);
|
||||
}
|
||||
|
||||
private void saveAssignmentIfChanged(String objectName, Long routeId) {
|
||||
Optional<ObjectRouteAssignment> existingAssignment = objectRouteAssignmentRepository
|
||||
.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
objectName,
|
||||
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||
);
|
||||
|
||||
if (existingAssignment.isPresent() && Objects.equals(existingAssignment.get().getAssignedRouteId(), routeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
objectRouteAssignmentRepository.save(ObjectRouteAssignment.builder()
|
||||
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.objectName(objectName)
|
||||
.assignedRouteId(routeId)
|
||||
.assignedAt(LocalDateTime.now())
|
||||
.build());
|
||||
}
|
||||
|
||||
private void deactivatePreparedRouteIfPresent(String objectName) {
|
||||
objectRouteAssignmentRepository
|
||||
.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
objectName,
|
||||
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||
)
|
||||
.flatMap(assignment -> transportRouteRepository.findById(assignment.getAssignedRouteId()))
|
||||
.filter(this::isPreparedUnmannedVehicleRoute)
|
||||
.filter(route -> route.getStatus() == TransportRoute.RouteStatus.ACTIVE)
|
||||
.ifPresent(route -> {
|
||||
route.setStatus(TransportRoute.RouteStatus.INACTIVE);
|
||||
route.setUpdatedBy(CREATED_BY);
|
||||
route.setUpdatedAt(LocalDateTime.now());
|
||||
transportRouteRepository.save(route);
|
||||
});
|
||||
}
|
||||
|
||||
private void deactivateOtherPreparedRoutes(String objectName, Long activeRouteId) {
|
||||
List<ObjectRouteAssignment> assignments = objectRouteAssignmentRepository
|
||||
.findByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
objectName,
|
||||
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||
);
|
||||
|
||||
for (ObjectRouteAssignment assignment : assignments) {
|
||||
if (Objects.equals(assignment.getAssignedRouteId(), activeRouteId)) {
|
||||
continue;
|
||||
}
|
||||
transportRouteRepository.findById(assignment.getAssignedRouteId())
|
||||
.filter(this::isPreparedUnmannedVehicleRoute)
|
||||
.filter(route -> route.getStatus() == TransportRoute.RouteStatus.ACTIVE)
|
||||
.ifPresent(route -> {
|
||||
route.setStatus(TransportRoute.RouteStatus.INACTIVE);
|
||||
route.setUpdatedBy(CREATED_BY);
|
||||
route.setUpdatedAt(LocalDateTime.now());
|
||||
transportRouteRepository.save(route);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPreparedUnmannedVehicleRoute(TransportRoute route) {
|
||||
return route != null
|
||||
&& route.getRouteType() == TransportRoute.RouteType.UNMANNED_VEHICLE
|
||||
&& CREATED_BY.equals(route.getCreatedBy());
|
||||
}
|
||||
|
||||
private boolean shouldParticipateInConflictDetection(UnmannedVehicle unmannedVehicle) {
|
||||
if (unmannedVehicle.getMissionStatus() == UnmannedVehicle.MissionStatus.COMPLETED
|
||||
|| unmannedVehicle.getMissionStatus() == UnmannedVehicle.MissionStatus.CANCELLED
|
||||
|| unmannedVehicle.getMissionStatus() == UnmannedVehicle.MissionStatus.NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (unmannedVehicle.getProgress() != null && unmannedVehicle.getProgress() >= 100.0d) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (unmannedVehicle.getMissionId() == null || unmannedVehicle.getMissionId().isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (unmannedVehicle.getWaypoints() == null || unmannedVehicle.getWaypoints().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean allFinished = unmannedVehicle.getWaypoints().stream()
|
||||
.allMatch(waypoint -> waypoint != null
|
||||
&& (waypoint.getStatus() == UnmannedVehicle.WaypointStatus.COMPLETED
|
||||
|| waypoint.getStatus() == UnmannedVehicle.WaypointStatus.SKIPPED));
|
||||
return !allFinished;
|
||||
}
|
||||
|
||||
private String buildUnmannedVehicleRouteName(UnmannedVehicle unmannedVehicle) {
|
||||
String missionId = unmannedVehicle.getMissionId();
|
||||
if (missionId != null && !missionId.isBlank()) {
|
||||
return "UV_ROUTE_" + unmannedVehicle.getObjectId() + "_" + missionId;
|
||||
}
|
||||
return "UV_ROUTE_" + unmannedVehicle.getObjectId();
|
||||
}
|
||||
|
||||
private String buildRouteDescription(UnmannedVehicle unmannedVehicle, LineString routeGeometry) {
|
||||
return String.format(
|
||||
"Unmanned vehicle route prepared from mission context, vehicle=%s, mission=%s, points=%d",
|
||||
unmannedVehicle.getObjectId(),
|
||||
unmannedVehicle.getMissionId(),
|
||||
routeGeometry.getNumPoints()
|
||||
);
|
||||
}
|
||||
|
||||
private String normalizeObjectName(UnmannedVehicle unmannedVehicle) {
|
||||
if (unmannedVehicle.getObjectName() != null && !unmannedVehicle.getObjectName().isBlank()) {
|
||||
return unmannedVehicle.getObjectName();
|
||||
}
|
||||
return unmannedVehicle.getObjectId();
|
||||
}
|
||||
|
||||
private LineString buildUnmannedVehicleRouteGeometry(UnmannedVehicle unmannedVehicle) {
|
||||
List<Coordinate> coordinates = new ArrayList<>();
|
||||
|
||||
if (unmannedVehicle.getCurrentPosition() != null && !unmannedVehicle.getCurrentPosition().isEmpty()) {
|
||||
coordinates.add(unmannedVehicle.getCurrentPosition().getCoordinate());
|
||||
}
|
||||
|
||||
if (unmannedVehicle.getWaypoints() != null) {
|
||||
for (UnmannedVehicle.WaypointInfo waypoint : unmannedVehicle.getWaypoints()) {
|
||||
if (waypoint == null || waypoint.getLatitude() == null || waypoint.getLongitude() == null) {
|
||||
continue;
|
||||
}
|
||||
Coordinate coordinate = new Coordinate(waypoint.getLongitude(), waypoint.getLatitude());
|
||||
if (coordinates.isEmpty() || !sameCoordinate(coordinates.get(coordinates.size() - 1), coordinate)) {
|
||||
coordinates.add(coordinate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (coordinates.size() < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LineString route = geometryFactory.createLineString(coordinates.toArray(Coordinate[]::new));
|
||||
if (!routeGeometryProcessor.isValidLineString(route)) {
|
||||
return null;
|
||||
}
|
||||
return routeGeometryProcessor.simplifyLineString(route, 0.000001d);
|
||||
}
|
||||
|
||||
private boolean sameCoordinate(Coordinate left, Coordinate right) {
|
||||
return left != null
|
||||
&& right != null
|
||||
&& Double.compare(left.x, right.x) == 0
|
||||
&& Double.compare(left.y, right.y) == 0;
|
||||
}
|
||||
|
||||
private boolean sameLineString(LineString left, LineString right) {
|
||||
if (left == null || right == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Coordinate[] leftCoordinates = left.getCoordinates();
|
||||
Coordinate[] rightCoordinates = right.getCoordinates();
|
||||
if (leftCoordinates.length != rightCoordinates.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < leftCoordinates.length; i++) {
|
||||
if (!sameCoordinate(leftCoordinates[i], rightCoordinates[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import com.qaup.collision.common.model.UnmannedVehicle;
|
||||
import com.qaup.collision.common.model.spatial.VehicleLocation;
|
||||
import com.qaup.collision.common.adapter.QuapDataAdapter;
|
||||
import com.qaup.collision.datacollector.service.VehicleDataPersistenceService;
|
||||
import com.qaup.collision.datacollector.service.RoutePreparationService;
|
||||
import com.qaup.collision.dataprocessing.parser.TrafficLightSignalParser;
|
||||
import com.qaup.collision.websocket.event.PositionUpdateEvent;
|
||||
import com.qaup.collision.websocket.message.PositionUpdatePayload;
|
||||
@ -14,6 +15,7 @@ import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload;
|
||||
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
||||
import com.qaup.collision.datacollector.service.DataCollectorService;
|
||||
import com.qaup.collision.common.model.FlightNotification;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import com.qaup.collision.websocket.event.FlightNotificationEvent;
|
||||
import com.qaup.common.core.redis.RedisCache;
|
||||
import com.qaup.system.domain.SysVehicleInfo;
|
||||
@ -75,6 +77,9 @@ public class DataProcessingService {
|
||||
@Autowired
|
||||
private VehicleDataPersistenceService vehicleDataPersistenceService;
|
||||
|
||||
@Autowired
|
||||
private RoutePreparationService routePreparationService;
|
||||
|
||||
@Autowired
|
||||
private QuapDataAdapter quapDataAdapter;
|
||||
|
||||
@ -88,6 +93,9 @@ public class DataProcessingService {
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Autowired
|
||||
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
// 从DataCollectorService获取缓存的引用
|
||||
private Map<String, MovingObject> activeMovingObjectsCache;
|
||||
|
||||
@ -147,18 +155,43 @@ public class DataProcessingService {
|
||||
// 注意:采集侧只缓存;发送侧在处理完成后会清理缓存,避免同一通知被每秒重复推送(刷屏)。
|
||||
processFlightNotificationUpdates();
|
||||
|
||||
// 第五步:执行路径冲突检测
|
||||
pathConflictDetectionService.detectPathConflicts(currentActiveObjects);
|
||||
List<MovingObject> collisionManagedObjects = filterCollisionManagedObjects(currentActiveObjects);
|
||||
|
||||
// 第六步:执行违规检测
|
||||
// 第五步:将注册的无人车任务路径准备为正式路线输入
|
||||
if (routePreparationService != null) {
|
||||
routePreparationService.synchronizeRoutes(collisionManagedObjects);
|
||||
}
|
||||
|
||||
// 第六步:执行路径冲突检测
|
||||
pathConflictDetectionService.detectPathConflicts(collisionManagedObjects);
|
||||
|
||||
// 第七步:执行违规检测
|
||||
performViolationDetection(currentActiveObjects);
|
||||
|
||||
// 第七步:保存无人车数据到数据库
|
||||
// 第八步:保存无人车数据到数据库
|
||||
saveUnmannedVehicleDataPeriodically(currentActiveObjects);
|
||||
|
||||
log.info("周期性数据处理完成");
|
||||
}
|
||||
|
||||
private List<MovingObject> filterCollisionManagedObjects(List<MovingObject> activeObjects) {
|
||||
if (activeObjects == null || activeObjects.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<MovingObject> filteredObjects = activeObjects.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(object -> platformRuntimeStateService == null
|
||||
|| platformRuntimeStateService.isRegisteredForCollision(object.getObjectId(), object.getObjectType()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (filteredObjects.size() != activeObjects.size()) {
|
||||
log.debug("碰撞检测对象已按注册表过滤: 原始数量={}, 过滤后数量={}",
|
||||
activeObjects.size(), filteredObjects.size());
|
||||
}
|
||||
return filteredObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为所有缓存对象计算速度和方向
|
||||
*/
|
||||
|
||||
@ -10,6 +10,7 @@ import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
@ -39,9 +40,8 @@ public class PathConflictDetectionService {
|
||||
private final ConflictAlertLogRepository conflictAlertLogRepository;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final CoordinateSystemService coordinateSystemService;
|
||||
private final PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
private static final double WARNING_DISTANCE_THRESHOLD = 200.0;
|
||||
private static final double ALERT_DISTANCE_THRESHOLD = 100.0;
|
||||
private static final int MAX_PREDICTION_TIME_SECONDS = 300;
|
||||
private static final double MIN_TIME_GAP_SECONDS = 30.0;
|
||||
|
||||
@ -76,7 +76,7 @@ public class PathConflictDetectionService {
|
||||
}
|
||||
|
||||
private Optional<ConflictAlertEvent> detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
|
||||
if (obj1.getObjectType() != MovingObjectType.UNMANNED_VEHICLE && obj2.getObjectType() != MovingObjectType.UNMANNED_VEHICLE) {
|
||||
if (!isSupportedConflictPair(obj1.getObjectType(), obj2.getObjectType())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@ -200,6 +200,7 @@ public class PathConflictDetectionService {
|
||||
|
||||
return assignmentOptional
|
||||
.map(assignment -> routeRepository.findById(assignment.getAssignedRouteId()).orElse(null))
|
||||
.filter(route -> route.getStatus() == TransportRoute.RouteStatus.ACTIVE)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@ -288,27 +289,25 @@ public class PathConflictDetectionService {
|
||||
double distance2,
|
||||
MovingObjectType obj2Type,
|
||||
double timeGap) {
|
||||
|
||||
boolean obj1IsUnmannedVehicle = MovingObjectType.UNMANNED_VEHICLE.equals(obj1Type);
|
||||
boolean obj2IsUnmannedVehicle = MovingObjectType.UNMANNED_VEHICLE.equals(obj2Type);
|
||||
|
||||
if (!obj1IsUnmannedVehicle && !obj2IsUnmannedVehicle) {
|
||||
if (!isSupportedConflictPair(obj1Type, obj2Type)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
double distanceToEvaluate;
|
||||
|
||||
if (obj1IsUnmannedVehicle && obj2IsUnmannedVehicle) {
|
||||
distanceToEvaluate = Math.min(distance1, distance2);
|
||||
} else if (obj1IsUnmannedVehicle) {
|
||||
if (obj1Type == MovingObjectType.AIRCRAFT) {
|
||||
distanceToEvaluate = distance2;
|
||||
} else if (obj2Type == MovingObjectType.AIRCRAFT) {
|
||||
distanceToEvaluate = distance1;
|
||||
} else {
|
||||
distanceToEvaluate = distance2;
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (distanceToEvaluate <= ALERT_DISTANCE_THRESHOLD && timeGap <= MIN_TIME_GAP_SECONDS) {
|
||||
double alertDistanceThreshold = platformRuntimeStateService.getRunwayAlertZoneRadiusAircraft();
|
||||
double warningDistanceThreshold = platformRuntimeStateService.getRunwayWarningZoneRadiusAircraft();
|
||||
|
||||
if (distanceToEvaluate <= alertDistanceThreshold && timeGap <= MIN_TIME_GAP_SECONDS) {
|
||||
return Optional.of(ConflictAlertLog.AlertLevel.CRITICAL);
|
||||
} else if (distanceToEvaluate <= WARNING_DISTANCE_THRESHOLD && timeGap <= MIN_TIME_GAP_SECONDS * 2) {
|
||||
} else if (distanceToEvaluate <= warningDistanceThreshold && timeGap <= MIN_TIME_GAP_SECONDS * 2) {
|
||||
return Optional.of(ConflictAlertLog.AlertLevel.WARNING);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
@ -370,6 +369,13 @@ public class PathConflictDetectionService {
|
||||
return dot >= HEADING_ALIGNMENT_MIN_COS;
|
||||
}
|
||||
|
||||
private boolean isSupportedConflictPair(MovingObjectType left, MovingObjectType right) {
|
||||
return (left == MovingObjectType.UNMANNED_VEHICLE && right == MovingObjectType.AIRCRAFT)
|
||||
|| (left == MovingObjectType.AIRCRAFT && right == MovingObjectType.UNMANNED_VEHICLE)
|
||||
|| (left == MovingObjectType.SPECIAL_VEHICLE && right == MovingObjectType.AIRCRAFT)
|
||||
|| (left == MovingObjectType.AIRCRAFT && right == MovingObjectType.SPECIAL_VEHICLE);
|
||||
}
|
||||
|
||||
private Point toLocalPoint(Point wgs84Point) {
|
||||
try {
|
||||
double[] local = coordinateSystemService.convertToLocalCoordinate(wgs84Point.getX(), wgs84Point.getY());
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
package com.qaup.collision.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class PlatformRuntimeStateService {
|
||||
|
||||
private final ConcurrentHashMap<String, VehicleRegistryType> vehicleTypes = new ConcurrentHashMap<>();
|
||||
|
||||
private volatile double runwayWarningZoneRadiusAircraft;
|
||||
private volatile double runwayAlertZoneRadiusAircraft;
|
||||
private volatile double collisionDivergingReleaseDistance;
|
||||
|
||||
public PlatformRuntimeStateService(
|
||||
@Value("${qaup.runtime-config.runway.warning-zone-radius.aircraft:200.0}") double runwayWarningZoneRadiusAircraft,
|
||||
@Value("${qaup.runtime-config.runway.alert-zone-radius.aircraft:100.0}") double runwayAlertZoneRadiusAircraft,
|
||||
@Value("${qaup.runtime-config.collision.diverging-release-distance:40.0}") double collisionDivergingReleaseDistance) {
|
||||
|
||||
this.runwayWarningZoneRadiusAircraft = runwayWarningZoneRadiusAircraft;
|
||||
this.runwayAlertZoneRadiusAircraft = runwayAlertZoneRadiusAircraft;
|
||||
this.collisionDivergingReleaseDistance = collisionDivergingReleaseDistance;
|
||||
}
|
||||
|
||||
public VehicleRegistryUpdateResult updateVehicleRegistry(List<VehicleRegistryEntry> entries) {
|
||||
Objects.requireNonNull(entries, "entries");
|
||||
|
||||
EnumMap<VehicleRegistryType, Integer> requestTypeCounts = new EnumMap<>(VehicleRegistryType.class);
|
||||
for (VehicleRegistryEntry entry : entries) {
|
||||
vehicleTypes.put(entry.vehicleID(), entry.vehicleType());
|
||||
requestTypeCounts.merge(entry.vehicleType(), 1, Integer::sum);
|
||||
}
|
||||
|
||||
Set<String> controllableVehicleIds = new TreeSet<>();
|
||||
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
||||
if (entry.getValue() == VehicleRegistryType.WUREN) {
|
||||
controllableVehicleIds.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Integer> responseTypeCounts = new LinkedHashMap<>();
|
||||
for (VehicleRegistryType type : VehicleRegistryType.values()) {
|
||||
Integer count = requestTypeCounts.get(type);
|
||||
if (count != null && count > 0) {
|
||||
responseTypeCounts.put(type.name(), count);
|
||||
}
|
||||
}
|
||||
|
||||
return new VehicleRegistryUpdateResult(
|
||||
System.currentTimeMillis(),
|
||||
entries.size(),
|
||||
controllableVehicleIds.size(),
|
||||
responseTypeCounts,
|
||||
new ArrayList<>(controllableVehicleIds));
|
||||
}
|
||||
|
||||
public List<String> getControllableVehicleIds() {
|
||||
TreeSet<String> vehicleIds = new TreeSet<>();
|
||||
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
||||
if (entry.getValue() == VehicleRegistryType.WUREN) {
|
||||
vehicleIds.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableList(new ArrayList<>(vehicleIds));
|
||||
}
|
||||
|
||||
public VehicleRegistryType getVehicleRegistryType(String vehicleId) {
|
||||
if (vehicleId == null || vehicleId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return vehicleTypes.get(vehicleId);
|
||||
}
|
||||
|
||||
public boolean isRegisteredForCollision(String vehicleId, MovingObject.MovingObjectType objectType) {
|
||||
VehicleRegistryType registryType = getVehicleRegistryType(vehicleId);
|
||||
if (registryType == null || objectType == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return switch (registryType) {
|
||||
case WUREN -> objectType == MovingObject.MovingObjectType.UNMANNED_VEHICLE;
|
||||
case TEQIN -> objectType == MovingObject.MovingObjectType.SPECIAL_VEHICLE;
|
||||
case HANGKONG -> objectType == MovingObject.MovingObjectType.AIRCRAFT;
|
||||
case PUTONG, JIUYUAN -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public double getRunwayWarningZoneRadiusAircraft() {
|
||||
return runwayWarningZoneRadiusAircraft;
|
||||
}
|
||||
|
||||
public double updateRunwayWarningZoneRadiusAircraft(double newValue) {
|
||||
double oldValue = runwayWarningZoneRadiusAircraft;
|
||||
runwayWarningZoneRadiusAircraft = newValue;
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
public double getRunwayAlertZoneRadiusAircraft() {
|
||||
return runwayAlertZoneRadiusAircraft;
|
||||
}
|
||||
|
||||
public double updateRunwayAlertZoneRadiusAircraft(double newValue) {
|
||||
double oldValue = runwayAlertZoneRadiusAircraft;
|
||||
runwayAlertZoneRadiusAircraft = newValue;
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
public double getCollisionDivergingReleaseDistance() {
|
||||
return collisionDivergingReleaseDistance;
|
||||
}
|
||||
|
||||
public double updateCollisionDivergingReleaseDistance(double newValue) {
|
||||
double oldValue = collisionDivergingReleaseDistance;
|
||||
collisionDivergingReleaseDistance = newValue;
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
public record VehicleRegistryEntry(String vehicleID, VehicleRegistryType vehicleType) {
|
||||
}
|
||||
|
||||
public record VehicleRegistryUpdateResult(
|
||||
long updatedAt,
|
||||
int updated,
|
||||
int controllableCount,
|
||||
Map<String, Integer> typesCount,
|
||||
List<String> controllableVehicleIDs) {
|
||||
}
|
||||
|
||||
public enum VehicleRegistryType {
|
||||
WUREN,
|
||||
TEQIN,
|
||||
HANGKONG,
|
||||
PUTONG,
|
||||
JIUYUAN
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
package com.qaup.collision.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class PlatformIntegrationControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
PlatformIntegrationController controller =
|
||||
new PlatformIntegrationController(new ObjectMapper(), runtimeStateService);
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptVehicleRegistryPayload() throws Exception {
|
||||
mockMvc.perform(post("/api/VehicleRegistry")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
[
|
||||
{ "vehicleID": "QN001", "vehicleType": "WUREN" },
|
||||
{ "vehicleID": "TQ001", "vehicleType": "TEQIN" },
|
||||
{ "vehicleID": "AC001", "vehicleType": "HANGKONG" }
|
||||
]
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.status").value("success"))
|
||||
.andExpect(jsonPath("$.updated").value(3))
|
||||
.andExpect(jsonPath("$.controllableCount").value(1))
|
||||
.andExpect(jsonPath("$.typesCount.WUREN").value(1))
|
||||
.andExpect(jsonPath("$.typesCount.TEQIN").value(1))
|
||||
.andExpect(jsonPath("$.typesCount.HANGKONG").value(1))
|
||||
.andExpect(jsonPath("$.controllableVehicleIDs[0]").value("QN001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectInvalidVehicleRegistryPayload() throws Exception {
|
||||
mockMvc.perform(post("/api/VehicleRegistry")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
[
|
||||
{ "vehicleID": "", "vehicleType": "WUREN" },
|
||||
{ "vehicleID": "QN002", "vehicleType": "UNKNOWN" },
|
||||
1
|
||||
]
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.status").value("error"))
|
||||
.andExpect(jsonPath("$.message").value("Invalid request"))
|
||||
.andExpect(jsonPath("$.errors[0]").value("vehicleID must be non-empty"))
|
||||
.andExpect(jsonPath("$.errors[1]").value("invalid vehicleType=UNKNOWN"))
|
||||
.andExpect(jsonPath("$.errors[2]").value("item must be an object"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectNonArrayVehicleRegistryPayload() throws Exception {
|
||||
mockMvc.perform(post("/api/VehicleRegistry")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"vehicleID\":\"QN001\",\"vehicleType\":\"WUREN\"}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value("Body must be an array"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateRunwayWarningZoneRadius() throws Exception {
|
||||
mockMvc.perform(post("/config/runway/warning_zone_radius/aircraft")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"value\":300}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("success"))
|
||||
.andExpect(jsonPath("$.area").value("runway"))
|
||||
.andExpect(jsonPath("$.field").value("warning_zone_radius.aircraft"))
|
||||
.andExpect(jsonPath("$.old").value(200.0))
|
||||
.andExpect(jsonPath("$.new").value(300.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectMissingNumericConfigValue() throws Exception {
|
||||
mockMvc.perform(post("/config/collision/diverging_release_distance")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value("Missing field: value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectInvalidNumericConfigValueType() throws Exception {
|
||||
mockMvc.perform(post("/config/runway/alert_zone_radius/aircraft")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"value\":\"oops\"}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value("Invalid field type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectInvalidJson() throws Exception {
|
||||
mockMvc.perform(post("/config/runway/alert_zone_radius/aircraft")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"value\":"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value("Invalid JSON"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
package com.qaup.collision.controller;
|
||||
|
||||
import com.qaup.framework.config.SecurityConfig;
|
||||
import com.qaup.framework.config.properties.PermitAllUrlProperties;
|
||||
import com.qaup.framework.security.filter.JwtAuthenticationTokenFilter;
|
||||
import com.qaup.framework.security.handle.AuthenticationEntryPointImpl;
|
||||
import com.qaup.framework.security.handle.LogoutSuccessHandlerImpl;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@WebMvcTest(controllers = PlatformIntegrationController.class)
|
||||
@ContextConfiguration(classes = {
|
||||
PlatformIntegrationSecurityTest.TestApplication.class,
|
||||
PlatformIntegrationController.class,
|
||||
SecurityConfig.class
|
||||
})
|
||||
@Import(SecurityConfig.class)
|
||||
class PlatformIntegrationSecurityTest {
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
static class TestApplication {
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
@MockitoBean
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
@MockitoBean
|
||||
private AuthenticationEntryPointImpl authenticationEntryPoint;
|
||||
|
||||
@MockitoBean
|
||||
private LogoutSuccessHandlerImpl logoutSuccessHandler;
|
||||
|
||||
@MockitoBean
|
||||
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
|
||||
|
||||
@MockitoBean
|
||||
private CorsFilter corsFilter;
|
||||
|
||||
@MockitoBean
|
||||
private PermitAllUrlProperties permitAllUrlProperties;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
Mockito.when(permitAllUrlProperties.getUrls()).thenReturn(List.of());
|
||||
Mockito.when(userDetailsService.loadUserByUsername(Mockito.anyString()))
|
||||
.thenReturn(User.withUsername("user").password("{noop}pwd").authorities("ROLE_USER").build());
|
||||
|
||||
Mockito.doAnswer(invocation -> {
|
||||
HttpServletRequest request = invocation.getArgument(0);
|
||||
HttpServletResponse response = invocation.getArgument(1);
|
||||
FilterChain chain = invocation.getArgument(2);
|
||||
chain.doFilter(request, response);
|
||||
return null;
|
||||
}).when(jwtAuthenticationTokenFilter).doFilter(
|
||||
Mockito.any(HttpServletRequest.class),
|
||||
Mockito.any(HttpServletResponse.class),
|
||||
Mockito.any(FilterChain.class)
|
||||
);
|
||||
|
||||
Mockito.doAnswer(invocation -> {
|
||||
HttpServletRequest request = invocation.getArgument(0);
|
||||
HttpServletResponse response = invocation.getArgument(1);
|
||||
FilterChain chain = invocation.getArgument(2);
|
||||
chain.doFilter(request, response);
|
||||
return null;
|
||||
}).when(corsFilter).doFilter(
|
||||
Mockito.any(HttpServletRequest.class),
|
||||
Mockito.any(HttpServletResponse.class),
|
||||
Mockito.any(FilterChain.class)
|
||||
);
|
||||
|
||||
Mockito.when(platformRuntimeStateService.updateCollisionDivergingReleaseDistance(Mockito.anyDouble()))
|
||||
.thenReturn(40.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowAnonymousAccessToNewPlatformEndpoints() throws Exception {
|
||||
mockMvc.perform(post("/config/collision/diverging_release_distance")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"value\":50}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("success"))
|
||||
.andExpect(jsonPath("$.old").value(40.0))
|
||||
.andExpect(jsonPath("$.new").value(50.0));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,228 @@
|
||||
package com.qaup.collision.datacollector.service;
|
||||
|
||||
import com.qaup.collision.common.model.UnmannedVehicle;
|
||||
import com.qaup.collision.datacollector.util.RouteGeometryProcessor;
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
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.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RoutePreparationServiceTest {
|
||||
|
||||
@Mock
|
||||
private TransportRouteRepository transportRouteRepository;
|
||||
|
||||
@Mock
|
||||
private ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||
|
||||
private RoutePreparationService routePreparationService;
|
||||
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
|
||||
private RoutePreparationService service() {
|
||||
if (routePreparationService == null) {
|
||||
routePreparationService = new RoutePreparationService(
|
||||
transportRouteRepository,
|
||||
objectRouteAssignmentRepository,
|
||||
new RouteGeometryProcessor()
|
||||
);
|
||||
}
|
||||
return routePreparationService;
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrepareFormalRouteForUnmannedVehicleMission() {
|
||||
UnmannedVehicle vehicle = buildVehicleWithWaypoints("UV-100", "MISSION-1");
|
||||
|
||||
when(transportRouteRepository.findByRouteNameAndRouteType(
|
||||
"UV_ROUTE_UV-100_MISSION-1",
|
||||
TransportRoute.RouteType.UNMANNED_VEHICLE
|
||||
)).thenReturn(Optional.empty());
|
||||
when(transportRouteRepository.save(any(TransportRoute.class))).thenAnswer(invocation -> {
|
||||
TransportRoute route = invocation.getArgument(0);
|
||||
route.setId(11L);
|
||||
return route;
|
||||
});
|
||||
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
"UV-100",
|
||||
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||
)).thenReturn(Optional.empty());
|
||||
when(objectRouteAssignmentRepository.findByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
"UV-100",
|
||||
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||
)).thenReturn(List.of());
|
||||
|
||||
service().synchronizeUnmannedVehicleRoute(vehicle);
|
||||
|
||||
ArgumentCaptor<TransportRoute> routeCaptor = ArgumentCaptor.forClass(TransportRoute.class);
|
||||
verify(transportRouteRepository).save(routeCaptor.capture());
|
||||
TransportRoute savedRoute = routeCaptor.getValue();
|
||||
assertEquals(TransportRoute.RouteType.UNMANNED_VEHICLE, savedRoute.getRouteType());
|
||||
assertEquals(TransportRoute.RouteStatus.ACTIVE, savedRoute.getStatus());
|
||||
assertEquals("UV_ROUTE_UV-100_MISSION-1", savedRoute.getRouteName());
|
||||
assertNotNull(savedRoute.getRouteGeometry());
|
||||
assertTrue(savedRoute.getRouteGeometry().getNumPoints() >= 2);
|
||||
|
||||
ArgumentCaptor<ObjectRouteAssignment> assignmentCaptor = ArgumentCaptor.forClass(ObjectRouteAssignment.class);
|
||||
verify(objectRouteAssignmentRepository).save(assignmentCaptor.capture());
|
||||
assertEquals("UV-100", assignmentCaptor.getValue().getObjectName());
|
||||
assertEquals(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE, assignmentCaptor.getValue().getObjectType());
|
||||
assertEquals(11L, assignmentCaptor.getValue().getAssignedRouteId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeactivatePreparedRouteWhenMissionPathMissing() {
|
||||
UnmannedVehicle vehicle = UnmannedVehicle.builder()
|
||||
.objectId("UV-200")
|
||||
.objectName("UV-200")
|
||||
.build();
|
||||
|
||||
ObjectRouteAssignment latestAssignment = ObjectRouteAssignment.builder()
|
||||
.objectName("UV-200")
|
||||
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.assignedRouteId(21L)
|
||||
.build();
|
||||
TransportRoute existingRoute = TransportRoute.builder()
|
||||
.id(21L)
|
||||
.routeName("UV_ROUTE_UV-200_MISSION-X")
|
||||
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
|
||||
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||
.createdBy("RoutePreparationService")
|
||||
.routeGeometry(geometryFactory.createLineString(new Coordinate[]{
|
||||
new Coordinate(120.0, 36.0),
|
||||
new Coordinate(120.1, 36.1)
|
||||
}))
|
||||
.build();
|
||||
|
||||
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
"UV-200",
|
||||
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||
)).thenReturn(Optional.of(latestAssignment));
|
||||
when(transportRouteRepository.findById(21L)).thenReturn(Optional.of(existingRoute));
|
||||
|
||||
service().synchronizeUnmannedVehicleRoute(vehicle);
|
||||
|
||||
verify(transportRouteRepository).save(existingRoute);
|
||||
assertEquals(TransportRoute.RouteStatus.INACTIVE, existingRoute.getStatus());
|
||||
verify(objectRouteAssignmentRepository, never()).save(any(ObjectRouteAssignment.class));
|
||||
}
|
||||
|
||||
private UnmannedVehicle buildVehicleWithWaypoints(String vehicleId, String missionId) {
|
||||
Point currentPosition = geometryFactory.createPoint(new Coordinate(120.0834104, 36.35406879));
|
||||
return UnmannedVehicle.builder()
|
||||
.objectId(vehicleId)
|
||||
.objectName(vehicleId)
|
||||
.missionId(missionId)
|
||||
.missionStatus(UnmannedVehicle.MissionStatus.IN_PROGRESS)
|
||||
.currentPosition(currentPosition)
|
||||
.waypoints(List.of(
|
||||
UnmannedVehicle.WaypointInfo.builder()
|
||||
.waypointId("1")
|
||||
.latitude(36.35416879)
|
||||
.longitude(120.0835104)
|
||||
.status(UnmannedVehicle.WaypointStatus.PENDING)
|
||||
.build(),
|
||||
UnmannedVehicle.WaypointInfo.builder()
|
||||
.waypointId("2")
|
||||
.latitude(36.35426879)
|
||||
.longitude(120.0836104)
|
||||
.status(UnmannedVehicle.WaypointStatus.PENDING)
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRewriteRouteWhenPreparedInputIsUnchanged() {
|
||||
UnmannedVehicle vehicle = buildVehicleWithWaypoints("UV-300", "MISSION-3");
|
||||
TransportRoute existingRoute = TransportRoute.builder()
|
||||
.id(31L)
|
||||
.routeName("UV_ROUTE_UV-300_MISSION-3")
|
||||
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
|
||||
.description("Unmanned vehicle route prepared from mission context, vehicle=UV-300, mission=MISSION-3, points=2")
|
||||
.routeGeometry(new GeometryFactory().createLineString(new Coordinate[]{
|
||||
new Coordinate(120.0834104, 36.35406879),
|
||||
new Coordinate(120.0836104, 36.35426879)
|
||||
}))
|
||||
.typicalSpeedKph(15.0)
|
||||
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||
.createdBy("RoutePreparationService")
|
||||
.build();
|
||||
|
||||
when(transportRouteRepository.findByRouteNameAndRouteType(
|
||||
"UV_ROUTE_UV-300_MISSION-3",
|
||||
TransportRoute.RouteType.UNMANNED_VEHICLE
|
||||
)).thenReturn(Optional.of(existingRoute));
|
||||
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
"UV-300",
|
||||
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||
)).thenReturn(Optional.of(ObjectRouteAssignment.builder()
|
||||
.objectName("UV-300")
|
||||
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.assignedRouteId(31L)
|
||||
.build()));
|
||||
when(objectRouteAssignmentRepository.findByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
"UV-300",
|
||||
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||
)).thenReturn(List.of());
|
||||
|
||||
service().synchronizeUnmannedVehicleRoute(vehicle);
|
||||
|
||||
verify(transportRouteRepository, never()).save(any(TransportRoute.class));
|
||||
verify(objectRouteAssignmentRepository, never()).save(any(ObjectRouteAssignment.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeactivatePreparedRouteWhenMissionIsCompleted() {
|
||||
UnmannedVehicle vehicle = buildVehicleWithWaypoints("UV-400", "MISSION-4");
|
||||
vehicle.setMissionStatus(UnmannedVehicle.MissionStatus.COMPLETED);
|
||||
|
||||
ObjectRouteAssignment latestAssignment = ObjectRouteAssignment.builder()
|
||||
.objectName("UV-400")
|
||||
.objectType(ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.assignedRouteId(41L)
|
||||
.build();
|
||||
TransportRoute existingRoute = TransportRoute.builder()
|
||||
.id(41L)
|
||||
.routeName("UV_ROUTE_UV-400_MISSION-4")
|
||||
.routeType(TransportRoute.RouteType.UNMANNED_VEHICLE)
|
||||
.status(TransportRoute.RouteStatus.ACTIVE)
|
||||
.createdBy("RoutePreparationService")
|
||||
.routeGeometry(new GeometryFactory().createLineString(new Coordinate[]{
|
||||
new Coordinate(120.0, 36.0),
|
||||
new Coordinate(120.1, 36.1)
|
||||
}))
|
||||
.build();
|
||||
|
||||
when(objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
"UV-400",
|
||||
ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE
|
||||
)).thenReturn(Optional.of(latestAssignment));
|
||||
when(transportRouteRepository.findById(41L)).thenReturn(Optional.of(existingRoute));
|
||||
|
||||
service().synchronizeUnmannedVehicleRoute(vehicle);
|
||||
|
||||
verify(transportRouteRepository).save(existingRoute);
|
||||
assertEquals(TransportRoute.RouteStatus.INACTIVE, existingRoute.getStatus());
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ import com.qaup.collision.dataprocessing.service.SpeedCalculationService;
|
||||
import com.qaup.collision.datacollector.service.VehicleDataPersistenceService;
|
||||
import com.qaup.collision.pathconflict.service.PathConflictDetectionService;
|
||||
import com.qaup.collision.rule.service.RuleExecutionEngine;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import com.qaup.common.core.redis.RedisCache;
|
||||
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@ -55,6 +56,7 @@ class VehicleStatusUpdateIntegrationTest {
|
||||
private QuapDataAdapter quapDataAdapter;
|
||||
private RuleExecutionEngine ruleExecutionEngine;
|
||||
private RedisCache redisCache;
|
||||
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
@ -97,6 +99,7 @@ class VehicleStatusUpdateIntegrationTest {
|
||||
quapDataAdapter = mock(QuapDataAdapter.class);
|
||||
ruleExecutionEngine = mock(RuleExecutionEngine.class);
|
||||
redisCache = mock(RedisCache.class);
|
||||
platformRuntimeStateService = mock(PlatformRuntimeStateService.class);
|
||||
|
||||
lenient().when(speedCalculationService.calculateRealtimeSpeed(anyString(), anyDouble(), anyDouble(), anyLong()))
|
||||
.thenReturn(20.0);
|
||||
@ -117,7 +120,11 @@ class VehicleStatusUpdateIntegrationTest {
|
||||
ReflectionTestUtils.setField(dataProcessingService, "quapDataAdapter", quapDataAdapter);
|
||||
ReflectionTestUtils.setField(dataProcessingService, "ruleExecutionEngine", ruleExecutionEngine);
|
||||
ReflectionTestUtils.setField(dataProcessingService, "redisCache", redisCache);
|
||||
ReflectionTestUtils.setField(dataProcessingService, "platformRuntimeStateService", platformRuntimeStateService);
|
||||
dataProcessingService.setActiveMovingObjectsCache(activeMovingObjectsCache);
|
||||
|
||||
lenient().when(platformRuntimeStateService.isRegisteredForCollision(anyString(), any()))
|
||||
.thenReturn(true);
|
||||
}
|
||||
|
||||
private UniversalVehicleStatusDTO buildSampleStatus(String vehicleId) {
|
||||
@ -280,6 +287,40 @@ class VehicleStatusUpdateIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPathConflictDetectionUsesRegisteredObjectsOnly() {
|
||||
String registeredUvId = "QN001";
|
||||
String unregisteredUvId = "QN999";
|
||||
|
||||
activeMovingObjectsCache.put(registeredUvId, MovingObject.builder()
|
||||
.objectId(registeredUvId)
|
||||
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||
.objectName(registeredUvId)
|
||||
.currentPosition(geometryFactory.createPoint(new Coordinate(120.0834104, 36.35406879)))
|
||||
.altitude(0.0)
|
||||
.build());
|
||||
activeMovingObjectsCache.put(unregisteredUvId, MovingObject.builder()
|
||||
.objectId(unregisteredUvId)
|
||||
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||
.objectName(unregisteredUvId)
|
||||
.currentPosition(geometryFactory.createPoint(new Coordinate(120.0844104, 36.35506879)))
|
||||
.altitude(0.0)
|
||||
.build());
|
||||
|
||||
when(platformRuntimeStateService.isRegisteredForCollision(eq(registeredUvId), eq(MovingObjectType.UNMANNED_VEHICLE)))
|
||||
.thenReturn(true);
|
||||
when(platformRuntimeStateService.isRegisteredForCollision(eq(unregisteredUvId), eq(MovingObjectType.UNMANNED_VEHICLE)))
|
||||
.thenReturn(false);
|
||||
|
||||
dataProcessingService.performPeriodicDataProcessing();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<List<MovingObject>> movingObjectsCaptor = ArgumentCaptor.forClass(List.class);
|
||||
verify(pathConflictDetectionService).detectPathConflicts(movingObjectsCaptor.capture());
|
||||
assertEquals(1, movingObjectsCaptor.getValue().size());
|
||||
assertEquals(registeredUvId, movingObjectsCaptor.getValue().get(0).getObjectId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试无活跃车辆情况的处理
|
||||
* 如果没有活跃车辆,应该跳过处理而不是报错
|
||||
|
||||
@ -8,6 +8,7 @@ import com.qaup.collision.pathconflict.model.entity.TransportRoute;
|
||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
@ -46,6 +47,9 @@ class PathConflictDetectionDirectionalTest {
|
||||
@Mock
|
||||
private CoordinateSystemService coordinateSystemService;
|
||||
|
||||
@Mock
|
||||
private PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
@InjectMocks
|
||||
private PathConflictDetectionService pathConflictDetectionService;
|
||||
|
||||
@ -131,4 +135,34 @@ class PathConflictDetectionDirectionalTest {
|
||||
verify(conflictAlertLogRepository, never()).save(any());
|
||||
verify(eventPublisher, never()).publishEvent(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreUnmannedVehicleAndSpecialVehiclePairWithoutAircraft() throws Exception {
|
||||
GeometryFactory geometryFactory = new GeometryFactory();
|
||||
|
||||
MovingObject obj1 = MovingObject.builder()
|
||||
.objectId("UV-1")
|
||||
.objectName("UV-1")
|
||||
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||
.currentPosition(geometryFactory.createPoint(new Coordinate(0.0, 0.0)))
|
||||
.currentSpeed(20.0)
|
||||
.currentHeading(0.0)
|
||||
.altitude(0.0)
|
||||
.build();
|
||||
|
||||
MovingObject obj2 = MovingObject.builder()
|
||||
.objectId("SV-1")
|
||||
.objectName("SV-1")
|
||||
.objectType(MovingObjectType.SPECIAL_VEHICLE)
|
||||
.currentPosition(geometryFactory.createPoint(new Coordinate(0.0, 1.0)))
|
||||
.currentSpeed(20.0)
|
||||
.currentHeading(180.0)
|
||||
.altitude(0.0)
|
||||
.build();
|
||||
|
||||
pathConflictDetectionService.detectPathConflicts(List.of(obj1, obj2));
|
||||
|
||||
verify(conflictAlertLogRepository, never()).save(any());
|
||||
verify(eventPublisher, never()).publishEvent(any());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package com.qaup.collision.pathconflict.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.dataprocessing.service.CoordinateSystemService;
|
||||
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
||||
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class PathConflictDetectionServiceRuntimeConfigTest {
|
||||
|
||||
@Test
|
||||
void shouldUseRuntimeThresholdsWhenEvaluatingAlertLevel() {
|
||||
PlatformRuntimeStateService runtimeStateService = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
PathConflictDetectionService service = new PathConflictDetectionService(
|
||||
mock(TransportRouteRepository.class),
|
||||
mock(ObjectRouteAssignmentRepository.class),
|
||||
mock(ConflictAlertLogRepository.class),
|
||||
mock(ApplicationEventPublisher.class),
|
||||
mock(CoordinateSystemService.class),
|
||||
runtimeStateService
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Optional<ConflictAlertLog.AlertLevel> beforeUpdate =
|
||||
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"evaluateAlertLevel",
|
||||
140.0,
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
500.0,
|
||||
MovingObject.MovingObjectType.AIRCRAFT,
|
||||
20.0
|
||||
);
|
||||
|
||||
assertEquals(Optional.of(ConflictAlertLog.AlertLevel.CRITICAL), beforeUpdate);
|
||||
|
||||
runtimeStateService.updateRunwayAlertZoneRadiusAircraft(100.0);
|
||||
runtimeStateService.updateRunwayWarningZoneRadiusAircraft(130.0);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Optional<ConflictAlertLog.AlertLevel> afterUpdate =
|
||||
(Optional<ConflictAlertLog.AlertLevel>) ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"evaluateAlertLevel",
|
||||
140.0,
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
500.0,
|
||||
MovingObject.MovingObjectType.AIRCRAFT,
|
||||
20.0
|
||||
);
|
||||
|
||||
assertEquals(Optional.empty(), afterUpdate);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package com.qaup.collision.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PlatformRuntimeStateServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldKeepPreviousVehicleRegistrationsOnIncrementalUpdate() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
|
||||
PlatformRuntimeStateService.VehicleRegistryUpdateResult first = service.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("TQ001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||
));
|
||||
|
||||
assertEquals(3, first.updated());
|
||||
assertEquals(1, first.controllableCount());
|
||||
assertEquals(List.of("QN001"), first.controllableVehicleIDs());
|
||||
assertEquals(1, first.typesCount().get("WUREN"));
|
||||
assertEquals(1, first.typesCount().get("TEQIN"));
|
||||
assertEquals(1, first.typesCount().get("HANGKONG"));
|
||||
|
||||
PlatformRuntimeStateService.VehicleRegistryUpdateResult second = service.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("QN002", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
|
||||
assertEquals(1, second.updated());
|
||||
assertEquals(2, second.controllableCount());
|
||||
assertEquals(List.of("QN001", "QN002"), second.controllableVehicleIDs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateRuntimeThresholdValuesInMemory() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 150.0, 40.0);
|
||||
|
||||
assertEquals(200.0, service.updateRunwayWarningZoneRadiusAircraft(300.0));
|
||||
assertEquals(300.0, service.getRunwayWarningZoneRadiusAircraft());
|
||||
|
||||
assertEquals(150.0, service.updateRunwayAlertZoneRadiusAircraft(220.0));
|
||||
assertEquals(220.0, service.getRunwayAlertZoneRadiusAircraft());
|
||||
|
||||
assertEquals(40.0, service.updateCollisionDivergingReleaseDistance(55.0));
|
||||
assertEquals(55.0, service.getCollisionDivergingReleaseDistance());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOnlyAllowRegisteredCollisionTypesToParticipate() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
service.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("TQ001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("PT001", PlatformRuntimeStateService.VehicleRegistryType.PUTONG)
|
||||
));
|
||||
|
||||
assertTrue(service.isRegisteredForCollision("QN001", MovingObject.MovingObjectType.UNMANNED_VEHICLE));
|
||||
assertTrue(service.isRegisteredForCollision("TQ001", MovingObject.MovingObjectType.SPECIAL_VEHICLE));
|
||||
assertTrue(service.isRegisteredForCollision("AC001", MovingObject.MovingObjectType.AIRCRAFT));
|
||||
|
||||
assertFalse(service.isRegisteredForCollision("QN001", MovingObject.MovingObjectType.SPECIAL_VEHICLE));
|
||||
assertFalse(service.isRegisteredForCollision("PT001", MovingObject.MovingObjectType.NORMAL_VEHICLE));
|
||||
assertFalse(service.isRegisteredForCollision("UNKNOWN", MovingObject.MovingObjectType.AIRCRAFT));
|
||||
}
|
||||
}
|
||||
@ -120,6 +120,12 @@ public class SecurityConfig
|
||||
requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
|
||||
// WebSocket端点,允许匿名访问
|
||||
.requestMatchers("/collision", "/collision/**", "/VehicleCommandInfo", "/test/websocket/**").permitAll()
|
||||
.requestMatchers(
|
||||
"/api/VehicleRegistry",
|
||||
"/config/runway/warning_zone_radius/aircraft",
|
||||
"/config/runway/alert_zone_radius/aircraft",
|
||||
"/config/collision/diverging_release_distance"
|
||||
).permitAll()
|
||||
// 调试接口:仅在显式开启开关时放行(默认不放行)
|
||||
.requestMatchers(runwayPathPlanningDebugEnabled ? "/debug/runway-path-planning/**" : "/__debug_disabled__").permitAll()
|
||||
// 静态资源,可匿名访问
|
||||
|
||||
4
命令.md
4
命令.md
@ -374,3 +374,7 @@ Date: Sat, 28 Feb 2026 10:56:18 GMT
|
||||
root@root:/home/project_20250804/qaup# echo $TOKEN
|
||||
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NzIzNjA0NTYsInVzZXJuYW1lIjoiZGlhbnhpbiJ9.kTPxkoFR64eJT7eZOZWSN_ed-qvWbMFqr2WlGofBE60
|
||||
root@root:/home/project_20250804/qaup#
|
||||
|
||||
|
||||
|
||||
docker logs --since 5m qaup-app 2>&1 | grep '处理航空器数据并更新缓存' | grep -E '([A-Z]{1,3})?4963\b'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user