增加航空器路由接口的模拟和对接

This commit is contained in:
Tian jianyong 2025-07-15 16:51:31 +08:00
parent 1efb1e6b7b
commit 462bf3dc77
105 changed files with 4697 additions and 546 deletions

View File

@ -1,6 +1,6 @@
# QAUP-Management 平台管理系统
基于若依框架的综合管理平台,集成了机场冲突避免系统,提供完整的车辆管理、空间分析、实时监控等功能。
机场无人车冲突管理平台,集成了机场冲突避免系统,提供完整的车辆管理、空间分析、实时监控等功能。
## 项目架构
@ -138,5 +138,5 @@ java -jar target/qaup-admin.jar
3. 使用`QuapDataAdapter`获取车辆和司机数据避免直接访问DAO
## 版本信息
- 当前版本: 3.8.9
- 当前版本: 1.0.1
- 更新日志: 详见 `change_log.md`

View File

@ -1 +1 @@
0.3.8
0.3.9

View File

@ -5,6 +5,84 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
版本规范基于 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
## [0.3.9] - 2025-07-15
### 🚀 **新功能CA3456航空器生命周期模拟集成**
- **航空器路由和状态数据采集**
- 新增 `collectAircraftRouteAndStatus()` 方法每5秒采集航空器状态和路由信息
- 支持根据航空器状态IN/OUT/ARRIVED动态获取对应路由数据
- 实现 `AircraftRouteDTO``AircraftStatusDTO` 数据传输对象
- **航空器路由模型扩展**
- 为 `Aircraft` 类新增 `arrivalRoute`、`departureRoute` 和 `currentRoute` 字段
- 添加 `activateArrivalRoute()``activateDepartureRoute()` 便捷方法
- 支持路由时间戳和状态跟踪
- **Mock API服务完善**
- 实现CA3456状态循环模拟进港(30秒) → 停留(60秒) → 出港(30秒) → 循环
- 新增4个API端点登录、航空器状态、进港路由、出港路由
- 改进认证机制支持多种参数传递方式query、form、JSON
### 🔌 **WebSocket实时推送增强**
- **新增航空器路由更新事件**
- 创建 `AircraftRouteUpdateEvent` 事件类型
- 实现 `AircraftRouteUpdateEventListener` 事件监听器
- 支持航空器路由变更的实时WebSocket推送
- **事件推送优化**
- 路由更新事件包含完整的航班号、路由类型、状态和几何数据
- 统一事件格式,便于前端处理
### 🔧 **数据采集服务重构**
- **新增路由数据采集任务**
- 配置项:`data.collector.route.interval: 5000`5秒间隔
- 支持进港和出港路由的智能切换
- 完整的DTO到模型对象转换
- **数据转换优化**
- 实现 `convertToAircraftRoute()` 方法支持GeoJSON到JTS几何对象转换
- 坐标格式转换:`List<List<Double>>` → `List<Point>`
- 路由段数据结构完善
### 🛠️ **技术债务清理**
- **MovingObjectType枚举修复**
- 移除已废弃的 `AIRPORT_VEHICLE` 枚举常量
- 更新数据库规则配置,使用 `SPECIAL_VEHICLE` 替代
- 修复 `VehicleTypePermissionServiceImpl` 中的枚举不匹配错误
- **Mock服务认证优化**
- 改进 `check_auth()` 函数支持Bearer token验证
- 添加调试日志,便于问题排查
- 修复token格式验证逻辑
### 📊 **系统集成测试**
- **端到端测试验证**
- CA3456状态循环正常IN → ARRIVED → OUT → IN122秒周期
- 路由API正常返回进港路由F1,L4,138、出港路由138,L4,F1
- WebSocket事件推送正常航空器路由更新事件成功广播
- **数据采集验证**
- 每5秒成功采集航空器状态和路由数据
- 实时数据缓存更新正常
- 事件发布和监听机制工作正常
### 🔧 **配置文件更新**
- **application.yml配置扩展**
- 新增 `data.collector.route.interval` 配置项
- 支持路由数据采集间隔自定义
### 📝 **文档更新**
- **SQL脚本新增**
- 创建 `fix_vehicle_type_enum.sql` 修复枚举不匹配问题
- 提供完整的数据库修复方案
## [0.3.8] - 2025-07-14
### 🐛 **关键Bug修复路径冲突检测服务NullPointerException**

View File

@ -0,0 +1,232 @@
# 车辆轨迹查询和回放API文档
## 概述
本文档描述了车辆轨迹查询和回放功能的API接口支持无人车轨迹回放和车辆行驶信息轨迹回放需求。
## 接口列表
### 1. 车辆轨迹查询接口
**接口地址:** `GET /system/vehicle_location/trajectory/{vehicleId}`
**功能描述:** 查询指定车辆在指定时间段内的运行轨迹,支持数据简化和采样优化
**请求参数:**
- `vehicleId` (路径参数): 车辆ID
- `startTime` (查询参数): 开始时间格式yyyy-MM-dd HH:mm:ss
- `endTime` (查询参数): 结束时间格式yyyy-MM-dd HH:mm:ss
- `simplified` (查询参数,可选): 是否简化数据默认false
- `maxPoints` (查询参数,可选): 最大轨迹点数量默认1000
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"vehicleId": 1,
"licensePlate": "测A12345",
"vehicleType": "无人车",
"brand": "品牌A",
"owningUnit": "测试单位",
"startTime": "2025-01-17 08:00:00",
"endTime": "2025-01-17 18:00:00",
"statistics": {
"pointCount": 360,
"totalDistance": 15.68,
"averageSpeed": 25.5,
"maxSpeed": 45.2,
"durationSeconds": 3600,
"durationFormatted": "1小时0分钟"
},
"points": [
{
"longitude": 116.397128,
"latitude": 39.916527,
"altitude": 45.5,
"speed": 25.8,
"heading": 135.5,
"timestamp": "2025-01-17 08:00:00",
"dataQuality": "HIGH"
}
],
"simplified": false
}
}
```
### 2. 轨迹回放数据接口
**接口地址:** `GET /system/vehicle_location/trajectory/{vehicleId}/playback`
**功能描述:** 获取用于轨迹回放的数据,按指定时间间隔采样,包含回放所需的序列和时间信息
**请求参数:**
- `vehicleId` (路径参数): 车辆ID
- `startTime` (查询参数): 开始时间格式yyyy-MM-dd HH:mm:ss
- `endTime` (查询参数): 结束时间格式yyyy-MM-dd HH:mm:ss
- `intervalSeconds` (查询参数,可选): 时间间隔(秒)默认10
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"vehicleId": 1,
"licensePlate": "测A12345",
"sequenceNumber": 1,
"longitude": 116.397128,
"latitude": 39.916527,
"speed": 25.8,
"timestamp": "2025-01-17 08:00:00",
"intervalMs": 0,
"cumulativeDistance": 0.0,
"progressPercentage": 0.0,
"isKeyFrame": true,
"keyFrameType": "START",
"suggestedDelay": 1000
},
{
"vehicleId": 1,
"licensePlate": "测A12345",
"sequenceNumber": 2,
"longitude": 116.398128,
"latitude": 39.917527,
"speed": 30.0,
"timestamp": "2025-01-17 08:00:10",
"intervalMs": 10000,
"cumulativeDistance": 125.6,
"progressPercentage": 25.5,
"isKeyFrame": false,
"suggestedDelay": 5000
}
]
}
```
### 3. 多车辆轨迹查询接口
**接口地址:** `POST /system/vehicle_location/trajectory/batch`
**功能描述:** 批量查询多个车辆的运行轨迹,支持复杂查询条件
**请求体示例:**
```json
{
"vehicleIds": [1, 2, 3],
"startTime": "2025-01-17 08:00:00",
"endTime": "2025-01-17 18:00:00",
"simplified": false,
"maxPoints": 1000,
"includeStatistics": true,
"includeGeoJson": false
}
```
### 4. 区域内轨迹查询接口
**接口地址:** `GET /system/vehicle_location/trajectory/{vehicleId}/area`
**功能描述:** 查询车辆在指定区域内的运行轨迹
**请求参数:**
- `vehicleId` (路径参数): 车辆ID
- `startTime` (查询参数): 开始时间
- `endTime` (查询参数): 结束时间
- `areaWkt` (查询参数): 区域WKT格式字符串
### 5. 轨迹统计信息查询接口
**接口地址:** `GET /system/vehicle_location/trajectory/{vehicleId}/statistics`
**功能描述:** 获取车辆轨迹的统计信息,如总距离、平均速度、行驶时长等
**请求参数:**
- `vehicleId` (路径参数): 车辆ID
- `startTime` (查询参数): 开始时间
- `endTime` (查询参数): 结束时间
## 使用场景
### 轨迹查询 vs 轨迹回放
1. **轨迹查询接口** 适用于:
- 静态轨迹展示
- 轨迹分析
- 数据导出
- 历史轨迹查看
2. **轨迹回放接口** 适用于:
- 动画回放
- 实时模拟
- 时间轴播放
- 交互式回放控制
## 技术特性
- **智能采样**:根据时间间隔和点数限制自动优化轨迹数据
- **统计计算**利用PostGIS计算精确的距离和轨迹统计
- **回放优化**:提供专门的回放数据格式,支持前端动画
- **性能优化**:通过索引和查询优化支持大数据量轨迹查询
- **空间查询**:支持区域内轨迹过滤功能
## 错误码
- `200`: 操作成功
- `400`: 参数错误
- `404`: 数据不存在
- `500`: 服务器内部错误
## 测试命令
1. 轨迹查询接口
```bash
curl -X GET "http://localhost:8080/system/vehicle_location/trajectory/5?startTime=2025-01-01%2000:00:00&endTime=2025-10-17%2023:59:59&simplified=false&maxPoints=100" \
-H "Content-Type: application/json" \
-w "\nHTTP Status: %{http_code}\n"
```
2. 轨迹回放接口
```bash
curl -X GET "http://localhost:8080/system/vehicle_location/trajectory/5/playback?startTime=2025-01-01%2000:00:00&endTime=2025-10-17%2023:59:59&intervalSeconds=10" \
-H "Content-Type: application/json" \
-w "\nHTTP Status: %{http_code}\n"
```
3. 轨迹统计信息接口
```bash
curl -X GET "http://localhost:8080/system/vehicle_location/trajectory/5/statistics?startTime=2025-01-01%2000:00:00&endTime=2025-10-17%2023:59:59" \
-H "Content-Type: application/json" \
-w "\nHTTP Status: %{http_code}\n"
```
4. 批量轨迹查询接口
```bash
curl -X POST "http://localhost:8080/system/vehicle_location/trajectory/batch" \
-H "Content-Type: application/json" \
-d '{
"vehicleIds": [5, 6],
"startTime": "2025-01-01 00:00:00",
"endTime": "2025-10-17 23:59:59",
"simplified": false,
"maxPoints": 100,
"includeStatistics": true
}' \
-w "\nHTTP Status: %{http_code}\n"
```
5. 区域内轨迹查询接口
```bash
curl -X GET "http://localhost:8080/system/vehicle_location/trajectory/5/area?startTime=2025-01-01%2000:00:00&endTime=2025-10-17%2023:59:59&areaWkt=POLYGON((116.3%2039.9,116.4%2039.9,116.4%2040.0,116.3%2040.0,116.3%2039.9))" \
-H "Content-Type: application/json" \
-w "\nHTTP Status: %{http_code}\n"
```

View File

@ -7,6 +7,252 @@ QAUP是一个集成的机场车辆管理系统包含
- **实时位置监控**基于PostGIS的空间数据管理
- **碰撞避免系统**:实时安全规则检测和预警
- **轨迹管理**:车辆历史路径记录和分析
- **航空器生命周期模拟**CA3456航空器状态和路由模拟系统
## API接口测试
### 航空器生命周期模拟API
系统提供了完整的航空器生命周期模拟API支持CA3456航空器的状态循环和路由管理。
#### 1. 登录认证
```bash
# 基本登录(推荐)
curl -X POST "http://localhost:8090/login?username=dianxin&password=dianxin@123"
# 使用表单数据登录
curl -X POST "http://localhost:8090/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=dianxin&password=dianxin@123"
# 使用JSON数据登录
curl -X POST "http://localhost:8090/login" \
-H "Content-Type: application/json" \
-d '{"username":"dianxin","password":"dianxin@123"}'
```
**预期响应:**
```json
{
"data": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY",
"msg": "登入成功",
"status": 200
}
```
#### 2. 航空器状态查询
```bash
# 查询CA3456当前状态
curl -X GET "http://localhost:8090/aircraftStatusController/getAircraftStatus" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY"
```
**预期响应:**
```json
{
"data": {
"contactCross": "F1",
"flightNo": "CA3456",
"inRunway": "35",
"outRunway": "34",
"seat": "138",
"timestamp": 1752567616199,
"type": "IN" // 状态: IN(进港), ARRIVED(到达), OUT(出港)
},
"msg": "航空器状态查询成功",
"status": 200
}
```
#### 3. 进港路由查询
```bash
# 查询进港滑行路线
curl -X GET "http://localhost:8090/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&outRunway=34&contactCross=F1&seat=138" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY"
```
**预期响应:**
```json
{
"data": {
"type": "IN",
"status": "COMPLETE",
"codes": "F1,L4,138",
"geometry": null,
"geoPath": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[120.086263, 36.370484],
[120.085000, 36.370000],
[120.084000, 36.369500],
[120.083000, 36.369000],
[120.082000, 36.368500],
[120.081000, 36.368000],
[120.080996, 36.369105]
]
},
"properties": {
"code": "L4"
}
}
]
}
},
"msg": "进港滑行路线查询成功",
"status": 200
}
```
#### 4. 出港路由查询
```bash
# 查询出港滑行路线
curl -X GET "http://localhost:8090/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&outRunway=34&startSeat=138" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY"
```
**预期响应:**
```json
{
"data": {
"type": "OUT",
"status": "COMPLETE",
"codes": "138,L4,F1",
"geometry": null,
"geoPath": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[120.080996, 36.369105],
[120.081000, 36.368000],
[120.082000, 36.368500],
[120.083000, 36.369000],
[120.084000, 36.369500],
[120.085000, 36.370000],
[120.086263, 36.370484]
]
},
"properties": {
"code": "L4"
}
}
]
}
},
"msg": "出港滑行路线查询成功",
"status": 200
}
```
#### 5. 状态循环测试
```bash
# 连续监控CA3456状态变化
for i in {1..6}; do
echo "=== 第 $i 次调用 ($(date +%H:%M:%S)) ==="
curl -s -X GET "http://localhost:8090/aircraftStatusController/getAircraftStatus" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY" \
| python3 -c "import sys,json; data=json.load(sys.stdin); print(f'状态: {data[\"data\"][\"type\"]}, 航班号: {data[\"data\"][\"flightNo\"]}, 时间戳: {data[\"data\"][\"timestamp\"]}')"
sleep 15
done
```
**预期输出:**
```
=== 第 1 次调用 (16:22:45) ===
状态: IN, 航班号: CA3456, 时间戳: 1752567765233
=== 第 2 次调用 (16:23:00) ===
状态: IN, 航班号: CA3456, 时间戳: 1752567780372
=== 第 3 次调用 (16:23:15) ===
状态: ARRIVED, 航班号: CA3456, 时间戳: 1752567795496
=== 第 4 次调用 (16:23:30) ===
状态: ARRIVED, 航班号: CA3456, 时间戳: 1752567810629
=== 第 5 次调用 (16:23:45) ===
状态: IN, 航班号: CA3456, 时间戳: 1752567825769
=== 第 6 次调用 (16:24:00) ===
状态: IN, 航班号: CA3456, 时间戳: 1752567840897
```
#### 6. 路由API简化测试
```bash
# 简化的路由测试
echo "=== 测试进港路由 ==="
curl -s -X GET "http://localhost:8090/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&outRunway=34&contactCross=F1&seat=138" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY" \
| python3 -c "import sys,json; data=json.load(sys.stdin); print(f'路由类型: {data[\"data\"][\"type\"]}, 编码: {data[\"data\"][\"codes\"]}, 状态: {data[\"data\"][\"status\"]}')"
echo "=== 测试出港路由 ==="
curl -s -X GET "http://localhost:8090/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat?inRunway=35&outRunway=34&startSeat=138" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY" \
| python3 -c "import sys,json; data=json.load(sys.stdin); print(f'路由类型: {data[\"data\"][\"type\"]}, 编码: {data[\"data\"][\"codes\"]}, 状态: {data[\"data\"][\"status\"]}')"
```
### CA3456生命周期说明
CA3456航空器按照以下周期进行状态模拟
1. **进港阶段30秒**`type = "IN"`
- 航空器正在进场,执行进港滑行
- 使用进港路由F1,L4,138
2. **停留阶段60秒**`type = "ARRIVED"`
- 航空器已到达机位,进行客货作业
- 停留在138机位
3. **出港阶段30秒**`type = "OUT"`
- 航空器开始出港,执行出港滑行
- 使用出港路由138,L4,F1
4. **循环周期**总计122秒包含2秒状态切换时间
### WebSocket连接测试
系统支持WebSocket实时推送可以通过以下方式连接
```javascript
// WebSocket连接示例
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'aircraftRouteUpdate') {
console.log('收到航空器路由更新:', data.data);
}
};
```
### 故障排除
#### 常见错误
1. **认证失败401**
- 检查token是否正确
- 确认Authorization header格式`Bearer <token>`
2. **连接超时**
- 检查Mock服务是否运行在端口8090
- 确认Java服务是否运行在端口8080
3. **状态不变化**
- 等待完整的状态周期122秒
- 检查Mock服务日志
#### 服务状态检查
```bash
# 检查Mock服务状态
curl -I http://localhost:8090/login
# 检查Java服务状态
curl -I http://localhost:8080/actuator/health
```
## 核心数据结构

View File

@ -0,0 +1,30 @@
## 航空器状态接口(模拟)
### 1.1 航空器进出港状态
1.1.1 接口地址http://IP:端口/aircraftStatusController/getAircraftStatus
1.1.2 请求方式get
1.1.3 请求参数:
1.1.4 返回格式json
type进出港类型 IN进港 OUT出港
flightNo航班号 CA3456
inRunway :进港跑道编号 35
outRunway出港跑道编号 34
contactCross联络道口 F1
seat目的机位 138
timestamp时间戳 1715769600000
示例:
{
"type": "IN",
"flightNo": "CA3456",
"inRunway": "35",
"outRunway": "34",
"contactCross": "F1",
"seat": "138",
"timestamp": 1715769600000
}

10
doc/requirement/area.md Normal file
View File

@ -0,0 +1,10 @@
## 区域设置(测试)
### 电子围栏
- 无人车 AB567
POLYGON ((120.08483597765024 36.36489863472901, 120.08549104408633 36.36594996564427, 120.08416649728491 36.36673180599232, 120.08378219088705 36.36561378252756, 120.08483597765024 36.36489863472901))
- 无人车 B B579
POLYGON ((120.08646134220226 36.36997466676866, 120.086830266589 36.37063980458865, 120.08515882034526 36.37156622537344, 120.08543839423878 36.37073891647525, 120.0851419887492 36.370416189872486, 120.0858980160674 36.36986114517269, 120.08646134220226 36.36997466676866))

View File

@ -2,6 +2,54 @@
## 需求列表(按时间跟踪)
### 2025-06-03
- 需求:无人车轨迹回放
- 分析:
- 需要对无人车的运行轨迹进行查询和显示
- 功能模块:
- 数据存储模块:存储无人车的位置变更数据(具备)
- 数据处理模块根据无人车的位置历史数据提供轨迹查询API新增
- 前端展示模块:对无人车的运行轨迹进行查询和显示(新增)
### 2025-06-02
- 需求:无人车路由导入
- 分析:
- 需要对无人车的路由(文件形式)进行导入
- 需要对无人车的路由列表进行查询和显示
- 功能模块:
- 后台管理模块:导入无人车的路由列表(新增)并提供查询接口
- 数据存储模块:存储无人车的路由列表(新增)
- 技术细节:
- 路由数据结构(编号、名称、路径地理信息、生效时间段、优先级等)
- 导入功能支持文件上传和解析CSV、JSON等格式
- 查询功能:提供分页查询和条件过滤
- 显示功能:在前端页面展示路由列表和详情
### 2025-06-01
- 需求:采集和显示无人车的运行状态
- 分析:
- 需要获取无人车的运行状态数据(当前路径、运行时间、运行距离、电池电量等)
- 需要对无人车的运行状态进行显示
- 功能模块:
- 数据采集模块:获取无人车的运行状态数据(新增)
- 数据存储模块:存储无人车的运行状态数据(新增)
- 数据处理模块根据无人车的运行状态数据发送websocket无人车状态消息到前端新增
### 2025-05-01
- 需求:基于路径的碰撞预警,当航空器或机场车辆的路径与无人车路径之间可能发生碰撞时,进行预警或报警
- 分析:
- 需要获取车辆的实时位置数据
- 需要获取航空器和车辆的路径数据
- 需要对车辆进行碰撞避免预警
- 功能模块:
- 数据采集模块:获取车辆的实时位置数据(具备)和路径数据(新增)
- 数据存储模块:存储车辆的实时位置数据和路径数据(增加路径数据)
- 数据处理模块:根据车辆的实时位置数据,对车辆进行碰撞避免预警(新增)
### 2025-05-01
- 需求:

View File

@ -0,0 +1,895 @@
## 航空器滑行路由接口
### 1.1.登录认证
1、登录接口http://IP:端口/login
2、请求方式post
3、参数username、password
4、示例127.0.0.1:8080/login?username=XXXX&password=XXXX
5、返回值 data 为返回的鉴权token后续接口需要再header中携带data所有的数据是一个token不要截断
示例:{
    "status": 200,
    "msg": "登入成功",
    "data": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY"
}
### 1.2.Token续时
1、登录接口http://IP:端口/userInfoController/refreshToken
2、请求方式get
3、参数header 携带登录返回的token
4、示例127.0.0.1:8080/userInfoController/refreshToken
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY
5、返回值 data 为返回的鉴权token后续接口需要再header中携带data所有的数据是一个token不要截断
示例:{
    "status": 200,
    "msg": "OK",
    "data": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzQ0OTA0NTcsInVzZXJuYW1lIjoiZGlhbnhpbiJ9.1Ss3Gd6ijjmUmVW2DFwQF7lsdUwlB0m1TP73zf-B5mk"
}
### 1.3.航空器进港滑行路线
1、接口地址 http://IP:端口/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat
2、请求方式 get
3、返回格式以对象形式数据返回
4、请求接口示例
参数:
inRunway :进港跑道编号 35
outRunway出港跑道编号 34
contactCross联络道口 F1
seat目的机位 138
5、数据结构
type:进出港类型
status接口状态
codes滑行线编码
geoPath返回路线
{
typeJeoJson集合
geometry返回的路线JeoJson集合,前端直接绘制即可拿出features直接绘制JeoJson即可
}
示例:
{
    "type": "IN",
    "status": "COMPLETE",
    "codes": "F1,L4,138",
    "geometry": null,
    "geoPath": {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050742275893088E7,
                            4026164.644604296
                        ],
                        [
                            4.050742342874898E7,
                            4026162.545793306
                        ]
                    ]
                },
                "properties": {
                    "code": "L4"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050743615407222E7,
                            4026122.672208275
                        ],
                        [
                            4.050743684026714E7,
                            4026120.146600441
                        ],
                        [
                            4.050743730372977E7,
                            4026117.570797326
                        ],
                        [
                            4.050743754093282E7,
                            4026114.964402468
                        ],
                        [
                            4.050743757419489E7,
                            4026113.602043673
                        ],
                        [
                            4.050743755007106E7,
                            4026112.347252104
                        ],
                        [
                            4.050743733107493E7,
                            4026109.739264329
                        ],
                        [
                            4.050743688561112E7,
                            4026107.160287504
                        ]
                    ]
                },
                "properties": {
                    "code": "L4"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050717462298063E7,
                            4026091.904402129
                        ],
                        [
                            4.050716820216861E7,
                            4026089.855066455
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050722536188381E7,
                            4026108.097315812
                        ],
                        [
                            4.050720821283463E7,
                            4026102.624334418
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050727144214725E7,
                            4026112.527790001
                        ],
                        [
                            4.050726278505515E7,
                            4026114.415332655
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050731882638656E7,
                            4026102.196402456
                        ],
                        [
                            4.050727312768086E7,
                            4026112.160285922
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050738651815705E7,
                            4026087.437277401
                        ],
                        [
                            4.050734647450486E7,
                            4026096.168165339
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050714461981621E7,
                            4026082.328947974
                        ],
                        [
                            4.05071119278174E7,
                            4026071.895744022
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050734647450486E7,
                            4026096.168165339
                        ],
                        [
                            4.050733913391775E7,
                            4026097.768664928
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050689454491971E7,
                            4026002.519737061
                        ],
                        [
                            4.050693265139649E7,
                            4026014.681113256
                        ],
                        [
                            4.050697075787329E7,
                            4026026.842489458
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050741162298967E7,
                            4026083.825606086
                        ],
                        [
                            4.050741416963529E7,
                            4026084.285112275
                        ],
                        [
                            4.050741669524226E7,
                            4026084.971307588
                        ],
                        [
                            4.050741915143272E7,
                            4026085.875012957
                        ],
                        [
                            4.050742151951354E7,
                            4026086.989350639
                        ],
                        [
                            4.050742378146222E7,
                            4026088.305839852
                        ],
                        [
                            4.050742592006397E7,
                            4026089.814461317
                        ],
                        [
                            4.050742791904272E7,
                            4026091.503733515
                        ],
                        [
                            4.050742976318505E7,
                            4026093.360800063
                        ],
                        [
                            4.050743143845592E7,
                            4026095.371527565
                        ],
                        [
                            4.050743293210549E7,
                            4026097.52061317
                        ],
                        [
                            4.050743423276621E7,
                            4026099.791701039
                        ],
                        [
                            4.050743533053925E7,
                            4026102.167506821
                        ],
                        [
                            4.05074362170699E7,
                            4026104.629949201
                        ],
                        [
                            4.050743683431807E7,
                            4026106.966150228
                        ],
                        [
                            4.050743688561112E7,
                            4026107.160287504
                        ]
                    ]
                },
                "properties": {
                    "code": {}
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050697552118288E7,
                            4026028.362661481
                        ],
                        [
                            4.050697075787329E7,
                            4026026.842489458
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050704221346159E7,
                            4026049.646966941
                        ],
                        [
                            4.050703137036284E7,
                            4026046.18647901
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050708746004742E7,
                            4026064.087051627
                        ],
                        [
                            4.050704840096232E7,
                            4026051.621473066
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.05071119278174E7,
                            4026071.895744022
                        ],
                        [
                            4.050710556055213E7,
                            4026069.863682419
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050741939071107E7,
                            4026175.198599438
                        ],
                        [
                            4.05074216811156E7,
                            4026168.021835575
                        ],
                        [
                            4.050742275893088E7,
                            4026164.644604296
                        ]
                    ]
                },
                "properties": {
                    "code": "L4"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050753774654577E7,
                            4026246.448261945
                        ],
                        [
                            4.050749515081406E7,
                            4026236.848849251
                        ],
                        [
                            4.050744870329395E7,
                            4026226.381394062
                        ]
                    ]
                },
                "properties": {
                    "code": "138"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050753774654577E7,
                            4026246.448261945
                        ],
                        [
                            4.0507613391983E7,
                            4026263.495786141
                        ],
                        [
                            4.05076192451935E7,
                            4026264.814870958
                        ],
                        [
                            4.050762119626365E7,
                            4026265.254565894
                        ]
                    ]
                },
                "properties": {
                    "code": "138"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050742342874898E7,
                            4026162.545793306
                        ],
                        [
                            4.050743615407222E7,
                            4026122.672208275
                        ]
                    ]
                },
                "properties": {
                    "code": "L4"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050716820216861E7,
                            4026089.855066455
                        ],
                        [
                            4.050714461981621E7,
                            4026082.328947974
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050720821283463E7,
                            4026102.624334418
                        ],
                        [
                            4.050717462298063E7,
                            4026091.904402129
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050726278505515E7,
                            4026114.415332655
                        ],
                        [
                            4.050725934642086E7,
                            4026115.009285077
                        ],
                        [
                            4.050725586910526E7,
                            4026115.301280484
                        ],
                        [
                            4.050725237957282E7,
                            4026115.289096617
                        ],
                        [
                            4.050724890438099E7,
                            4026114.9728262
                        ],
                        [
                            4.050724546997807E7,
                            4026114.354876244
                        ],
                        [
                            4.050724210250195E7,
                            4026113.43994972
                        ],
                        [
                            4.050722536188381E7,
                            4026108.097315812
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050727312768086E7,
                            4026112.160285922
                        ],
                        [
                            4.050727144214725E7,
                            4026112.527790001
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050733913391775E7,
                            4026097.768664928
                        ],
                        [
                            4.050731882638656E7,
                            4026102.196402456
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.05074011833275E7,
                            4026084.239767811
                        ],
                        [
                            4.050738651815705E7,
                            4026087.437277401
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.05074011833275E7,
                            4026084.239767811
                        ],
                        [
                            4.050740376230329E7,
                            4026083.794303922
                        ],
                        [
                            4.050740637029002E7,
                            4026083.5753078
                        ],
                        [
                            4.050740898743934E7,
                            4026083.584446135
                        ],
                        [
                            4.050741162298967E7,
                            4026083.825606086
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050744870329395E7,
                            4026226.381394062
                        ],
                        [
                            4.050744533581797E7,
                            4026225.466467002
                        ],
                        [
                            4.050744206089737E7,
                            4026224.261526533
                        ],
                        [
                            4.050743890345625E7,
                            4026222.775742978
                        ],
                        [
                            4.050743588752466E7,
                            4026221.020424048
                        ],
                        [
                            4.050743303605565E7,
                            4026219.00892878
                        ],
                        [
                            4.050743037075064E7,
                            4026216.756565868
                        ],
                        [
                            4.050742791189419E7,
                            4026214.280477153
                        ],
                        [
                            4.050742567819968E7,
                            4026211.599507165
                        ],
                        [
                            4.050742368666689E7,
                            4026208.734059705
                        ],
                        [
                            4.050742195245258E7,
                            4026205.705942559
                        ],
                        [
                            4.050742048875517E7,
                            4026202.538201526
                        ],
                        [
                            4.050741930671428E7,
                            4026199.254945029
                        ],
                        [
                            4.050741841532595E7,
                            4026195.88116063
                        ],
                        [
                            4.050741782137419E7,
                            4026192.442524868
                        ],
                        [
                            4.050741752937933E7,
                            4026188.965207836
                        ],
                        [
                            4.050741754156362E7,
                            4026185.475674017
                        ],
                        [
                            4.050741785783435E7,
                            4026182.000480871
                        ],
                        [
                            4.050741847578449E7,
                            4026178.566076715
                        ],
                        [
                            4.050741939071107E7,
                            4026175.198599438
                        ]
                    ]
                },
                "properties": {
                    "code": {}
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050684675534101E7,
                            4025987.268076611
                        ],
                        [
                            4.050685643844293E7,
                            4025990.358360866
                        ],
                        [
                            4.050689454491971E7,
                            4026002.519737061
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050703137036284E7,
                            4026046.18647901
                        ],
                        [
                            4.05070295640735E7,
                            4026045.610016264
                        ],
                        [
                            4.050701981996215E7,
                            4026042.500261312
                        ],
                        [
                            4.050697623399237E7,
                            4026028.590148907
                        ],
                        [
                            4.050697552118288E7,
                            4026028.362661481
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050704840096232E7,
                            4026051.621473066
                        ],
                        [
                            4.050704221346159E7,
                            4026049.646966941
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            },
            {
                "type": "Feature",
                "geometry": {
                    "type": "LineString",
                    "coordinates": [
                        [
                            4.050710556055213E7,
                            4026069.863682419
                        ],
                        [
                            4.050708746004742E7,
                            4026064.087051627
                        ]
                    ]
                },
                "properties": {
                    "code": "F1"
                }
            }
        ]
    }
}
### 1.4.航空器出港滑行路线
1、接口地址 http://IP:端口/runwayPathPlanningController
/findDepTaxiwayByRunwayAndContactCrossAndSeat
2、请求方式 get
3、返回格式以对象形式数据返回
4、请求接口示例
inRunway :进港跑道编号
outRunway出港跑道编号
startSeat起始机位
5、数据结构
type:进出港类型
status接口状态
codes滑行线编码
geoPath返回路线
{
typeJeoJson集合
geometry返回的路线JeoJson集合,前端直接绘制即可
}
数据返回示例与1.3一致

View File

@ -65,7 +65,6 @@ SELECT * FROM vehicle_complete_info LIMIT 5;
- `sys_vehicle_info` - 车辆基础信息
- `sys_driver_info` - 驾驶员信息
- `sys_vehicle_type` - 车辆类型
- 其他若依框架基础表...
### 新增表来自CollisionAvoidanceSystem
- `vehicle_locations` - 车辆实时位置支持PostGIS空间查询

View File

@ -7,7 +7,7 @@
将CollisionAvoidanceSystem项目的空间数据表合并到QAUP-Management项目数据库中实现统一的数据库架构。
# 项目概述
QAUP-Management是基于若依框架的车辆管理系统CollisionAvoidanceSystem是机场碰撞避免系统。两个项目都使用PostgreSQL数据库表结构完全互补无冲突。
QAUP-Management是车辆管理系统CollisionAvoidanceSystem是机场碰撞避免系统。两个项目都使用PostgreSQL数据库表结构完全互补无冲突。
---
*以下部分由 AI 在协议执行过程中维护*

View File

@ -23,7 +23,7 @@ psql -h localhost -U postgres
# 连接到qaup数据库
psql -h localhost -U postgres -d qaup
# 导入若依框架的基础表结构和数据
# 导入基础表结构和数据
\i /path/to/QAUP-Management/sql/postgresql.sql
```

View File

@ -2,7 +2,7 @@
**项目名称**: QAUP 机场车辆管理和碰撞避免系统
**优化日期**: 2025-01-15
**项目版本**: 3.8.9
**项目版本**: 1.0.1
**分析人员**: Claude Code Assistant
## 📋 执行摘要

View File

@ -14,7 +14,7 @@
## 项目概述
QAUP机场车辆管理系统包含
- QAUP-Management基础车辆信息管理(若依框架)
- QAUP-Management基础车辆信息管理
- CollisionAvoidanceSystem实时位置监控和碰撞避免
---

View File

@ -21,7 +21,7 @@
**QAUP-Management技术栈**
- Spring Boot 2.5.15 + Java 8
- MyBatis + PostgreSQL
- 若依框架(用户权限、菜单管理等)
- 用户权限、菜单管理等)
- Maven多模块结构qaup-admin、qaup-system、qaup-framework等
**CollisionAvoidanceSystem技术栈**

View File

@ -33,7 +33,7 @@
## 🔧 数据适配器架构
### QuapDataAdapter功能
- **数据桥接**: 连接若依Service层与CollisionAvoidanceSystem
- **数据桥接**: 连接Service层与CollisionAvoidanceSystem
- **类型转换**: SysVehicleInfo ↔ VehicleLocation
- **统一接口**: 避免重复编写DAO组件
- **测试覆盖**: 15个单元测试全部通过

View File

@ -6,10 +6,10 @@
<groupId>com.qaup</groupId>
<artifactId>qaup-management</artifactId>
<version>3.8.9</version>
<version>1.0.1</version>
<properties>
<qaup.version>3.8.9</qaup.version>
<qaup.version>1.0.1</qaup.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>qaup-management</artifactId>
<groupId>com.qaup</groupId>
<version>3.8.9</version>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>

View File

@ -1,19 +1,28 @@
package com.qaup.web.controller.system;
import java.util.Date;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.qaup.common.annotation.Log;
import com.qaup.common.core.controller.BaseController;
import com.qaup.common.core.domain.AjaxResult;
import com.qaup.common.enums.BusinessType;
import com.qaup.system.domain.SysVehicleLocation;
import com.qaup.system.domain.vo.VehicleTrajectoryVO;
import com.qaup.system.domain.vo.VehicleLocationPlaybackVO;
import com.qaup.system.domain.vo.TrajectoryStatistics;
import com.qaup.system.domain.dto.TrajectoryQueryRequest;
import com.qaup.system.service.ISysVehicleLocationService;
import com.qaup.common.utils.poi.ExcelUtil;
import com.qaup.common.core.page.TableDataInfo;
@ -107,4 +116,80 @@ public class SysVehicleLocationController extends BaseController
SysVehicleLocation sysVehicleLocation = sysVehicleLocationService.selectLatestSysVehicleLocationByLicensePlate(licensePlate);
return success(sysVehicleLocation);
}
}
/**
* 车辆轨迹查询接口
*/
@Operation(summary = "车辆轨迹查询", description = "查询指定车辆在指定时间段内的运行轨迹,支持数据简化和采样优化")
@GetMapping("/trajectory/{vehicleId}")
public AjaxResult getVehicleTrajectory(
@PathVariable("vehicleId") Long vehicleId,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime,
@RequestParam(defaultValue = "false") Boolean simplified,
@RequestParam(defaultValue = "1000") Integer maxPoints)
{
VehicleTrajectoryVO trajectory = sysVehicleLocationService
.getVehicleTrajectory(vehicleId, startTime, endTime, simplified, maxPoints);
return success(trajectory);
}
/**
* 多车辆轨迹查询接口
*/
@Operation(summary = "多车辆轨迹查询", description = "批量查询多个车辆的运行轨迹,支持复杂查询条件")
@PostMapping("/trajectory/batch")
public AjaxResult getBatchVehicleTrajectory(@Valid @RequestBody TrajectoryQueryRequest request)
{
List<VehicleTrajectoryVO> trajectories = sysVehicleLocationService
.getBatchVehicleTrajectory(request);
return success(trajectories);
}
/**
* 轨迹回放数据接口
*/
@Operation(summary = "轨迹回放数据查询", description = "获取用于轨迹回放的数据,按指定时间间隔采样,包含回放所需的序列和时间信息")
@GetMapping("/trajectory/{vehicleId}/playback")
public AjaxResult getTrajectoryPlaybackData(
@PathVariable("vehicleId") Long vehicleId,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime,
@RequestParam(defaultValue = "10") Integer intervalSeconds)
{
List<VehicleLocationPlaybackVO> playbackData = sysVehicleLocationService
.getTrajectoryPlaybackData(vehicleId, startTime, endTime, intervalSeconds);
return success(playbackData);
}
/**
* 区域内轨迹查询接口
*/
@Operation(summary = "区域内轨迹查询", description = "查询车辆在指定区域内的运行轨迹")
@GetMapping("/trajectory/{vehicleId}/area")
public AjaxResult getVehicleTrajectoryInArea(
@PathVariable("vehicleId") Long vehicleId,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime,
@RequestParam String areaWkt)
{
VehicleTrajectoryVO trajectory = sysVehicleLocationService
.getVehicleTrajectoryInArea(vehicleId, startTime, endTime, areaWkt);
return success(trajectory);
}
/**
* 轨迹统计信息查询接口
*/
@Operation(summary = "轨迹统计信息查询", description = "获取车辆轨迹的统计信息,如总距离、平均速度、行驶时长等")
@GetMapping("/trajectory/{vehicleId}/statistics")
public AjaxResult getTrajectoryStatistics(
@PathVariable("vehicleId") Long vehicleId,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime)
{
TrajectoryStatistics statistics = sysVehicleLocationService
.getTrajectoryStatistics(vehicleId, startTime, endTime);
return success(statistics);
}
}

View File

@ -3,7 +3,7 @@ qaup:
# 名称
name: Qaup
# 版本
version: 3.8.9
version: 1.0.1
# 版权年份
copyrightYear: 2025
# 文件路径 示例( Windows配置D:/qaup/uploadPathLinux配置 /home/qaup/uploadPath
@ -113,7 +113,7 @@ spring:
# JPA配置collision模块空间数据处理
jpa:
hibernate:
ddl-auto: none # 使用若依的数据库管理方式
ddl-auto: none # 使用数据库管理方式
show-sql: false
properties:
hibernate:
@ -178,19 +178,26 @@ xss:
data:
collector:
# 数据采集间隔,单位:毫秒(高频采集保证数据新鲜度)
interval: 1000
interval: 250
# 路由数据采集间隔,单位:毫秒(较低频率采集路由信息)
route:
interval: 5000
# 检测和推送间隔配置
detection:
# 检测间隔单位毫秒控制围栏检测、冲突检测、违规检测和WebSocket推送频率
interval: 5000
interval: 1000
# 机场数据源配置
airport-api:
base-url: http://localhost:8090
endpoints:
login: /login
refresh: /userInfoController/refreshToken
aircraft: /openApi/getCurrentFlightPositions
vehicle: /openApi/getCurrentVehiclePositions
refresh: /refresh
arrival-route: /runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat
departure-route: /runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat
aircraft-status: /aircraftStatusController/getAircraftStatus
auth:
username: dianxin
password: dianxin@123
@ -201,7 +208,7 @@ data:
vehicle-location: /api/VehicleLocationInfo
vehicle-state: /api/VehicleStateInfo
vehicle-command: /api/VehicleCommandInfo
timeout: 5000
timeout: 1000
retry-attempts: 3
# 无人车数据持久化配置
unmanned-vehicle:
@ -211,7 +218,7 @@ data:
location-retention-days: 90
command-retention-days: 365
command:
timeout: 5000
timeout: 1000
retry-attempts: 3
validation:
enabled: true

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>qaup-management</artifactId>
<groupId>com.qaup</groupId>
<version>3.8.9</version>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -23,7 +23,6 @@ import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* 区域配置导入服务 - 将YAML配置文件中的区域数据导入到PostGIS数据库
@ -238,7 +237,6 @@ public class AreaConfigImportService {
// 几何边界假设是坐标点数组
Object boundaryObj = areaMap.get("boundary");
if (boundaryObj instanceof List<?>) {
@SuppressWarnings("unchecked")
List<?> list = (List<?>) boundaryObj;
if (!list.isEmpty() && list.get(0) instanceof List) {
@SuppressWarnings("unchecked")

View File

@ -1,7 +1,6 @@
package com.qaup.collision.common.adapter;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.system.domain.SysVehicleInfo;
import com.qaup.system.domain.SysVehicleType;
import com.qaup.system.service.ISysVehicleInfoService;
@ -21,7 +20,7 @@ import java.util.stream.Collectors;
/**
* QAUP数据适配器
*
* 连接CollisionAvoidanceSystem和若依系统的Service层
* 连接CollisionAvoidanceSystem和QAUP系统的Service层
* 提供统一的数据访问接口避免重复编写DAO组件
*
* 主要功能
@ -191,68 +190,7 @@ public class QuapDataAdapter {
return vehicleLocation;
}
/**
* 将车辆类型编码转换为CollisionAvoidanceSystem的MovingObjectType
*
* @param typeCode 车辆类型编码
* @return MovingObjectType
*/
public MovingObjectType convertToMovingObjectType(String typeCode) {
if (typeCode == null || typeCode.trim().isEmpty()) {
return MovingObjectType.UNKNOWN;
}
try {
// 使用路径编码模式进行分类
String topLevelCode = typeCode.split("\\.")[0];
switch (topLevelCode) {
case "UV":
return MovingObjectType.UNMANNED_VEHICLE;
case "SP":
return MovingObjectType.SPECIAL_VEHICLE;
case "NM":
return MovingObjectType.NORMAL_VEHICLE;
default:
logger.warn("未知的车辆类型编码: typeCode={}", typeCode);
return MovingObjectType.UNKNOWN;
}
} catch (Exception e) {
logger.error("转换车辆类型失败: typeCode={}", typeCode, e);
return MovingObjectType.UNKNOWN;
}
}
/**
* 将车辆类型ID转换为CollisionAvoidanceSystem的MovingObjectType向后兼容方法
*
* @param typeId 车辆类型ID已废弃
* @return MovingObjectType
* @deprecated 使用 convertToMovingObjectType(String typeCode) 代替
*/
@Deprecated
public MovingObjectType convertToMovingObjectType(Long typeId) {
if (typeId == null) {
return MovingObjectType.UNKNOWN;
}
try {
// 通过ID查找类型编码然后使用新方法
SysVehicleType vehicleType = vehicleTypeService.selectSysVehicleTypeById(typeId);
if (vehicleType != null && vehicleType.getTypeCode() != null) {
return convertToMovingObjectType(vehicleType.getTypeCode());
}
logger.warn("未找到车辆类型: typeId={}", typeId);
return MovingObjectType.UNKNOWN;
} catch (Exception e) {
logger.error("转换车辆类型失败: typeId={}", typeId, e);
return MovingObjectType.UNKNOWN;
}
}
// ================== 查询条件构建辅助方法 ==================
@ -292,16 +230,6 @@ public class QuapDataAdapter {
// ================== 日志和监控方法 ==================
/**
* 记录数据访问操作日志
*
* @param operation 操作类型
* @param details 操作详情
*/
private void logDataAccess(String operation, String details) {
logger.info("数据访问操作 - 操作: {}, 详情: {}", operation, details);
}
/**
* 验证适配器初始化状态
*

View File

@ -2,12 +2,10 @@ package com.qaup.collision.common.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; // 重新引入无参构造函数
// import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
// import com.fasterxml.jackson.annotation.JsonProperty;
import org.locationtech.jts.geom.Point;
/**
* 航空器模型
@ -18,13 +16,114 @@ import org.locationtech.jts.geom.Point;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor // 重新引入无参构造函数
// @AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略JSON中未知的字段
@JsonIgnoreProperties(ignoreUnknown = true)
public class Aircraft extends MovingObject {
// 根据用户反馈移除 flightNo time 字段这些信息应从 sys_vehicle_info 表中查询
// 外部API的原始字段将通过 DataCollectorDao 中的内部 DTO 进行处理和映射
/**
* 航班号
*/
private String flightNumber;
/**
* 进港跑道编号
*/
private String inRunway;
/**
* 出港跑道编号
*/
private String outRunway;
/**
* 联络道口
*/
private String contactCross;
/**
* 目的机位
*/
private String seat;
/**
* 航班状态
*/
private FlightStatus flightStatus;
/**
* 当前进港路由信息
*/
private AircraftRoute arrivalRoute;
/**
* 当前出港路由信息
*/
private AircraftRoute departureRoute;
/**
* 当前激活的路由 (指向arrivalRoute或departureRoute)
*/
private AircraftRoute currentRoute;
/**
* 路由状态时间戳
*/
private Long routeTimestamp;
/**
* 航班状态枚举
*/
public enum FlightStatus {
SCHEDULED, // 计划
TAXIING, // 滑行
TAKEOFF, // 起飞
AIRBORNE, // 空中
LANDING, // 降落
ARRIVED, // 进港
DEPARTURE, // 出港
CANCELLED // 取消
}
/**
* 检查是否有激活的路由
*/
public boolean hasActiveRoute() {
return currentRoute != null && currentRoute.isValid();
}
/**
* 获取当前路由类型
*/
public String getCurrentRouteType() {
return currentRoute != null ? currentRoute.getType() : null;
}
/**
* 设置进港路由为当前激活路由
*/
public void activateArrivalRoute() {
if (arrivalRoute != null) {
this.currentRoute = arrivalRoute;
this.routeTimestamp = System.currentTimeMillis();
}
}
/**
* 设置出港路由为当前激活路由
*/
public void activateDepartureRoute() {
if (departureRoute != null) {
this.currentRoute = departureRoute;
this.routeTimestamp = System.currentTimeMillis();
}
}
/**
* 清除当前激活路由
*/
public void clearCurrentRoute() {
this.currentRoute = null;
this.routeTimestamp = null;
}
}

View File

@ -0,0 +1,165 @@
package com.qaup.collision.common.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import java.util.List;
/**
* 航空器路由模型类
*
* 用于存储机场实时计算的航空器滑行路由信息
* 该类不持久化到数据库仅用于实时处理
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-17
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AircraftRoute {
/**
* 路由类型 (IN: 进港, OUT: 出港)
*/
private String type;
/**
* 路由状态 (COMPLETE: 完成, CALCULATING: 计算中, MODIFIED: 已修改)
*/
private String status;
/**
* 路由编码序列 (: "F1,L4,138")
*/
private String codes;
/**
* 路由几何图形 (LineString)
*/
private LineString geometry;
/**
* 路由路径集合 (GeoJSON格式的路径段)
*/
private List<RouteSegment> routeSegments;
/**
* 路由总长度 ()
*/
private Double totalLength;
/**
* 预计滑行时间 ()
*/
private Integer estimatedTime;
/**
* 路由创建时间戳
*/
private Long timestamp;
/**
* 路由修改次数
*/
private Integer modificationCount;
/**
* 路由段模型
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class RouteSegment {
/**
* 路径段编码 (: "L4", "F1")
*/
private String code;
/**
* 路径段类型 (TAXIWAY: 滑行道, CONTACT: 联络道, RUNWAY: 跑道)
*/
private String segmentType;
/**
* 路径段坐标点列表
*/
private List<Point> coordinates;
/**
* 路径段长度 ()
*/
private Double length;
/**
* 建议滑行速度 (km/h)
*/
private Double recommendedSpeed;
/**
* 路径段顺序
*/
private Integer sequence;
/**
* 是否为关键路径段
*/
private Boolean critical;
}
/**
* 检查路由是否有效
*/
public boolean isValid() {
return type != null &&
status != null &&
codes != null &&
!codes.trim().isEmpty() &&
routeSegments != null &&
!routeSegments.isEmpty();
}
/**
* 获取路由段数量
*/
public int getSegmentCount() {
return routeSegments != null ? routeSegments.size() : 0;
}
/**
* 检查是否为进港路由
*/
public boolean isArrivalRoute() {
return "IN".equals(type);
}
/**
* 检查是否为出港路由
*/
public boolean isDepartureRoute() {
return "OUT".equals(type);
}
/**
* 检查路由是否完成
*/
public boolean isComplete() {
return "COMPLETE".equals(status);
}
/**
* 检查路由是否被修改过
*/
public boolean isModified() {
return "MODIFIED".equals(status) ||
(modificationCount != null && modificationCount > 0);
}
}

View File

@ -2,16 +2,12 @@ package com.qaup.collision.common.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; // 重新引入无参构造函数
// import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
// import com.fasterxml.jackson.annotation.JsonProperty;
import org.locationtech.jts.geom.Point;
/**
* 机场车辆模型
* 表示无人车以外的机场内车辆如服务车清洁车加油车等
* 机场车辆模型包括特勤车和普通车
*
* @author AI Assistant
* @version 1.0
@ -19,13 +15,34 @@ import org.locationtech.jts.geom.Point;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor // 重新引入无参构造函数
// @AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略JSON中未知的字段
@JsonIgnoreProperties(ignoreUnknown = true)
public class AirportVehicle extends MovingObject {
// 根据用户反馈移除 vehicleNo time 字段这些信息应从 sys_vehicle_info 表中查询
// 外部API的原始字段将通过 DataCollectorDao 中的内部 DTO 进行处理和映射
/**
* 车牌号主要识别信息
*/
private String plateNumber;
/**
* 获取车牌号如果为空则返回对象名称
*/
public String getPlateNumber() {
return plateNumber != null ? plateNumber : getObjectName();
}
/**
* 是否为特勤车
*/
public boolean isSpecialVehicle() {
return getObjectType() == MovingObjectType.SPECIAL_VEHICLE;
}
/**
* 是否为普通车
*/
public boolean isNormalVehicle() {
return getObjectType() == MovingObjectType.NORMAL_VEHICLE;
}
}

View File

@ -20,7 +20,7 @@ public class MovingObject {
/**
* 对象类型
*/
private ObjectType objectType;
private MovingObjectType objectType;
/**
* 对象名称车牌号或航班号
@ -48,30 +48,41 @@ public class MovingObject {
private Double altitude;
/**
* 对象类型枚举
* 对象类型枚举 - 四种基本分类
*/
public enum ObjectType {
UNMANNED_VEHICLE, // 无人车
AIRCRAFT, // 航空器
SPECIAL_VEHICLE, // 特种车辆如机场车辆
NORMAL_VEHICLE; // 普通车辆
public enum MovingObjectType {
UNMANNED_VEHICLE("UV"), // 无人车
AIRCRAFT("AC"), // 航空器
SPECIAL_VEHICLE("SP"), // 特勤车
NORMAL_VEHICLE("NM"); // 普通车
private final String typeCode;
MovingObjectType(String typeCode) {
this.typeCode = typeCode;
}
public String getTypeCode() {
return typeCode;
}
/**
* 转换为MovingObjectType
* 根据类型编码获取对象类型
*/
public MovingObjectType toMovingObjectType() {
switch (this) {
case UNMANNED_VEHICLE:
return MovingObjectType.UNMANNED_VEHICLE;
case AIRCRAFT:
return MovingObjectType.AIRCRAFT;
case SPECIAL_VEHICLE:
return MovingObjectType.SPECIAL_VEHICLE;
case NORMAL_VEHICLE:
return MovingObjectType.NORMAL_VEHICLE;
default:
return MovingObjectType.UNMANNED_VEHICLE;
public static MovingObjectType fromTypeCode(String typeCode) {
for (MovingObjectType type : values()) {
if (type.typeCode.equals(typeCode)) {
return type;
}
}
throw new IllegalArgumentException("Unknown type code: " + typeCode);
}
/**
* 是否需要数据持久化
*/
public boolean shouldPersist() {
return this == UNMANNED_VEHICLE;
}
}

View File

@ -1,53 +0,0 @@
package com.qaup.collision.common.model;
import com.qaup.collision.common.model.enums.VehicleTypeCode;
import com.qaup.collision.common.model.enums.VehicleTypeConverter;
public enum MovingObjectType {
// 航空器飞机- 航空器位置数据接入
AIRCRAFT,
// 特勤车辆具有特殊权限- 特勤车辆如警车消防车救护车等
SPECIAL_VEHICLE,
// 普通车普通车辆- 新增的普通车辆类型
NORMAL_VEHICLE,
// 无人车可控- 无人车控制接口
UNMANNED_VEHICLE,
// 未知类型
UNKNOWN;
/**
* 从VehicleTypeCode转换为MovingObjectType
* 提供新旧枚举之间的兼容性
*/
public static MovingObjectType fromVehicleTypeCode(VehicleTypeCode vehicleTypeCode) {
return VehicleTypeConverter.toMovingObjectType(vehicleTypeCode);
}
/**
* 从路径编码字符串转换为MovingObjectType
*/
public static MovingObjectType fromTypeCode(String typeCode) {
return VehicleTypeConverter.fromTypeCode(typeCode);
}
/**
* 判断是否为车辆类型排除航空器
*/
public boolean isVehicle() {
return this != AIRCRAFT && this != UNKNOWN;
}
/**
* 判断是否为特殊权限车辆
*/
public boolean hasSpecialPrivileges() {
return this == SPECIAL_VEHICLE;
}
/**
* 判断是否为自动驾驶车辆
*/
public boolean isAutonomous() {
return this == UNMANNED_VEHICLE;
}
}

View File

@ -2,11 +2,10 @@ package com.qaup.collision.common.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; // 重新引入无参构造函数
// import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
// import com.fasterxml.jackson.annotation.JsonProperty;
import org.locationtech.jts.geom.Point;
/**
* 无人车模型
@ -17,13 +16,83 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor // 重新引入无参构造函数
// @AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略JSON中未知的字段
@JsonIgnoreProperties(ignoreUnknown = true)
public class UnmannedVehicle extends MovingObject {
// 根据用户反馈移除 vehicleId, licensePlate, vehicleModel, usagePurpose timestamp 字段
// 外部API的原始字段将通过 DataCollectorDao 中的内部 DTO 进行处理和映射
/**
* 电池电量百分比 (0-100)
*/
private Integer batteryLevel;
/**
* 运行状态
*/
private VehicleStatus vehicleStatus;
/**
* 当前任务ID
*/
private String missionId;
/**
* 任务状态
*/
private MissionStatus missionStatus;
/**
* 目标位置
*/
private Point targetPosition;
/**
* 无人车运行状态枚举
*/
public enum VehicleStatus {
IDLE, // 空闲
WORKING, // 工作中
CHARGING, // 充电中
MAINTENANCE, // 维护中
ERROR, // 故障
OFFLINE // 离线
}
/**
* 任务状态枚举
*/
public enum MissionStatus {
NONE, // 无任务
ASSIGNED, // 已分配
IN_PROGRESS, // 执行中
COMPLETED, // 已完成
PAUSED, // 暂停
CANCELLED // 已取消
}
/**
* 是否可以接收新指令
*/
public boolean canReceiveCommand() {
return vehicleStatus != VehicleStatus.MAINTENANCE &&
vehicleStatus != VehicleStatus.ERROR &&
vehicleStatus != VehicleStatus.OFFLINE;
}
/**
* 是否电量不足
*/
public boolean isLowBattery() {
return batteryLevel != null && batteryLevel < 20;
}
/**
* 计算到目标位置的距离
*/
public double getDistanceToTarget() {
if (targetPosition == null || getCurrentPosition() == null) {
return Double.MAX_VALUE;
}
return getCurrentPosition().distance(targetPosition) * 111320; // 度转米的近似值
}
}

View File

@ -1,10 +1,10 @@
package com.qaup.collision.common.model.enums;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
/**
* 车辆类型转换工具类
* 负责新旧车辆类型枚举之间的转换
* 负责车辆类型枚举和编码之间的转换
*
* @author Claude Code
* @version 1.0
@ -12,11 +12,11 @@ import com.qaup.collision.common.model.MovingObjectType;
public class VehicleTypeConverter {
/**
* 将VehicleTypeCode转换为MovingObjectType
* 将VehicleTypeCode转换为ObjectType
*/
public static MovingObjectType toMovingObjectType(VehicleTypeCode vehicleTypeCode) {
public static MovingObjectType toObjectType(VehicleTypeCode vehicleTypeCode) {
if (vehicleTypeCode == null) {
return MovingObjectType.UNKNOWN;
return MovingObjectType.NORMAL_VEHICLE;
}
switch (vehicleTypeCode.getTopLevelType()) {
@ -27,45 +27,50 @@ public class VehicleTypeConverter {
case NM:
return MovingObjectType.NORMAL_VEHICLE;
default:
return MovingObjectType.UNKNOWN;
return MovingObjectType.NORMAL_VEHICLE;
}
}
/**
* MovingObjectType转换为VehicleTypeCode一级分类
* ObjectType转换为VehicleTypeCode一级分类
*/
public static VehicleTypeCode fromMovingObjectType(MovingObjectType movingObjectType) {
if (movingObjectType == null) {
public static VehicleTypeCode fromObjectType(MovingObjectType objectType) {
if (objectType == null) {
return VehicleTypeCode.NM;
}
switch (movingObjectType) {
switch (objectType) {
case UNMANNED_VEHICLE:
return VehicleTypeCode.UV;
case SPECIAL_VEHICLE:
return VehicleTypeCode.SP;
case NORMAL_VEHICLE:
case AIRPORT_VEHICLE: // 兼容旧的机场车辆类型
return VehicleTypeCode.NM;
case AIRCRAFT:
return VehicleTypeCode.NM; // 航空器默认映射为普通车虽然不会用到
default:
return VehicleTypeCode.NM; // 默认返回普通车
return VehicleTypeCode.NM;
}
}
/**
* 从路径编码字符串转换为MovingObjectType
* 从路径编码字符串转换为ObjectType
*/
public static MovingObjectType fromTypeCode(String typeCode) {
if (typeCode == null || typeCode.trim().isEmpty()) {
return MovingObjectType.UNKNOWN;
return MovingObjectType.NORMAL_VEHICLE;
}
VehicleTypeCode vehicleTypeCode = VehicleTypeCode.fromCodeSafe(typeCode);
if (vehicleTypeCode != null) {
return toMovingObjectType(vehicleTypeCode);
// 直接根据编码前缀判断
if (typeCode.startsWith("UV")) {
return MovingObjectType.UNMANNED_VEHICLE;
} else if (typeCode.startsWith("SP")) {
return MovingObjectType.SPECIAL_VEHICLE;
} else if (typeCode.startsWith("AC")) {
return MovingObjectType.AIRCRAFT;
} else {
return MovingObjectType.NORMAL_VEHICLE;
}
return MovingObjectType.UNKNOWN;
}
/**
@ -73,9 +78,9 @@ public class VehicleTypeConverter {
* 这个方法需要和QuapDataAdapter配合使用
*/
public static MovingObjectType fromLegacyTypeId(Long typeId, String typeName) {
// 基于类型名称进行简单分类保持原有逻辑
// 基于类型名称进行简单分类
if (typeName == null || typeName.trim().isEmpty()) {
return MovingObjectType.UNKNOWN;
return MovingObjectType.NORMAL_VEHICLE;
}
if (typeName.contains("无人车")) {
@ -84,8 +89,10 @@ public class VehicleTypeConverter {
typeName.contains("警车") || typeName.contains("救护") ||
typeName.contains("引导")) {
return MovingObjectType.SPECIAL_VEHICLE;
} else if (typeName.contains("航空") || typeName.contains("飞机")) {
return MovingObjectType.AIRCRAFT;
} else {
// 所有其他类型都归类为普通车辆原来是机场车辆
// 所有其他类型都归类为普通车辆
return MovingObjectType.NORMAL_VEHICLE;
}
}

View File

@ -1,6 +1,6 @@
package com.qaup.collision.common.model.spatial;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;

View File

@ -2,6 +2,7 @@ package com.qaup.collision.common.service;
import com.qaup.collision.common.model.spatial.AirportArea;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.common.constant.GeoConstants;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.*;
@ -332,7 +333,7 @@ public class SpatialQueryService {
// ===== 私有辅助方法 =====
private double calculateDistance(Point point1, Point point2) {
return point1.distance(point2) * 111320; // 近似转换为米在赤道附近
return point1.distance(point2) * GeoConstants.DEGREE_TO_METER; // 近似转换为米在赤道附近
}
private boolean isHeadOnConflict(VehicleLocation vehicle1, VehicleLocation vehicle2, double distance) {

View File

@ -2,7 +2,8 @@ package com.qaup.collision.common.service;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.model.repository.VehicleLocationRepository;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.adapter.QuapDataAdapter;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.model.entity.SpatialRule;
@ -10,6 +11,7 @@ import com.qaup.collision.rule.model.enums.RuleExecutionResult;
import com.qaup.collision.rule.service.LocationRuleQueryService;
import com.qaup.collision.rule.service.RealTimeViolationDetector;
import com.qaup.collision.rule.service.RuleExecutionEngine;
import com.qaup.common.utils.GeoUtils;
import com.qaup.system.domain.SysVehicleInfo;
import lombok.RequiredArgsConstructor;
@ -125,18 +127,7 @@ public class VehicleLocationService {
* 计算两点之间的距离
*/
private static double calculateDistance(double lon1, double lat1, double lon2, double lat2) {
if (lon1 == lon2 && lat1 == lat2) {
return 0;
}
final double R = 6371000; // 地球半径
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
return GeoUtils.calculateDistance(lat1, lon1, lat2, lon2);
}
}
@ -319,7 +310,7 @@ public class VehicleLocationService {
public List<VehicleLocation> getActiveVehiclesByType(MovingObjectType vehicleType, int minutesBack) {
LocalDateTime since = LocalDateTime.now().minusMinutes(minutesBack);
try {
// 将MovingObjectType转换为数据库中的类型名称模式
// 将MovingObject.ObjectType转换为数据库中的类型名称模式
String typeName = convertMovingObjectTypeToTypeName(vehicleType);
return vehicleLocationRepository.findActiveByVehicleType(typeName, since);
} catch (Exception e) {
@ -329,14 +320,16 @@ public class VehicleLocationService {
}
/**
* 将MovingObjectType转换为数据库中的类型名称模式
* 将MovingObject.ObjectType转换为数据库中的类型名称模式
*/
private String convertMovingObjectTypeToTypeName(MovingObjectType vehicleType) {
switch (vehicleType) {
case UNMANNED_VEHICLE:
return "%无人车%";
case AIRPORT_VEHICLE:
return "%车"; // 匹配包含""但不包含"无人车"的类型
case SPECIAL_VEHICLE:
return "%特勤车%";
case NORMAL_VEHICLE:
return "%普通车"; // 匹配包含""但不包含"无人车"的类型
case AIRCRAFT:
return "%飞机%";
default:
@ -764,7 +757,7 @@ public class VehicleLocationService {
}
SysVehicleInfo vehicleInfo = vehicleInfoOpt.get();
MovingObjectType vehicleType = quapDataAdapter.convertToMovingObjectType(vehicleInfo.getTypeCode());
MovingObjectType vehicleType = MovingObjectType.fromTypeCode(vehicleInfo.getTypeCode());
// 查询该位置适用的规则
List<SpatialRule> applicableRules = locationRuleQueryService.findApplicableRules(

View File

@ -1,6 +1,6 @@
package com.qaup.collision.common.service;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.enums.VehicleSubType;
import com.qaup.collision.common.model.spatial.AirportArea;
import lombok.extern.slf4j.Slf4j;
@ -34,9 +34,9 @@ public class VehiclePermissionService {
return checkUnmannedVehicleAccess(vehicleId, area);
}
// 机场车辆需要进一步检查子类型
if (vehicleType == MovingObjectType.AIRPORT_VEHICLE) {
return checkAirportVehicleAccess(vehicleId, area);
// 普通车辆需要进一步检查子类型
if (vehicleType == MovingObjectType.NORMAL_VEHICLE) {
return checkNormalVehicleAccess(vehicleId, area);
}
return false;
@ -48,9 +48,9 @@ public class VehiclePermissionService {
}
/**
* 检查机场车辆的区域访问权限
* 检查普通车辆的区域访问权限
*/
private boolean checkAirportVehicleAccess(Long vehicleId, AirportArea area) {
private boolean checkNormalVehicleAccess(Long vehicleId, AirportArea area) {
//VehicleSubType subType = VehicleSubType.inferFromVehicleId(vehicleId);
VehicleSubType subType = VehicleSubType.AMBULANCE;
@ -63,7 +63,7 @@ public class VehiclePermissionService {
return checkSpecialVehicleAccess(subType, area);
}
// 一般机场车辆按区域类型和默认规则检查
// 一般普通车辆按区域类型和默认规则检查
return checkRegularVehicleAccess(subType, area);
}
@ -176,8 +176,8 @@ public class VehiclePermissionService {
return true;
}
// 如果对方是机场车辆检查是否为特勤车辆
if (otherVehicleType == MovingObjectType.AIRPORT_VEHICLE) {
// 如果对方是普通车辆检查是否为特勤车辆
if (otherVehicleType == MovingObjectType.NORMAL_VEHICLE) {
VehicleSubType otherSubType = VehicleSubType.AMBULANCE;
if (otherSubType != null && otherSubType.hasSpecialRights()) {
log.info("车辆 {} 需要为特勤车辆 {} 避让", thisVehicleId, otherSubType.getDisplayName());
@ -203,8 +203,8 @@ public class VehiclePermissionService {
return false;
}
// 机场车辆中的特勤车辆不受红绿灯限制
if (vehicleType == MovingObjectType.AIRPORT_VEHICLE) {
// 普通车辆中的特勤车辆不受红绿灯限制
if (vehicleType == MovingObjectType.NORMAL_VEHICLE) {
VehicleSubType subType = VehicleSubType.AMBULANCE;
if (subType != null && subType.hasSpecialRights()) {
log.debug("特勤车辆 {} 无需遵守红绿灯", subType.getDisplayName());
@ -228,12 +228,12 @@ public class VehiclePermissionService {
return "特殊权限(特勤车辆)";
}
if (vehicleType == MovingObjectType.AIRPORT_VEHICLE) {
if (vehicleType == MovingObjectType.NORMAL_VEHICLE) {
VehicleSubType subType = VehicleSubType.AMBULANCE;
if (subType != null && subType.hasSpecialRights()) {
return "特殊权限(" + subType.getDisplayName() + "";
}
return "一般权限(机场车辆)";
return "一般权限(普通车辆)";
}
if (vehicleType == MovingObjectType.UNMANNED_VEHICLE) {

View File

@ -4,9 +4,11 @@ package com.qaup.collision.datacollector.dao;
import com.qaup.collision.common.model.Aircraft;
import com.qaup.collision.common.model.AirportVehicle;
import com.qaup.collision.common.model.UnmannedVehicle;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.dto.Response;
import com.qaup.collision.datacollector.service.AuthService;
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
import com.qaup.collision.datacollector.dto.AircraftStatusDTO;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -191,7 +193,7 @@ public class DataCollectorDao {
unmannedVehicle.setCurrentSpeed(rawData.getSpeed() != null ? rawData.getSpeed() * 3.6 : 0.0); // m/s to km/h
unmannedVehicle.setCurrentHeading(rawData.getDirection() != null ? Math.toDegrees(rawData.getDirection()) : 0.0); // 弧度 to 角度
unmannedVehicle.setAltitude(0.0); // 假设默认高度为0如果API有提供可以设置
unmannedVehicle.setObjectType(MovingObject.ObjectType.UNMANNED_VEHICLE); // 显式设置对象类型
unmannedVehicle.setObjectType(MovingObjectType.UNMANNED_VEHICLE); // 显式设置对象类型
return unmannedVehicle;
}).filter(java.util.Objects::nonNull).collect(Collectors.toList());
@ -259,4 +261,154 @@ public class DataCollectorDao {
@JsonProperty("direction")
private Double direction;
}
// 新增航空器路由和状态数据采集方法
/**
* 获取航空器进港路由
*
* @param inRunway 进港跑道编号
* @param outRunway 出港跑道编号
* @param contactCross 联络道口
* @param seat 目的机位
* @return 进港路由数据
*/
public AircraftRouteDTO getArrivalRoute(String inRunway, String outRunway, String contactCross, String seat) {
try {
String token = authService.getToken();
if (token == null) {
log.error("无法获取有效的认证token");
return null;
}
String url = UriComponentsBuilder.fromHttpUrl(vehicleBaseUrl)
.path("/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat")
.queryParam("inRunway", inRunway)
.queryParam("outRunway", outRunway)
.queryParam("contactCross", contactCross)
.queryParam("seat", seat)
.toUriString();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
headers.set("Content-Type", "application/json");
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Response<AircraftRouteDTO>> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
new ParameterizedTypeReference<Response<AircraftRouteDTO>>() {}
);
Response<AircraftRouteDTO> responseBody = response.getBody();
if (responseBody != null && responseBody.getStatus() == 200) {
log.info("成功获取进港路由数据: inRunway={}, outRunway={}, contactCross={}, seat={}",
inRunway, outRunway, contactCross, seat);
return responseBody.getData();
} else {
log.warn("获取进港路由数据失败: {}", responseBody != null ? responseBody.getMsg() : "未知错误");
return null;
}
} catch (Exception e) {
log.error("获取进港路由数据时发生异常: inRunway={}, outRunway={}, contactCross={}, seat={}",
inRunway, outRunway, contactCross, seat, e);
return null;
}
}
/**
* 获取航空器出港路由
*
* @param inRunway 进港跑道编号
* @param outRunway 出港跑道编号
* @param startSeat 起始机位
* @return 出港路由数据
*/
public AircraftRouteDTO getDepartureRoute(String inRunway, String outRunway, String startSeat) {
try {
String token = authService.getToken();
if (token == null) {
log.error("无法获取有效的认证token");
return null;
}
String url = UriComponentsBuilder.fromHttpUrl(vehicleBaseUrl)
.path("/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat")
.queryParam("inRunway", inRunway)
.queryParam("outRunway", outRunway)
.queryParam("startSeat", startSeat)
.toUriString();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
headers.set("Content-Type", "application/json");
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Response<AircraftRouteDTO>> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
new ParameterizedTypeReference<Response<AircraftRouteDTO>>() {}
);
Response<AircraftRouteDTO> responseBody = response.getBody();
if (responseBody != null && responseBody.getStatus() == 200) {
log.info("成功获取出港路由数据: inRunway={}, outRunway={}, startSeat={}",
inRunway, outRunway, startSeat);
return responseBody.getData();
} else {
log.warn("获取出港路由数据失败: {}", responseBody != null ? responseBody.getMsg() : "未知错误");
return null;
}
} catch (Exception e) {
log.error("获取出港路由数据时发生异常: inRunway={}, outRunway={}, startSeat={}",
inRunway, outRunway, startSeat, e);
return null;
}
}
/**
* 获取航空器状态
*
* @return 航空器状态数据
*/
public AircraftStatusDTO getAircraftStatus() {
try {
String token = authService.getToken();
if (token == null) {
log.error("无法获取有效的认证token");
return null;
}
String url = vehicleBaseUrl + "/aircraftStatusController/getAircraftStatus";
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
headers.set("Content-Type", "application/json");
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
ResponseEntity<Response<AircraftStatusDTO>> response = restTemplate.exchange(
url,
HttpMethod.GET,
requestEntity,
new ParameterizedTypeReference<Response<AircraftStatusDTO>>() {}
);
Response<AircraftStatusDTO> responseBody = response.getBody();
if (responseBody != null && responseBody.getStatus() == 200) {
log.info("成功获取航空器状态数据: {}", responseBody.getData().getFlightNo());
return responseBody.getData();
} else {
log.warn("获取航空器状态数据失败: {}", responseBody != null ? responseBody.getMsg() : "未知错误");
return null;
}
} catch (Exception e) {
log.error("获取航空器状态数据时发生异常", e);
return null;
}
}
}

View File

@ -0,0 +1,128 @@
package com.qaup.collision.datacollector.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* 航空器路由API响应DTO
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-17
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class AircraftRouteDTO {
/**
* 路由类型 (IN: 进港, OUT: 出港)
*/
private String type;
/**
* 路由状态 (COMPLETE: 完成, CALCULATING: 计算中, MODIFIED: 已修改)
*/
private String status;
/**
* 路由编码序列 (: "F1,L4,138")
*/
private String codes;
/**
* 路由几何图形 (通常为null使用geoPath)
*/
private Object geometry;
/**
* 路由路径 (GeoJSON格式)
*/
private GeoPath geoPath;
/**
* GeoJSON路径数据
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class GeoPath {
/**
* GeoJSON类型通常为"FeatureCollection"
*/
private String type;
/**
* 路径特征集合
*/
private List<Feature> features;
}
/**
* GeoJSON特征
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Feature {
/**
* 特征类型通常为"Feature"
*/
private String type;
/**
* 几何图形信息
*/
private Geometry geometry;
/**
* 属性信息
*/
private Properties properties;
}
/**
* 几何图形信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Geometry {
/**
* 几何类型通常为"LineString"
*/
private String type;
/**
* 坐标数组 [[经度, 纬度], [经度, 纬度], ...]
*/
private List<List<Double>> coordinates;
}
/**
* 属性信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Properties {
/**
* 路径段编码
*/
private String code;
}
}

View File

@ -0,0 +1,56 @@
package com.qaup.collision.datacollector.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 航空器状态API响应DTO
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-17
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class AircraftStatusDTO {
/**
* 进出港类型 (IN: 进港, OUT: 出港, ARRIVED: 到达)
*/
private String type;
/**
* 航班号
*/
private String flightNo;
/**
* 进港跑道编号
*/
private String inRunway;
/**
* 出港跑道编号
*/
private String outRunway;
/**
* 联络道口
*/
private String contactCross;
/**
* 目的机位
*/
private String seat;
/**
* 时间戳
*/
private Long timestamp;
}

View File

@ -1,11 +1,19 @@
package com.qaup.collision.datacollector.service;
import com.qaup.collision.common.model.*;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.Aircraft;
import com.qaup.collision.common.model.AirportVehicle;
import com.qaup.collision.common.model.UnmannedVehicle;
import com.qaup.collision.common.model.AircraftRoute;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.service.VehicleLocationService;
import com.qaup.collision.dataprocessing.service.SpeedCalculationService;
import com.qaup.collision.datacollector.dao.DataCollectorDao;
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
import com.qaup.collision.datacollector.dto.AircraftStatusDTO;
import com.qaup.collision.websocket.event.PositionUpdateEvent;
import com.qaup.collision.websocket.event.AircraftRouteUpdateEvent;
import com.qaup.collision.websocket.message.PositionUpdatePayload;
import com.qaup.collision.pathconflict.service.PathConflictDetectionService;
import com.qaup.collision.common.adapter.QuapDataAdapter;
@ -97,11 +105,187 @@ public class DataCollectorService {
private final Map<String, MovingObject> activeMovingObjectsCache = new ConcurrentHashMap<>();
/**
* 定时采集航空器数据
* 采集航空器路由和状态数据
*
* 重构说明
* - 航空器数据仅用于实时处理不存储到数据库
* - 数据采集后直接用于碰撞检测等实时计算
* 数据来源机场系统API
* - 获取航空器状态 (/aircraftStatusController/getAircraftStatus)
* - 获取进港路由 (/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat)
* - 获取出港路由 (/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat)
*
* 说明此方法用于更新航空器的路由信息支持CA3456生命周期模拟
*/
@Scheduled(fixedRateString = "${data.collector.route.interval:5000}")
public void collectAircraftRouteAndStatus() {
if (collectorDisabled) {
return;
}
try {
// 获取航空器状态
AircraftStatusDTO aircraftStatus = dataCollectorDao.getAircraftStatus();
if (aircraftStatus == null) {
log.debug("未获取到航空器状态数据");
return;
}
log.info("获取到航空器状态: 航班号={}, 类型={}, 跑道={}-{}, 机位={}",
aircraftStatus.getFlightNo(),
aircraftStatus.getType(),
aircraftStatus.getInRunway(),
aircraftStatus.getOutRunway(),
aircraftStatus.getSeat());
// 根据航空器状态类型获取相应的路由数据
AircraftRouteDTO routeData = null;
if ("IN".equals(aircraftStatus.getType())) {
// 进港状态获取进港路由
routeData = dataCollectorDao.getArrivalRoute(
aircraftStatus.getInRunway(),
aircraftStatus.getOutRunway(),
aircraftStatus.getContactCross(),
aircraftStatus.getSeat()
);
} else if ("OUT".equals(aircraftStatus.getType())) {
// 出港状态获取出港路由
routeData = dataCollectorDao.getDepartureRoute(
aircraftStatus.getInRunway(),
aircraftStatus.getOutRunway(),
aircraftStatus.getSeat()
);
}
if (routeData != null) {
log.info("获取到{}路由数据: 编码={}, 状态={}",
routeData.getType(),
routeData.getCodes(),
routeData.getStatus());
// 转换DTO为航空器路由对象
AircraftRoute aircraftRoute = convertToAircraftRoute(routeData);
if (aircraftRoute != null) {
// 更新缓存中的航空器路由信息
updateAircraftRouteInCache(aircraftStatus.getFlightNo(), aircraftRoute, aircraftStatus);
// 发布WebSocket事件通知前端路由更新
publishAircraftRouteUpdateEvent(aircraftStatus.getFlightNo(), aircraftRoute, aircraftStatus);
}
}
} catch (Exception e) {
log.error("采集航空器路由和状态数据异常", e);
}
}
/**
* 将AircraftRouteDTO转换为AircraftRoute对象
*/
private AircraftRoute convertToAircraftRoute(AircraftRouteDTO routeDTO) {
if (routeDTO == null || routeDTO.getGeoPath() == null) {
return null;
}
try {
// 创建路由段列表
List<AircraftRoute.RouteSegment> routeSegments = new ArrayList<>();
if (routeDTO.getGeoPath().getFeatures() != null) {
for (AircraftRouteDTO.Feature feature : routeDTO.getGeoPath().getFeatures()) {
if (feature.getGeometry() != null && feature.getGeometry().getCoordinates() != null) {
// 转换坐标格式从List<List<Double>>到List<Point>
List<org.locationtech.jts.geom.Point> points = feature.getGeometry().getCoordinates().stream()
.map(coord -> geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(coord.get(0), coord.get(1))))
.collect(java.util.stream.Collectors.toList());
AircraftRoute.RouteSegment segment = AircraftRoute.RouteSegment.builder()
.code(feature.getProperties() != null ? feature.getProperties().getCode() : "")
.coordinates(points)
.build();
routeSegments.add(segment);
}
}
}
// 如果有坐标数据创建LineString几何对象
org.locationtech.jts.geom.LineString geometry = null;
if (!routeSegments.isEmpty() && routeSegments.get(0).getCoordinates() != null) {
List<org.locationtech.jts.geom.Point> points = routeSegments.get(0).getCoordinates();
if (points.size() >= 2) {
org.locationtech.jts.geom.Coordinate[] coords = points.stream()
.map(point -> point.getCoordinate())
.toArray(org.locationtech.jts.geom.Coordinate[]::new);
geometry = geometryFactory.createLineString(coords);
}
}
return AircraftRoute.builder()
.type(routeDTO.getType())
.status(routeDTO.getStatus())
.codes(routeDTO.getCodes())
.geometry(geometry)
.routeSegments(routeSegments)
.build();
} catch (Exception e) {
log.error("转换航空器路由数据失败", e);
return null;
}
}
/**
* 更新缓存中的航空器路由信息
*/
private void updateAircraftRouteInCache(String flightNo, AircraftRoute route, AircraftStatusDTO status) {
MovingObject cachedAircraft = activeMovingObjectsCache.get(flightNo);
if (cachedAircraft != null && cachedAircraft instanceof Aircraft) {
Aircraft aircraft = (Aircraft) cachedAircraft;
// 根据航空器状态更新路由
if ("IN".equals(status.getType())) {
aircraft.setArrivalRoute(route);
aircraft.activateArrivalRoute();
} else if ("OUT".equals(status.getType())) {
aircraft.setDepartureRoute(route);
aircraft.activateDepartureRoute();
}
// 更新缓存
activeMovingObjectsCache.put(flightNo, aircraft);
log.debug("更新航空器 {} 路由信息到缓存: 类型={}, 编码={}",
flightNo, route.getType(), route.getCodes());
}
}
/**
* 发布航空器路由更新事件
*/
private void publishAircraftRouteUpdateEvent(String flightNo, AircraftRoute route, AircraftStatusDTO status) {
try {
// 创建路由更新事件
AircraftRouteUpdateEvent routeUpdateEvent = AircraftRouteUpdateEvent.builder()
.flightNo(flightNo)
.routeType(route.getType())
.routeStatus(route.getStatus())
.routeCodes(route.getCodes())
.aircraftStatus(status.getType())
.routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null)
.timestamp(System.currentTimeMillis())
.build();
// 发布WebSocket事件
eventPublisher.publishEvent(routeUpdateEvent);
log.info("发布航空器路由更新事件: 航班号={}, 路由类型={}, 状态={}",
flightNo, route.getType(), status.getType());
} catch (Exception e) {
log.error("发布航空器路由更新事件失败: flightNo={}", flightNo, e);
}
}
/**
* 定时采集航空器数据
* - 不进行数据持久化
*/
@Scheduled(fixedRateString = "${data.collector.interval}")
@ -145,7 +329,7 @@ public class DataCollectorService {
MovingObject movingObject = MovingObject.builder()
.objectId(aircraft.getObjectId()) // Changed from getFlightNo()
.objectType(MovingObject.ObjectType.AIRCRAFT)
.objectType(MovingObjectType.AIRCRAFT)
.objectName(aircraft.getObjectName()) // Changed from getFlightNo()
.currentPosition(currentPosition)
.currentSpeed(calculatedSpeed)
@ -244,7 +428,7 @@ public class DataCollectorService {
MovingObject movingObject = MovingObject.builder()
.objectId(vehicle.getObjectId())
.objectType(MovingObject.ObjectType.SPECIAL_VEHICLE)
.objectType(MovingObjectType.SPECIAL_VEHICLE)
.objectName(vehicle.getObjectName())
.currentPosition(currentPosition)
.currentSpeed(calculatedSpeed)
@ -345,7 +529,7 @@ public class DataCollectorService {
MovingObject movingObject = MovingObject.builder()
.objectId(unmannedVehicle.getObjectId()) // Changed from getLicensePlate()
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName(unmannedVehicle.getObjectName()) // Changed from getLicensePlate()
.currentPosition(currentPosition)
.currentSpeed(calculatedSpeed)
@ -442,7 +626,7 @@ public class DataCollectorService {
for (VehicleLocation vehicleLocation : detectionVehicleLocations) {
try {
// 只处理无人车
if (vehicleLocation.getVehicleType() == com.qaup.collision.common.model.MovingObjectType.UNMANNED_VEHICLE) {
if (vehicleLocation.getVehicleType() == MovingObjectType.UNMANNED_VEHICLE) {
// 构建车辆状态参数
Map<String, Object> vehicleState = new HashMap<>();
vehicleState.put("speed", vehicleLocation.getSpeed());
@ -518,7 +702,7 @@ public class DataCollectorService {
.heading(vehicle.getCurrentHeading())
.timestamp(java.time.LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, java.time.ZoneOffset.UTC))
.licensePlate(sysVehicleInfo.getLicensePlate()) // 设置运行时属性 licensePlate
.vehicleType(com.qaup.collision.common.model.MovingObjectType.UNMANNED_VEHICLE) // 从无人车接口获取的数据直接设置为无人车类型
.vehicleType(MovingObjectType.UNMANNED_VEHICLE) // 从无人车接口获取的数据直接设置为无人车类型
.build();
} catch (Exception e) {
log.error("转换无人车位置数据时发生未知错误: objectId={}", vehicle.getObjectId(), e);
@ -538,9 +722,9 @@ public class DataCollectorService {
try {
Long vehicleIdForDetection;
String licensePlateForDetection;
com.qaup.collision.common.model.MovingObjectType objectTypeForDetection = com.qaup.collision.common.model.MovingObjectType.valueOf(movingObject.getObjectType().name());
MovingObjectType objectTypeForDetection = MovingObjectType.valueOf(movingObject.getObjectType().name());
if (MovingObject.ObjectType.UNMANNED_VEHICLE.equals(movingObject.getObjectType())) {
if (MovingObjectType.UNMANNED_VEHICLE.equals(movingObject.getObjectType())) {
// 对于无人车其objectId就是车牌号可以从sys_vehicle_info中查到vehicleId
Optional<SysVehicleInfo> vehicleInfoOptional = quapDataAdapter.findVehicleByLicensePlate(movingObject.getObjectId());
if (vehicleInfoOptional.isEmpty()) {
@ -640,7 +824,7 @@ public class DataCollectorService {
// 过滤出无人车对象
List<MovingObject> unmannedVehicles = activeObjects.stream()
.filter(obj -> obj.getObjectType() == MovingObject.ObjectType.UNMANNED_VEHICLE)
.filter(obj -> obj.getObjectType() == MovingObjectType.UNMANNED_VEHICLE)
.collect(Collectors.toList());
if (unmannedVehicles.isEmpty()) {
@ -662,7 +846,7 @@ public class DataCollectorService {
.currentSpeed(unmannedVehicle.getCurrentSpeed())
.currentHeading(unmannedVehicle.getCurrentHeading())
.altitude(unmannedVehicle.getAltitude())
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.build();
// 转换无人车数据为VehicleLocation

View File

@ -1,6 +1,5 @@
package com.qaup.collision.datacollector.service;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.datacollector.model.converter.VehicleCommandConverter;
import com.qaup.collision.datacollector.model.dto.VehicleCommand;
@ -328,16 +327,18 @@ public class UnmannedVehicleControlService {
private String transId;
private long timestamp;
private String vehicleId;
private boolean isSingle;
private boolean single;
// Getters and Setters
public String getTransId() { return transId; }
public void setTransId(String transId) { this.transId = transId; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
public String getVehicleId() { return vehicleId; }
public void setVehicleId(String vehicleId) { this.vehicleId = vehicleId; }
public boolean isSingle() { return isSingle; }
public void setSingle(boolean single) { isSingle = single; }
public boolean isSingle() { return single; }
public void setSingle(boolean single) { this.single = single; }
}
}

View File

@ -1,6 +1,6 @@
package com.qaup.collision.datacollector.service;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.service.VehicleLocationService;
import com.qaup.collision.datacollector.model.entity.VehicleCommandEntity;

View File

@ -1,10 +1,10 @@
package com.qaup.collision.dataprocessing.service;
import com.qaup.common.utils.GeoUtils;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.beans.factory.annotation.Value;
@Service
@ -191,19 +191,7 @@ public class SpeedCalculationService {
* @return 距离
*/
private double calculateGeographicDistance(double lat1, double lon1, double lat2, double lon2) {
final double R = 6371000; // 地球半径
double lat1Rad = Math.toRadians(lat1);
double lat2Rad = Math.toRadians(lat2);
double deltaLatRad = Math.toRadians(lat2 - lat1);
double deltaLonRad = Math.toRadians(lon2 - lon1);
double a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
return GeoUtils.calculateDistance(lat1, lon1, lat2, lon2);
}
/**

View File

@ -3,6 +3,7 @@ package com.qaup.collision.geofence.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.spatial.AirportArea;
import com.qaup.collision.common.service.AirportAreaService;
import com.qaup.collision.geofence.model.dto.GeofenceAccessCheckResult;
@ -57,7 +58,7 @@ public class UnmannedVehicleGeofenceService {
*/
public GeofenceAccessCheckResult checkAreaAccess(MovingObject vehicle, String areaCode) {
// 前置检查必须是无人车
if (vehicle.getObjectType() != MovingObject.ObjectType.UNMANNED_VEHICLE) {
if (vehicle.getObjectType() != MovingObjectType.UNMANNED_VEHICLE) {
log.debug("跳过非无人车的围栏检测: {} (类型: {})", vehicle.getObjectId(), vehicle.getObjectType());
return GeofenceAccessCheckResult.skipped(vehicle.getObjectId());
}
@ -103,7 +104,7 @@ public class UnmannedVehicleGeofenceService {
List<GeofenceAccessCheckResult> results = new ArrayList<>();
// 类型检查只处理无人车
if (vehicle.getObjectType() != MovingObject.ObjectType.UNMANNED_VEHICLE) {
if (vehicle.getObjectType() != MovingObjectType.UNMANNED_VEHICLE) {
log.debug("跳过非无人车位置检测: {}", vehicle.getObjectId());
results.add(GeofenceAccessCheckResult.skipped(vehicle.getObjectId()));
return results;
@ -165,7 +166,7 @@ public class UnmannedVehicleGeofenceService {
}
// 检查车辆类型是否被允许
if (!rule.isVehicleTypeAllowed(vehicle.getObjectType().toMovingObjectType())) {
if (!rule.isVehicleTypeAllowed(vehicle.getObjectType())) {
log.debug("规则 {} 不允许车辆类型 {}", rule.getRuleId(), vehicle.getObjectType());
continue;
}
@ -342,8 +343,6 @@ public class UnmannedVehicleGeofenceService {
* 检查时间限制
*/
private RuleCheckResult checkTimeRestrictions(UnmannedVehicleGeofenceRuleConfig.TimeRestrictionsConfig config) {
LocalTime now = LocalTime.now();
// 检查活跃时间段
if (config.getActiveHours() != null) {
// TODO: 解析时间段格式 "06:00-22:00" 并检查

View File

@ -1,7 +1,8 @@
package com.qaup.collision.pathconflict.model.dto;
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import lombok.Builder;
import lombok.Data;
import org.locationtech.jts.geom.Point;
@ -20,9 +21,9 @@ public class ConflictAlertEvent {
private Optional<ConflictAlertLog.AlertLevel> alertLevel; // 告警级别
private String message; // 告警消息
private String object1Name; // 对象1名称
private MovingObject.ObjectType object1Type; // 对象1类型
private MovingObjectType object1Type; // 对象1类型
private String object2Name; // 对象2名称
private MovingObject.ObjectType object2Type; // 对象2类型
private MovingObjectType object2Type; // 对象2类型
private Point conflictPoint; // 冲突点
private Double object1Distance; // 对象1距离冲突点距离 ()
private Double object2Distance; // 对象2距离冲突点距离 ()

View File

@ -3,6 +3,7 @@ package com.qaup.collision.pathconflict.service;
import com.qaup.collision.pathconflict.model.entity.TransportRoute;
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
@ -84,7 +85,7 @@ public class PathConflictDetectionService {
* 检测两个对象之间的路径冲突
*/
private Optional<ConflictAlertEvent> detectConflictBetweenObjects(MovingObject obj1, MovingObject obj2) {
if (obj1.getObjectType() != MovingObject.ObjectType.UNMANNED_VEHICLE && obj2.getObjectType() != MovingObject.ObjectType.UNMANNED_VEHICLE) {
if (obj1.getObjectType() != MovingObjectType.UNMANNED_VEHICLE && obj2.getObjectType() != MovingObjectType.UNMANNED_VEHICLE) {
log.debug("对象 {} 和 {} 都不是无人车,跳过冲突检测", obj1.getObjectName(), obj2.getObjectName());
return Optional.empty();
}
@ -247,12 +248,12 @@ public class PathConflictDetectionService {
* 评估冲突的告警级别
*/
private Optional<ConflictAlertLog.AlertLevel> evaluateAlertLevel(
double distance1, MovingObject.ObjectType obj1Type,
double distance2, MovingObject.ObjectType obj2Type,
double distance1, MovingObjectType obj1Type,
double distance2, MovingObjectType obj2Type,
double timeGap) {
boolean obj1IsUnmannedVehicle = MovingObject.ObjectType.UNMANNED_VEHICLE.equals(obj1Type);
boolean obj2IsUnmannedVehicle = MovingObject.ObjectType.UNMANNED_VEHICLE.equals(obj2Type);
boolean obj1IsUnmannedVehicle = MovingObjectType.UNMANNED_VEHICLE.equals(obj1Type);
boolean obj2IsUnmannedVehicle = MovingObjectType.UNMANNED_VEHICLE.equals(obj2Type);
if (!obj1IsUnmannedVehicle && !obj2IsUnmannedVehicle) {
// 双方都不是无人车根据业务规则不生成告警
@ -429,16 +430,16 @@ public class PathConflictDetectionService {
private final double timeGap;
private final Optional<ConflictAlertLog.AlertType> alertType; // Changed to Optional
private final Optional<ConflictAlertLog.AlertLevel> alertLevel; // Changed to Optional
private final MovingObject.ObjectType object1Type; // New
private final MovingObject.ObjectType object2Type; // New
private final MovingObjectType object1Type; // New
private final MovingObjectType object2Type; // New
public ConflictCalculationResult(double distance1, double distance2,
int timeToConflict1, int timeToConflict2,
double timeGap,
Optional<ConflictAlertLog.AlertType> alertType, // Changed to Optional
Optional<ConflictAlertLog.AlertLevel> alertLevel, // Changed to Optional
MovingObject.ObjectType object1Type, // New
MovingObject.ObjectType object2Type) { // New
MovingObjectType object1Type, // New
MovingObjectType object2Type) { // New
this.distance1 = distance1;
this.distance2 = distance2;
this.timeToConflict1 = timeToConflict1;
@ -457,7 +458,7 @@ public class PathConflictDetectionService {
public double getTimeGap() { return timeGap; }
public Optional<ConflictAlertLog.AlertType> getAlertType() { return alertType; } // Changed
public Optional<ConflictAlertLog.AlertLevel> getAlertLevel() { return alertLevel; } // Changed
public MovingObject.ObjectType getObject1Type() { return object1Type; } // New
public MovingObject.ObjectType getObject2Type() { return object2Type; } // New
public MovingObjectType getObject1Type() { return object1Type; } // New
public MovingObjectType getObject2Type() { return object2Type; } // New
}
}

View File

@ -1,6 +1,6 @@
package com.qaup.collision.rule.model.entity;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.rule.model.enums.RuleAlertLevel;
import com.qaup.collision.rule.model.enums.RuleCategory;
import com.qaup.collision.rule.model.enums.RuleStatus;

View File

@ -1,6 +1,6 @@
package com.qaup.collision.rule.service;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;
@ -167,7 +167,7 @@ public interface LocationRuleQueryService {
* @param timestamp 时间戳
* @return 如果有权限返回true
*/
boolean hasAccessPermission(org.locationtech.jts.geom.Point location, com.qaup.collision.common.model.MovingObjectType vehicleType, java.time.LocalDateTime timestamp);
boolean hasAccessPermission(Point location, MovingObjectType vehicleType, LocalDateTime timestamp);
/**
* 获取车辆类型被禁止访问的规则列表
@ -176,7 +176,7 @@ public interface LocationRuleQueryService {
* @param timestamp 时间戳
* @return 被禁止的规则列表
*/
java.util.List<com.qaup.collision.rule.model.entity.SpatialRule> getRestrictedRules(org.locationtech.jts.geom.Point location, com.qaup.collision.common.model.MovingObjectType vehicleType, java.time.LocalDateTime timestamp);
List<SpatialRule> getRestrictedRules(Point location, MovingObjectType vehicleType, LocalDateTime timestamp);
/**
* 获取车辆类型允许访问的规则列表
@ -185,7 +185,7 @@ public interface LocationRuleQueryService {
* @param timestamp 时间戳
* @return 允许的规则列表
*/
java.util.List<com.qaup.collision.rule.model.entity.SpatialRule> getAllowedRules(org.locationtech.jts.geom.Point location, com.qaup.collision.common.model.MovingObjectType vehicleType, java.time.LocalDateTime timestamp);
List<SpatialRule> getAllowedRules(Point location, MovingObjectType vehicleType, LocalDateTime timestamp);
/**
* 规则覆盖统计信息

View File

@ -1,9 +1,8 @@
package com.qaup.collision.rule.service;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleAlertLevel;
import com.qaup.collision.rule.model.enums.ViolationType;

View File

@ -1,6 +1,6 @@
package com.qaup.collision.rule.service;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleExecutionResult;

View File

@ -1,6 +1,6 @@
package com.qaup.collision.rule.service;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;

View File

@ -1,6 +1,6 @@
package com.qaup.collision.rule.service;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;
import com.qaup.collision.rule.model.enums.RuleStatus;

View File

@ -1,6 +1,6 @@
package com.qaup.collision.rule.service;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;

View File

@ -1,12 +1,12 @@
package com.qaup.collision.rule.service.impl;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.model.spatial.AirportArea;
import com.qaup.collision.common.service.AirportAreaService;
import com.qaup.collision.road.service.RoadNetworkService;
import com.qaup.collision.road.model.RoadInfo;
import com.qaup.collision.common.model.GeoPosition;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;
import com.qaup.collision.rule.model.enums.RuleStatus;
@ -16,6 +16,7 @@ import com.qaup.collision.rule.service.RulePriorityService;
import com.qaup.collision.rule.service.SpatialRuleService;
import com.qaup.collision.rule.service.TimeWindowMatchingService;
import com.qaup.collision.rule.service.VehicleTypePermissionService;
import com.qaup.common.constant.GeoConstants;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
@ -367,7 +368,7 @@ public class LocationRuleQueryServiceImpl implements LocationRuleQueryService {
return 0.0;
}
return ruleGeometry.distance(location) * 111320; // 转换为米近似
return ruleGeometry.distance(location) * GeoConstants.DEGREE_TO_METER; // 转换为米近似
} catch (Exception e) {
logger.error("计算到规则距离失败: ruleId={}", rule.getRuleId(), e);

View File

@ -1,6 +1,6 @@
package com.qaup.collision.rule.service.impl;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.event.RuleViolationEventOccurred; // 导入RuleViolationEventOccurred

View File

@ -1,6 +1,6 @@
package com.qaup.collision.rule.service.impl;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;
@ -560,13 +560,6 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
}
}
/**
* 生成违规描述为了兼容接口
*/
private String generateViolationDescription(SpatialRule rule, Object result, Map<String, Object> vehicleState) {
return generateSimpleDescription(rule, vehicleState);
}
// ===============================================
// 统计和查询方法无人车专用
// ===============================================

View File

@ -1,6 +1,7 @@
package com.qaup.collision.rule.service.impl;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;
import com.qaup.collision.rule.service.RulePriorityService;

View File

@ -1,8 +1,6 @@
package com.qaup.collision.rule.service.impl;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.event.RuleEventPublisher;
import com.qaup.collision.rule.event.RuleViolationEventOccurred;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleExecutionResult;
import com.qaup.collision.rule.model.enums.ViolationType;
@ -10,11 +8,9 @@ import com.qaup.collision.rule.repository.RuleViolationEventRepository;
import com.qaup.collision.rule.service.RuleViolationProcessor;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -40,15 +36,6 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
@Autowired
private RuleViolationEventRepository violationEventRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private RuleEventPublisher ruleEventPublisher;
@Autowired
private ObjectMapper objectMapper;
// ============================================
// 违规事件生成
// ============================================
@ -191,7 +178,6 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
return false;
}
RuleViolationEvent event = eventOpt.get();
// 简化版本只记录日志不修改复杂状态
logger.info("违规事件已确认: eventId={}, acknowledgedBy={}, notes={}", eventId, acknowledgedBy, notes);
return true;
@ -211,7 +197,6 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
return false;
}
RuleViolationEvent event = eventOpt.get();
// 简化版本只记录日志不修改复杂状态
logger.info("违规事件已解决: eventId={}, resolvedBy={}, resolution={}", eventId, resolvedBy, resolution);
return true;
@ -231,7 +216,6 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
return false;
}
RuleViolationEvent event = eventOpt.get();
// 简化版本只记录日志不修改复杂状态
logger.info("违规事件已忽略: eventId={}, ignoredBy={}, reason={}", eventId, ignoredBy, reason);
return true;
@ -415,17 +399,6 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
&& event.getLocation() != null;
}
/**
* 验证违规事件数据完整性基于vehicle_id
*/
private boolean isValidViolationEvent(RuleViolationEvent event) {
return event != null
&& event.getId() != null
&& event.getRuleName() != null
&& event.getVehicleId() != null // 基于vehicle_id验证
&& event.getLocation() != null;
}
private List<String> executeResponseActions(RuleViolationEvent event) {
List<String> actionsExecuted = new ArrayList<>();
@ -448,19 +421,4 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
return actionsExecuted;
}
private void publishViolationEvent(RuleViolationEvent event) {
try {
// 使用统一的事件发布器发布违规事件用于审计日志
ruleEventPublisher.publishViolationEvent(event, this,
String.format("违规处理器处理事件: %s", event.getId()));
// 发布WebSocket事件
RuleViolationEventOccurred webSocketEvent = new RuleViolationEventOccurred(this, event);
eventPublisher.publishEvent(webSocketEvent);
logger.info("违规事件已发布到WebSocket: eventId={}", event.getId());
} catch (Exception e) {
logger.warn("发布违规事件失败: eventId={}", event.getId(), e);
}
}
}

View File

@ -1,6 +1,7 @@
package com.qaup.collision.rule.service.impl;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.spatial.AirportArea;
import com.qaup.collision.common.service.AirportAreaService;
import com.qaup.collision.rule.event.RuleEventPublisher;

View File

@ -1,6 +1,6 @@
package com.qaup.collision.rule.service.impl;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;
import com.qaup.collision.rule.service.VehicleTypePermissionService;
@ -30,7 +30,7 @@ public class VehicleTypePermissionServiceImpl implements VehicleTypePermissionSe
private static final Map<MovingObjectType, Integer> PERMISSION_LEVELS = Map.of(
MovingObjectType.AIRCRAFT, 1, // 最高权限航空器
MovingObjectType.SPECIAL_VEHICLE, 2, // 特殊权限特勤车辆
MovingObjectType.AIRPORT_VEHICLE, 3, // 中等权限机场车辆
MovingObjectType.NORMAL_VEHICLE, 3, // 中等权限普通车辆
MovingObjectType.UNMANNED_VEHICLE, 4 // 受控权限无人车
);
@ -38,7 +38,7 @@ public class VehicleTypePermissionServiceImpl implements VehicleTypePermissionSe
private static final Map<MovingObjectType, Integer> PRIORITY_LEVELS = Map.of(
MovingObjectType.AIRCRAFT, 1, // 最高优先级航空器
MovingObjectType.SPECIAL_VEHICLE, 2, // 特殊优先级特勤车辆
MovingObjectType.AIRPORT_VEHICLE, 3, // 中等优先级机场车辆
MovingObjectType.NORMAL_VEHICLE, 3, // 中等优先级普通车辆
MovingObjectType.UNMANNED_VEHICLE, 4 // 低优先级无人车
);
@ -52,7 +52,7 @@ public class VehicleTypePermissionServiceImpl implements VehicleTypePermissionSe
private static final Map<MovingObjectType, Set<String>> ACCESSIBLE_AREA_TYPES = Map.of(
MovingObjectType.AIRCRAFT, Set.of("RUNWAY", "TAXIWAY", "APRON", "GATE", "RESTRICTED", "MAINTENANCE", "CARGO", "SAFETY", "SERVICE"),
MovingObjectType.SPECIAL_VEHICLE, Set.of("TAXIWAY", "APRON", "GATE", "RESTRICTED", "MAINTENANCE", "CARGO", "SAFETY", "SERVICE"),
MovingObjectType.AIRPORT_VEHICLE, Set.of("APRON", "GATE", "MAINTENANCE", "CARGO", "SERVICE", "TAXIWAY"),
MovingObjectType.NORMAL_VEHICLE, Set.of("APRON", "GATE", "MAINTENANCE", "CARGO", "SERVICE", "TAXIWAY"),
MovingObjectType.UNMANNED_VEHICLE, Set.of("APRON", "MAINTENANCE", "CARGO", "SERVICE")
);
@ -149,8 +149,8 @@ public class VehicleTypePermissionServiceImpl implements VehicleTypePermissionSe
// 特勤车辆24小时可访问紧急需要
return true;
case AIRPORT_VEHICLE:
// 机场车辆一般在05:00-23:00可访问
case NORMAL_VEHICLE:
// 普通车辆一般在05:00-23:00可访问
return !currentTime.isBefore(LocalTime.of(5, 0)) &&
!currentTime.isAfter(LocalTime.of(23, 0));
@ -193,14 +193,14 @@ public class VehicleTypePermissionServiceImpl implements VehicleTypePermissionSe
"特勤车辆具有特殊权限,可紧急通行大部分区域"
);
case AIRPORT_VEHICLE:
case NORMAL_VEHICLE:
return new VehicleAccessRestriction(
vehicleType,
Set.of(RuleCategory.PRIORITY_CONTROL), // 限制优先级控制
Set.of("RUNWAY", "RESTRICTED"), // 限制跑道和受限区域
true, // 需要特殊批准访问某些区域
80, // 最大速度80km/h
"机场车辆有限制访问,不能进入跑道和受限区域"
"普通车辆有限制访问,不能进入跑道和受限区域"
);
case UNMANNED_VEHICLE:
@ -299,25 +299,4 @@ public class VehicleTypePermissionServiceImpl implements VehicleTypePermissionSe
return false;
}
}
// ============================================
// 私有辅助方法
// ============================================
private boolean isAccessProhibited(SpatialRule rule, MovingObjectType vehicleType) {
try {
// 检查规则参数中是否有明确的禁止配置
if (rule.getParameters() != null) {
// 这里可以解析JSON参数检查是否有禁止访问的配置
// 暂时使用简单逻辑如果不在允许列表中则认为被禁止
return !isVehicleTypeInAllowedList(rule, vehicleType);
}
return false;
} catch (Exception e) {
logger.warn("检查访问禁止状态失败: ruleId={}, vehicleType={}", rule.getRuleId(), vehicleType, e);
return false;
}
}
}

View File

@ -14,9 +14,8 @@ import com.qaup.collision.websocket.message.RuleExecutionStatusPayload;
import com.qaup.collision.websocket.message.RuleStateChangePayload;
import com.qaup.collision.websocket.message.RuleViolationPayload;
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
import com.qaup.collision.common.adapter.QuapDataAdapter; // 导入 QuapDataAdapter
import com.qaup.system.domain.SysVehicleInfo; // 导入 SysVehicleInfo
import org.locationtech.jts.io.WKTWriter;
import com.qaup.collision.common.adapter.QuapDataAdapter;
import com.qaup.system.domain.SysVehicleInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
@ -42,7 +41,7 @@ public class RuleEventWebSocketPublisher {
private static final Logger logger = LoggerFactory.getLogger(RuleEventWebSocketPublisher.class);
private final ApplicationEventPublisher eventPublisher;
private final QuapDataAdapter quapDataAdapter; // 注入 QuapDataAdapter
private final QuapDataAdapter quapDataAdapter;
// 构造函数中初始化ObjectMapper
public RuleEventWebSocketPublisher(ApplicationEventPublisher eventPublisher, QuapDataAdapter quapDataAdapter) {

View File

@ -1,6 +1,6 @@
package com.qaup.collision.websocket.controller;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.service.VehicleLocationService;
import lombok.RequiredArgsConstructor;

View File

@ -0,0 +1,5 @@
package com.qaup.collision.websocket.controller;
public class MovingObjectType {
}

View File

@ -0,0 +1,57 @@
package com.qaup.collision.websocket.event;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
/**
* 航空器路由更新事件
*
* 用于通知前端航空器路由信息的变更
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-17
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AircraftRouteUpdateEvent {
/**
* 航班号
*/
private String flightNo;
/**
* 路由类型 (IN: 进港, OUT: 出港)
*/
private String routeType;
/**
* 路由状态 (COMPLETE: 完成, CALCULATING: 计算中, MODIFIED: 已修改)
*/
private String routeStatus;
/**
* 路由编码 (: "F1,L4,138")
*/
private String routeCodes;
/**
* 航空器状态 (IN: 进港, OUT: 出港, ARRIVED: 到达)
*/
private String aircraftStatus;
/**
* 路由几何数据 (WKT格式)
*/
private String routeGeometry;
/**
* 事件时间戳
*/
private Long timestamp;
}

View File

@ -0,0 +1,70 @@
package com.qaup.collision.websocket.listener;
import com.qaup.collision.websocket.event.AircraftRouteUpdateEvent;
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 航空器路由更新事件监听器
*
* 监听航空器路由更新事件并通过WebSocket推送给前端客户端
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-17
*/
@Slf4j
@Component
public class AircraftRouteUpdateEventListener {
@Autowired
private CollisionWebSocketHandler webSocketHandler;
@Autowired
private ObjectMapper objectMapper;
/**
* 处理航空器路由更新事件
*
* @param event 航空器路由更新事件
*/
@EventListener
public void handleAircraftRouteUpdateEvent(AircraftRouteUpdateEvent event) {
try {
// 构建WebSocket消息
String message = objectMapper.writeValueAsString(new WebSocketMessage(
"aircraftRouteUpdate",
event,
System.currentTimeMillis()
));
// 广播消息给所有连接的客户端
webSocketHandler.broadcastMessage(message);
log.info("航空器路由更新事件已通过WebSocket推送: 航班号={}, 路由类型={}, 状态={}",
event.getFlightNo(), event.getRouteType(), event.getAircraftStatus());
} catch (Exception e) {
log.error("处理航空器路由更新事件失败: flightNo={}", event.getFlightNo(), e);
}
}
/**
* WebSocket消息包装类
*/
private static class WebSocketMessage {
public final String type;
public final Object data;
public final long timestamp;
public WebSocketMessage(String type, Object data, long timestamp) {
this.type = type;
this.data = data;
this.timestamp = timestamp;
}
}
}

View File

@ -6,7 +6,8 @@ import com.qaup.system.service.ISysDriverInfoService;
import com.qaup.system.service.ISysVehicleInfoService;
import com.qaup.system.service.ISysVehicleTypeService;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -283,38 +284,6 @@ class QuapDataAdapterTest {
assertNull(result);
}
@Test
void testConvertToMovingObjectType_TypeCode() {
// 测试各种类型编码映射根据QuapDataAdapter的实际实现
assertEquals(MovingObjectType.NORMAL_VEHICLE,
quapDataAdapter.convertToMovingObjectType("NM")); // NM -> NORMAL_VEHICLE
assertEquals(MovingObjectType.UNMANNED_VEHICLE,
quapDataAdapter.convertToMovingObjectType("UV")); // UV -> UNMANNED_VEHICLE
assertEquals(MovingObjectType.SPECIAL_VEHICLE,
quapDataAdapter.convertToMovingObjectType("SP")); // SP -> SPECIAL_VEHICLE
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType("UNKNOWN")); // 未知类型
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType((String) null)); // null
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType("")); // 空字符串
}
@Test
@SuppressWarnings("deprecation")
void testConvertToMovingObjectType_TypeId_Deprecated() {
// 测试已废弃的类型ID映射方法
assertEquals(MovingObjectType.NORMAL_VEHICLE,
quapDataAdapter.convertToMovingObjectType(1L)); // 1L -> NM -> NORMAL_VEHICLE
assertEquals(MovingObjectType.UNMANNED_VEHICLE,
quapDataAdapter.convertToMovingObjectType(2L)); // 2L -> UV -> UNMANNED_VEHICLE
assertEquals(MovingObjectType.SPECIAL_VEHICLE,
quapDataAdapter.convertToMovingObjectType(3L)); // 3L -> SP -> SPECIAL_VEHICLE
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType(999L)); // 999L -> 未找到 -> UNKNOWN
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType((Long) null)); // null
}
@Test
void testBuildVehicleQueryCondition() {

View File

@ -1,6 +1,7 @@
package com.qaup.collision.geofence.service;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.common.model.spatial.AirportArea;
import com.qaup.collision.common.service.AirportAreaService;
import com.qaup.collision.geofence.model.dto.GeofenceAccessCheckResult;
@ -63,7 +64,7 @@ class UnmannedVehicleGeofenceServiceTest {
MovingObject unmannedVehicle = MovingObject.builder()
.objectId("UV001")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车")
.currentPosition(position)
.currentSpeed(10.0)
@ -98,7 +99,7 @@ class UnmannedVehicleGeofenceServiceTest {
MovingObject aircraft = MovingObject.builder()
.objectId("AC001")
.objectType(MovingObject.ObjectType.AIRCRAFT)
.objectType(MovingObjectType.AIRCRAFT)
.objectName("测试航空器")
.currentPosition(position)
.build();
@ -126,7 +127,7 @@ class UnmannedVehicleGeofenceServiceTest {
MovingObject unmannedVehicle = MovingObject.builder()
.objectId("UV001")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车")
.currentPosition(position)
.build();
@ -153,7 +154,7 @@ class UnmannedVehicleGeofenceServiceTest {
MovingObject unmannedVehicle = MovingObject.builder()
.objectId("UV001")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车")
.currentPosition(position)
.build();
@ -184,7 +185,7 @@ class UnmannedVehicleGeofenceServiceTest {
MovingObject unmannedVehicle = MovingObject.builder()
.objectId("UV001")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车")
.currentPosition(position)
.currentSpeed(15.0)
@ -222,7 +223,7 @@ class UnmannedVehicleGeofenceServiceTest {
MovingObject aircraft = MovingObject.builder()
.objectId("AC001")
.objectType(MovingObject.ObjectType.AIRCRAFT)
.objectType(MovingObjectType.AIRCRAFT)
.objectName("测试航空器")
.currentPosition(position)
.build();
@ -249,7 +250,7 @@ class UnmannedVehicleGeofenceServiceTest {
// Arrange
MovingObject unmannedVehicle = MovingObject.builder()
.objectId("UV001")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车")
.currentPosition(null) // 位置为空
.build();
@ -273,7 +274,7 @@ class UnmannedVehicleGeofenceServiceTest {
MovingObject unmannedVehicle = MovingObject.builder()
.objectId("UV001")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车")
.currentPosition(position)
.currentSpeed(15.0)

View File

@ -1,6 +1,7 @@
package com.qaup.collision.pathconflict.service;
import com.qaup.collision.common.model.MovingObject;
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
import com.qaup.collision.pathconflict.repository.TransportRouteRepository;
import com.qaup.collision.pathconflict.repository.ConflictAlertLogRepository;
@ -65,7 +66,7 @@ class PathConflictDetectionServiceNullSpeedTest {
MovingObject obj1 = MovingObject.builder()
.objectId("UV001")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车1")
.currentPosition(position1)
.currentSpeed(null) // 速度为null
@ -75,7 +76,7 @@ class PathConflictDetectionServiceNullSpeedTest {
MovingObject obj2 = MovingObject.builder()
.objectId("UV002")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车2")
.currentPosition(position2)
.currentSpeed(15.0) // 正常速度
@ -120,7 +121,7 @@ class PathConflictDetectionServiceNullSpeedTest {
MovingObject obj1 = MovingObject.builder()
.objectId("UV001")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车1")
.currentPosition(position1)
.currentSpeed(null) // 速度为null
@ -130,7 +131,7 @@ class PathConflictDetectionServiceNullSpeedTest {
MovingObject obj2 = MovingObject.builder()
.objectId("UV002")
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
.objectType(MovingObjectType.UNMANNED_VEHICLE)
.objectName("测试无人车2")
.currentPosition(position2)
.currentSpeed(null) // 速度也为null

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>qaup-management</artifactId>
<groupId>com.qaup</groupId>
<version>3.8.9</version>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -4,7 +4,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.math.BigDecimal;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import com.qaup.common.utils.poi.ExcelHandlerAdapter;
@ -54,9 +54,9 @@ public @interface Excel
public int scale() default -1;
/**
* BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
* BigDecimal 舍入规则 默认:RoundingMode.HALF_EVEN (值为6)
*/
public int roundingMode() default BigDecimal.ROUND_HALF_EVEN;
public int roundingMode() default 6;
/**
* 导出时在excel中每个列的高度

View File

@ -0,0 +1,77 @@
package com.qaup.common.constant;
/**
* 地理计算相关常量
*
* @author qaup
*/
public class GeoConstants {
/**
* 地球半径
* 使用WGS84椭球体的赤道半径
*/
public static final double EARTH_RADIUS_METERS = 6378137.0;
/**
* 地球半径千米
*/
public static final double EARTH_RADIUS_KM = 6378.137;
/**
* WGS84椭球体赤道半径
* 用于高精度计算
*/
public static final double WGS84_EQUATORIAL_RADIUS = 6378137.0;
/**
* WGS84椭球体极半径
*/
public static final double WGS84_POLAR_RADIUS = 6356752.314245;
/**
* 度到米的近似转换系数在赤道附近
* 1度经度或纬度约等于111320米
*/
public static final double DEGREE_TO_METER = 111320.0;
/**
* 米到度的近似转换系数
*/
public static final double METER_TO_DEGREE = 1.0 / DEGREE_TO_METER;
/**
* 米每秒到公里每小时的转换系数
*/
public static final double MPS_TO_KPH = 3.6;
/**
* 公里每小时到米每秒的转换系数
*/
public static final double KPH_TO_MPS = 1.0 / MPS_TO_KPH;
/**
* WGS84坐标系SRID
*/
public static final int WGS84_SRID = 4326;
/**
* 默认坐标精度小数位数
*/
public static final int DEFAULT_COORDINATE_PRECISION = 6;
/**
* 距离计算精度小数位数
*/
public static final int DISTANCE_PRECISION = 2;
/**
* 速度计算精度小数位数
*/
public static final int SPEED_PRECISION = 2;
/**
* 方向计算精度小数位数
*/
public static final int DIRECTION_PRECISION = 2;
}

View File

@ -18,6 +18,13 @@ import org.apache.commons.lang3.time.DateFormatUtils;
*/
public class DateUtils extends org.apache.commons.lang3.time.DateUtils
{
/**
* 私有构造函数防止实例化
*/
private DateUtils()
{
}
public static String YYYY = "yyyy";
public static String YYYY_MM = "yyyy-MM";

View File

@ -0,0 +1,200 @@
package com.qaup.common.utils;
import com.qaup.common.constant.GeoConstants;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 地理计算工具类
* 提供统一的地理距离方向速度计算方法
*
* @author qaup
*/
public class GeoUtils {
/**
* 使用Haversine公式计算两点间的地理距离
*
* @param lat1 第一个点的纬度
* @param lon1 第一个点的经度
* @param lat2 第二个点的纬度
* @param lon2 第二个点的经度
* @return 距离
*/
public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
if (lat1 == lat2 && lon1 == lon2) {
return 0.0;
}
double lat1Rad = Math.toRadians(lat1);
double lat2Rad = Math.toRadians(lat2);
double deltaLatRad = Math.toRadians(lat2 - lat1);
double deltaLonRad = Math.toRadians(lon2 - lon1);
double a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return GeoConstants.EARTH_RADIUS_METERS * c;
}
/**
* 使用Haversine公式计算两点间的地理距离返回BigDecimal
*
* @param lat1 第一个点的纬度
* @param lon1 第一个点的经度
* @param lat2 第二个点的纬度
* @param lon2 第二个点的经度
* @return 距离保留指定精度
*/
public static BigDecimal calculateDistanceAsBigDecimal(double lat1, double lon1, double lat2, double lon2) {
double distance = calculateDistance(lat1, lon1, lat2, lon2);
return BigDecimal.valueOf(distance).setScale(GeoConstants.DISTANCE_PRECISION, RoundingMode.HALF_UP);
}
/**
* 计算两点间的方向角方位角
*
* @param lat1 起点纬度
* @param lon1 起点经度
* @param lat2 终点纬度
* @param lon2 终点经度
* @return 方向角0-360度正北为0度
*/
public static double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
double lat1Rad = Math.toRadians(lat1);
double lat2Rad = Math.toRadians(lat2);
double deltaLonRad = Math.toRadians(lon2 - lon1);
double y = Math.sin(deltaLonRad) * Math.cos(lat2Rad);
double x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(deltaLonRad);
double bearingRad = Math.atan2(y, x);
double bearingDeg = Math.toDegrees(bearingRad);
// 转换为0-360度范围
return (bearingDeg + 360) % 360;
}
/**
* 根据距离和时间计算速度
*
* @param distanceMeters 距离
* @param timeSeconds 时间
* @return 速度公里/小时
*/
public static double calculateSpeed(double distanceMeters, double timeSeconds) {
if (timeSeconds <= 0) {
return 0.0;
}
double speedMps = distanceMeters / timeSeconds;
return speedMps * GeoConstants.MPS_TO_KPH;
}
/**
* 根据距离和时间计算速度返回指定精度
*
* @param distanceMeters 距离
* @param timeSeconds 时间
* @return 速度公里/小时保留指定精度
*/
public static double calculateSpeedWithPrecision(double distanceMeters, double timeSeconds) {
double speed = calculateSpeed(distanceMeters, timeSeconds);
return Math.round(speed * Math.pow(10, GeoConstants.SPEED_PRECISION)) / Math.pow(10, GeoConstants.SPEED_PRECISION);
}
/**
* 度到米的近似转换适用于小范围计算
*
* @param degrees 度数
* @return 米数近似值
*/
public static double degreesToMeters(double degrees) {
return degrees * GeoConstants.DEGREE_TO_METER;
}
/**
* 米到度的近似转换适用于小范围计算
*
* @param meters 米数
* @return 度数近似值
*/
public static double metersToDegrees(double meters) {
return meters * GeoConstants.METER_TO_DEGREE;
}
/**
* 米每秒转换为公里每小时
*
* @param mps 米每秒
* @return 公里每小时
*/
public static double mpsToKph(double mps) {
return mps * GeoConstants.MPS_TO_KPH;
}
/**
* 公里每小时转换为米每秒
*
* @param kph 公里每小时
* @return 米每秒
*/
public static double kphToMps(double kph) {
return kph * GeoConstants.KPH_TO_MPS;
}
/**
* 格式化坐标到指定精度
*
* @param coordinate 坐标值
* @param precision 精度小数位数
* @return 格式化后的坐标字符串
*/
public static String formatCoordinate(double coordinate, int precision) {
return String.format("%." + precision + "f", coordinate);
}
/**
* 格式化坐标到默认精度
*
* @param coordinate 坐标值
* @return 格式化后的坐标字符串
*/
public static String formatCoordinate(double coordinate) {
return formatCoordinate(coordinate, GeoConstants.DEFAULT_COORDINATE_PRECISION);
}
/**
* 验证经度是否有效
*
* @param longitude 经度
* @return 是否有效
*/
public static boolean isValidLongitude(double longitude) {
return longitude >= -180.0 && longitude <= 180.0;
}
/**
* 验证纬度是否有效
*
* @param latitude 纬度
* @return 是否有效
*/
public static boolean isValidLatitude(double latitude) {
return latitude >= -90.0 && latitude <= 90.0;
}
/**
* 验证坐标是否有效
*
* @param longitude 经度
* @param latitude 纬度
* @return 是否有效
*/
public static boolean isValidCoordinate(double longitude, double latitude) {
return isValidLongitude(longitude) && isValidLatitude(latitude);
}
}

View File

@ -17,6 +17,13 @@ import com.qaup.common.core.text.StrFormatter;
*/
public class StringUtils extends org.apache.commons.lang3.StringUtils
{
/**
* 私有构造函数防止实例化
*/
private StringUtils()
{
}
/** 空字符串 */
private static final String NULLSTR = "";

View File

@ -419,7 +419,7 @@ public class ExcelUtil<T>
Object val = this.getCellValue(row, entry.getKey());
// 如果不存在实例则新建.
entity = (entity == null ? clazz.newInstance() : entity);
entity = (entity == null ? clazz.getDeclaredConstructor().newInstance() : entity);
// 从map中得到对应列的field.
Field field = (Field) entry.getValue()[0];
Excel attr = (Excel) entry.getValue()[1];
@ -1170,7 +1170,9 @@ public class ExcelUtil<T>
}
else if (value instanceof BigDecimal && -1 != attr.scale())
{
cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue());
cell.setCellValue(
(((BigDecimal) value).setScale(attr.scale(), java.math.RoundingMode.valueOf(attr.roundingMode()))).doubleValue()
);
}
else if (!attr.handler().equals(ExcelHandlerAdapter.class))
{
@ -1391,7 +1393,7 @@ public class ExcelUtil<T>
{
try
{
Object instance = excel.handler().newInstance();
Object instance = excel.handler().getDeclaredConstructor().newInstance();
Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class, Cell.class, Workbook.class });
value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb);
}

View File

@ -313,7 +313,7 @@ public class ReflectUtils
public static void makeAccessible(Method method)
{
if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers()))
&& !method.isAccessible())
&& !method.canAccess(null))
{
method.setAccessible(true);
}
@ -325,7 +325,7 @@ public class ReflectUtils
public static void makeAccessible(Field field)
{
if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())
|| Modifier.isFinal(field.getModifiers())) && !field.isAccessible())
|| Modifier.isFinal(field.getModifiers())) && !field.canAccess(null))
{
field.setAccessible(true);
}

View File

@ -10,6 +10,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import com.qaup.common.utils.StringUtils;
import org.springframework.lang.NonNull;
/**
* spring工具类 方便在非spring管理环境中获取bean
@ -25,13 +26,13 @@ public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationC
private static ApplicationContext applicationContext;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
public void postProcessBeanFactory(@NonNull ConfigurableListableBeanFactory beanFactory) throws BeansException
{
SpringUtils.beanFactory = beanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException
{
SpringUtils.applicationContext = applicationContext;
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>qaup-management</artifactId>
<groupId>com.qaup</groupId>
<version>3.8.9</version>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -8,6 +8,7 @@ import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;
import com.qaup.common.constant.Constants;
import org.springframework.lang.Nullable;
/**
* Redis使用FastJson序列化
@ -29,7 +30,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
}
@Override
public byte[] serialize(T t) throws SerializationException
public byte[] serialize(@Nullable T t) throws SerializationException
{
if (t == null)
{
@ -39,7 +40,7 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
public T deserialize(@Nullable byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{

View File

@ -9,6 +9,8 @@ import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import com.qaup.common.constant.Constants;
import org.springframework.lang.NonNull;
/**
* 资源文件配置加载
*
@ -36,7 +38,7 @@ public class I18nConfig implements WebMvcConfigurer
}
@Override
public void addInterceptors(InterceptorRegistry registry)
public void addInterceptors(@NonNull InterceptorRegistry registry)
{
registry.addInterceptor(localeChangeInterceptor());
}

View File

@ -126,7 +126,11 @@ public class MyBatisConfig
sessionFactory.setDataSource(dataSource);
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
sessionFactory.setConfigLocation(
new DefaultResourceLoader().getResource(
java.util.Objects.requireNonNull(configLocation, "mybatis.configLocation must not be null")
)
);
return sessionFactory.getObject();
}
}

View File

@ -1,6 +1,5 @@
package com.qaup.framework.config;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -16,7 +15,7 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
public class RedisConfig
{
@Bean("redisTemplate")
@SuppressWarnings(value = { "unchecked", "rawtypes" })

View File

@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.lang.NonNull;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@ -27,7 +28,7 @@ public class ResourcesConfig implements WebMvcConfigurer
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry)
public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry)
{
/** 本地文件上传路径 */
registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**")
@ -47,7 +48,7 @@ public class ResourcesConfig implements WebMvcConfigurer
* 自定义拦截规则
*/
@Override
public void addInterceptors(InterceptorRegistry registry)
public void addInterceptors(@NonNull InterceptorRegistry registry)
{
registry.addInterceptor(repeatSubmitInterceptor)
.addPathPatterns("/**");

View File

@ -3,7 +3,6 @@ package com.qaup.framework.config.properties;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
import org.apache.commons.lang3.RegExUtils;
@ -17,6 +16,7 @@ import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import com.qaup.common.annotation.Anonymous;
import org.springframework.lang.NonNull;
/**
* 设置Anonymous注解允许匿名访问的url
@ -66,18 +66,24 @@ public class PermitAllUrlProperties implements InitializingBean, ApplicationCont
// 获取方法上边的注解 替代path variable *
Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues()) //
.forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
Optional.ofNullable(method).ifPresent(anonymous ->
Optional.ofNullable(info.getPathPatternsCondition())
.ifPresent(condition -> condition.getPatternValues()
.forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))))
);
// 获取类上边的注解, 替代path variable *
Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);
Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues())
.forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
Optional.ofNullable(controller).ifPresent(anonymous ->
Optional.ofNullable(info.getPathPatternsCondition())
.ifPresent(condition -> condition.getPatternValues()
.forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))))
);
});
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException
public void setApplicationContext(@NonNull ApplicationContext context) throws BeansException
{
this.applicationContext = context;
}

View File

@ -3,6 +3,8 @@ package com.qaup.framework.interceptor;
import java.lang.reflect.Method;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
@ -10,8 +12,6 @@ import com.alibaba.fastjson2.JSON;
import com.qaup.common.annotation.RepeatSubmit;
import com.qaup.common.core.domain.AjaxResult;
import com.qaup.common.utils.ServletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 防止重复提交拦截器
@ -21,10 +21,8 @@ import org.slf4j.LoggerFactory;
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
private static final Logger log = LoggerFactory.getLogger(RepeatSubmitInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception
{
if (handler instanceof HandlerMethod)
{

View File

@ -6,6 +6,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
@ -15,8 +16,6 @@ import com.qaup.common.core.domain.model.LoginUser;
import com.qaup.common.utils.SecurityUtils;
import com.qaup.common.utils.StringUtils;
import com.qaup.framework.web.service.TokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* token过滤器 验证token有效性
@ -25,14 +24,12 @@ import org.slf4j.LoggerFactory;
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);

View File

@ -12,8 +12,6 @@ import com.qaup.common.constant.HttpStatus;
import com.qaup.common.core.domain.AjaxResult;
import com.qaup.common.utils.ServletUtils;
import com.qaup.common.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 认证失败处理类 返回未授权
@ -25,8 +23,6 @@ public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, S
{
private static final long serialVersionUID = -8970718410437077606L;
private static final Logger log = LoggerFactory.getLogger(AuthenticationEntryPointImpl.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException

View File

@ -77,6 +77,7 @@ public class GlobalExceptionHandler
/**
* 请求参数类型不匹配
*/
@SuppressWarnings("null")
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request)
{
@ -130,6 +131,7 @@ public class GlobalExceptionHandler
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e)
{
log.error(e.getMessage(), e);
@SuppressWarnings("null")
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return AjaxResult.error(message);
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>qaup-management</artifactId>
<groupId>com.qaup</groupId>
<version>3.8.9</version>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>qaup-management</artifactId>
<groupId>com.qaup</groupId>
<version>3.8.9</version>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>qaup-management</artifactId>
<groupId>com.qaup</groupId>
<version>3.8.9</version>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -24,6 +24,50 @@
<version>${qaup.version}</version>
</dependency>
<!-- Lombok支持 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- SpringDoc OpenAPI UI 依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- Jakarta Validation API -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<release>${java.version}</release>
<encoding>${project.build.sourceEncoding}</encoding>
<parameters>true</parameters>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -7,9 +7,9 @@ import com.qaup.common.core.domain.BaseEntity;
/**
* 车辆类型路径编码模式对象 sys_vehicle_type
*
* @author Claude Code
* @date 2025-07-13
*
* @author qaup
* @date 2025-07-14
*/
public class SysVehicleType extends BaseEntity
{

View File

@ -0,0 +1,70 @@
package com.qaup.system.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 轨迹查询请求DTO
*
* @author qaup
* @date 2025-01-17
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TrajectoryQueryRequest {
/** 车辆ID列表 */
private List<Long> vehicleIds;
/** 开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/** 结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
/** 是否简化数据 */
@Builder.Default
private Boolean simplified = false;
/** 最大轨迹点数量 */
@Builder.Default
private Integer maxPoints = 1000;
/** 是否包含统计信息 */
@Builder.Default
private Boolean includeStatistics = true;
/** 是否包含GeoJSON格式 */
@Builder.Default
private Boolean includeGeoJson = false;
/** 数据质量过滤 */
private String dataQuality;
/** 最小速度过滤(km/h) */
private BigDecimal minSpeed;
/** 最大速度过滤(km/h) */
private BigDecimal maxSpeed;
/** 区域过滤(WKT格式多边形) */
private String areaWkt;
/** 是否只返回移动轨迹(过滤停车点) */
@Builder.Default
private Boolean movingOnly = false;
/** 车牌号列表(可选,用于按车牌查询) */
private List<String> licensePlates;
}

View File

@ -0,0 +1,57 @@
package com.qaup.system.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
/**
* 轨迹点VO
*
* @author qaup
* @date 2025-01-17
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TrajectoryPoint {
/** 经度 */
private BigDecimal longitude;
/** 纬度 */
private BigDecimal latitude;
/** 海拔高度(米) */
private BigDecimal altitude;
/** 速度(km/h) */
private BigDecimal speed;
/** 朝向角度(度) */
private BigDecimal heading;
/** 位置时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date timestamp;
/** 数据质量 */
private String dataQuality;
/** 原始位置数据(WKT格式) */
private String location;
/** 是否为停车点 */
private Boolean isStopPoint;
/** 与前一点的距离(米) */
private BigDecimal distanceFromPrevious;
/** 与前一点的时间间隔(秒) */
private Long timeIntervalFromPrevious;
}

View File

@ -0,0 +1,67 @@
package com.qaup.system.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
/**
* 轨迹统计信息VO
*
* @author qaup
* @date 2025-01-17
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TrajectoryStatistics {
/** 总距离(公里) */
private BigDecimal totalDistance;
/** 平均速度(km/h) */
private BigDecimal averageSpeed;
/** 最大速度(km/h) */
private BigDecimal maxSpeed;
/** 最小速度(km/h) */
private BigDecimal minSpeed;
/** 行驶时长(秒) */
private Long durationSeconds;
/** 行驶时长(格式化) */
private String durationFormatted;
/** 轨迹点数量 */
private Integer pointCount;
/** 首个轨迹点时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date firstPointTime;
/** 最后轨迹点时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastPointTime;
/** 停车次数 */
private Integer stopCount;
/** 停车总时长(秒) */
private Long totalStopDuration;
/** 最高海拔(米) */
private BigDecimal maxAltitude;
/** 最低海拔(米) */
private BigDecimal minAltitude;
/** 平均海拔(米) */
private BigDecimal averageAltitude;
}

View File

@ -0,0 +1,79 @@
package com.qaup.system.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
/**
* 车辆位置回放数据VO
* 专门用于轨迹回放功能包含回放所需的序列和时间信息
*
* @author qaup
* @date 2025-01-17
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VehicleLocationPlaybackVO {
/** 车辆ID */
private Long vehicleId;
/** 车牌号 */
private String licensePlate;
/** 车辆类型 */
private String vehicleType;
/** 回放序列号 */
private Long sequenceNumber;
/** 经度 */
private BigDecimal longitude;
/** 纬度 */
private BigDecimal latitude;
/** 海拔高度(米) */
private BigDecimal altitude;
/** 速度(km/h) */
private BigDecimal speed;
/** 朝向角度(度) */
private BigDecimal heading;
/** 位置时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date timestamp;
/** 数据质量 */
private String dataQuality;
/** 与上一点的时间间隔(毫秒) */
private Long intervalMs;
/** 累计行驶距离(米) */
private BigDecimal cumulativeDistance;
/** 累计行驶时间(秒) */
private Long cumulativeTime;
/** 回放进度百分比 */
private BigDecimal progressPercentage;
/** 是否为关键帧(起点、终点、转向点等) */
private Boolean isKeyFrame;
/** 关键帧类型 */
private String keyFrameType;
/** 回放建议延迟(毫秒) */
private Long suggestedDelay;
}

View File

@ -0,0 +1,61 @@
package com.qaup.system.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
/**
* 车辆轨迹查询结果VO
*
* @author qaup
* @date 2025-01-17
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VehicleTrajectoryVO {
/** 车辆ID */
private Long vehicleId;
/** 车牌号 */
private String licensePlate;
/** 车辆类型 */
private String vehicleType;
/** 品牌 */
private String brand;
/** 所属单位 */
private String owningUnit;
/** 轨迹开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/** 轨迹结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
/** 轨迹统计信息 */
private TrajectoryStatistics statistics;
/** 轨迹点数据 */
private List<TrajectoryPoint> points;
/** GeoJSON格式轨迹线 */
private String trajectoryLineGeoJson;
/** 是否为简化数据 */
private Boolean simplified;
/** 数据质量 */
private String dataQuality;
}

View File

@ -1,7 +1,10 @@
package com.qaup.system.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.qaup.system.domain.SysVehicleLocation;
import com.qaup.system.domain.vo.TrajectoryStatistics;
/**
* 车辆运动信息Mapper接口
@ -45,9 +48,81 @@ public interface SysVehicleLocationMapper
/**
* 根据车牌号查询车辆最新位置信息
*
*
* @param licensePlate 车牌号
* @return 车辆运动信息
*/
public SysVehicleLocation selectLatestSysVehicleLocationByLicensePlate(String licensePlate);
}
/**
* 查询车辆轨迹数据支持采样优化
*
* @param vehicleId 车辆ID
* @param startTime 开始时间
* @param endTime 结束时间
* @param simplified 是否简化数据
* @param maxPoints 最大轨迹点数量
* @return 车辆运动信息列表
*/
public List<SysVehicleLocation> selectVehicleTrajectory(@Param("vehicleId") Long vehicleId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime,
@Param("simplified") Boolean simplified,
@Param("maxPoints") Integer maxPoints);
/**
* 查询轨迹统计信息
*
* @param vehicleId 车辆ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return 轨迹统计信息
*/
public TrajectoryStatistics selectTrajectoryStatistics(@Param("vehicleId") Long vehicleId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/**
* 查询轨迹回放数据
*
* @param vehicleId 车辆ID
* @param startTime 开始时间
* @param endTime 结束时间
* @param intervalSeconds 时间间隔()
* @return 车辆运动信息列表
*/
public List<SysVehicleLocation> selectTrajectoryPlaybackData(@Param("vehicleId") Long vehicleId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime,
@Param("intervalSeconds") Integer intervalSeconds);
/**
* 查询车辆在指定区域内的轨迹
*
* @param vehicleId 车辆ID
* @param startTime 开始时间
* @param endTime 结束时间
* @param areaWkt 区域WKT格式字符串
* @return 车辆运动信息列表
*/
public List<SysVehicleLocation> selectVehicleTrajectoryInArea(@Param("vehicleId") Long vehicleId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime,
@Param("areaWkt") String areaWkt);
/**
* 批量查询多车辆轨迹数据
*
* @param vehicleIds 车辆ID列表
* @param startTime 开始时间
* @param endTime 结束时间
* @param simplified 是否简化数据
* @param maxPoints 最大轨迹点数量
* @return 车辆运动信息列表
*/
public List<SysVehicleLocation> selectBatchVehicleTrajectory(@Param("vehicleIds") List<Long> vehicleIds,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime,
@Param("simplified") Boolean simplified,
@Param("maxPoints") Integer maxPoints);
}

View File

@ -1,7 +1,12 @@
package com.qaup.system.service;
import java.util.Date;
import java.util.List;
import com.qaup.system.domain.SysVehicleLocation;
import com.qaup.system.domain.vo.VehicleTrajectoryVO;
import com.qaup.system.domain.vo.VehicleLocationPlaybackVO;
import com.qaup.system.domain.vo.TrajectoryStatistics;
import com.qaup.system.domain.dto.TrajectoryQueryRequest;
/**
* 车辆运动信息Service接口
@ -45,9 +50,64 @@ public interface ISysVehicleLocationService
/**
* 根据车牌号查询车辆最新位置信息
*
*
* @param licensePlate 车牌号
* @return 车辆运动信息
*/
public SysVehicleLocation selectLatestSysVehicleLocationByLicensePlate(String licensePlate);
}
/**
* 获取车辆轨迹数据
*
* @param vehicleId 车辆ID
* @param startTime 开始时间
* @param endTime 结束时间
* @param simplified 是否简化数据
* @param maxPoints 最大轨迹点数量
* @return 车辆轨迹数据
*/
public VehicleTrajectoryVO getVehicleTrajectory(Long vehicleId, Date startTime, Date endTime,
Boolean simplified, Integer maxPoints);
/**
* 批量获取多车辆轨迹数据
*
* @param request 轨迹查询请求
* @return 车辆轨迹数据列表
*/
public List<VehicleTrajectoryVO> getBatchVehicleTrajectory(TrajectoryQueryRequest request);
/**
* 获取轨迹回放数据
*
* @param vehicleId 车辆ID
* @param startTime 开始时间
* @param endTime 结束时间
* @param intervalSeconds 时间间隔()
* @return 轨迹回放数据列表
*/
public List<VehicleLocationPlaybackVO> getTrajectoryPlaybackData(Long vehicleId, Date startTime,
Date endTime, Integer intervalSeconds);
/**
* 获取车辆在指定区域内的轨迹
*
* @param vehicleId 车辆ID
* @param startTime 开始时间
* @param endTime 结束时间
* @param areaWkt 区域WKT格式字符串
* @return 车辆轨迹数据
*/
public VehicleTrajectoryVO getVehicleTrajectoryInArea(Long vehicleId, Date startTime, Date endTime,
String areaWkt);
/**
* 获取轨迹统计信息
*
* @param vehicleId 车辆ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return 轨迹统计信息
*/
public TrajectoryStatistics getTrajectoryStatistics(Long vehicleId, Date startTime, Date endTime);
}

View File

@ -1,10 +1,27 @@
package com.qaup.system.service.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.qaup.common.utils.GeoUtils;
import com.qaup.system.mapper.SysVehicleLocationMapper;
import com.qaup.system.domain.SysVehicleLocation;
import com.qaup.system.domain.vo.VehicleTrajectoryVO;
import com.qaup.system.domain.vo.VehicleLocationPlaybackVO;
import com.qaup.system.domain.vo.TrajectoryStatistics;
import com.qaup.system.domain.vo.TrajectoryPoint;
import com.qaup.system.domain.dto.TrajectoryQueryRequest;
import com.qaup.system.service.ISysVehicleLocationService;
/**
@ -69,7 +86,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
/**
* 根据车牌号查询车辆最新位置信息
*
*
* @param licensePlate 车牌号
* @return 车辆运动信息
*/
@ -78,4 +95,498 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
{
return sysVehicleLocationMapper.selectLatestSysVehicleLocationByLicensePlate(licensePlate);
}
}
/**
* 获取车辆轨迹数据
*/
@Override
public VehicleTrajectoryVO getVehicleTrajectory(Long vehicleId, Date startTime, Date endTime,
Boolean simplified, Integer maxPoints) {
// 参数校验
if (vehicleId == null || startTime == null || endTime == null) {
throw new IllegalArgumentException("车辆ID、开始时间和结束时间不能为空");
}
if (startTime.after(endTime)) {
throw new IllegalArgumentException("开始时间不能晚于结束时间");
}
// 查询轨迹数据
List<SysVehicleLocation> locations = sysVehicleLocationMapper
.selectVehicleTrajectory(vehicleId, startTime, endTime, simplified, maxPoints);
if (CollectionUtils.isEmpty(locations)) {
return createEmptyTrajectory(vehicleId, startTime, endTime);
}
// 获取车辆基础信息从第一条记录中获取
SysVehicleLocation firstLocation = locations.get(0);
// 转换轨迹点数据
List<TrajectoryPoint> points = convertToTrajectoryPoints(locations);
// 获取统计信息
TrajectoryStatistics statistics = getTrajectoryStatistics(vehicleId, startTime, endTime);
// 构建返回结果
return VehicleTrajectoryVO.builder()
.vehicleId(vehicleId)
.licensePlate(firstLocation.getLicensePlate())
.vehicleType(firstLocation.getVehicleType())
.brand(firstLocation.getBrand())
.owningUnit(firstLocation.getOwningUnit())
.startTime(startTime)
.endTime(endTime)
.statistics(statistics)
.points(points)
.simplified(simplified)
.dataQuality(calculateOverallDataQuality(locations))
.build();
}
/**
* 批量获取多车辆轨迹数据
*/
@Override
public List<VehicleTrajectoryVO> getBatchVehicleTrajectory(TrajectoryQueryRequest request) {
// 参数校验
if (request == null || CollectionUtils.isEmpty(request.getVehicleIds())
|| request.getStartTime() == null || request.getEndTime() == null) {
throw new IllegalArgumentException("请求参数不能为空");
}
List<VehicleTrajectoryVO> result = new ArrayList<>();
// 批量查询轨迹数据
List<SysVehicleLocation> allLocations = sysVehicleLocationMapper
.selectBatchVehicleTrajectory(request.getVehicleIds(), request.getStartTime(),
request.getEndTime(), request.getSimplified(), request.getMaxPoints());
// 按车辆ID分组
Map<Long, List<SysVehicleLocation>> locationsByVehicle = allLocations.stream()
.collect(Collectors.groupingBy(SysVehicleLocation::getVehicleId));
// 为每个车辆构建轨迹数据
for (Long vehicleId : request.getVehicleIds()) {
List<SysVehicleLocation> vehicleLocations = locationsByVehicle.get(vehicleId);
if (CollectionUtils.isEmpty(vehicleLocations)) {
result.add(createEmptyTrajectory(vehicleId, request.getStartTime(), request.getEndTime()));
continue;
}
VehicleTrajectoryVO trajectory = buildTrajectoryFromLocations(vehicleId, vehicleLocations,
request.getStartTime(), request.getEndTime(), request.getSimplified(), request.getIncludeStatistics());
result.add(trajectory);
}
return result;
}
/**
* 获取轨迹回放数据
*/
@Override
public List<VehicleLocationPlaybackVO> getTrajectoryPlaybackData(Long vehicleId, Date startTime,
Date endTime, Integer intervalSeconds) {
// 参数校验
if (vehicleId == null || startTime == null || endTime == null || intervalSeconds == null) {
throw new IllegalArgumentException("参数不能为空");
}
if (intervalSeconds <= 0) {
throw new IllegalArgumentException("时间间隔必须大于0");
}
// 查询回放数据
List<SysVehicleLocation> locations = sysVehicleLocationMapper
.selectTrajectoryPlaybackData(vehicleId, startTime, endTime, intervalSeconds);
if (CollectionUtils.isEmpty(locations)) {
return new ArrayList<>();
}
// 转换为回放数据
return convertToPlaybackData(locations, startTime, endTime);
}
/**
* 获取车辆在指定区域内的轨迹
*/
@Override
public VehicleTrajectoryVO getVehicleTrajectoryInArea(Long vehicleId, Date startTime, Date endTime,
String areaWkt) {
// 参数校验
if (vehicleId == null || startTime == null || endTime == null || !StringUtils.hasText(areaWkt)) {
throw new IllegalArgumentException("参数不能为空");
}
// 查询区域内轨迹数据
List<SysVehicleLocation> locations = sysVehicleLocationMapper
.selectVehicleTrajectoryInArea(vehicleId, startTime, endTime, areaWkt);
if (CollectionUtils.isEmpty(locations)) {
return createEmptyTrajectory(vehicleId, startTime, endTime);
}
// 构建轨迹数据
return buildTrajectoryFromLocations(vehicleId, locations, startTime, endTime, false, true);
}
/**
* 获取轨迹统计信息
*/
@Override
public TrajectoryStatistics getTrajectoryStatistics(Long vehicleId, Date startTime, Date endTime) {
// 参数校验
if (vehicleId == null || startTime == null || endTime == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 查询统计信息
TrajectoryStatistics statistics = sysVehicleLocationMapper
.selectTrajectoryStatistics(vehicleId, startTime, endTime);
if (statistics == null) {
return createEmptyStatistics(startTime, endTime);
}
// 格式化时长
if (statistics.getDurationSeconds() != null) {
statistics.setDurationFormatted(formatDuration(statistics.getDurationSeconds()));
}
return statistics;
}
// ==================== 私有辅助方法 ====================
/**
* 创建空轨迹对象
*/
private VehicleTrajectoryVO createEmptyTrajectory(Long vehicleId, Date startTime, Date endTime) {
return VehicleTrajectoryVO.builder()
.vehicleId(vehicleId)
.startTime(startTime)
.endTime(endTime)
.statistics(createEmptyStatistics(startTime, endTime))
.points(new ArrayList<>())
.simplified(false)
.build();
}
/**
* 创建空统计信息
*/
private TrajectoryStatistics createEmptyStatistics(Date startTime, Date endTime) {
return TrajectoryStatistics.builder()
.pointCount(0)
.firstPointTime(startTime)
.lastPointTime(endTime)
.averageSpeed(BigDecimal.ZERO)
.maxSpeed(BigDecimal.ZERO)
.minSpeed(BigDecimal.ZERO)
.durationSeconds(0L)
.durationFormatted("0分钟")
.totalDistance(BigDecimal.ZERO)
.stopCount(0)
.totalStopDuration(0L)
.maxAltitude(BigDecimal.ZERO)
.minAltitude(BigDecimal.ZERO)
.averageAltitude(BigDecimal.ZERO)
.build();
}
/**
* 转换为轨迹点列表
*/
private List<TrajectoryPoint> convertToTrajectoryPoints(List<SysVehicleLocation> locations) {
if (CollectionUtils.isEmpty(locations)) {
return new ArrayList<>();
}
List<TrajectoryPoint> points = new ArrayList<>();
SysVehicleLocation previousLocation = null;
for (SysVehicleLocation location : locations) {
TrajectoryPoint.TrajectoryPointBuilder pointBuilder = TrajectoryPoint.builder()
.longitude(location.getLongitude())
.latitude(location.getLatitude())
.altitude(location.getAltitude())
.speed(location.getSpeed())
.heading(location.getHeading())
.timestamp(location.getTimestamp())
.dataQuality(location.getDataQuality())
.location(location.getLocation())
.isStopPoint(isStopPoint(location));
// 计算与前一点的距离和时间间隔
if (previousLocation != null) {
pointBuilder.distanceFromPrevious(calculateDistance(previousLocation, location));
pointBuilder.timeIntervalFromPrevious(calculateTimeInterval(previousLocation, location));
}
points.add(pointBuilder.build());
previousLocation = location;
}
return points;
}
/**
* 转换为回放数据
*/
private List<VehicleLocationPlaybackVO> convertToPlaybackData(List<SysVehicleLocation> locations,
Date startTime, Date endTime) {
if (CollectionUtils.isEmpty(locations)) {
return new ArrayList<>();
}
List<VehicleLocationPlaybackVO> playbackData = new ArrayList<>();
AtomicLong sequenceNumber = new AtomicLong(1);
BigDecimal cumulativeDistance = BigDecimal.ZERO;
long cumulativeTime = 0;
long totalDuration = (endTime.getTime() - startTime.getTime()) / 1000; // 总时长()
SysVehicleLocation previousLocation = null;
for (SysVehicleLocation location : locations) {
// 计算累计距离和时间
if (previousLocation != null) {
BigDecimal distance = calculateDistance(previousLocation, location);
cumulativeDistance = cumulativeDistance.add(distance);
cumulativeTime += calculateTimeInterval(previousLocation, location);
}
// 计算进度百分比
long currentTime = (location.getTimestamp().getTime() - startTime.getTime()) / 1000;
BigDecimal progressPercentage = totalDuration > 0 ?
BigDecimal.valueOf(currentTime * 100.0 / totalDuration).setScale(2, RoundingMode.HALF_UP) :
BigDecimal.ZERO;
// 计算与上一点的时间间隔
Long intervalMs = previousLocation != null ?
location.getTimestamp().getTime() - previousLocation.getTimestamp().getTime() : 0L;
VehicleLocationPlaybackVO playbackVO = VehicleLocationPlaybackVO.builder()
.vehicleId(location.getVehicleId())
.licensePlate(location.getLicensePlate())
.vehicleType(location.getVehicleType())
.sequenceNumber(sequenceNumber.getAndIncrement())
.longitude(location.getLongitude())
.latitude(location.getLatitude())
.altitude(location.getAltitude())
.speed(location.getSpeed())
.heading(location.getHeading())
.timestamp(location.getTimestamp())
.dataQuality(location.getDataQuality())
.intervalMs(intervalMs)
.cumulativeDistance(cumulativeDistance)
.cumulativeTime(cumulativeTime)
.progressPercentage(progressPercentage)
.isKeyFrame(isKeyFrame(location, previousLocation))
.keyFrameType(getKeyFrameType(location, previousLocation, sequenceNumber.get() == 2,
sequenceNumber.get() == locations.size() + 1))
.suggestedDelay(calculateSuggestedDelay(intervalMs))
.build();
playbackData.add(playbackVO);
previousLocation = location;
}
return playbackData;
}
/**
* 从位置数据构建轨迹对象
*/
private VehicleTrajectoryVO buildTrajectoryFromLocations(Long vehicleId, List<SysVehicleLocation> locations,
Date startTime, Date endTime, Boolean simplified,
Boolean includeStatistics) {
if (CollectionUtils.isEmpty(locations)) {
return createEmptyTrajectory(vehicleId, startTime, endTime);
}
SysVehicleLocation firstLocation = locations.get(0);
List<TrajectoryPoint> points = convertToTrajectoryPoints(locations);
TrajectoryStatistics statistics = null;
if (includeStatistics != null && includeStatistics) {
statistics = getTrajectoryStatistics(vehicleId, startTime, endTime);
}
return VehicleTrajectoryVO.builder()
.vehicleId(vehicleId)
.licensePlate(firstLocation.getLicensePlate())
.vehicleType(firstLocation.getVehicleType())
.brand(firstLocation.getBrand())
.owningUnit(firstLocation.getOwningUnit())
.startTime(startTime)
.endTime(endTime)
.statistics(statistics)
.points(points)
.simplified(simplified)
.dataQuality(calculateOverallDataQuality(locations))
.build();
}
/**
* 计算两点间距离()
*/
private BigDecimal calculateDistance(SysVehicleLocation from, SysVehicleLocation to) {
if (from == null || to == null ||
from.getLongitude() == null || from.getLatitude() == null ||
to.getLongitude() == null || to.getLatitude() == null) {
return BigDecimal.ZERO;
}
return GeoUtils.calculateDistanceAsBigDecimal(
from.getLatitude().doubleValue(),
from.getLongitude().doubleValue(),
to.getLatitude().doubleValue(),
to.getLongitude().doubleValue()
);
}
/**
* 计算两点间时间间隔()
*/
private Long calculateTimeInterval(SysVehicleLocation from, SysVehicleLocation to) {
if (from == null || to == null || from.getTimestamp() == null || to.getTimestamp() == null) {
return 0L;
}
return (to.getTimestamp().getTime() - from.getTimestamp().getTime()) / 1000;
}
/**
* 判断是否为停车点
*/
private Boolean isStopPoint(SysVehicleLocation location) {
if (location == null || location.getSpeed() == null) {
return false;
}
return location.getSpeed().compareTo(BigDecimal.valueOf(2.0)) <= 0; // 速度小于等于2km/h认为是停车
}
/**
* 判断是否为关键帧
*/
private Boolean isKeyFrame(SysVehicleLocation current, SysVehicleLocation previous) {
if (current == null) {
return false;
}
if (previous == null) {
return true; // 起点
}
// 速度变化较大
if (current.getSpeed() != null && previous.getSpeed() != null) {
BigDecimal speedDiff = current.getSpeed().subtract(previous.getSpeed()).abs();
if (speedDiff.compareTo(BigDecimal.valueOf(10.0)) > 0) {
return true;
}
}
// 方向变化较大
if (current.getHeading() != null && previous.getHeading() != null) {
BigDecimal headingDiff = current.getHeading().subtract(previous.getHeading()).abs();
if (headingDiff.compareTo(BigDecimal.valueOf(30.0)) > 0) {
return true;
}
}
return false;
}
/**
* 获取关键帧类型
*/
private String getKeyFrameType(SysVehicleLocation current, SysVehicleLocation previous,
boolean isFirst, boolean isLast) {
if (isFirst) {
return "START";
}
if (isLast) {
return "END";
}
if (isStopPoint(current)) {
return "STOP";
}
if (isKeyFrame(current, previous)) {
return "TURN";
}
return null;
}
/**
* 计算建议延迟时间(毫秒)
*/
private Long calculateSuggestedDelay(Long intervalMs) {
if (intervalMs == null || intervalMs <= 0) {
return 1000L; // 默认1秒
}
// 根据实际时间间隔调整回放速度
if (intervalMs < 5000) {
return intervalMs; // 小于5秒按实际时间播放
} else if (intervalMs < 30000) {
return intervalMs / 2; // 5-30秒2倍速播放
} else {
return Math.min(intervalMs / 5, 10000L); // 大于30秒5倍速播放最大10秒
}
}
/**
* 计算整体数据质量
*/
private String calculateOverallDataQuality(List<SysVehicleLocation> locations) {
if (CollectionUtils.isEmpty(locations)) {
return "UNKNOWN";
}
Map<String, Long> qualityCount = locations.stream()
.filter(loc -> StringUtils.hasText(loc.getDataQuality()))
.collect(Collectors.groupingBy(SysVehicleLocation::getDataQuality, Collectors.counting()));
if (qualityCount.isEmpty()) {
return "UNKNOWN";
}
// 按质量等级排序返回最高质量
if (qualityCount.containsKey("HIGH")) {
return "HIGH";
} else if (qualityCount.containsKey("MEDIUM")) {
return "MEDIUM";
} else if (qualityCount.containsKey("LOW")) {
return "LOW";
} else {
return "UNKNOWN";
}
}
/**
* 格式化时长
*/
private String formatDuration(Long durationSeconds) {
if (durationSeconds == null || durationSeconds <= 0) {
return "0分钟";
}
long hours = durationSeconds / 3600;
long minutes = (durationSeconds % 3600) / 60;
long seconds = durationSeconds % 60;
StringBuilder sb = new StringBuilder();
if (hours > 0) {
sb.append(hours).append("小时");
}
if (minutes > 0) {
sb.append(minutes).append("分钟");
}
if (seconds > 0 && hours == 0) { // 只有在小于1小时时才显示秒
sb.append(seconds).append("");
}
return sb.length() > 0 ? sb.toString() : "0分钟";
}
}

View File

@ -43,7 +43,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
vl.updated_at
from vehicle_locations vl
left join sys_vehicle_info vi on vl.vehicle_id = vi.vehicle_id
left join sys_vehicle_type vt on vi.type_id = vt.type_id
left join sys_vehicle_type vt on vi.type_code = vt.type_code
</sql>
<select id="selectSysVehicleLocationList" parameterType="SysVehicleLocation" resultMap="SysVehicleLocationResult">
@ -88,4 +88,138 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit 1
</select>
</mapper>
<!-- 轨迹查询(支持采样优化) -->
<select id="selectVehicleTrajectory" resultMap="SysVehicleLocationResult">
<include refid="selectSysVehicleLocationVo"/>
WHERE vl.vehicle_id = #{vehicleId}
AND vl.timestamp BETWEEN #{startTime} AND #{endTime}
<if test="simplified == true and maxPoints != null">
AND vl.id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY timestamp) as rn,
COUNT(*) OVER() as total_count
FROM vehicle_locations
WHERE vehicle_id = #{vehicleId}
AND timestamp BETWEEN #{startTime} AND #{endTime}
) t
WHERE rn % GREATEST(1, total_count / #{maxPoints}) = 1
)
</if>
ORDER BY vl.timestamp
</select>
<!-- 轨迹统计信息查询 -->
<resultMap type="com.qaup.system.domain.vo.TrajectoryStatistics" id="TrajectoryStatisticsResult">
<result property="pointCount" column="point_count" />
<result property="firstPointTime" column="first_point_time" />
<result property="lastPointTime" column="last_point_time" />
<result property="averageSpeed" column="average_speed" />
<result property="maxSpeed" column="max_speed" />
<result property="minSpeed" column="min_speed" />
<result property="durationSeconds" column="duration_seconds" />
<result property="totalDistance" column="total_distance" />
<result property="maxAltitude" column="max_altitude" />
<result property="minAltitude" column="min_altitude" />
<result property="averageAltitude" column="average_altitude" />
</resultMap>
<select id="selectTrajectoryStatistics" resultMap="TrajectoryStatisticsResult">
SELECT
COUNT(*) as point_count,
MIN(timestamp) as first_point_time,
MAX(timestamp) as last_point_time,
AVG(speed) as average_speed,
MAX(speed) as max_speed,
MIN(speed) as min_speed,
EXTRACT(EPOCH FROM (MAX(timestamp) - MIN(timestamp))) as duration_seconds,
MAX(altitude) as max_altitude,
MIN(altitude) as min_altitude,
AVG(altitude) as average_altitude,
-- 使用PostGIS计算总距离
COALESCE(
ST_Length(
ST_MakeLine(
location ORDER BY timestamp
)::geography
) / 1000, 0
) as total_distance
FROM vehicle_locations
WHERE vehicle_id = #{vehicleId}
AND timestamp BETWEEN #{startTime} AND #{endTime}
</select>
<!-- 轨迹回放数据查询 -->
<select id="selectTrajectoryPlaybackData" resultMap="SysVehicleLocationResult">
WITH trajectory_with_intervals AS (
SELECT vl.*,
ROW_NUMBER() OVER (ORDER BY vl.timestamp) as seq_num,
EXTRACT(EPOCH FROM (vl.timestamp - LAG(vl.timestamp) OVER (ORDER BY vl.timestamp))) as interval_seconds
FROM vehicle_locations vl
WHERE vl.vehicle_id = #{vehicleId}
AND vl.timestamp BETWEEN #{startTime} AND #{endTime}
),
sampled_trajectory AS (
SELECT * FROM trajectory_with_intervals
WHERE seq_num = 1
OR interval_seconds >= #{intervalSeconds}
OR seq_num = (SELECT MAX(seq_num) FROM trajectory_with_intervals)
)
SELECT
st.id,
st.vehicle_id,
vi.license_plate,
vt.type_name,
vi.brand,
vi.owning_unit,
ST_AsText(st.location) as location,
ST_X(st.location) as longitude,
ST_Y(st.location) as latitude,
st.altitude,
st.speed,
st.heading,
st.data_quality,
st.timestamp,
st.created_at,
st.updated_at
FROM sampled_trajectory st
LEFT JOIN sys_vehicle_info vi ON st.vehicle_id = vi.vehicle_id
LEFT JOIN sys_vehicle_type vt ON vi.type_code = vt.type_code
ORDER BY st.timestamp
</select>
<!-- 区域内轨迹查询 -->
<select id="selectVehicleTrajectoryInArea" resultMap="SysVehicleLocationResult">
<include refid="selectSysVehicleLocationVo"/>
WHERE vl.vehicle_id = #{vehicleId}
AND vl.timestamp BETWEEN #{startTime} AND #{endTime}
AND ST_Contains(ST_GeomFromText(#{areaWkt}, 4326), vl.location)
ORDER BY vl.timestamp
</select>
<!-- 批量车辆轨迹查询 -->
<select id="selectBatchVehicleTrajectory" resultMap="SysVehicleLocationResult">
<include refid="selectSysVehicleLocationVo"/>
WHERE vl.vehicle_id IN
<foreach item="vehicleId" collection="vehicleIds" open="(" separator="," close=")">
#{vehicleId}
</foreach>
AND vl.timestamp BETWEEN #{startTime} AND #{endTime}
<if test="simplified == true and maxPoints != null">
AND vl.id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (PARTITION BY vehicle_id ORDER BY timestamp) as rn,
COUNT(*) OVER(PARTITION BY vehicle_id) as total_count
FROM vehicle_locations
WHERE vehicle_id IN
<foreach item="vehicleId" collection="vehicleIds" open="(" separator="," close=")">
#{vehicleId}
</foreach>
AND timestamp BETWEEN #{startTime} AND #{endTime}
) t
WHERE rn % GREATEST(1, total_count / #{maxPoints}) = 1
)
</if>
ORDER BY vl.vehicle_id, vl.timestamp
</select>
</mapper>

View File

@ -1,6 +1,6 @@
{
"name": "qaup",
"version": "3.8.9",
"version": "1.0.1",
"description": "后台管理系统",
"author": "Tellme",
"license": "MIT",

Some files were not shown because too many files have changed in this diff Show More