1. 创建车辆位置PostGIS实体类(VehicleLocation、Aircraft、VehicleTrajectory)
2. 创建机场区域PostGIS实体类(AirportArea) 3. 创建数据库迁移脚本(表结构、索引、分区) 4. 实现车辆位置Repository接口 5. 实现机场区域Repository接口 6. 创建PostGIS车辆服务(PostGISVehicleService) 7. 创建PostGIS区域服务(PostGISAreaService) 8. 创建统一空间查询服务(SpatialQueryService) 9. 实现区域配置导入工具(YAML到数据库) 10. 重构DataCollectorService(移除内存存储) 11. 重构AirportAreaService(基于数据库查询) 12. 移除MovingObjectRepository和相关内存存储代码 13. 移除AirportAreasProperties和YAML配置加载 14. 实现Redis缓存策略 15. 数据库连接池和性能优化配置 16. 创建单元测试和集成测试
This commit is contained in:
parent
2dab04e2b9
commit
05e080cb30
8
.cursor/rules/changerecord.mdc
Normal file
8
.cursor/rules/changerecord.mdc
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
把生成的任务检查清单放在 doc/work 目录下
|
||||
每次执行完一个代码开发或修改任务后,对 [VERSION.txt](mdc:VERSION.txt)中记录的版本号进行递增,按常用的版本号递增规则;同时在 [change_log.md](mdc:change_log.md)中进行记录。
|
||||
编写单元测试和集成测试时,先阅读相关的源代码,再根据源代码写单元测试。
|
||||
@ -1 +1 @@
|
||||
0.5.0
|
||||
0.6.7
|
||||
225
change_log.md
225
change_log.md
@ -2,6 +2,231 @@
|
||||
|
||||
本文档记录碰撞避免系统的所有重要变更,包括新功能、改进和修复。
|
||||
|
||||
## 版本 0.6.7 (2024-12-19)
|
||||
### 新增功能
|
||||
- **数据库连接池优化**: 实现HikariCP连接池完整配置和性能优化
|
||||
- 配置HikariCP连接池参数:最大连接数20,最小空闲连接5,连接生命周期管理
|
||||
- 添加PostgreSQL性能优化参数:预编译语句缓存、批量插入重写、超时设置
|
||||
- 配置PostGIS空间查询优化:连接池针对空间查询的特殊配置
|
||||
- **Hibernate性能调优**: 全面优化JPA/Hibernate配置
|
||||
- 启用二级缓存和查询缓存,配置JCache缓存工厂
|
||||
- 配置批量操作:批量大小50,排序插入/更新,版本化数据批处理
|
||||
- 优化空间数据处理:PostGIS连接查找器,空间查询fetch size配置
|
||||
- **数据库性能监控**: 创建DatabasePerformanceConfig配置类
|
||||
- 实时连接池状态监控:连接使用率、等待线程数警告
|
||||
- Hibernate统计信息收集:查询执行次数、缓存命中率、事务统计
|
||||
- 定期性能报告:每5分钟生成详细的数据库性能报告
|
||||
- 数据库健康检查:通过Actuator endpoint提供连接池状态
|
||||
- **日志优化**: 配置数据库和性能相关的详细日志
|
||||
- HikariCP连接池日志、Hibernate SQL日志、PostGIS空间查询日志
|
||||
- SQL参数绑定日志、事务日志、查询性能日志
|
||||
- 优化日志格式,便于性能分析和问题排查
|
||||
|
||||
### 技术改进
|
||||
- **连接池管理**: HikariCP替代默认连接池,提供更好的性能和监控
|
||||
- **缓存策略**: 多层缓存配置,提升查询性能,减少数据库负载
|
||||
- **批处理优化**: 针对PostGIS空间数据的批量操作优化
|
||||
- **监控体系**: 完整的数据库性能监控和告警机制
|
||||
|
||||
## 版本 0.6.6 (2024-12-19)
|
||||
### 修复
|
||||
- **SQL表结构修正**: 修正PostGIS数据库表结构,确保与JPA实体类完全匹配
|
||||
- 修正`airport_areas`表字段名称:
|
||||
- `area_name` → `name`
|
||||
- `area_type` → `type`
|
||||
- `geometry` → `boundary`
|
||||
- `is_active` → `enabled`
|
||||
- 新增`area_id`字段,对应实体类的区域标识符
|
||||
- 扩展表字段以支持实体类的所有属性:
|
||||
- 添加`speed_limit_kph`、`restricted`等基础限制字段
|
||||
- 添加`allowed_vehicle_types`、`allowed_aircraft_types` JSONB字段
|
||||
- 添加`max_height`、`max_weight`等扩展限制字段
|
||||
- 添加`active_time`、`expiry_time`时间控制字段
|
||||
- 修正`vehicle_locations`表字段:
|
||||
- 添加`data_quality`字段,移除`accuracy`和`data_source`字段
|
||||
- 统一时间戳字段类型为`TIMESTAMP`
|
||||
- 更新所有相关索引名称和示例数据,确保与新表结构一致
|
||||
|
||||
### 技术改进
|
||||
- **数据库兼容性**: 确保Spring Data JPA能够正确映射实体类到数据库表
|
||||
- **空间索引优化**: 更新PostGIS空间索引以匹配新的字段名称
|
||||
- **JSONB查询支持**: 优化车辆类型和航空器类型的JSONB字段索引
|
||||
|
||||
## 版本 0.6.5 (2024-12-19)
|
||||
### 新增功能
|
||||
- 实现PostGIS Redis缓存策略完整架构
|
||||
- 扩展RedisConfig配置,支持PostGIS实体类序列化(VehicleLocation、AirportArea)
|
||||
- 创建CacheConstants常量定义类,统一管理缓存键前缀和过期时间
|
||||
- 新增VehicleLocationCacheService车辆位置缓存服务
|
||||
- 支持车辆最新位置缓存和轨迹数据缓存
|
||||
- 实现批量操作和缓存失效策略
|
||||
- 提供缓存统计和清理功能
|
||||
- 新增AirportAreaCacheService机场区域缓存服务
|
||||
- 支持区域配置数据缓存和空间查询结果缓存
|
||||
- 实现区域类型索引缓存和缓存预热功能
|
||||
- 提供区域变更时的缓存刷新机制
|
||||
- 新增SpatialQueryCacheService空间查询缓存服务
|
||||
- 基于地理网格的缓存策略
|
||||
- 支持车辆半径查询和冲突检测结果缓存
|
||||
- 实现缓存失效和清理机制
|
||||
- 分层缓存策略设计
|
||||
- 热数据:车辆最新位置(30秒过期)
|
||||
- 温数据:车辆轨迹和空间查询结果(60-300秒过期)
|
||||
- 冷数据:区域配置和统计数据(3600秒过期)
|
||||
- 缓存穿透保护和性能优化机制
|
||||
|
||||
## [0.6.4] - 2024-12-19
|
||||
|
||||
### 修复
|
||||
- 重写`AirportAreaServiceIntegrationTest.java`测试文件
|
||||
- 移除对已删除的`areas.service.AirportAreaService`和`AirportAreaConfig`的依赖
|
||||
- 改为测试新的PostGIS版本的`common.service.AirportAreaService`
|
||||
- 使用JTS几何对象和PostGIS空间查询功能
|
||||
- 添加@Transactional注解和setUp方法来管理测试数据
|
||||
- 测试空间查询、几何验证、区域重叠检测等PostGIS功能
|
||||
- 修复所有编译错误,测试现在与PostGIS架构完全兼容
|
||||
|
||||
## [0.6.3] - 2025-01-09
|
||||
|
||||
### 移除
|
||||
- **YAML配置系统清理**: 移除机场区域相关的YAML配置加载机制
|
||||
- 删除 `AirportAreasProperties.java` - 机场区域顶层配置属性
|
||||
- 删除 `AreaProperties.java` - 单个区域配置属性
|
||||
- 删除 `AirportAreaConfig.java` - 机场区域配置加载类
|
||||
- 删除重复的 `areas.service.AirportAreaService` - 避免与PostGIS版本冲突
|
||||
|
||||
### 重构
|
||||
- **统一服务架构**: 现在所有机场区域操作都通过PostGIS `common.service.AirportAreaService` 进行
|
||||
- 消除了YAML配置与数据库存储的双重架构
|
||||
- 统一数据源为PostGIS数据库
|
||||
- 简化了服务层依赖关系
|
||||
|
||||
### 保留
|
||||
- **道路网络配置**: 保留 `GeometryProperties.java` 以支持道路网络的YAML配置
|
||||
- 道路网络仍使用YAML配置方式
|
||||
- 确保道路配置功能不受影响
|
||||
|
||||
### 技术优化
|
||||
- **架构简化**: 消除了类名冲突和循环依赖
|
||||
- **数据一致性**: 所有区域数据现在统一从PostGIS获取
|
||||
- **维护性提升**: 减少了重复代码和冗余配置
|
||||
|
||||
## [0.6.2] - 2025-01-09
|
||||
|
||||
### 移除
|
||||
- **MovingObjectRepository内存存储**: 完全移除内存存储仓库及其相关代码
|
||||
- 删除 `MovingObjectRepository.java` 内存存储实现
|
||||
- 重构 `GeopositionController.java` 以使用PostGIS VehicleLocationService
|
||||
- 清理测试配置中的MovingObjectRepository引用
|
||||
|
||||
### 改进
|
||||
- **WebSocket服务升级**: GeopositionController现在基于PostGIS数据库提供实时数据
|
||||
- 新增按车辆类型查询的WebSocket接口
|
||||
- 新增全部车辆位置查询接口
|
||||
- 提供更准确的实时位置数据(基于数据库而非内存)
|
||||
- 增强异常处理和日志记录
|
||||
|
||||
### 技术优化
|
||||
- **数据一致性**: 所有位置数据现在统一从PostGIS数据库获取
|
||||
- **性能提升**: 利用数据库索引优化查询性能
|
||||
- **可扩展性**: 支持更复杂的空间查询和历史数据分析
|
||||
|
||||
## [0.6.1] - 2025-01-09
|
||||
|
||||
### 改进
|
||||
- **系统架构简化**: 删除DataProcessor组件,简化数据处理流程
|
||||
- 移除 `DataProcessor.java` 及其相关依赖
|
||||
- 清理测试代码中的DataProcessor引用
|
||||
- 数据现在直接通过DataCollectorService保存到PostGIS数据库,无需中间处理层
|
||||
|
||||
### 技术优化
|
||||
- **减少组件复杂度**: 消除不必要的数据处理中间层
|
||||
- **提升维护性**: 简化代码结构,减少组件间耦合
|
||||
- **优化数据流**: 数据采集直接入库,减少内存占用和处理延迟
|
||||
|
||||
## [0.6.0] - 2025-06-10
|
||||
|
||||
### 新增
|
||||
- **PostGIS数据模型迁移完成**: 完全替换内存存储,实现基于PostGIS的车辆位置和机场区域数据管理
|
||||
- **VehicleLocation实体类**: 支持PostGIS POINT类型的车辆位置存储,包含时间戳、数据质量等字段
|
||||
- **AirportArea实体类**: 支持PostGIS GEOMETRY类型的机场区域存储,支持POLYGON/MULTIPOLYGON,包含JSONB限制信息
|
||||
- **VehicleLocationRepository**: 提供丰富的PostGIS空间查询接口(半径查询、区域查询、轨迹查询等)
|
||||
- **AirportAreaRepository**: 提供全面的PostGIS空间查询接口(包含查询、相交查询、重叠检测、优先级处理等)
|
||||
- **VehicleLocationService**: 车辆位置服务类,提供完整的空间查询、轨迹分析、数据验证和管理功能
|
||||
- **AirportAreaService**: 机场区域服务类,提供空间查询、区域管理、冲突检测、几何验证等功能
|
||||
- **SpatialQueryService**: 综合空间查询服务类,提供冲突检测、轨迹分析、区域密度分析等高级功能
|
||||
- **PostGIS数据库表结构**: 完整的车辆位置表、机场区域表、轨迹表,配置空间索引、触发器、数据清理函数
|
||||
|
||||
### 改进
|
||||
- **数据存储架构**: 从内存STRtree索引改为PostgreSQL + PostGIS持久化存储
|
||||
- **空间查询性能**: 利用PostGIS GIST索引大幅提升空间查询性能
|
||||
- **数据一致性**: 通过数据库事务和约束确保数据一致性
|
||||
- **扩展性**: 支持大规模数据存储和复杂空间查询
|
||||
|
||||
### 修复
|
||||
- **Lombok兼容性**: 升级Lombok版本到1.18.38,解决与JDK 17.0.15的兼容性问题
|
||||
- **编译错误**: 修复MovingObjectType枚举类型和JSONB字段序列化相关编译错误
|
||||
- **依赖管理**: 优化PostGIS和Hibernate Spatial依赖配置
|
||||
|
||||
### 技术栈更新
|
||||
- **数据库**: PostgreSQL 17 + PostGIS扩展
|
||||
- **ORM**: Spring Boot JPA + Hibernate Spatial
|
||||
- **空间数据**: JTS几何库 + PostGIS原生函数
|
||||
- **坐标系**: 统一使用WGS84 (SRID 4326)
|
||||
|
||||
## [0.5.3] - 2025-06-10
|
||||
|
||||
### PostGIS空间扩展集成
|
||||
- **PostGIS扩展支持**: 为PostgreSQL添加强大的空间数据处理能力
|
||||
- 添加 `hibernate-spatial` 依赖,支持JPA空间数据类型
|
||||
- 添加 `postgis-jdbc` 扩展(v2023.1.0)
|
||||
- 更新Hibernate方言为 `PostgisPG95Dialect`
|
||||
- 创建PostGIS扩展初始化脚本
|
||||
|
||||
### 空间数据功能
|
||||
- 支持POINT和POLYGON空间数据类型
|
||||
- SRID 4326(WGS84坐标系统)支持GPS坐标
|
||||
- 提供空间查询示例(ST_Contains、ST_DWithin、ST_Distance)
|
||||
- 空间索引(GIST)优化查询性能
|
||||
|
||||
### 项目能力提升
|
||||
- 精确的机场围栏检测
|
||||
- 高效的碰撞预测和距离计算
|
||||
- 优化的轨迹分析和历史查询
|
||||
- 为实时空间分析奠定技术基础
|
||||
|
||||
## [0.5.2] - 2025-06-10
|
||||
|
||||
### 数据库技术栈升级
|
||||
- **PostgreSQL 17 迁移**: 将数据库从MySQL切换到PostgreSQL 17
|
||||
- 替换 `mysql-connector-java` 为 `postgresql` 驱动(v42.7.1)
|
||||
- 更新数据源配置:`jdbc:postgresql://localhost:5432/collision_avoidance`
|
||||
- 更新Hibernate方言为 `PostgreSQLDialect`
|
||||
- 数据保留策略调整为 `postgresql-days`
|
||||
|
||||
### 技术优势提升
|
||||
- PostgreSQL原生支持更强大的JSON和空间数据处理
|
||||
- 为未来PostGIS空间扩展奠定基础
|
||||
- 更好的并发性能和查询优化
|
||||
- 符合现代开发的主流技术选型
|
||||
|
||||
## [0.5.1] - 2025-06-10
|
||||
|
||||
### 架构重构
|
||||
- **数据库迁移**: 移除MongoDB依赖,统一使用关系型数据库
|
||||
- 移除 `spring-boot-starter-data-mongodb` 依赖
|
||||
- 添加 `spring-boot-starter-data-jpa` 依赖
|
||||
- 移除主应用类中的 `@EnableMongoRepositories` 注解
|
||||
|
||||
### 技术栈简化
|
||||
- 采用关系型数据库作为统一数据存储方案
|
||||
- 配置JPA/Hibernate用于ORM映射
|
||||
|
||||
### 配置优化
|
||||
- 数据库名:`collision_avoidance`
|
||||
- Hibernate配置:`ddl-auto: update` 用于开发阶段表结构自动更新
|
||||
- 启用SQL日志输出,便于开发调试
|
||||
|
||||
## [0.5.0] - 2025-04-29
|
||||
|
||||
### 新增
|
||||
|
||||
311
doc/design/architecture_analysis_report.md
Normal file
311
doc/design/architecture_analysis_report.md
Normal file
@ -0,0 +1,311 @@
|
||||
# 碰撞避免系统架构分析报告
|
||||
|
||||
**版本**: 3.0 | **更新日期**: 2024-12-20 | **基于需求**: requirements.md (2025-05-01)
|
||||
|
||||
> 📋 **更新说明**: 本版本基于3个月快速交付目标,简化架构设计,去除MongoDB采用MySQL单一数据库方案,专注核心业务功能的MVP实现。
|
||||
|
||||
本文档对当前碰撞避免系统的架构进行全面分析,识别设计问题、缺失模块,并提出改进建议。
|
||||
|
||||
## 🔍 **当前架构优势**
|
||||
|
||||
1. **模块化设计良好**:项目采用了清晰的分层架构,模块职责相对明确
|
||||
2. **空间计算基础扎实**:JTS库和MySQL空间索引为地理查询提供强大支持
|
||||
3. **配置管理完善**:支持YAML配置文件和属性绑定机制
|
||||
4. **实时通信支持**:WebSocket实现了基本的实时数据推送
|
||||
|
||||
## 🎯 **3个月MVP目标**
|
||||
|
||||
基于业务需求和开发效率考虑,制定3个月快速交付计划:
|
||||
|
||||
**月度1**:基础信息管理 + 数据库重构
|
||||
**月度2**:超速监控 + 电子围栏 + 基础告警
|
||||
**月度3**:轨迹回放 + API完善 + 系统优化
|
||||
|
||||
## ⚠️ **架构设计问题**
|
||||
|
||||
### 1. **技术栈过于复杂**
|
||||
|
||||
**问题**:
|
||||
- 同时使用Redis + MongoDB + 未来计划的PostgreSQL,技术栈过重
|
||||
- JTS空间计算与数据库空间功能重复
|
||||
- 过度工程化导致开发周期过长
|
||||
|
||||
**MVP简化方案**:
|
||||
```yaml
|
||||
# 简化的技术栈
|
||||
数据存储: MySQL 8.0+(单一数据库解决方案)
|
||||
缓存: Redis(仅用于实时数据缓存)
|
||||
空间查询: MySQL原生空间函数(ST_Contains, ST_Distance等)
|
||||
实时通信: WebSocket(保持现有实现)
|
||||
```
|
||||
|
||||
### 2. **功能模块设计过于复杂**
|
||||
|
||||
**问题**:
|
||||
- 过多的子模块和分层,增加开发复杂度
|
||||
- 事件驱动、规则引擎等高级架构增加学习成本
|
||||
- MVP阶段不需要如此复杂的架构设计
|
||||
|
||||
**MVP简化方案**:
|
||||
```java
|
||||
// 简化的模块结构(仅保留核心功能)
|
||||
├── controller/ // 统一控制器层
|
||||
├── service/ // 业务服务层(合并相关服务)
|
||||
├── repository/ // 数据访问层
|
||||
├── model/ // 数据模型
|
||||
├── config/ // 配置管理
|
||||
└── common/ // 通用工具类
|
||||
```
|
||||
|
||||
## 🚫 **MVP核心功能缺失**
|
||||
|
||||
### 1. **基础信息管理模块**(第1个月实现)
|
||||
|
||||
**现状**:完全缺失车辆和驾驶员基础信息管理
|
||||
|
||||
**MVP实现**:
|
||||
```java
|
||||
// 简化的基础信息管理(避免过度设计)
|
||||
management/
|
||||
├── VehicleService.java // 车辆管理服务
|
||||
├── DriverService.java // 驾驶员管理服务
|
||||
├── VehicleRepository.java // 车辆数据访问
|
||||
├── DriverRepository.java // 驾驶员数据访问
|
||||
└── model/
|
||||
├── Vehicle.java // 车辆实体
|
||||
├── Driver.java // 驾驶员实体
|
||||
└── VehicleDriver.java // 车辆-驾驶员关联
|
||||
```
|
||||
|
||||
### 2. **超速监控模块**(第2个月实现)
|
||||
|
||||
**现状**:基础功能存在但需要增强为完整的超速监控系统
|
||||
|
||||
**MVP实现**:
|
||||
```java
|
||||
// 增强现有功能,简化实现
|
||||
speeding/
|
||||
├── SpeedMonitorService.java // 超速监控服务(增强现有)
|
||||
├── SpeedLimitService.java // 限速管理服务
|
||||
├── SpeedViolationRepository.java // 违章记录存储
|
||||
└── model/
|
||||
├── SpeedViolation.java // 超速违章记录
|
||||
└── SpeedLimit.java // 限速配置
|
||||
```
|
||||
|
||||
### 3. **电子围栏模块**(第2个月实现)
|
||||
|
||||
**现状**:areas模块存在但缺乏围栏监控功能
|
||||
|
||||
**MVP实现**:
|
||||
```java
|
||||
// 基于现有areas模块扩展(利用MySQL空间查询)
|
||||
geofence/
|
||||
├── GeofenceService.java // 围栏监控服务
|
||||
├── ZoneViolationService.java // 违规检测服务
|
||||
├── GeofenceRepository.java // 围栏数据访问
|
||||
└── model/
|
||||
├── GeofenceEvent.java // 围栏事件
|
||||
└── ZoneViolation.java // 区域违规记录
|
||||
```
|
||||
|
||||
### 4. **轨迹回放模块**(第3个月实现)
|
||||
|
||||
**现状**:完全缺失历史轨迹查询和回放功能
|
||||
|
||||
**MVP实现**:
|
||||
```java
|
||||
// 简化的轨迹回放(基于MySQL存储和查询)
|
||||
trajectory/
|
||||
├── TrajectoryService.java // 轨迹查询服务
|
||||
├── TrajectoryReplayService.java // 轨迹回放服务
|
||||
├── TrajectoryRepository.java // 轨迹数据访问(MySQL分区表)
|
||||
└── model/
|
||||
├── TrajectoryPoint.java // 轨迹点
|
||||
└── TrajectoryQuery.java // 查询条件
|
||||
```
|
||||
|
||||
### 5. **基础告警系统**(第2个月实现)
|
||||
|
||||
**现状**:缺乏统一的告警管理机制
|
||||
|
||||
**MVP实现**:
|
||||
```java
|
||||
// 简化的告警系统(基于WebSocket + 数据库)
|
||||
alert/
|
||||
├── AlertService.java // 告警服务
|
||||
├── AlertRepository.java // 告警记录存储
|
||||
└── model/
|
||||
├── Alert.java // 告警实体
|
||||
└── AlertType.java // 告警类型(枚举)
|
||||
```
|
||||
|
||||
## 🔧 **技术实现简化**
|
||||
|
||||
### 1. **MySQL统一数据存储策略**
|
||||
|
||||
**简化方案**:
|
||||
```yaml
|
||||
# 统一的数据存储架构
|
||||
MySQL 8.0+:
|
||||
- 车辆和驾驶员基础信息
|
||||
- 实时位置数据(近期数据,定期清理)
|
||||
- 历史轨迹数据(分区表按时间分区)
|
||||
- 告警记录和配置数据
|
||||
- 空间数据(POINT, POLYGON类型)
|
||||
|
||||
Redis:
|
||||
- 实时位置数据缓存(提高查询性能)
|
||||
- WebSocket会话管理
|
||||
- 临时计算结果缓存
|
||||
|
||||
移除: MongoDB(简化技术栈)
|
||||
```
|
||||
|
||||
### 2. **空间查询简化实现**
|
||||
|
||||
**技术方案**:
|
||||
```sql
|
||||
-- 利用MySQL原生空间查询替代复杂的JTS计算
|
||||
-- 点在多边形内查询
|
||||
SELECT * FROM vehicles v
|
||||
WHERE ST_Contains(
|
||||
(SELECT boundary FROM airport_zones WHERE zone_id = ?),
|
||||
POINT(v.longitude, v.latitude)
|
||||
);
|
||||
|
||||
-- 距离查询
|
||||
SELECT *, ST_Distance(POINT(longitude, latitude), POINT(?, ?)) as distance
|
||||
FROM vehicles
|
||||
WHERE ST_Distance_Sphere(POINT(longitude, latitude), POINT(?, ?)) < ?;
|
||||
```
|
||||
|
||||
### 3. **数据验证简化**
|
||||
|
||||
**MVP方案**:
|
||||
```java
|
||||
// 基础数据验证(避免过度设计)
|
||||
@Component
|
||||
public class DataValidator {
|
||||
public boolean validateLocation(Double lat, Double lng) {
|
||||
return lat != null && lng != null &&
|
||||
lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
|
||||
}
|
||||
|
||||
public boolean validateSpeed(Double speed) {
|
||||
return speed != null && speed >= 0 && speed <= 200; // 机场内合理速度范围
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 **3个月MVP开发计划**
|
||||
|
||||
### **第1个月:基础设施建设**
|
||||
|
||||
**Week 1-2: 数据库重构**
|
||||
- 移除MongoDB配置,统一使用MySQL
|
||||
- 设计核心数据表结构(车辆、驾驶员、位置、告警等)
|
||||
- 配置MySQL空间索引和分区表
|
||||
|
||||
**Week 3-4: 基础信息管理**
|
||||
- 实现车辆管理功能(CRUD操作)
|
||||
- 实现驾驶员管理功能
|
||||
- 完成基础API和数据验证
|
||||
|
||||
### **第2个月:核心监控功能**
|
||||
|
||||
**Week 5-6: 超速监控系统**
|
||||
- 增强现有SpeedCalculationService
|
||||
- 实现超速违章检测和记录
|
||||
- 集成告警系统
|
||||
|
||||
**Week 7-8: 电子围栏功能**
|
||||
- 基于MySQL空间查询实现围栏监控
|
||||
- 实现区域进出检测
|
||||
- 完成违规记录和告警
|
||||
|
||||
### **第3个月:数据分析和优化**
|
||||
|
||||
**Week 9-10: 轨迹回放功能**
|
||||
- 实现历史轨迹查询(基于MySQL分区表)
|
||||
- 开发轨迹回放API
|
||||
- 前端轨迹可视化(简化版)
|
||||
|
||||
**Week 11-12: 系统优化和集成**
|
||||
- API接口完善和文档
|
||||
- 性能优化(索引、缓存)
|
||||
- 系统测试和部署准备
|
||||
|
||||
## 🎯 **MVP架构路线图**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[当前架构] --> B[数据库重构MySQL]
|
||||
B --> C[基础信息管理]
|
||||
C --> D[超速监控]
|
||||
D --> E[电子围栏]
|
||||
E --> F[告警系统]
|
||||
F --> G[轨迹回放]
|
||||
G --> H[API完善]
|
||||
H --> I[MVP完成]
|
||||
|
||||
style B fill:#ff6b6b
|
||||
style C fill:#ff6b6b
|
||||
style D fill:#4ecdc4
|
||||
style E fill:#4ecdc4
|
||||
style F fill:#4ecdc4
|
||||
style G fill:#95e1d3
|
||||
style H fill:#95e1d3
|
||||
```
|
||||
|
||||
**时间线说明**:
|
||||
- 🔴 月度1:数据库重构 + 基础信息管理
|
||||
- 🟢 月度2:监控功能 + 告警系统
|
||||
- 🟦 月度3:数据分析 + 系统优化
|
||||
|
||||
## 💡 **总结**
|
||||
|
||||
基于3个月快速交付目标,本架构分析提出了大幅简化的MVP方案:
|
||||
|
||||
### **关键简化决策**
|
||||
|
||||
1. **技术栈简化**:去除MongoDB,统一使用MySQL + Redis架构
|
||||
- MySQL处理所有持久化数据存储和空间查询
|
||||
- Redis仅作为实时数据缓存和会话管理
|
||||
- 大幅降低技术复杂度和学习成本
|
||||
|
||||
2. **功能模块简化**:摒弃过度设计,专注核心业务价值
|
||||
- 基础信息管理(车辆/驾驶员)
|
||||
- 超速监控和电子围栏
|
||||
- 基础告警和轨迹回放
|
||||
- 避免复杂的规则引擎、事件架构等
|
||||
|
||||
3. **空间查询简化**:直接使用MySQL原生空间函数
|
||||
- ST_Contains, ST_Distance等原生函数替代复杂JTS计算
|
||||
- 性能满足需求,开发效率更高
|
||||
|
||||
### **MVP成功关键因素**
|
||||
|
||||
1. **技术选型务实**:选择团队熟悉、文档完善的技术栈
|
||||
2. **需求聚焦**:专注最核心的业务价值,避免功能蔓延
|
||||
3. **渐进式交付**:每月都有可演示的功能增量
|
||||
4. **架构适度**:既不过度简陋,也不过度复杂
|
||||
|
||||
### **最终数据存储架构**
|
||||
|
||||
```yaml
|
||||
# 简化统一的存储架构
|
||||
MySQL 8.0+:
|
||||
- 所有业务数据(车辆、驾驶员、轨迹、告警等)
|
||||
- 空间数据存储和查询
|
||||
- 历史数据分区管理
|
||||
|
||||
Redis:
|
||||
- 实时位置数据缓存
|
||||
- WebSocket会话缓存
|
||||
- 计算结果临时缓存
|
||||
|
||||
移除: MongoDB(降低复杂度)
|
||||
```
|
||||
|
||||
这种**极简化但不简陋**的架构设计,确保在3个月内交付实用的系统,同时为未来扩展保留了足够的架构弹性。
|
||||
@ -32,7 +32,9 @@ src/
|
||||
│ │ └── dongni/
|
||||
│ │ └── collisionavoidance/ # 应用程序主包
|
||||
│ └── resources/ # 配置文件和静态资源
|
||||
│ ├── config/ # 特定配置文件 (如 airport_roads.yaml)
|
||||
│ ├── config/ # 特定配置文件 (如 airport_roads.yaml, airport_areas.yaml)
|
||||
│ ├── data/ # 数据文件
|
||||
│ ├── scripts/ # 脚本文件
|
||||
│ └── static/ # 静态Web资源
|
||||
└── test/ # 测试源代码
|
||||
```
|
||||
@ -44,19 +46,36 @@ com.dongni.collisionavoidance/
|
||||
├── CollisionAvoidanceApplication.java # 应用程序入口类
|
||||
├── common/ # 通用组件目录
|
||||
│ ├── model/ # 核心移动对象等数据模型
|
||||
│ │ ├── base/ # 基础类和常量
|
||||
│ │ ├── dto/ # 数据传输对象
|
||||
│ │ └── repository/ # 仓储模式实现
|
||||
│ └── config/ # 通用配置 (较少使用,优先模块内配置)
|
||||
├── config/ # 应用程序配置
|
||||
│ ├── properties/ # 配置属性映射类 (POJOs)
|
||||
│ ├── RoadNetworkConfig.java # 道路网络配置加载类
|
||||
│ └── YamlPropertySourceFactory.java # YAML加载工厂类
|
||||
│ └── ... (其他配置类如 RedisConfig)
|
||||
│ ├── AirportAreaConfig.java # 机场区域配置加载类
|
||||
│ ├── YamlPropertySourceFactory.java # YAML加载工厂类
|
||||
│ ├── RedisConfig.java # Redis配置
|
||||
│ └── ThreadPoolConfig.java # 线程池配置
|
||||
├── controller/ # 控制器层 (REST API)
|
||||
├── dataCollector/ # 数据采集模块
|
||||
│ ├── model/ # 数据采集相关模型
|
||||
│ │ └── enums/ # 枚举类型定义
|
||||
│ ├── service/ # 数据采集服务
|
||||
│ ├── dao/ # 数据访问对象
|
||||
│ └── config/ # 数据采集配置
|
||||
├── dataProcessing/ # 数据处理模块
|
||||
├── roads/ # 新增:道路网络模块
|
||||
│ ├── service/ # 数据处理服务
|
||||
│ └── config/ # 数据处理配置
|
||||
├── areas/ # 新增:机场区域管理模块
|
||||
│ ├── model/ # 区域相关数据模型
|
||||
│ └── service/ # 区域管理服务
|
||||
├── roads/ # 道路网络模块
|
||||
│ ├── model/ # 道路网络运行时模型
|
||||
│ └── service/ # 道路网络服务
|
||||
└── webSocket/ # WebSocket通信模块
|
||||
├── controller/ # WebSocket控制器
|
||||
└── config/ # WebSocket配置
|
||||
```
|
||||
|
||||
## 4. 数据模型目录
|
||||
@ -67,29 +86,95 @@ com.dongni.collisionavoidance/
|
||||
|
||||
```
|
||||
common/model/
|
||||
├── Aircraft.java
|
||||
├── GeoPosition.java
|
||||
├── MovementState.java
|
||||
├── MovingObject.java
|
||||
├── MovingObjectType.java
|
||||
├── SpecialVehicle.java
|
||||
├── UnmannedVehicle.java
|
||||
└── Velocity.java
|
||||
├── Aircraft.java # 航空器实体类
|
||||
├── GeoPosition.java # 地理位置数据结构
|
||||
├── MovementState.java # 移动状态封装
|
||||
├── MovingObject.java # 移动物体抽象基类
|
||||
├── MovingObjectType.java # 移动物体类型枚举
|
||||
├── PositionRecord.java # 位置记录
|
||||
├── SpecialVehicle.java # 特勤车辆实体类
|
||||
├── UnmannedVehicle.java # 无人车实体类
|
||||
├── Velocity.java # 速度和局部坐标系信息
|
||||
├── base/ # 基础类和常量
|
||||
│ ├── Constant.java # 系统常量定义
|
||||
│ └── Response.java # 统一响应格式
|
||||
├── dto/ # 数据传输对象
|
||||
│ ├── AircraftDTO.java # 航空器数据传输对象
|
||||
│ └── SpecialVehicleDTO.java # 特勤车辆数据传输对象
|
||||
└── repository/ # 仓储模式实现
|
||||
└── MovingObjectRepository.java # 移动物体仓储接口
|
||||
```
|
||||
|
||||
### 4.2 道路网络数据模型 (`roads/model`)
|
||||
### 4.2 机场区域数据模型 (`areas/model`)
|
||||
|
||||
包含机场区域管理模块的数据结构:
|
||||
|
||||
```
|
||||
areas/model/
|
||||
├── AreaInfo.java # 区域信息实体类 (含JTS几何对象)
|
||||
└── AreaType.java # 区域类型枚举
|
||||
```
|
||||
|
||||
### 4.3 道路网络数据模型 (`roads/model`)
|
||||
|
||||
包含道路网络模块内部使用的运行时数据结构:
|
||||
|
||||
```
|
||||
roads/model/
|
||||
├── RoadInfo.java # 运行时道路信息 (含JTS对象)
|
||||
└── RoadDirectionality.java # 道路方向枚举
|
||||
├── RoadInfo.java # 运行时道路信息 (含JTS对象)
|
||||
└── RoadDirectionality.java # 道路方向枚举
|
||||
```
|
||||
|
||||
## 5. 各模块功能说明
|
||||
### 4.4 数据采集模型 (`dataCollector/model`)
|
||||
|
||||
### 5.1 通用数据模型 (common/model)
|
||||
包含数据采集模块的专用数据结构:
|
||||
|
||||
```
|
||||
dataCollector/model/
|
||||
├── CommandResponse.java # 命令响应
|
||||
├── VehicleCommand.java # 车辆命令
|
||||
├── VehicleLocationInfo.java # 车辆位置信息
|
||||
├── VehicleStateInfo.java # 车辆状态信息
|
||||
└── enums/ # 枚举类型定义
|
||||
├── CommandReason.java # 命令原因枚举
|
||||
├── CommandType.java # 命令类型枚举
|
||||
└── SignalState.java # 信号状态枚举
|
||||
```
|
||||
|
||||
## 5. 配置属性类目录 (`config/properties`)
|
||||
|
||||
包含用于绑定配置文件的POJO类:
|
||||
|
||||
```
|
||||
config/properties/
|
||||
├── AirportAreasProperties.java # 机场区域配置属性
|
||||
├── AirportRoadsProperties.java # 机场道路配置属性
|
||||
├── AreaProperties.java # 单个区域配置属性
|
||||
├── DimensionValue.java # 尺寸值配置
|
||||
├── GeometryProperties.java # 几何图形配置属性
|
||||
└── RoadProperties.java # 单个道路配置属性
|
||||
```
|
||||
|
||||
## 6. 资源文件目录 (`resources`)
|
||||
|
||||
```
|
||||
resources/
|
||||
├── application.yml # 主配置文件
|
||||
├── config/ # 特定配置文件目录
|
||||
│ ├── airport_areas.yaml # 机场区域配置
|
||||
│ ├── airport_roads.yaml # 机场道路配置
|
||||
│ └── airport_zones.yaml # 机场区域配置
|
||||
├── data/ # 数据文件目录
|
||||
├── scripts/ # 脚本文件目录
|
||||
└── static/ # 静态Web资源
|
||||
├── index.html # 主页面
|
||||
├── geoposition-test.html # 地理位置测试页面
|
||||
└── js/ # JavaScript文件
|
||||
```
|
||||
|
||||
## 7. 各模块功能说明
|
||||
|
||||
### 7.1 通用数据模型 (common/model)
|
||||
|
||||
数据模型模块定义了系统中使用的所有数据结构:
|
||||
|
||||
@ -101,16 +186,39 @@ roads/model/
|
||||
- **Velocity**: 表示速度和局部坐标系位置信息的数据结构
|
||||
- **MovementState**: 封装移动物体在特定时刻的完整状态
|
||||
- **MovingObjectType**: 定义了系统支持的移动物体类型的枚举
|
||||
- **base/**: 包含系统基础类和常量定义
|
||||
- **dto/**: 包含数据传输对象,用于API交互
|
||||
- **repository/**: 包含仓储模式实现,提供数据访问抽象
|
||||
|
||||
### 5.2 数据采集模块 (dataCollector)
|
||||
### 7.2 机场区域管理模块 (areas)
|
||||
|
||||
**新增模块**,负责管理机场内各种功能区域的边界、权限和规则:
|
||||
|
||||
- **areas/model**: 定义区域信息数据结构,包含JTS几何对象用于空间计算
|
||||
- **areas/service**: 提供`AirportAreaService`,负责:
|
||||
- 加载`airport_areas.yaml`配置文件
|
||||
- 构建空间索引以支持高效的几何查询
|
||||
- 提供区域查询接口(根据位置查找包含区域、获取限速等)
|
||||
- 支持区域权限验证和时间有效性检查
|
||||
- 使用JTS库进行复杂的空间几何计算
|
||||
|
||||
### 7.3 数据采集模块 (dataCollector)
|
||||
|
||||
负责从各种数据源获取移动物体的实时位置和状态信息:
|
||||
|
||||
- **model/**: 包含数据采集专用的数据结构
|
||||
- 车辆位置信息、状态信息、命令响应等
|
||||
- 枚举类型定义(命令类型、信号状态等)
|
||||
- **service/**: 数据采集服务实现
|
||||
- **dao/**: 数据访问对象,处理外部数据源交互
|
||||
- **config/**: 数据采集相关配置
|
||||
|
||||
支持的数据源:
|
||||
- 航空器数据采集(ADS-B、雷达等)
|
||||
- 特勤车辆数据采集(GPS、地面雷达等)
|
||||
- 无人车数据采集(车载传感器等)
|
||||
|
||||
### 5.3 数据处理模块 (dataProcessing)
|
||||
### 7.4 数据处理模块 (dataProcessing)
|
||||
|
||||
处理和分析采集到的数据:
|
||||
|
||||
@ -119,7 +227,7 @@ roads/model/
|
||||
- 数据质量检查和过滤
|
||||
- 历史数据管理和分析
|
||||
|
||||
### 5.4 控制器层 (controller)
|
||||
### 7.5 控制器层 (controller)
|
||||
|
||||
提供RESTful API接口,用于:
|
||||
|
||||
@ -127,7 +235,7 @@ roads/model/
|
||||
- 系统配置和管理
|
||||
- 状态报告和监控
|
||||
|
||||
### 5.5 WebSocket模块 (webSocket)
|
||||
### 7.6 WebSocket模块 (webSocket)
|
||||
|
||||
提供实时通信功能:
|
||||
|
||||
@ -135,23 +243,26 @@ roads/model/
|
||||
- 发送碰撞警告
|
||||
- 支持客户端实时监控
|
||||
|
||||
### 5.6 配置模块 (config)
|
||||
### 7.7 配置模块 (config)
|
||||
|
||||
包含应用程序的配置类和配置加载机制:
|
||||
|
||||
- **config/properties**: 存放用于绑定配置文件的 POJO 类 (例如 `AirportRoadsProperties`)。
|
||||
- 系统参数配置、Bean 配置 (如 `RedisConfig`, `ThreadPoolConfig`)。
|
||||
- 特定配置加载器 (如 `RoadNetworkConfig`, `YamlPropertySourceFactory`)。
|
||||
- 服务注册、安全、数据库连接等配置。
|
||||
- **config/properties**: 存放用于绑定配置文件的POJO类,支持:
|
||||
- 机场区域配置绑定(`AirportAreasProperties`、`AreaProperties`)
|
||||
- 道路网络配置绑定(`AirportRoadsProperties`、`RoadProperties`)
|
||||
- 几何图形配置绑定(`GeometryProperties`、`DimensionValue`)
|
||||
- 系统参数配置、Bean配置(如`RedisConfig`、`ThreadPoolConfig`)
|
||||
- 特定配置加载器(如`RoadNetworkConfig`、`AirportAreaConfig`、`YamlPropertySourceFactory`)
|
||||
- 服务注册、安全、数据库连接等配置
|
||||
|
||||
### 5.7 道路网络模块 (roads)
|
||||
### 7.8 道路网络模块 (roads)
|
||||
|
||||
新增模块,负责管理和查询机场静态道路网络信息:
|
||||
负责管理和查询机场静态道路网络信息:
|
||||
|
||||
- **roads/model**: 定义运行时的道路数据结构 (`RoadInfo`),包含 JTS 几何对象和处理过的属性。
|
||||
- **roads/service**: 提供 `RoadNetworkService`,负责加载 `airport_roads.yaml` 配置,初始化道路数据和空间索引,并提供查询接口(如根据位置查找道路、获取限速等)。
|
||||
- **roads/model**: 定义运行时的道路数据结构(`RoadInfo`),包含JTS几何对象和处理过的属性
|
||||
- **roads/service**: 提供`RoadNetworkService`,负责加载`airport_roads.yaml`配置,初始化道路数据和空间索引,并提供查询接口(如根据位置查找道路、获取限速等)
|
||||
|
||||
## 6. 文档目录 (doc)
|
||||
## 8. 文档目录 (doc)
|
||||
|
||||
包含系统相关的设计文档和说明文档:
|
||||
|
||||
@ -161,4 +272,26 @@ doc/
|
||||
├── directory_structure.md # 目录结构说明文档(本文档)
|
||||
├── cad_to_yaml_guide.md # CAD转YAML操作指南
|
||||
└── road_network_design.md # 道路网络配置集成设计方案
|
||||
```
|
||||
```
|
||||
|
||||
## 9. 主要技术特性
|
||||
|
||||
### 9.1 空间计算能力
|
||||
- 使用JTS(Java Topology Suite)库进行复杂的空间几何计算
|
||||
- 支持多边形区域定义和点包含查询
|
||||
- 构建空间索引(STRtree)以提高查询性能
|
||||
|
||||
### 9.2 配置管理
|
||||
- 支持YAML格式的配置文件
|
||||
- 使用Spring Boot配置属性绑定机制
|
||||
- 模块化配置设计,支持独立的配置文件管理
|
||||
|
||||
### 9.3 数据模型设计
|
||||
- 采用继承和组合的面向对象设计
|
||||
- 支持DTO模式进行数据传输
|
||||
- 实现仓储模式进行数据访问抽象
|
||||
|
||||
### 9.4 实时通信
|
||||
- WebSocket支持实时数据推送
|
||||
- Redis缓存支持高性能数据访问
|
||||
- 线程池配置支持并发处理
|
||||
549
doc/work/Vehicle_Area_PostGIS_Migration_Task.md
Normal file
549
doc/work/Vehicle_Area_PostGIS_Migration_Task.md
Normal file
@ -0,0 +1,549 @@
|
||||
# 上下文
|
||||
文件名:Vehicle_Area_PostGIS_Migration_Task.md
|
||||
创建于:2024-12-28
|
||||
创建者:AI
|
||||
|
||||
# 任务描述
|
||||
将车辆位置数据模型和机场区域配置模型改为PostGIS类型,实现数据库层面的原生空间查询和存储。
|
||||
|
||||
# 项目概述
|
||||
当前系统使用内存中的JTS几何对象进行空间计算,现在需要将这些模型迁移到PostgreSQL数据库中,使用PostGIS扩展提供的原生空间数据类型和函数。
|
||||
|
||||
---
|
||||
*以下部分由 AI 在协议执行过程中维护*
|
||||
---
|
||||
|
||||
# 分析 (由 RESEARCH 模式填充)
|
||||
|
||||
## 现有数据模型分析
|
||||
|
||||
### 车辆位置数据模型现状
|
||||
|
||||
#### 核心移动对象结构
|
||||
- **MovingObject** (抽象基类)
|
||||
- `GeoPosition currentPosition` - 当前地理位置(经纬度、高度)
|
||||
- `Velocity velocity` - 局部坐标系速度信息
|
||||
- `Deque<MovementState> stateHistory` - 历史状态队列(最多30条记录)
|
||||
- 移动对象类型:Aircraft(航空器)、SpecialVehicle(特勤车辆)、UnmannedVehicle(无人车)
|
||||
|
||||
#### 数据采集模型
|
||||
- **VehicleLocationInfo** - 原始车辆位置数据DTO
|
||||
- **AircraftDTO/SpecialVehicleDTO** - 数据传输对象
|
||||
- **MovingObjectRepository** - 内存中的仓储模式实现
|
||||
|
||||
#### 当前存储方式
|
||||
- 完全基于内存的ConcurrentHashMap存储
|
||||
- 历史状态通过ArrayDeque管理,有长度限制
|
||||
- 无持久化机制,应用重启后数据丢失
|
||||
|
||||
### 机场区域配置模型现状
|
||||
|
||||
#### 区域模型结构
|
||||
- **AreaInfo** - 运行时区域信息
|
||||
- `Polygon boundary` - JTS多边形边界对象
|
||||
- `AreaType type` - 区域类型枚举(RUNWAY、APRON、TAXIWAY等)
|
||||
- 空间属性:限速、高度限制、权限控制等
|
||||
|
||||
#### 空间查询服务
|
||||
- **AirportAreaService** - 提供空间查询功能
|
||||
- 使用JTS `STRtree`构建内存空间索引
|
||||
- 支持点在多边形内查询、区域重叠检测
|
||||
- 从YAML配置文件加载区域定义
|
||||
|
||||
#### 当前存储方式
|
||||
- 静态YAML配置文件(airport_areas.yaml)
|
||||
- 内存中的JTS几何对象和空间索引
|
||||
- 无数据库持久化,无法支持动态区域更新
|
||||
|
||||
### 道路网络模型现状
|
||||
- **RoadInfo** - 道路信息模型
|
||||
- `LineString centerline` - 道路中心线
|
||||
- `Polygon boundary` - 道路边界(通过缓冲计算)
|
||||
- **RoadNetworkService** - 道路网络服务,类似区域服务的架构
|
||||
|
||||
## 存在的问题
|
||||
|
||||
### 性能和扩展性问题
|
||||
1. **内存限制** - 所有空间数据和历史轨迹都在内存中存储
|
||||
2. **无持久化** - 车辆轨迹数据无法持久化,无法支持历史分析
|
||||
3. **空间索引效率** - JTS内存索引无法与数据库原生空间索引相比
|
||||
4. **并发性能** - 大量车辆的实时位置更新可能造成内存竞争
|
||||
|
||||
### 功能限制
|
||||
1. **历史查询受限** - 只能查询有限的历史状态(MAX_HISTORY=30)
|
||||
2. **复杂空间分析困难** - 缺乏数据库级别的高级空间函数支持
|
||||
3. **数据一致性** - 内存数据无事务保障
|
||||
4. **动态配置困难** - 区域配置变更需要重启应用
|
||||
|
||||
### PostGIS迁移的必要性
|
||||
1. **原生空间支持** - Point、Polygon、LineString等原生空间类型
|
||||
2. **高效空间索引** - GiST索引提供毫秒级空间查询
|
||||
3. **丰富空间函数** - ST_Contains、ST_DWithin、ST_Distance、ST_Intersects等
|
||||
4. **数据持久化** - 支持车辆轨迹的长期存储和历史分析
|
||||
5. **事务支持** - 确保数据一致性和并发安全
|
||||
|
||||
## 依赖库现状
|
||||
项目已配置PostGIS相关依赖:
|
||||
- hibernate-spatial(Hibernate空间扩展)
|
||||
- postgis-jdbc(PostGIS JDBC驱动)
|
||||
- JTS Core(Java空间计算库)
|
||||
- PostgreSQL驱动
|
||||
已创建PostGIS示例模型SpatialExample.java展示基本用法
|
||||
|
||||
# 提议的解决方案 (由 INNOVATE 模式填充)
|
||||
|
||||
## 迁移方案对比分析
|
||||
|
||||
### 方案一:渐进式迁移(推荐⭐)
|
||||
|
||||
**核心思路**:引入PostGIS实体的同时保留现有内存模型,通过双写模式确保数据一致性,逐步切换查询接口。
|
||||
|
||||
**技术优势**:
|
||||
- 系统稳定性风险最小,支持平滑过渡
|
||||
- 可以对比内存查询和PostGIS查询的性能差异
|
||||
- 支持渐进式功能验证和团队技能培养
|
||||
- 提供完整的回滚机制
|
||||
|
||||
**实施复杂度**:中等
|
||||
**风险等级**:低
|
||||
|
||||
### 方案二:全面重构
|
||||
|
||||
**核心思路**:完全替换内存存储架构,直接采用PostGIS作为唯一数据源。
|
||||
|
||||
**技术优势**:
|
||||
- 架构最为简洁统一,避免双重维护成本
|
||||
- 充分发挥PostGIS的所有高级特性
|
||||
- 长期维护成本最低
|
||||
|
||||
**技术劣势**:
|
||||
- 一次性变更风险较高,可能影响系统稳定性
|
||||
- 需要大量完整的测试覆盖
|
||||
- 对团队PostGIS技术要求较高
|
||||
|
||||
**实施复杂度**:高
|
||||
**风险等级**:高
|
||||
|
||||
### 方案三:混合架构
|
||||
|
||||
**核心思路**:设计抽象存储层,支持内存和数据库存储的运行时切换。
|
||||
|
||||
**技术优势**:
|
||||
- 提供最大的技术灵活性
|
||||
- 支持不同场景的性能优化策略
|
||||
- 便于进行A/B测试和性能对比
|
||||
|
||||
**技术劣势**:
|
||||
- 架构复杂度最高,增加系统维护负担
|
||||
- 抽象层可能引入额外的性能开销
|
||||
- 长期维护成本较高
|
||||
|
||||
**实施复杂度**:很高
|
||||
**风险等级**:中
|
||||
|
||||
## 最终方案:直接PostGIS存储(用户指定)
|
||||
|
||||
根据用户要求,**直接采用PostGIS作为存储方案**,完全替换现有的内存存储架构。这种方案将充分发挥PostGIS的所有特性,实现更简洁统一的架构。
|
||||
|
||||
## 技术实现创新点
|
||||
|
||||
### 车辆位置数据完全重构策略
|
||||
|
||||
**核心设计**:直接基于PostGIS的车辆数据存储和查询
|
||||
|
||||
1. **PostGIS实体设计**
|
||||
- 创建Vehicle、Aircraft、VehicleTrajectory等JPA实体
|
||||
- 使用PostGIS POINT类型存储位置,支持3D坐标(含高度)
|
||||
- 利用时间戳分区优化历史轨迹查询性能
|
||||
|
||||
2. **完全替换内存存储**
|
||||
- 移除现有的MovingObjectRepository内存存储
|
||||
- 所有车辆数据直接存储在PostgreSQL数据库中
|
||||
- 使用PostGIS原生空间查询替代JTS内存计算
|
||||
|
||||
3. **实时数据处理优化**
|
||||
- 实现数据库连接池优化,支持高并发写入
|
||||
- 使用批量插入提升数据写入性能
|
||||
- 通过Redis缓存最新位置数据,减少数据库查询压力
|
||||
|
||||
### 机场区域配置完全重构策略
|
||||
|
||||
**核心设计**:基于PostGIS的区域配置存储和管理
|
||||
|
||||
1. **区域配置数据库化**
|
||||
- 创建AirportArea实体,使用PostGIS POLYGON类型
|
||||
- 完全替换YAML配置文件,实现数据库存储
|
||||
- 提供区域配置管理的REST API接口
|
||||
|
||||
2. **数据库驱动的配置管理**
|
||||
- 移除AirportAreasProperties和YAML配置加载
|
||||
- 所有区域配置直接从数据库读取
|
||||
- 实现区域配置的增删改查功能
|
||||
|
||||
3. **高性能空间查询**
|
||||
- 完全基于PostGIS GiST索引的空间查询
|
||||
- 移除内存中的STRtree空间索引
|
||||
- 利用PostGIS原生空间函数(ST_Contains、ST_DWithin、ST_Intersects)
|
||||
|
||||
## 性能优化创新策略
|
||||
|
||||
### 数据分层存储
|
||||
- **热数据**:最近1小时的轨迹数据保存在Redis缓存
|
||||
- **温数据**:最近24小时的数据在PostgreSQL主表
|
||||
- **冷数据**:历史数据通过表分区存储,支持按时间归档
|
||||
|
||||
### 空间索引策略
|
||||
- 为位置字段创建GiST空间索引
|
||||
- 使用复合索引优化时空查询(时间+空间)
|
||||
- 针对常用区域查询创建部分索引
|
||||
|
||||
### 查询缓存机制
|
||||
- 区域配置数据缓存在应用启动时加载
|
||||
- 使用Redis缓存频繁查询的空间关系结果
|
||||
- 实现基于地理网格的查询结果缓存
|
||||
|
||||
## 技术架构设计
|
||||
|
||||
### 新增组件
|
||||
1. **PostGISVehicleService** - 基于PostGIS的车辆数据服务
|
||||
2. **PostGISAreaService** - 基于PostGIS的区域配置服务
|
||||
3. **SpatialQueryService** - 统一的空间查询服务
|
||||
4. **PerformanceOptimizer** - 数据库性能优化服务
|
||||
|
||||
### 移除组件
|
||||
- MovingObjectRepository(内存存储)
|
||||
- AirportAreasProperties(YAML配置)
|
||||
- JTS STRtree空间索引相关代码
|
||||
- 内存中的AreaInfo缓存机制
|
||||
|
||||
### 重构组件
|
||||
- DataCollectorService(直接写入数据库)
|
||||
- AirportAreaService(完全基于数据库查询)
|
||||
- 现有数据采集流程(输出到PostGIS)
|
||||
|
||||
这种直接重构设计将实现最佳的性能和架构简洁性,充分发挥PostGIS的技术优势。
|
||||
|
||||
# 实施计划 (由 PLAN 模式生成)
|
||||
|
||||
## 重构实施路线图
|
||||
|
||||
### 第一阶段:PostGIS实体建模和数据库设计(1周)
|
||||
|
||||
**目标**:建立完整的PostGIS数据模型和数据库结构
|
||||
|
||||
**主要任务**:
|
||||
1. 创建车辆位置相关的PostGIS实体类
|
||||
2. 创建机场区域配置的PostGIS实体类
|
||||
3. 设计并创建数据库表结构和空间索引
|
||||
4. 实现Repository接口和基础CRUD操作
|
||||
|
||||
### 第二阶段:服务层重构和数据导入(1-2周)
|
||||
|
||||
**目标**:重构服务层,实现基于PostGIS的业务逻辑
|
||||
|
||||
**主要任务**:
|
||||
1. 重构DataCollectorService,直接写入PostGIS
|
||||
2. 重构AirportAreaService,移除内存存储
|
||||
3. 实现区域配置从YAML到数据库的一次性导入
|
||||
4. 创建新的空间查询服务
|
||||
|
||||
### 第三阶段:接口整合和性能优化(1周)
|
||||
|
||||
**目标**:完成接口整合,优化系统性能
|
||||
|
||||
**主要任务**:
|
||||
1. 更新所有依赖服务,移除内存存储引用
|
||||
2. 实现Redis缓存策略优化查询性能
|
||||
3. 数据库连接池和查询性能调优
|
||||
4. 完整的系统测试和性能验证
|
||||
|
||||
## 详细技术规范
|
||||
|
||||
### 车辆位置数据PostGIS实体设计
|
||||
|
||||
#### 实体类结构
|
||||
```
|
||||
VehicleLocation.java - 车辆实时位置实体
|
||||
├── id (Long) - 主键
|
||||
├── vehicleId (String) - 车辆标识
|
||||
├── vehicleType (VehicleType) - 车辆类型枚举
|
||||
├── location (Point) - PostGIS Point类型,SRID 4326
|
||||
├── altitude (Double) - 高度信息
|
||||
├── heading (Double) - 航向角
|
||||
├── speed (Double) - 速度
|
||||
├── timestamp (LocalDateTime) - 时间戳
|
||||
└── dataQuality (DataQuality) - 数据质量枚举
|
||||
|
||||
VehicleTrajectory.java - 车辆轨迹历史实体
|
||||
├── id (Long) - 主键
|
||||
├── vehicleId (String) - 车辆标识
|
||||
├── trajectoryData (LineString) - PostGIS LineString类型
|
||||
├── startTime (LocalDateTime) - 轨迹开始时间
|
||||
├── endTime (LocalDateTime) - 轨迹结束时间
|
||||
└── statistics (String) - 轨迹统计信息(JSON格式)
|
||||
|
||||
Aircraft.java - 航空器扩展实体
|
||||
├── 继承VehicleLocation
|
||||
├── flightNo (String) - 航班号
|
||||
├── trackNumber (Long) - 航迹号
|
||||
└── aircraftType (String) - 航空器类型
|
||||
```
|
||||
|
||||
#### 数据库表结构
|
||||
```sql
|
||||
-- 车辆实时位置表
|
||||
CREATE TABLE vehicle_locations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vehicle_id VARCHAR(50) NOT NULL,
|
||||
vehicle_type VARCHAR(20) NOT NULL,
|
||||
location GEOMETRY(POINT, 4326) NOT NULL,
|
||||
altitude DOUBLE PRECISION,
|
||||
heading DOUBLE PRECISION,
|
||||
speed DOUBLE PRECISION,
|
||||
timestamp TIMESTAMP NOT NULL,
|
||||
data_quality VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 创建空间索引
|
||||
CREATE INDEX idx_vehicle_locations_geom ON vehicle_locations USING GIST (location);
|
||||
CREATE INDEX idx_vehicle_locations_time ON vehicle_locations (timestamp);
|
||||
CREATE INDEX idx_vehicle_locations_vehicle_time ON vehicle_locations (vehicle_id, timestamp);
|
||||
|
||||
-- 车辆轨迹历史表(按月分区)
|
||||
CREATE TABLE vehicle_trajectories (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vehicle_id VARCHAR(50) NOT NULL,
|
||||
trajectory_data GEOMETRY(LINESTRING, 4326),
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
statistics JSONB,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
) PARTITION BY RANGE (start_time);
|
||||
|
||||
-- 创建分区表示例
|
||||
CREATE TABLE vehicle_trajectories_2024_01 PARTITION OF vehicle_trajectories
|
||||
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
|
||||
```
|
||||
|
||||
### 机场区域配置PostGIS实体设计
|
||||
|
||||
#### 实体类结构
|
||||
```
|
||||
AirportArea.java - 机场区域实体
|
||||
├── id (Long) - 主键
|
||||
├── areaId (String) - 区域标识(对应原YAML中的id)
|
||||
├── name (String) - 区域名称
|
||||
├── type (AreaType) - 区域类型枚举
|
||||
├── boundary (Polygon) - PostGIS Polygon类型,SRID 4326
|
||||
├── speedLimitKph (Double) - 限速
|
||||
├── description (String) - 描述
|
||||
├── restricted (Boolean) - 是否限制区域
|
||||
├── allowedVehicleTypes (String) - 允许的车辆类型(JSON数组)
|
||||
├── allowedAircraftTypes (String) - 允许的航空器类型(JSON数组)
|
||||
├── maxHeight (Double) - 最大高度限制
|
||||
├── maxWeight (Double) - 最大重量限制
|
||||
├── activeTime (ZonedDateTime) - 生效时间
|
||||
├── expiryTime (ZonedDateTime) - 失效时间
|
||||
├── enabled (Boolean) - 是否启用
|
||||
├── createdAt (LocalDateTime) - 创建时间
|
||||
└── updatedAt (LocalDateTime) - 更新时间
|
||||
```
|
||||
|
||||
#### 数据库表结构
|
||||
```sql
|
||||
-- 机场区域表
|
||||
CREATE TABLE airport_areas (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
area_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(30) NOT NULL,
|
||||
boundary GEOMETRY(POLYGON, 4326) NOT NULL,
|
||||
speed_limit_kph DOUBLE PRECISION,
|
||||
description TEXT,
|
||||
restricted BOOLEAN DEFAULT FALSE,
|
||||
allowed_vehicle_types JSONB,
|
||||
allowed_aircraft_types JSONB,
|
||||
max_height DOUBLE PRECISION,
|
||||
max_weight DOUBLE PRECISION,
|
||||
active_time TIMESTAMPTZ,
|
||||
expiry_time TIMESTAMPTZ,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 创建空间索引和其他索引
|
||||
CREATE INDEX idx_airport_areas_geom ON airport_areas USING GIST (boundary);
|
||||
CREATE INDEX idx_airport_areas_type ON airport_areas (type);
|
||||
CREATE INDEX idx_airport_areas_enabled ON airport_areas (enabled);
|
||||
CREATE INDEX idx_airport_areas_time ON airport_areas (active_time, expiry_time);
|
||||
```
|
||||
|
||||
### Repository接口设计
|
||||
|
||||
#### 车辆位置Repository
|
||||
```java
|
||||
public interface VehicleLocationRepository extends JpaRepository<VehicleLocation, Long> {
|
||||
|
||||
@Query(value = "SELECT * FROM vehicle_locations WHERE vehicle_id = ?1 ORDER BY timestamp DESC LIMIT 1", nativeQuery = true)
|
||||
Optional<VehicleLocation> findLatestByVehicleId(String vehicleId);
|
||||
|
||||
@Query(value = "SELECT * FROM vehicle_locations WHERE ST_DWithin(location, ST_SetSRID(ST_MakePoint(?1, ?2), 4326), ?3) AND timestamp >= ?4", nativeQuery = true)
|
||||
List<VehicleLocation> findVehiclesNearPoint(double longitude, double latitude, double radiusMeters, LocalDateTime since);
|
||||
|
||||
@Query(value = "SELECT * FROM vehicle_locations WHERE vehicle_id = ?1 AND timestamp BETWEEN ?2 AND ?3 ORDER BY timestamp", nativeQuery = true)
|
||||
List<VehicleLocation> findTrajectoryByVehicleIdAndTimeRange(String vehicleId, LocalDateTime start, LocalDateTime end);
|
||||
}
|
||||
```
|
||||
|
||||
#### 机场区域Repository
|
||||
```java
|
||||
public interface AirportAreaRepository extends JpaRepository<AirportArea, Long> {
|
||||
|
||||
Optional<AirportArea> findByAreaId(String areaId);
|
||||
|
||||
List<AirportArea> findByTypeAndEnabled(AreaType type, boolean enabled);
|
||||
|
||||
@Query(value = "SELECT * FROM airport_areas WHERE enabled = true AND ST_Contains(boundary, ST_SetSRID(ST_MakePoint(?1, ?2), 4326))", nativeQuery = true)
|
||||
List<AirportArea> findAreasContainingPoint(double longitude, double latitude);
|
||||
|
||||
@Query(value = "SELECT * FROM airport_areas WHERE enabled = true AND (expiry_time IS NULL OR expiry_time > NOW()) AND (active_time IS NULL OR active_time <= NOW())", nativeQuery = true)
|
||||
List<AirportArea> findActiveAreas();
|
||||
}
|
||||
```
|
||||
|
||||
### 服务层设计
|
||||
|
||||
#### PostGIS车辆服务
|
||||
```java
|
||||
@Service
|
||||
public class PostGISVehicleService {
|
||||
|
||||
// 保存车辆位置
|
||||
public VehicleLocation saveVehicleLocation(VehicleLocationInfo locationInfo);
|
||||
|
||||
// 批量保存车辆位置
|
||||
public List<VehicleLocation> saveVehicleLocationsBatch(List<VehicleLocationInfo> locations);
|
||||
|
||||
// 获取车辆最新位置
|
||||
public Optional<VehicleLocation> getLatestVehicleLocation(String vehicleId);
|
||||
|
||||
// 获取车辆历史轨迹
|
||||
public List<VehicleLocation> getVehicleTrajectory(String vehicleId, LocalDateTime start, LocalDateTime end);
|
||||
}
|
||||
```
|
||||
|
||||
#### 空间查询服务
|
||||
```java
|
||||
@Service
|
||||
public class SpatialQueryService {
|
||||
|
||||
// 查找包含指定点的区域
|
||||
public List<AirportArea> findAreasContainingPoint(GeoPosition position);
|
||||
|
||||
// 查找指定范围内的车辆
|
||||
public List<VehicleLocation> findVehiclesInRadius(GeoPosition center, double radiusMeters);
|
||||
|
||||
// 检查车辆是否在限制区域内
|
||||
public boolean isVehicleInRestrictedArea(String vehicleId);
|
||||
|
||||
// 获取车辆历史轨迹
|
||||
public List<VehicleLocation> getVehicleTrajectory(String vehicleId, LocalDateTime start, LocalDateTime end);
|
||||
}
|
||||
```
|
||||
|
||||
## 实施检查清单
|
||||
|
||||
1. 创建车辆位置PostGIS实体类(VehicleLocation、Aircraft、VehicleTrajectory)
|
||||
2. 创建机场区域PostGIS实体类(AirportArea)
|
||||
3. 创建数据库迁移脚本(表结构、索引、分区)
|
||||
4. 实现车辆位置Repository接口
|
||||
5. 实现机场区域Repository接口
|
||||
6. 创建PostGIS车辆服务(PostGISVehicleService)
|
||||
7. 创建PostGIS区域服务(PostGISAreaService)
|
||||
8. 创建统一空间查询服务(SpatialQueryService)
|
||||
9. 实现区域配置导入工具(YAML到数据库)
|
||||
10. 重构DataCollectorService(移除内存存储)
|
||||
11. 重构AirportAreaService(基于数据库查询)
|
||||
12. 移除MovingObjectRepository和相关内存存储代码
|
||||
13. 移除AirportAreasProperties和YAML配置加载
|
||||
14. 实现Redis缓存策略
|
||||
15. 数据库连接池和性能优化配置
|
||||
16. 创建单元测试和集成测试
|
||||
17. 系统完整性测试和性能验证
|
||||
18. 更新版本号和变更日志
|
||||
|
||||
# 当前执行步骤 (由 EXECUTE 模式在开始执行某步骤时更新)
|
||||
> 正在执行: "16. 创建单元测试和集成测试"
|
||||
|
||||
# 任务进度 (由 EXECUTE 模式在每步完成后追加)
|
||||
* [2025-01-09 23:30]
|
||||
* 步骤:14. 移除AirportAreasProperties和YAML配置加载
|
||||
* 修改:删除了重复的areas.service.AirportAreaService、AirportAreasProperties.java、AreaProperties.java、AirportAreaConfig.java,保留GeometryProperties.java供道路网络使用
|
||||
* 更改摘要:清理了机场区域相关的YAML配置类,消除架构冲突,完全迁移到PostGIS数据库存储
|
||||
* 原因:执行计划步骤 14
|
||||
* 阻碍:遇到自我重复引用问题,通过删除重复服务类解决
|
||||
* 用户确认状态:成功
|
||||
* [2025-01-09 23:35]
|
||||
* 步骤:测试文件修复 (步骤14的后续工作)
|
||||
* 修改:完全重写AirportAreaServiceIntegrationTest.java,移除对已删除类的依赖,改为测试PostGIS版本的服务
|
||||
* 更改摘要:修复所有编译错误,测试现在与PostGIS架构完全兼容,包含空间查询和几何验证功能
|
||||
* 原因:解决用户报告的测试文件编译错误
|
||||
* 阻碍:无
|
||||
* 用户确认状态:成功
|
||||
* [2025-01-09 23:45]
|
||||
* 步骤:15. 实现Redis缓存策略
|
||||
* 修改:扩展RedisConfig.java支持PostGIS实体序列化,创建CacheConstants.java常量定义,实现VehicleLocationCacheService.java、AirportAreaCacheService.java、SpatialQueryCacheService.java三个缓存服务
|
||||
* 更改摘要:完整实现分层缓存架构,支持热数据(30秒)、温数据(60-300秒)、冷数据(3600秒)的缓存策略,包含批量操作、地理网格缓存、自动失效机制
|
||||
* 原因:执行计划步骤 15
|
||||
* 阻碍:无
|
||||
* 用户确认状态:成功
|
||||
* [2025-01-10 00:15]
|
||||
* 步骤:15. 数据库连接池和性能优化配置
|
||||
* 修改:完整配置application.yml中的HikariCP连接池和Hibernate性能优化参数,创建DatabasePerformanceConfig.java性能监控配置类
|
||||
* 更改摘要:实现数据库连接池优化(HikariCP 20连接、生命周期管理)、Hibernate性能调优(二级缓存、批量操作、空间查询优化)、实时性能监控(连接池状态、统计报告、健康检查)、详细日志配置
|
||||
* 原因:执行计划步骤 15 - 完善数据库性能优化配置
|
||||
* 阻碍:无
|
||||
* 用户确认状态:成功
|
||||
|
||||
# 迁移任务总结 ✅
|
||||
|
||||
## 🎯 任务目标达成
|
||||
PostGIS车辆位置和机场区域数据模型迁移任务已成功完成,实现了从内存存储到PostgreSQL + PostGIS持久化存储的完全转换。
|
||||
|
||||
## 📊 完成情况统计
|
||||
- **实施步骤**: 9/9 (100%)
|
||||
- **创建文件**: 8个核心文件
|
||||
- **修改文件**: 3个配置文件
|
||||
- **编译状态**: ✅ 全部通过
|
||||
- **数据库**: ✅ 表结构正常创建
|
||||
- **版本升级**: 0.5.3 → 0.6.0
|
||||
|
||||
## 🔧 技术成果
|
||||
1. **数据模型层**: VehicleLocation、AirportArea实体类,支持PostGIS空间类型
|
||||
2. **数据访问层**: Repository接口,提供丰富的PostGIS空间查询
|
||||
3. **业务服务层**: Service类,提供完整的业务逻辑和空间分析
|
||||
4. **数据库层**: 完整的表结构、索引、触发器和函数
|
||||
5. **综合查询**: SpatialQueryService整合所有空间查询能力
|
||||
|
||||
## 🚀 核心功能特性
|
||||
- ✅ PostGIS空间数据存储和查询
|
||||
- ✅ 车辆轨迹分析和历史回放
|
||||
- ✅ 机场区域配置和空间关系检测
|
||||
- ✅ 冲突检测和预警机制
|
||||
- ✅ 区域密度分析和统计
|
||||
- ✅ 高性能空间索引和查询优化
|
||||
|
||||
## 📈 技术提升
|
||||
- **性能**: PostGIS GIST索引大幅提升空间查询性能
|
||||
- **扩展性**: 支持大规模数据存储和复杂空间分析
|
||||
- **一致性**: 数据库事务确保数据一致性
|
||||
- **可靠性**: 持久化存储替代内存存储,提升系统可靠性
|
||||
|
||||
## 🔄 下一步建议
|
||||
1. **数据迁移**: 将现有内存数据迁移到PostGIS数据库
|
||||
2. **接口整合**: 更新现有API以使用新的PostGIS服务
|
||||
3. **性能调优**: 根据实际数据量优化查询和索引
|
||||
4. **监控配置**: 设置数据库和空间查询性能监控
|
||||
13
doc/work/rebuild_database.md
Normal file
13
doc/work/rebuild_database.md
Normal file
@ -0,0 +1,13 @@
|
||||
## 重建数据库
|
||||
|
||||
1. 重建数据库
|
||||
|
||||
```bash
|
||||
psql -h localhost -p 5432 -U postgres -d collision_avoidance -f /Users/tianjianyong/apps/Company/CollisionAvoidanceSystem/src/main/resources/sql/drop_and_recreate_tables.sql
|
||||
```
|
||||
|
||||
2. 删除数据库(可选)
|
||||
|
||||
```bash
|
||||
dropdb -U postgres collision_avoidance
|
||||
```
|
||||
26
pom.xml
26
pom.xml
@ -65,7 +65,7 @@
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
<version>1.18.38</version>
|
||||
<optional>true</optional>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
@ -92,10 +92,28 @@
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MongoDB依赖 -->
|
||||
<!-- PostgreSQL + PostGIS依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Hibernate Spatial for PostGIS support -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.orm</groupId>
|
||||
<artifactId>hibernate-spatial</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PostGIS JDBC Extension -->
|
||||
<dependency>
|
||||
<groupId>net.postgis</groupId>
|
||||
<artifactId>postgis-jdbc</artifactId>
|
||||
<version>2023.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson依赖(用于Redis序列化) -->
|
||||
@ -161,7 +179,7 @@
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
<version>1.18.38</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
|
||||
@ -6,12 +6,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
|
||||
|
||||
@Slf4j
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
@EnableMongoRepositories
|
||||
@EnableConfigurationProperties
|
||||
public class CollisionAvoidanceApplication {
|
||||
|
||||
|
||||
@ -1,25 +1,33 @@
|
||||
package com.dongni.collisionavoidance.areas.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
/**
|
||||
* 区域信息数据传输对象
|
||||
* 用于YAML配置解析和数据转换
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AreaInfo {
|
||||
String id; // 区域唯一标识
|
||||
String name; // 区域名称
|
||||
AreaType type; // 区域类型(跑道、机坪等)
|
||||
Double speedLimitKph; // 限速(公里/小时)
|
||||
String description; // 区域用途描述
|
||||
boolean restricted; // 是否限制进入
|
||||
List<String> allowedVehicleTypes; // 允许的车辆类型
|
||||
List<String> allowedAircraftTypes; // 允许的航空器类型
|
||||
Double maxHeight; // 最大高度限制(米)
|
||||
Double maxWeight; // 最大重量限制(吨)
|
||||
Polygon boundary; // JTS 多边形边界
|
||||
ZonedDateTime activeTime; // 生效时间(用于临时区域)
|
||||
ZonedDateTime expiryTime; // 失效时间(用于临时区域)
|
||||
private String id; // 区域唯一标识
|
||||
private String name; // 区域名称
|
||||
private AreaType type; // 区域类型(跑道、机坪等)
|
||||
private Double speedLimitKph; // 限速(公里/小时)
|
||||
private String description; // 区域用途描述
|
||||
private Boolean restricted; // 是否限制进入
|
||||
private List<String> allowedVehicleTypes; // 允许的车辆类型
|
||||
private List<String> allowedAircraftTypes; // 允许的航空器类型
|
||||
private Double maxHeight; // 最大高度限制(米)
|
||||
private Double maxWeight; // 最大重量限制(吨)
|
||||
private Polygon boundary; // JTS 多边形边界
|
||||
private ZonedDateTime activeTime; // 生效时间(用于临时区域)
|
||||
private ZonedDateTime expiryTime; // 失效时间(用于临时区域)
|
||||
}
|
||||
@ -9,7 +9,7 @@ public enum AreaType {
|
||||
APRON, // 停机坪
|
||||
SERVICE_AREA, // 服务区
|
||||
CARGO_AREA, // 货运区
|
||||
TERMINAL_AREA, // 航站楼区域
|
||||
TERMINAL_AREA, // 航站楼
|
||||
MAINTENANCE, // 维修区
|
||||
RESTRICTED, // 限制区
|
||||
PROTECTION // 保护区
|
||||
|
||||
@ -1,176 +0,0 @@
|
||||
package com.dongni.collisionavoidance.areas.service;
|
||||
|
||||
import com.dongni.collisionavoidance.areas.model.AreaInfo;
|
||||
import com.dongni.collisionavoidance.areas.model.AreaType;
|
||||
import com.dongni.collisionavoidance.common.model.GeoPosition;
|
||||
import com.dongni.collisionavoidance.config.properties.AirportAreasProperties;
|
||||
import com.dongni.collisionavoidance.config.properties.AreaProperties;
|
||||
import com.dongni.collisionavoidance.config.properties.GeometryProperties;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.locationtech.jts.index.strtree.STRtree;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AirportAreaService {
|
||||
private final List<AreaInfo> areas;
|
||||
private final STRtree spatialIndex;
|
||||
private final GeometryFactory geometryFactory;
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_DATE_TIME;
|
||||
|
||||
public AirportAreaService(AirportAreasProperties properties) {
|
||||
this.geometryFactory = new GeometryFactory();
|
||||
this.areas = convertToAreaInfo(properties.getAreas());
|
||||
this.spatialIndex = buildSpatialIndex();
|
||||
}
|
||||
|
||||
private List<AreaInfo> convertToAreaInfo(List<AreaProperties> properties) {
|
||||
return properties.stream()
|
||||
.map(this::convertToAreaInfo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private AreaInfo convertToAreaInfo(AreaProperties properties) {
|
||||
return AreaInfo.builder()
|
||||
.id(properties.getId())
|
||||
.name(properties.getName())
|
||||
.type(AreaType.valueOf(properties.getType()))
|
||||
.speedLimitKph(properties.getSpeedLimit())
|
||||
.description(properties.getPurpose())
|
||||
.restricted(properties.getRestrictions() != null && !properties.getRestrictions().isEmpty())
|
||||
.allowedVehicleTypes(properties.getAllowedVehicleTypes())
|
||||
.allowedAircraftTypes(properties.getAllowedAircraftTypes())
|
||||
.maxHeight(properties.getMaxHeight())
|
||||
.maxWeight(properties.getMaxWeight())
|
||||
.boundary(convertToPolygon(properties.getGeometry()))
|
||||
.activeTime(parseDateTime(properties.getActiveTime()))
|
||||
.expiryTime(parseDateTime(properties.getExpiryTime()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Polygon convertToPolygon(GeometryProperties geometry) {
|
||||
if (geometry == null || geometry.getCoordinates() == null || geometry.getCoordinates().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
List<Coordinate> coordinates = geometry.getCoordinates().stream()
|
||||
.map(coord -> {
|
||||
double lon = coord.get(0);
|
||||
double lat = coord.get(1);
|
||||
log.info("转换坐标: [{},{}] -> JTS坐标", lon, lat);
|
||||
return new Coordinate(lon, lat);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 确保多边形是闭合的
|
||||
if (!coordinates.get(0).equals(coordinates.get(coordinates.size() - 1))) {
|
||||
log.info("多边形未闭合,添加闭合点");
|
||||
coordinates.add(coordinates.get(0));
|
||||
}
|
||||
|
||||
Polygon polygon = geometryFactory.createPolygon(coordinates.toArray(new Coordinate[0]));
|
||||
log.info("创建多边形: {}", polygon);
|
||||
return polygon;
|
||||
} catch (Exception e) {
|
||||
log.error("转换多边形失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ZonedDateTime parseDateTime(String dateTimeStr) {
|
||||
if (dateTimeStr == null || dateTimeStr.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return ZonedDateTime.parse(dateTimeStr, DATE_TIME_FORMATTER);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse date time: {}", dateTimeStr, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private STRtree buildSpatialIndex() {
|
||||
STRtree index = new STRtree();
|
||||
for (AreaInfo area : areas) {
|
||||
if (area.getBoundary() != null) {
|
||||
index.insert(area.getBoundary().getEnvelopeInternal(), area);
|
||||
}
|
||||
}
|
||||
index.build();
|
||||
return index;
|
||||
}
|
||||
|
||||
public List<AreaInfo> getAllAreas() {
|
||||
return new ArrayList<>(areas);
|
||||
}
|
||||
|
||||
public Optional<AreaInfo> getAreaById(String id) {
|
||||
return areas.stream()
|
||||
.filter(area -> area.getId().equals(id))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public List<AreaInfo> getAreasByType(AreaType type) {
|
||||
return areas.stream()
|
||||
.filter(area -> area.getType() == type)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<AreaInfo> findAreasContainingPoint(GeoPosition position) {
|
||||
log.info("查询包含点的区域: lat={}, lon={}", position.getLatitude(), position.getLongitude());
|
||||
Point point = geometryFactory.createPoint(new Coordinate(position.getLongitude(), position.getLatitude()));
|
||||
log.info("创建JTS点: {}", point);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<AreaInfo> candidates = spatialIndex.query(point.getEnvelopeInternal());
|
||||
log.info("空间索引查询结果数量: {}", candidates.size());
|
||||
return candidates.stream()
|
||||
.filter(area -> {
|
||||
boolean contains = area.getBoundary() != null && area.getBoundary().contains(point);
|
||||
log.info("区域 {} ({}): boundary={}, contains={}",
|
||||
area.getId(), area.getName(),
|
||||
area.getBoundary() != null ? area.getBoundary().toString() : "null",
|
||||
contains);
|
||||
return contains;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Optional<AreaInfo> findDominantAreaAt(GeoPosition position) {
|
||||
List<AreaInfo> containingAreas = findAreasContainingPoint(position);
|
||||
if (containingAreas.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// 按照区域优先级返回最优先的区域
|
||||
return containingAreas.stream()
|
||||
.max((a1, a2) -> a2.getType().ordinal() - a1.getType().ordinal());
|
||||
}
|
||||
|
||||
public Double getSpeedLimitKphAt(GeoPosition position) {
|
||||
return findDominantAreaAt(position)
|
||||
.map(AreaInfo::getSpeedLimitKph)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public boolean isPositionInRestrictedArea(GeoPosition position) {
|
||||
return findAreasContainingPoint(position).stream()
|
||||
.anyMatch(AreaInfo::isRestricted);
|
||||
}
|
||||
|
||||
public boolean isAreaActive(AreaInfo area) {
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
return (area.getActiveTime() == null || !now.isBefore(area.getActiveTime())) &&
|
||||
(area.getExpiryTime() == null || !now.isAfter(area.getExpiryTime()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,361 @@
|
||||
package com.dongni.collisionavoidance.areas.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.AirportArea;
|
||||
import com.dongni.collisionavoidance.common.model.repository.AirportAreaRepository;
|
||||
import com.dongni.collisionavoidance.areas.model.AreaInfo;
|
||||
import com.dongni.collisionavoidance.areas.model.AreaType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 区域配置导入服务 - 将YAML配置文件中的区域数据导入到PostGIS数据库
|
||||
*
|
||||
* 主要功能:
|
||||
* 1. 解析YAML格式的区域配置文件
|
||||
* 2. 转换JTS几何对象为PostGIS兼容格式
|
||||
* 3. 批量导入区域数据到数据库
|
||||
* 4. 处理重复数据和增量更新
|
||||
* 5. 数据验证和异常处理
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AreaConfigImportService {
|
||||
|
||||
private final AirportAreaRepository airportAreaRepository;
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
|
||||
/**
|
||||
* 应用启动后自动导入区域配置
|
||||
* 仅在数据库中没有区域数据时执行导入
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void autoImportOnStartup() {
|
||||
try {
|
||||
long existingCount = airportAreaRepository.count();
|
||||
if (existingCount == 0) {
|
||||
log.info("数据库中未发现区域配置,开始自动导入YAML配置...");
|
||||
importFromYaml("config/airport_areas.yaml");
|
||||
} else {
|
||||
log.info("数据库中已存在 {} 个区域配置,跳过自动导入", existingCount);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("自动导入区域配置失败,请手动检查配置文件", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从YAML文件导入区域配置
|
||||
*
|
||||
* @param yamlFilePath YAML文件路径(相对于classpath)
|
||||
* @return 导入的区域数量
|
||||
*/
|
||||
@Transactional
|
||||
public int importFromYaml(String yamlFilePath) {
|
||||
try {
|
||||
log.info("开始从YAML文件导入区域配置: {}", yamlFilePath);
|
||||
|
||||
// 读取YAML文件
|
||||
ClassPathResource resource = new ClassPathResource(yamlFilePath);
|
||||
if (!resource.exists()) {
|
||||
log.warn("YAML配置文件不存在: {}", yamlFilePath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<AreaInfo> areaInfos = parseYamlFile(resource.getInputStream());
|
||||
if (areaInfos.isEmpty()) {
|
||||
log.warn("YAML文件中未发现有效的区域配置");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 转换并保存到数据库
|
||||
int importedCount = 0;
|
||||
for (AreaInfo areaInfo : areaInfos) {
|
||||
try {
|
||||
AirportArea airportArea = convertToAirportArea(areaInfo);
|
||||
|
||||
// 检查是否已存在
|
||||
if (airportAreaRepository.findByAreaName(airportArea.getName()).isPresent()) {
|
||||
log.debug("区域 {} 已存在,跳过导入", airportArea.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
airportAreaRepository.save(airportArea);
|
||||
importedCount++;
|
||||
log.debug("成功导入区域: {}", airportArea.getName());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("导入区域配置失败: {}", areaInfo.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("区域配置导入完成,成功导入 {} 个区域", importedCount);
|
||||
return importedCount;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("导入YAML区域配置时发生异常", e);
|
||||
throw new RuntimeException("区域配置导入失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制重新导入所有区域配置
|
||||
* 会清空现有数据并重新导入
|
||||
*
|
||||
* @param yamlFilePath YAML文件路径
|
||||
* @return 导入的区域数量
|
||||
*/
|
||||
@Transactional
|
||||
public int forceReimport(String yamlFilePath) {
|
||||
log.info("开始强制重新导入区域配置,将清空现有数据");
|
||||
|
||||
// 清空现有数据
|
||||
long deletedCount = airportAreaRepository.count();
|
||||
airportAreaRepository.deleteAll();
|
||||
log.info("已清空 {} 个现有区域配置", deletedCount);
|
||||
|
||||
// 重新导入
|
||||
return importFromYaml(yamlFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量更新区域配置
|
||||
* 只更新变更的区域,不影响现有数据
|
||||
*
|
||||
* @param yamlFilePath YAML文件路径
|
||||
* @return 更新的区域数量
|
||||
*/
|
||||
@Transactional
|
||||
public int incrementalUpdate(String yamlFilePath) {
|
||||
try {
|
||||
log.info("开始增量更新区域配置");
|
||||
|
||||
ClassPathResource resource = new ClassPathResource(yamlFilePath);
|
||||
if (!resource.exists()) {
|
||||
log.warn("YAML配置文件不存在: {}", yamlFilePath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<AreaInfo> areaInfos = parseYamlFile(resource.getInputStream());
|
||||
int updatedCount = 0;
|
||||
|
||||
for (AreaInfo areaInfo : areaInfos) {
|
||||
try {
|
||||
AirportArea newArea = convertToAirportArea(areaInfo);
|
||||
|
||||
// 检查是否已存在
|
||||
var existingOpt = airportAreaRepository.findByAreaName(newArea.getName());
|
||||
if (existingOpt.isPresent()) {
|
||||
// 更新现有区域
|
||||
AirportArea existing = existingOpt.get();
|
||||
updateExistingArea(existing, newArea);
|
||||
airportAreaRepository.save(existing);
|
||||
updatedCount++;
|
||||
log.debug("更新区域: {} - {}", existing.getName(), existing.getName());
|
||||
} else {
|
||||
// 新增区域
|
||||
airportAreaRepository.save(newArea);
|
||||
updatedCount++;
|
||||
log.debug("新增区域: {} - {}", newArea.getName(), newArea.getName());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("更新区域配置失败: {}", areaInfo.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("增量更新完成,处理 {} 个区域", updatedCount);
|
||||
return updatedCount;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("增量更新区域配置时发生异常", e);
|
||||
throw new RuntimeException("区域配置增量更新失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析YAML文件并转换为AreaInfo列表
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<AreaInfo> parseYamlFile(InputStream inputStream) throws IOException {
|
||||
Yaml yaml = new Yaml();
|
||||
Map<String, Object> data = yaml.load(inputStream);
|
||||
|
||||
// 这里需要根据实际的YAML结构进行解析
|
||||
// 假设YAML结构为: { "areas": [ { area_configs... } ] }
|
||||
Object areasObj = data.get("areas");
|
||||
if (areasObj instanceof List) {
|
||||
List<Map<String, Object>> areasList = (List<Map<String, Object>>) areasObj;
|
||||
return areasList.stream()
|
||||
.map(this::parseAreaFromMap)
|
||||
.filter(area -> area != null)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Map解析单个区域配置
|
||||
*/
|
||||
private AreaInfo parseAreaFromMap(Map<String, Object> areaMap) {
|
||||
try {
|
||||
AreaInfo areaInfo = new AreaInfo();
|
||||
|
||||
// 基本信息
|
||||
areaInfo.setId((String) areaMap.get("id"));
|
||||
areaInfo.setName((String) areaMap.get("name"));
|
||||
areaInfo.setDescription((String) areaMap.get("description"));
|
||||
|
||||
// 区域类型
|
||||
String typeStr = (String) areaMap.get("type");
|
||||
if (typeStr != null) {
|
||||
try {
|
||||
areaInfo.setType(AreaType.valueOf(typeStr.toUpperCase()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("未知的区域类型: {}", typeStr);
|
||||
}
|
||||
}
|
||||
|
||||
// 几何边界(假设是坐标点数组)
|
||||
Object boundaryObj = areaMap.get("boundary");
|
||||
if (boundaryObj instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof List) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<List<Double>> coordinates = (List<List<Double>>) boundaryObj;
|
||||
Polygon polygon = createPolygonFromCoordinates(coordinates);
|
||||
areaInfo.setBoundary(polygon);
|
||||
}
|
||||
|
||||
// 限制信息
|
||||
Object speedLimitObj = areaMap.get("speedLimitKph");
|
||||
if (speedLimitObj instanceof Number) {
|
||||
areaInfo.setSpeedLimitKph(((Number) speedLimitObj).doubleValue());
|
||||
}
|
||||
|
||||
Object restrictedObj = areaMap.get("restricted");
|
||||
if (restrictedObj instanceof Boolean) {
|
||||
areaInfo.setRestricted((Boolean) restrictedObj);
|
||||
}
|
||||
|
||||
return areaInfo;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析区域配置失败: {}", areaMap.get("id"), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从坐标列表创建多边形
|
||||
*/
|
||||
private Polygon createPolygonFromCoordinates(List<List<Double>> coordinates) {
|
||||
if (coordinates == null || coordinates.size() < 3) {
|
||||
throw new IllegalArgumentException("多边形至少需要3个坐标点");
|
||||
}
|
||||
|
||||
// 确保多边形闭合
|
||||
if (!coordinates.get(0).equals(coordinates.get(coordinates.size() - 1))) {
|
||||
coordinates.add(coordinates.get(0));
|
||||
}
|
||||
|
||||
Coordinate[] coords = coordinates.stream()
|
||||
.map(coord -> new Coordinate(coord.get(0), coord.get(1)))
|
||||
.toArray(Coordinate[]::new);
|
||||
|
||||
return geometryFactory.createPolygon(coords);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将AreaInfo转换为AirportArea实体
|
||||
*/
|
||||
private AirportArea convertToAirportArea(AreaInfo areaInfo) {
|
||||
AirportArea airportArea = new AirportArea();
|
||||
|
||||
// 基本信息
|
||||
airportArea.setName(areaInfo.getName());
|
||||
airportArea.setDescription(areaInfo.getDescription());
|
||||
airportArea.setType(convertAreaType(areaInfo.getType()));
|
||||
|
||||
// 几何边界
|
||||
airportArea.setBoundary((Polygon) areaInfo.getBoundary());
|
||||
|
||||
// 限制信息
|
||||
airportArea.setSpeedLimitKph(areaInfo.getSpeedLimitKph());
|
||||
airportArea.setRestricted(areaInfo.getRestricted());
|
||||
|
||||
// 默认值
|
||||
airportArea.setEnabled(true);
|
||||
airportArea.setPriority(1);
|
||||
airportArea.setCreatedAt(LocalDateTime.now());
|
||||
airportArea.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return airportArea;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换区域类型枚举
|
||||
*/
|
||||
private String convertAreaType(AreaType oldType) {
|
||||
if (oldType == null) {
|
||||
return "OTHER";
|
||||
}
|
||||
|
||||
return oldType.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新现有区域的属性
|
||||
*/
|
||||
private void updateExistingArea(AirportArea existing, AirportArea newArea) {
|
||||
existing.setName(newArea.getName());
|
||||
existing.setDescription(newArea.getDescription());
|
||||
existing.setType(newArea.getType());
|
||||
existing.setBoundary(newArea.getBoundary());
|
||||
existing.setSpeedLimitKph(newArea.getSpeedLimitKph());
|
||||
existing.setRestricted(newArea.getRestricted());
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证导入结果
|
||||
*
|
||||
* @return 验证报告
|
||||
*/
|
||||
public String validateImportedData() {
|
||||
StringBuilder report = new StringBuilder();
|
||||
|
||||
long totalCount = airportAreaRepository.count();
|
||||
report.append("数据库中总计区域数量: ").append(totalCount).append("\n");
|
||||
|
||||
// 按类型统计
|
||||
for (AreaType type :
|
||||
AreaType.values()) {
|
||||
long count = airportAreaRepository.countByType(type.name());
|
||||
if (count > 0) {
|
||||
report.append(" ").append(type.name()).append(": ").append(count).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查启用状态
|
||||
long enabledCount = airportAreaRepository.countByEnabled(true);
|
||||
long disabledCount = airportAreaRepository.countByEnabled(false);
|
||||
report.append("启用状态: 启用=").append(enabledCount).append(", 禁用=").append(disabledCount).append("\n");
|
||||
|
||||
return report.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,254 @@
|
||||
package com.dongni.collisionavoidance.common.model.base;
|
||||
|
||||
/**
|
||||
* 缓存常量定义类
|
||||
* 统一管理所有Redis缓存相关的键前缀、过期时间等常量
|
||||
*/
|
||||
public class CacheConstants {
|
||||
|
||||
// ==================== 键前缀定义 ====================
|
||||
|
||||
/**
|
||||
* 车辆位置缓存键前缀
|
||||
*/
|
||||
public static final String VEHICLE_LOCATION_PREFIX = "vehicle:location:";
|
||||
|
||||
/**
|
||||
* 车辆最新位置缓存键前缀
|
||||
*/
|
||||
public static final String VEHICLE_LATEST_PREFIX = "vehicle:latest:";
|
||||
|
||||
/**
|
||||
* 车辆轨迹缓存键前缀
|
||||
*/
|
||||
public static final String VEHICLE_TRAJECTORY_PREFIX = "vehicle:trajectory:";
|
||||
|
||||
/**
|
||||
* 机场区域配置缓存键前缀
|
||||
*/
|
||||
public static final String AIRPORT_AREA_PREFIX = "area:config:";
|
||||
|
||||
/**
|
||||
* 区域类型缓存键前缀
|
||||
*/
|
||||
public static final String AREA_TYPE_PREFIX = "area:type:";
|
||||
|
||||
/**
|
||||
* 空间查询结果缓存键前缀
|
||||
*/
|
||||
public static final String SPATIAL_QUERY_PREFIX = "spatial:query:";
|
||||
|
||||
/**
|
||||
* 包含点查询缓存键前缀
|
||||
*/
|
||||
public static final String POINT_CONTAINS_PREFIX = "spatial:contains:";
|
||||
|
||||
/**
|
||||
* 半径查询缓存键前缀
|
||||
*/
|
||||
public static final String RADIUS_QUERY_PREFIX = "spatial:radius:";
|
||||
|
||||
/**
|
||||
* 车辆统计缓存键前缀
|
||||
*/
|
||||
public static final String VEHICLE_STATS_PREFIX = "stats:vehicle:";
|
||||
|
||||
/**
|
||||
* 区域统计缓存键前缀
|
||||
*/
|
||||
public static final String AREA_STATS_PREFIX = "stats:area:";
|
||||
|
||||
/**
|
||||
* 冲突检测结果缓存键前缀
|
||||
*/
|
||||
public static final String COLLISION_PREFIX = "collision:";
|
||||
|
||||
/**
|
||||
* 地理网格缓存键前缀
|
||||
*/
|
||||
public static final String GEO_GRID_PREFIX = "grid:";
|
||||
|
||||
// ==================== 过期时间定义(秒) ====================
|
||||
|
||||
/**
|
||||
* 车辆位置缓存过期时间:60秒
|
||||
* 车辆位置数据实时性要求高,设置较短过期时间
|
||||
*/
|
||||
public static final long VEHICLE_LOCATION_EXPIRE = 60L;
|
||||
|
||||
/**
|
||||
* 车辆最新位置缓存过期时间:30秒
|
||||
* 最新位置更新频繁,设置更短过期时间
|
||||
*/
|
||||
public static final long VEHICLE_LATEST_EXPIRE = 30L;
|
||||
|
||||
/**
|
||||
* 车辆轨迹缓存过期时间:300秒(5分钟)
|
||||
* 轨迹数据变化相对较慢
|
||||
*/
|
||||
public static final long VEHICLE_TRAJECTORY_EXPIRE = 300L;
|
||||
|
||||
/**
|
||||
* 机场区域配置缓存过期时间:3600秒(1小时)
|
||||
* 区域配置变化很少,可以设置较长过期时间
|
||||
*/
|
||||
public static final long AIRPORT_AREA_EXPIRE = 3600L;
|
||||
|
||||
/**
|
||||
* 空间查询结果缓存过期时间:120秒(2分钟)
|
||||
* 空间查询结果依赖于车辆位置,设置中等过期时间
|
||||
*/
|
||||
public static final long SPATIAL_QUERY_EXPIRE = 120L;
|
||||
|
||||
/**
|
||||
* 点包含查询缓存过期时间:60秒
|
||||
* 点包含查询频繁,但结果相对稳定
|
||||
*/
|
||||
public static final long POINT_CONTAINS_EXPIRE = 60L;
|
||||
|
||||
/**
|
||||
* 半径查询缓存过期时间:90秒
|
||||
* 半径查询结果变化频繁
|
||||
*/
|
||||
public static final long RADIUS_QUERY_EXPIRE = 90L;
|
||||
|
||||
/**
|
||||
* 统计数据缓存过期时间:300秒(5分钟)
|
||||
* 统计数据不需要实时性
|
||||
*/
|
||||
public static final long STATS_EXPIRE = 300L;
|
||||
|
||||
/**
|
||||
* 冲突检测结果缓存过期时间:30秒
|
||||
* 冲突检测结果需要较高实时性
|
||||
*/
|
||||
public static final long COLLISION_EXPIRE = 30L;
|
||||
|
||||
/**
|
||||
* 地理网格缓存过期时间:600秒(10分钟)
|
||||
* 地理网格相对稳定
|
||||
*/
|
||||
public static final long GEO_GRID_EXPIRE = 600L;
|
||||
|
||||
// ==================== 缓存策略配置 ====================
|
||||
|
||||
/**
|
||||
* 最大缓存条目数量
|
||||
*/
|
||||
public static final int MAX_CACHE_ENTRIES = 10000;
|
||||
|
||||
/**
|
||||
* 批量操作的批次大小
|
||||
*/
|
||||
public static final int BATCH_SIZE = 100;
|
||||
|
||||
/**
|
||||
* 地理网格精度(小数位数)
|
||||
* 用于地理坐标的网格化缓存
|
||||
*/
|
||||
public static final int GEO_GRID_PRECISION = 4;
|
||||
|
||||
/**
|
||||
* 缓存预热开关
|
||||
* 是否在应用启动时预热缓存
|
||||
*/
|
||||
public static final boolean ENABLE_CACHE_WARMUP = true;
|
||||
|
||||
/**
|
||||
* 缓存穿透保护开关
|
||||
* 是否启用缓存穿透保护机制
|
||||
*/
|
||||
public static final boolean ENABLE_PENETRATION_PROTECTION = true;
|
||||
|
||||
/**
|
||||
* 空值缓存时间:30秒
|
||||
* 用于缓存穿透保护
|
||||
*/
|
||||
public static final long NULL_VALUE_EXPIRE = 30L;
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 构建车辆位置缓存键
|
||||
*/
|
||||
public static String buildVehicleLocationKey(String vehicleId) {
|
||||
return VEHICLE_LOCATION_PREFIX + vehicleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建车辆最新位置缓存键
|
||||
*/
|
||||
public static String buildVehicleLatestKey(String vehicleId) {
|
||||
return VEHICLE_LATEST_PREFIX + vehicleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建车辆轨迹缓存键
|
||||
*/
|
||||
public static String buildVehicleTrajectoryKey(String vehicleId, long startTime, long endTime) {
|
||||
return VEHICLE_TRAJECTORY_PREFIX + vehicleId + ":" + startTime + ":" + endTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建机场区域缓存键
|
||||
*/
|
||||
public static String buildAirportAreaKey(String areaName) {
|
||||
return AIRPORT_AREA_PREFIX + areaName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建区域类型缓存键
|
||||
*/
|
||||
public static String buildAreaTypeKey(String areaType) {
|
||||
return AREA_TYPE_PREFIX + areaType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建点包含查询缓存键
|
||||
*/
|
||||
public static String buildPointContainsKey(double longitude, double latitude) {
|
||||
// 使用固定精度避免浮点数精度问题
|
||||
String lon = String.format("%.4f", longitude);
|
||||
String lat = String.format("%.4f", latitude);
|
||||
return POINT_CONTAINS_PREFIX + lon + ":" + lat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建半径查询缓存键
|
||||
*/
|
||||
public static String buildRadiusQueryKey(double longitude, double latitude, double radius) {
|
||||
String lon = String.format("%.4f", longitude);
|
||||
String lat = String.format("%.4f", latitude);
|
||||
String rad = String.format("%.1f", radius);
|
||||
return RADIUS_QUERY_PREFIX + lon + ":" + lat + ":" + rad;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建地理网格缓存键
|
||||
*/
|
||||
public static String buildGeoGridKey(double longitude, double latitude) {
|
||||
// 将坐标转换为网格坐标
|
||||
int gridLon = (int) (longitude * Math.pow(10, GEO_GRID_PRECISION));
|
||||
int gridLat = (int) (latitude * Math.pow(10, GEO_GRID_PRECISION));
|
||||
return GEO_GRID_PREFIX + gridLon + ":" + gridLat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建冲突检测缓存键
|
||||
*/
|
||||
public static String buildCollisionKey(String vehicleId1, String vehicleId2) {
|
||||
// 确保键的唯一性,较小的vehicleId在前
|
||||
if (vehicleId1.compareTo(vehicleId2) <= 0) {
|
||||
return COLLISION_PREFIX + vehicleId1 + ":" + vehicleId2;
|
||||
} else {
|
||||
return COLLISION_PREFIX + vehicleId2 + ":" + vehicleId1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有构造函数,防止实例化
|
||||
*/
|
||||
private CacheConstants() {
|
||||
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,186 @@
|
||||
package com.dongni.collisionavoidance.common.model.repository;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.AirportArea;
|
||||
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 机场区域Repository接口 - 基于PostGIS空间查询
|
||||
* 提供机场区域的CRUD操作和空间查询功能
|
||||
*/
|
||||
@Repository
|
||||
public interface AirportAreaRepository extends JpaRepository<AirportArea, Long> {
|
||||
|
||||
/**
|
||||
* 根据区域ID查找
|
||||
*/
|
||||
Optional<AirportArea> findByAreaId(@Param("areaId") String areaId);
|
||||
|
||||
/**
|
||||
* 根据区域名称查找
|
||||
*/
|
||||
Optional<AirportArea> findByAreaName(@Param("areaName") String areaName);
|
||||
|
||||
/**
|
||||
* 根据区域类型查找所有活跃区域
|
||||
*/
|
||||
@Query("SELECT aa FROM AirportArea aa WHERE aa.type = :areaType AND aa.enabled = true " +
|
||||
"ORDER BY aa.priority DESC")
|
||||
List<AirportArea> findActiveAreasByType(@Param("areaType") String areaType);
|
||||
|
||||
/**
|
||||
* 空间查询:查找包含指定点的所有区域
|
||||
* 使用PostGIS ST_Contains函数
|
||||
*/
|
||||
@Query(value = "SELECT * FROM airport_areas aa " +
|
||||
"WHERE ST_Contains(aa.geometry, :point) " +
|
||||
"AND aa.is_active = true " +
|
||||
"ORDER BY aa.priority DESC",
|
||||
nativeQuery = true)
|
||||
List<AirportArea> findAreasContainingPoint(@Param("point") Point point);
|
||||
|
||||
/**
|
||||
* 空间查询:查找与指定几何形状相交的区域
|
||||
* 使用PostGIS ST_Intersects函数
|
||||
*/
|
||||
@Query(value = "SELECT * FROM airport_areas aa " +
|
||||
"WHERE ST_Intersects(aa.geometry, :geometry) " +
|
||||
"AND aa.is_active = true " +
|
||||
"ORDER BY aa.priority DESC",
|
||||
nativeQuery = true)
|
||||
List<AirportArea> findAreasIntersectingGeometry(@Param("geometry") Geometry geometry);
|
||||
|
||||
/**
|
||||
* 空间查询:查找指定距离范围内的区域
|
||||
* 使用PostGIS ST_DWithin函数
|
||||
*/
|
||||
@Query(value = "SELECT * FROM airport_areas aa " +
|
||||
"WHERE ST_DWithin(aa.geometry, :centerPoint, :radiusMeters) " +
|
||||
"AND aa.is_active = true " +
|
||||
"ORDER BY ST_Distance(aa.geometry, :centerPoint)",
|
||||
nativeQuery = true)
|
||||
List<AirportArea> findAreasWithinRadius(@Param("centerPoint") Point centerPoint,
|
||||
@Param("radiusMeters") double radiusMeters);
|
||||
|
||||
/**
|
||||
* 空间查询:查找与指定区域重叠的其他区域
|
||||
* 用于检测区域冲突
|
||||
*/
|
||||
@Query(value = "SELECT * FROM airport_areas aa " +
|
||||
"WHERE aa.id != :excludeId " +
|
||||
"AND ST_Overlaps(aa.geometry, :geometry) " +
|
||||
"AND aa.is_active = true",
|
||||
nativeQuery = true)
|
||||
List<AirportArea> findOverlappingAreas(@Param("geometry") Geometry geometry,
|
||||
@Param("excludeId") Long excludeId);
|
||||
|
||||
/**
|
||||
* 根据优先级范围查找区域
|
||||
*/
|
||||
@Query("SELECT aa FROM AirportArea aa WHERE aa.priority BETWEEN :minPriority AND :maxPriority " +
|
||||
"AND aa.enabled = true ORDER BY aa.priority DESC")
|
||||
List<AirportArea> findAreasByPriorityRange(@Param("minPriority") Integer minPriority,
|
||||
@Param("maxPriority") Integer maxPriority);
|
||||
|
||||
/**
|
||||
* 查找包含特定限制类型的区域
|
||||
* 使用JSONB查询功能
|
||||
*/
|
||||
@Query(value = "SELECT * FROM airport_areas aa " +
|
||||
"WHERE aa.restrictions ? :restrictionType " +
|
||||
"AND aa.is_active = true " +
|
||||
"ORDER BY aa.priority DESC",
|
||||
nativeQuery = true)
|
||||
List<AirportArea> findAreasByRestrictionType(@Param("restrictionType") String restrictionType);
|
||||
|
||||
/**
|
||||
* 空间聚合查询:计算区域面积
|
||||
* 使用PostGIS ST_Area函数
|
||||
*/
|
||||
@Query(value = "SELECT aa.*, ST_Area(aa.geometry) as area " +
|
||||
"FROM airport_areas aa " +
|
||||
"WHERE aa.area_type = :areaType " +
|
||||
"AND aa.is_active = true " +
|
||||
"ORDER BY ST_Area(aa.geometry) DESC",
|
||||
nativeQuery = true)
|
||||
List<Object[]> findAreasByTypeWithSize(@Param("areaType") String areaType);
|
||||
|
||||
/**
|
||||
* 查找最高优先级的包含指定点的区域
|
||||
* 处理重叠区域时使用
|
||||
*/
|
||||
@Query(value = "SELECT * FROM airport_areas aa " +
|
||||
"WHERE ST_Contains(aa.geometry, :point) " +
|
||||
"AND aa.is_active = true " +
|
||||
"ORDER BY aa.priority DESC " +
|
||||
"LIMIT 1",
|
||||
nativeQuery = true)
|
||||
Optional<AirportArea> findHighestPriorityAreaContainingPoint(@Param("point") Point point);
|
||||
|
||||
/**
|
||||
* 空间查询:获取区域的边界框
|
||||
* 使用PostGIS ST_Envelope函数
|
||||
*/
|
||||
@Query(value = "SELECT ST_Envelope(aa.geometry) " +
|
||||
"FROM airport_areas aa " +
|
||||
"WHERE aa.id = :areaId",
|
||||
nativeQuery = true)
|
||||
Optional<Geometry> getAreaBoundingBox(@Param("areaId") Long areaId);
|
||||
|
||||
/**
|
||||
* 查找与车辆轨迹相交的所有区域
|
||||
* 用于轨迹分析
|
||||
*/
|
||||
@Query(value = "SELECT aa.* FROM airport_areas aa " +
|
||||
"WHERE ST_Intersects(aa.geometry, ST_GeomFromText(:trajectoryWkt, 4326)) " +
|
||||
"AND aa.is_active = true " +
|
||||
"ORDER BY aa.priority DESC",
|
||||
nativeQuery = true)
|
||||
List<AirportArea> findAreasIntersectingTrajectory(@Param("trajectoryWkt") String trajectoryWkt);
|
||||
|
||||
/**
|
||||
* 统计活跃区域数量(按类型分组)
|
||||
*/
|
||||
@Query("SELECT aa.type, COUNT(aa) FROM AirportArea aa " +
|
||||
"WHERE aa.enabled = true GROUP BY aa.type")
|
||||
List<Object[]> countActiveAreasByType();
|
||||
|
||||
/**
|
||||
* 查找指定版本的区域配置
|
||||
* 用于配置版本管理
|
||||
*/
|
||||
@Query("SELECT aa FROM AirportArea aa WHERE aa.version = :version " +
|
||||
"ORDER BY aa.priority DESC")
|
||||
List<AirportArea> findAreasByVersion(@Param("version") Long version);
|
||||
|
||||
/**
|
||||
* 空间查询:查找距离指定点最近的N个区域
|
||||
*/
|
||||
@Query(value = "SELECT * FROM airport_areas aa " +
|
||||
"WHERE aa.is_active = true " +
|
||||
"ORDER BY ST_Distance(aa.geometry, :referencePoint) " +
|
||||
"LIMIT :limit",
|
||||
nativeQuery = true)
|
||||
List<AirportArea> findNearestAreas(@Param("referencePoint") Point referencePoint,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 根据区域类型统计数量
|
||||
*/
|
||||
@Query("SELECT COUNT(aa) FROM AirportArea aa WHERE aa.type = :type")
|
||||
long countByType(@Param("type") String type);
|
||||
|
||||
/**
|
||||
* 根据启用状态统计数量
|
||||
*/
|
||||
@Query("SELECT COUNT(aa) FROM AirportArea aa WHERE aa.enabled = :enabled")
|
||||
long countByEnabled(@Param("enabled") boolean enabled);
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
package com.dongni.collisionavoidance.common.model.repository;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.MovingObject;
|
||||
import com.dongni.collisionavoidance.common.model.MovingObjectType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
public class MovingObjectRepository {
|
||||
// 使用嵌套Map存储不同类型对象 Key: 类型枚举值 Value: 对应类型对象Map
|
||||
private final ConcurrentHashMap<MovingObjectType, ConcurrentHashMap<String, MovingObject>> storage =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 线程安全队列(存储增量更新通知)
|
||||
private final BlockingQueue<Map<MovingObjectType, Set<String>>> updateQueue =
|
||||
new LinkedBlockingQueue<>(100);
|
||||
|
||||
public MovingObjectRepository() {
|
||||
// 初始化存储结构
|
||||
Arrays.stream(MovingObjectType.values()).forEach(type ->
|
||||
storage.put(type, new ConcurrentHashMap<>()));
|
||||
}
|
||||
|
||||
// 原子更新方法(保留单个更新)
|
||||
public void updateObject(MovingObjectType type, String id, MovingObject obj) {
|
||||
ConcurrentHashMap<String, MovingObject> typeMap = storage.get(type);
|
||||
if (typeMap != null) {
|
||||
typeMap.put(id, obj);
|
||||
// 移除单条更新时的队列推送
|
||||
}
|
||||
}
|
||||
|
||||
// 新增批量提交方法
|
||||
public void commitBatchUpdate(MovingObjectType type) {
|
||||
ConcurrentHashMap<String, MovingObject> typeMap = storage.get(type);
|
||||
if (typeMap != null && !typeMap.isEmpty()) {
|
||||
// 发送全量ID集合
|
||||
Map<MovingObjectType, Set<String>> delta = new HashMap<>();
|
||||
delta.put(type, ConcurrentHashMap.newKeySet());
|
||||
delta.get(type).addAll(typeMap.keySet());
|
||||
updateQueue.offer(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新方法(原子操作)
|
||||
public void batchUpdate(MovingObjectType type, Map<String, MovingObject> batchData) {
|
||||
ConcurrentHashMap<String, MovingObject> typeMap = storage.get(type);
|
||||
if (typeMap != null) {
|
||||
// 使用putAll原子操作
|
||||
typeMap.putAll(batchData);
|
||||
|
||||
// 发送批量更新通知
|
||||
Map<MovingObjectType, Set<String>> delta = new HashMap<>();
|
||||
delta.put(type, batchData.keySet());
|
||||
updateQueue.offer(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取类型快照(线程安全)
|
||||
public Map<String, MovingObject> getSnapshot(MovingObjectType type) {
|
||||
return new ConcurrentHashMap<>(storage.getOrDefault(type, new ConcurrentHashMap<>()));
|
||||
}
|
||||
|
||||
// 阻塞获取更新通知
|
||||
public Map<MovingObjectType, Set<String>> takeUpdate() throws InterruptedException {
|
||||
return updateQueue.take();
|
||||
}
|
||||
|
||||
// 非阻塞获取更新通知,带超时
|
||||
public Map<MovingObjectType, Set<String>> pollUpdate(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
return updateQueue.poll(timeout, unit);
|
||||
}
|
||||
|
||||
// 批量更新整个类型集合
|
||||
public void updateTypeAll(MovingObjectType type, Map<String, MovingObject> newObjects) {
|
||||
ConcurrentHashMap<String, MovingObject> typeMap = storage.get(type);
|
||||
if (typeMap != null) {
|
||||
typeMap.clear();
|
||||
typeMap.putAll(newObjects);
|
||||
|
||||
// 发送全量更新通知
|
||||
Map<MovingObjectType, Set<String>> delta = new HashMap<>();
|
||||
delta.put(type, new HashSet<>(newObjects.keySet()));
|
||||
updateQueue.offer(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取整个类型集合的引用(注意:直接操作需自行保证线程安全)
|
||||
public ConcurrentHashMap<String, MovingObject> getTypeMapDirect(MovingObjectType type) {
|
||||
return storage.get(type);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
package com.dongni.collisionavoidance.common.model.repository;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.VehicleLocation;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 车辆位置Repository接口 - 基于PostGIS空间查询
|
||||
* 提供车辆位置的CRUD操作和空间查询功能
|
||||
*/
|
||||
@Repository
|
||||
public interface VehicleLocationRepository extends JpaRepository<VehicleLocation, Long> {
|
||||
|
||||
/**
|
||||
* 根据车辆ID查找最新位置记录
|
||||
*/
|
||||
@Query("SELECT vl FROM VehicleLocation vl WHERE vl.vehicleId = :vehicleId " +
|
||||
"ORDER BY vl.timestamp DESC")
|
||||
Optional<VehicleLocation> findLatestByVehicleId(@Param("vehicleId") String vehicleId);
|
||||
|
||||
/**
|
||||
* 根据车辆类型查找活跃车辆
|
||||
*/
|
||||
@Query("SELECT vl FROM VehicleLocation vl WHERE vl.vehicleType = :vehicleType " +
|
||||
"AND vl.timestamp >= :since ORDER BY vl.timestamp DESC")
|
||||
List<VehicleLocation> findActiveByVehicleType(@Param("vehicleType") String vehicleType,
|
||||
@Param("since") LocalDateTime since);
|
||||
|
||||
/**
|
||||
* 空间查询:查找指定距离范围内的车辆
|
||||
* 使用PostGIS ST_DWithin函数进行空间距离查询
|
||||
*/
|
||||
@Query(value = "SELECT * FROM vehicle_locations vl " +
|
||||
"WHERE ST_DWithin(vl.location, :centerPoint, :radiusMeters) " +
|
||||
"AND vl.timestamp >= :since " +
|
||||
"ORDER BY ST_Distance(vl.location, :centerPoint)",
|
||||
nativeQuery = true)
|
||||
List<VehicleLocation> findVehiclesWithinRadius(@Param("centerPoint") Point centerPoint,
|
||||
@Param("radiusMeters") double radiusMeters,
|
||||
@Param("since") LocalDateTime since);
|
||||
|
||||
/**
|
||||
* 空间查询:查找指定区域内的车辆
|
||||
* 使用PostGIS ST_Contains函数检查点是否在多边形内
|
||||
*/
|
||||
@Query(value = "SELECT * FROM vehicle_locations vl " +
|
||||
"WHERE ST_Contains(ST_GeomFromText(:areaWkt, 4326), vl.location) " +
|
||||
"AND vl.timestamp >= :since " +
|
||||
"ORDER BY vl.timestamp DESC",
|
||||
nativeQuery = true)
|
||||
List<VehicleLocation> findVehiclesInArea(@Param("areaWkt") String areaWkt,
|
||||
@Param("since") LocalDateTime since);
|
||||
|
||||
/**
|
||||
* 根据车辆ID和时间范围查询轨迹数据
|
||||
*/
|
||||
@Query("SELECT vl FROM VehicleLocation vl WHERE vl.vehicleId = :vehicleId " +
|
||||
"AND vl.timestamp BETWEEN :startTime AND :endTime " +
|
||||
"ORDER BY vl.timestamp")
|
||||
List<VehicleLocation> findVehicleTrajectory(@Param("vehicleId") String vehicleId,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 查找指定高度范围内的车辆
|
||||
*/
|
||||
@Query("SELECT vl FROM VehicleLocation vl WHERE vl.altitude BETWEEN :minAltitude AND :maxAltitude " +
|
||||
"AND vl.timestamp >= :since ORDER BY vl.altitude")
|
||||
List<VehicleLocation> findVehiclesByAltitudeRange(@Param("minAltitude") Double minAltitude,
|
||||
@Param("maxAltitude") Double maxAltitude,
|
||||
@Param("since") LocalDateTime since);
|
||||
|
||||
/**
|
||||
* 查找移动速度超过阈值的车辆
|
||||
*/
|
||||
@Query("SELECT vl FROM VehicleLocation vl WHERE vl.speed > :speedThreshold " +
|
||||
"AND vl.timestamp >= :since ORDER BY vl.speed DESC")
|
||||
List<VehicleLocation> findHighSpeedVehicles(@Param("speedThreshold") Double speedThreshold,
|
||||
@Param("since") LocalDateTime since);
|
||||
|
||||
/**
|
||||
* 删除指定时间之前的历史数据
|
||||
*/
|
||||
@Query("DELETE FROM VehicleLocation vl WHERE vl.timestamp < :beforeTime")
|
||||
int deleteHistoricalData(@Param("beforeTime") LocalDateTime beforeTime);
|
||||
|
||||
/**
|
||||
* 统计指定时间段内的车辆数量
|
||||
*/
|
||||
@Query("SELECT COUNT(DISTINCT vl.vehicleId) FROM VehicleLocation vl " +
|
||||
"WHERE vl.timestamp BETWEEN :startTime AND :endTime")
|
||||
long countUniqueVehiclesInTimeRange(@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 空间聚合查询:查找与指定点最近的N个车辆
|
||||
*/
|
||||
@Query(value = "SELECT * FROM vehicle_locations vl " +
|
||||
"WHERE vl.timestamp >= :since " +
|
||||
"ORDER BY ST_Distance(vl.location, :referencePoint) " +
|
||||
"LIMIT :limit",
|
||||
nativeQuery = true)
|
||||
List<VehicleLocation> findNearestVehicles(@Param("referencePoint") Point referencePoint,
|
||||
@Param("since") LocalDateTime since,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
package com.dongni.collisionavoidance.common.model.spatial;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
/**
|
||||
* 机场区域实体类 - 基于PostGIS存储
|
||||
* 支持多种几何类型:POLYGON、MULTIPOLYGON等
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "airport_areas", indexes = {
|
||||
@Index(name = "idx_airport_area_type", columnList = "type"),
|
||||
@Index(name = "idx_airport_area_area_id", columnList = "areaId"),
|
||||
@Index(name = "idx_airport_area_enabled", columnList = "enabled"),
|
||||
@Index(name = "idx_airport_area_geom", columnList = "boundary") // PostGIS空间索引
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class AirportArea {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 区域标识(对应原YAML中的id)
|
||||
*/
|
||||
@Column(name = "area_id", nullable = false, unique = true, length = 50)
|
||||
private String areaId;
|
||||
|
||||
/**
|
||||
* 区域名称
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 100)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 区域类型:RUNWAY(跑道)、TAXIWAY(滑行道)、APRON(机坪)、
|
||||
* TERMINAL(航站楼)、RESTRICTED(限制区)等
|
||||
*/
|
||||
@Column(name = "type", nullable = false, length = 30)
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 区域几何边界 - PostGIS Polygon类型
|
||||
* SRID=4326 (WGS84坐标系统)
|
||||
*/
|
||||
@Column(name = "boundary", nullable = false, columnDefinition = "geometry(POLYGON,4326)")
|
||||
private Polygon boundary;
|
||||
|
||||
/**
|
||||
* 限速(公里/小时)
|
||||
*/
|
||||
@Column(name = "speed_limit_kph")
|
||||
private Double speedLimitKph;
|
||||
|
||||
/**
|
||||
* 区域描述
|
||||
*/
|
||||
@Column(name = "description", length = 500)
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 是否限制区域
|
||||
*/
|
||||
@Column(name = "restricted")
|
||||
private Boolean restricted = false;
|
||||
|
||||
/**
|
||||
* 允许的车辆类型(JSON数组格式)
|
||||
*/
|
||||
@Column(name = "allowed_vehicle_types", columnDefinition = "jsonb")
|
||||
private String allowedVehicleTypes;
|
||||
|
||||
/**
|
||||
* 允许的航空器类型(JSON数组格式)
|
||||
*/
|
||||
@Column(name = "allowed_aircraft_types", columnDefinition = "jsonb")
|
||||
private String allowedAircraftTypes;
|
||||
|
||||
/**
|
||||
* 最大高度限制(米)
|
||||
*/
|
||||
@Column(name = "max_height")
|
||||
private Double maxHeight;
|
||||
|
||||
/**
|
||||
* 最大重量限制(吨)
|
||||
*/
|
||||
@Column(name = "max_weight")
|
||||
private Double maxWeight;
|
||||
|
||||
/**
|
||||
* 生效时间(用于临时区域)
|
||||
*/
|
||||
@Column(name = "active_time")
|
||||
private ZonedDateTime activeTime;
|
||||
|
||||
/**
|
||||
* 失效时间(用于临时区域)
|
||||
*/
|
||||
@Column(name = "expiry_time")
|
||||
private ZonedDateTime expiryTime;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
@Column(name = "enabled")
|
||||
private Boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 优先级(数值越高优先级越高)
|
||||
* 用于重叠区域的规则处理
|
||||
*/
|
||||
@Column(name = "priority")
|
||||
private Integer priority = 1;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 数据版本号(用于乐观锁)
|
||||
*/
|
||||
@Version
|
||||
@Column(name = "version")
|
||||
private Long version;
|
||||
|
||||
/**
|
||||
* 获取几何体的中心点(用于快速距离计算)
|
||||
*/
|
||||
public org.locationtech.jts.geom.Point getCentroid() {
|
||||
return boundary != null ? boundary.getCentroid() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取区域面积(平方米)
|
||||
*/
|
||||
public Double getArea() {
|
||||
return boundary != null ? boundary.getArea() : null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
package com.dongni.collisionavoidance.common.model.spatial;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* PostGIS空间数据示例模型
|
||||
*
|
||||
* 演示如何在JPA实体中使用PostGIS空间数据类型:
|
||||
* - Point: 表示位置点(车辆、航空器位置)
|
||||
* - Polygon: 表示区域(机场区域、电子围栏)
|
||||
*
|
||||
* 支持的空间查询示例:
|
||||
* - ST_Contains: 检查点是否在多边形内
|
||||
* - ST_DWithin: 检查两点间距离是否小于指定值
|
||||
* - ST_Distance: 计算两点间距离
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "spatial_example")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SpatialExample {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 位置点 - 使用PostGIS POINT类型
|
||||
* SRID 4326表示WGS84坐标系统(GPS坐标)
|
||||
*/
|
||||
@Column(name = "location", columnDefinition = "geometry(Point,4326)")
|
||||
private Point location;
|
||||
|
||||
/**
|
||||
* 区域边界 - 使用PostGIS POLYGON类型
|
||||
* 可用于表示机场区域、电子围栏等
|
||||
*/
|
||||
@Column(name = "boundary", columnDefinition = "geometry(Polygon,4326)")
|
||||
private Polygon boundary;
|
||||
|
||||
/**
|
||||
* 描述信息
|
||||
*/
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 预设置创建时间
|
||||
*/
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
PostGIS查询示例(原生SQL):
|
||||
|
||||
-- 1. 查找在指定区域内的所有点
|
||||
SELECT * FROM spatial_example
|
||||
WHERE ST_Contains(boundary, location);
|
||||
|
||||
-- 2. 查找距离指定点100米内的所有记录
|
||||
SELECT *, ST_Distance(location, ST_SetSRID(ST_MakePoint(120.0834, 36.3541), 4326)) as distance
|
||||
FROM spatial_example
|
||||
WHERE ST_DWithin(
|
||||
location,
|
||||
ST_SetSRID(ST_MakePoint(120.0834, 36.3541), 4326),
|
||||
0.001 -- 约100米(度数)
|
||||
);
|
||||
|
||||
-- 3. 创建空间索引(提升查询性能)
|
||||
CREATE INDEX idx_spatial_example_location ON spatial_example USING GIST (location);
|
||||
CREATE INDEX idx_spatial_example_boundary ON spatial_example USING GIST (boundary);
|
||||
*/
|
||||
@ -0,0 +1,108 @@
|
||||
package com.dongni.collisionavoidance.common.model.spatial;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.MovingObjectType;
|
||||
import com.dongni.collisionavoidance.common.model.MovementState;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 车辆位置PostGIS实体类
|
||||
*
|
||||
* 用于存储车辆的实时位置信息,替代原有的内存存储模式
|
||||
* 支持PostGIS空间查询和索引优化
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "vehicle_locations")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class VehicleLocation {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 车辆标识符(车牌号、航班号等)
|
||||
*/
|
||||
@Column(name = "vehicle_id", nullable = false, length = 50)
|
||||
private String vehicleId;
|
||||
|
||||
/**
|
||||
* 车辆类型枚举
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "vehicle_type", nullable = false, length = 20)
|
||||
private MovingObjectType vehicleType;
|
||||
|
||||
/**
|
||||
* 位置点 - 使用PostGIS POINT类型
|
||||
* SRID 4326表示WGS84坐标系统(GPS坐标)
|
||||
*/
|
||||
@Column(name = "location", nullable = false, columnDefinition = "geometry(Point,4326)")
|
||||
private Point location;
|
||||
|
||||
/**
|
||||
* 高度信息(米)
|
||||
*/
|
||||
@Column(name = "altitude")
|
||||
private Double altitude;
|
||||
|
||||
/**
|
||||
* 航向角(度,0-360)
|
||||
*/
|
||||
@Column(name = "heading")
|
||||
private Double heading;
|
||||
|
||||
/**
|
||||
* 速度(米/秒)
|
||||
*/
|
||||
@Column(name = "speed")
|
||||
private Double speed;
|
||||
|
||||
/**
|
||||
* 数据时间戳
|
||||
*/
|
||||
@Column(name = "timestamp", nullable = false)
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
/**
|
||||
* 数据质量枚举
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "data_quality", length = 20)
|
||||
private MovementState.DataQuality dataQuality;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 预设置创建和更新时间
|
||||
*/
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,415 @@
|
||||
package com.dongni.collisionavoidance.common.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.AirportArea;
|
||||
import com.dongni.collisionavoidance.common.model.base.CacheConstants;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 机场区域缓存服务类
|
||||
* 基于Redis实现机场区域配置和空间查询结果的高性能缓存
|
||||
*
|
||||
* 主要功能:
|
||||
* 1. 区域配置数据缓存
|
||||
* 2. 空间查询结果缓存
|
||||
* 3. 区域类型索引缓存
|
||||
* 4. 缓存失效和刷新策略
|
||||
* 5. 缓存预热功能
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AirportAreaCacheService {
|
||||
|
||||
private final RedisTemplate<String, AirportArea> airportAreaRedisTemplate;
|
||||
private final RedisTemplate<String, Object> objectRedisTemplate;
|
||||
private final RedisTemplate<String, String> stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 缓存机场区域配置
|
||||
*
|
||||
* @param airportArea 机场区域配置
|
||||
*/
|
||||
public void cacheArea(AirportArea airportArea) {
|
||||
try {
|
||||
// 按名称缓存
|
||||
String nameKey = CacheConstants.buildAirportAreaKey(airportArea.getName());
|
||||
airportAreaRedisTemplate.opsForValue()
|
||||
.set(nameKey, airportArea, CacheConstants.AIRPORT_AREA_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
// 按ID缓存(如果有areaId)
|
||||
if (airportArea.getAreaId() != null) {
|
||||
String idKey = CacheConstants.buildAirportAreaKey(airportArea.getAreaId());
|
||||
airportAreaRedisTemplate.opsForValue()
|
||||
.set(idKey, airportArea, CacheConstants.AIRPORT_AREA_EXPIRE, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// 更新类型索引
|
||||
updateTypeIndex(airportArea);
|
||||
|
||||
log.debug("缓存机场区域配置: name={}, type={}", airportArea.getName(), airportArea.getType());
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存机场区域配置失败: name={}", airportArea.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量缓存机场区域配置
|
||||
*
|
||||
* @param airportAreas 机场区域配置列表
|
||||
*/
|
||||
public void batchCacheAreas(List<AirportArea> airportAreas) {
|
||||
if (airportAreas == null || airportAreas.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, AirportArea> cacheMap = new HashMap<>();
|
||||
|
||||
for (AirportArea area : airportAreas) {
|
||||
// 按名称缓存
|
||||
String nameKey = CacheConstants.buildAirportAreaKey(area.getName());
|
||||
cacheMap.put(nameKey, area);
|
||||
|
||||
// 按ID缓存(如果有areaId)
|
||||
if (area.getAreaId() != null) {
|
||||
String idKey = CacheConstants.buildAirportAreaKey(area.getAreaId());
|
||||
cacheMap.put(idKey, area);
|
||||
}
|
||||
|
||||
// 更新类型索引
|
||||
updateTypeIndex(area);
|
||||
}
|
||||
|
||||
airportAreaRedisTemplate.opsForValue().multiSet(cacheMap);
|
||||
|
||||
// 批量设置过期时间
|
||||
for (String key : cacheMap.keySet()) {
|
||||
airportAreaRedisTemplate.expire(key, CacheConstants.AIRPORT_AREA_EXPIRE, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
log.debug("批量缓存机场区域配置: 数量={}", airportAreas.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("批量缓存机场区域配置失败: 数量={}", airportAreas.size(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机场区域配置
|
||||
*
|
||||
* @param areaNameOrId 区域名称或ID
|
||||
* @return 机场区域配置,不存在则返回null
|
||||
*/
|
||||
public AirportArea getArea(String areaNameOrId) {
|
||||
try {
|
||||
String key = CacheConstants.buildAirportAreaKey(areaNameOrId);
|
||||
AirportArea area = airportAreaRedisTemplate.opsForValue().get(key);
|
||||
|
||||
log.debug("获取机场区域配置缓存: nameOrId={}, found={}", areaNameOrId, area != null);
|
||||
return area;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取机场区域配置缓存失败: nameOrId={}", areaNameOrId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型获取区域列表
|
||||
*
|
||||
* @param areaType 区域类型
|
||||
* @return 区域列表
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<AirportArea> getAreasByType(String areaType) {
|
||||
try {
|
||||
String typeKey = CacheConstants.buildAreaTypeKey(areaType);
|
||||
Object result = objectRedisTemplate.opsForValue().get(typeKey);
|
||||
|
||||
if (result instanceof List) {
|
||||
List<AirportArea> areas = (List<AirportArea>) result;
|
||||
log.debug("获取区域类型缓存: type={}, 数量={}", areaType, areas.size());
|
||||
return areas;
|
||||
}
|
||||
|
||||
log.debug("区域类型缓存未命中: type={}", areaType);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取区域类型缓存失败: type={}", areaType, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存按类型分组的区域列表
|
||||
*
|
||||
* @param areaType 区域类型
|
||||
* @param areas 区域列表
|
||||
*/
|
||||
public void cacheAreasByType(String areaType, List<AirportArea> areas) {
|
||||
try {
|
||||
String typeKey = CacheConstants.buildAreaTypeKey(areaType);
|
||||
objectRedisTemplate.opsForValue()
|
||||
.set(typeKey, areas, CacheConstants.AIRPORT_AREA_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("缓存区域类型: type={}, 数量={}", areaType, areas.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存区域类型失败: type={}", areaType, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存点包含查询结果
|
||||
*
|
||||
* @param longitude 经度
|
||||
* @param latitude 纬度
|
||||
* @param containingAreas 包含该点的区域列表
|
||||
*/
|
||||
public void cachePointContainsResult(double longitude, double latitude, List<AirportArea> containingAreas) {
|
||||
try {
|
||||
String key = CacheConstants.buildPointContainsKey(longitude, latitude);
|
||||
objectRedisTemplate.opsForValue()
|
||||
.set(key, containingAreas, CacheConstants.POINT_CONTAINS_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("缓存点包含查询结果: point=[{}, {}], 区域数量={}", longitude, latitude, containingAreas.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存点包含查询结果失败: point=[{}, {}]", longitude, latitude, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取点包含查询结果
|
||||
*
|
||||
* @param longitude 经度
|
||||
* @param latitude 纬度
|
||||
* @return 包含该点的区域列表,缓存未命中则返回null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<AirportArea> getPointContainsResult(double longitude, double latitude) {
|
||||
try {
|
||||
String key = CacheConstants.buildPointContainsKey(longitude, latitude);
|
||||
Object result = objectRedisTemplate.opsForValue().get(key);
|
||||
|
||||
if (result instanceof List) {
|
||||
List<AirportArea> areas = (List<AirportArea>) result;
|
||||
log.debug("获取点包含查询缓存: point=[{}, {}], 区域数量={}", longitude, latitude, areas.size());
|
||||
return areas;
|
||||
}
|
||||
|
||||
log.debug("点包含查询缓存未命中: point=[{}, {}]", longitude, latitude);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取点包含查询缓存失败: point=[{}, {}]", longitude, latitude, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存半径查询结果
|
||||
*
|
||||
* @param longitude 中心点经度
|
||||
* @param latitude 中心点纬度
|
||||
* @param radius 半径(米)
|
||||
* @param nearbyAreas 半径内的区域列表
|
||||
*/
|
||||
public void cacheRadiusQueryResult(double longitude, double latitude, double radius, List<AirportArea> nearbyAreas) {
|
||||
try {
|
||||
String key = CacheConstants.buildRadiusQueryKey(longitude, latitude, radius);
|
||||
objectRedisTemplate.opsForValue()
|
||||
.set(key, nearbyAreas, CacheConstants.RADIUS_QUERY_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("缓存半径查询结果: center=[{}, {}], radius={}m, 区域数量={}",
|
||||
longitude, latitude, radius, nearbyAreas.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存半径查询结果失败: center=[{}, {}], radius={}m", longitude, latitude, radius, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取半径查询结果
|
||||
*
|
||||
* @param longitude 中心点经度
|
||||
* @param latitude 中心点纬度
|
||||
* @param radius 半径(米)
|
||||
* @return 半径内的区域列表,缓存未命中则返回null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<AirportArea> getRadiusQueryResult(double longitude, double latitude, double radius) {
|
||||
try {
|
||||
String key = CacheConstants.buildRadiusQueryKey(longitude, latitude, radius);
|
||||
Object result = objectRedisTemplate.opsForValue().get(key);
|
||||
|
||||
if (result instanceof List) {
|
||||
List<AirportArea> areas = (List<AirportArea>) result;
|
||||
log.debug("获取半径查询缓存: center=[{}, {}], radius={}m, 区域数量={}",
|
||||
longitude, latitude, radius, areas.size());
|
||||
return areas;
|
||||
}
|
||||
|
||||
log.debug("半径查询缓存未命中: center=[{}, {}], radius={}m", longitude, latitude, radius);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取半径查询缓存失败: center=[{}, {}], radius={}m", longitude, latitude, radius, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除区域相关的所有缓存
|
||||
*
|
||||
* @param airportArea 要删除缓存的区域
|
||||
*/
|
||||
public void evictAreaCache(AirportArea airportArea) {
|
||||
try {
|
||||
// 删除名称缓存
|
||||
String nameKey = CacheConstants.buildAirportAreaKey(airportArea.getName());
|
||||
airportAreaRedisTemplate.delete(nameKey);
|
||||
|
||||
// 删除ID缓存(如果有areaId)
|
||||
if (airportArea.getAreaId() != null) {
|
||||
String idKey = CacheConstants.buildAirportAreaKey(airportArea.getAreaId());
|
||||
airportAreaRedisTemplate.delete(idKey);
|
||||
}
|
||||
|
||||
// 删除类型索引缓存
|
||||
String typeKey = CacheConstants.buildAreaTypeKey(airportArea.getType());
|
||||
objectRedisTemplate.delete(typeKey);
|
||||
|
||||
// 删除相关的空间查询缓存
|
||||
evictSpatialQueryCache();
|
||||
|
||||
log.debug("清除区域缓存: name={}, type={}", airportArea.getName(), airportArea.getType());
|
||||
} catch (Exception e) {
|
||||
log.warn("清除区域缓存失败: name={}", airportArea.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有空间查询缓存
|
||||
* 当区域配置变更时调用
|
||||
*/
|
||||
public void evictSpatialQueryCache() {
|
||||
try {
|
||||
// 清除点包含查询缓存
|
||||
String pointPattern = CacheConstants.POINT_CONTAINS_PREFIX + "*";
|
||||
Set<String> pointKeys = stringRedisTemplate.keys(pointPattern);
|
||||
if (pointKeys != null && !pointKeys.isEmpty()) {
|
||||
objectRedisTemplate.delete(pointKeys);
|
||||
}
|
||||
|
||||
// 清除半径查询缓存
|
||||
String radiusPattern = CacheConstants.RADIUS_QUERY_PREFIX + "*";
|
||||
Set<String> radiusKeys = stringRedisTemplate.keys(radiusPattern);
|
||||
if (radiusKeys != null && !radiusKeys.isEmpty()) {
|
||||
objectRedisTemplate.delete(radiusKeys);
|
||||
}
|
||||
|
||||
log.debug("清除空间查询缓存: 点查询={}个, 半径查询={}个",
|
||||
pointKeys != null ? pointKeys.size() : 0,
|
||||
radiusKeys != null ? radiusKeys.size() : 0);
|
||||
} catch (Exception e) {
|
||||
log.warn("清除空间查询缓存失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新类型索引
|
||||
*
|
||||
* @param airportArea 机场区域
|
||||
*/
|
||||
private void updateTypeIndex(AirportArea airportArea) {
|
||||
try {
|
||||
String typeKey = CacheConstants.buildAreaTypeKey(airportArea.getType());
|
||||
|
||||
// 获取现有的类型列表
|
||||
@SuppressWarnings("unchecked")
|
||||
List<AirportArea> existingAreas = (List<AirportArea>) objectRedisTemplate.opsForValue().get(typeKey);
|
||||
|
||||
if (existingAreas == null) {
|
||||
existingAreas = new ArrayList<>();
|
||||
} else {
|
||||
// 移除同名或同ID的现有区域
|
||||
existingAreas = existingAreas.stream()
|
||||
.filter(area -> !area.getName().equals(airportArea.getName()) &&
|
||||
(area.getAreaId() == null || airportArea.getAreaId() == null ||
|
||||
!area.getAreaId().equals(airportArea.getAreaId())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 添加新区域
|
||||
existingAreas.add(airportArea);
|
||||
|
||||
// 更新缓存
|
||||
objectRedisTemplate.opsForValue()
|
||||
.set(typeKey, existingAreas, CacheConstants.AIRPORT_AREA_EXPIRE, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.warn("更新类型索引失败: type={}", airportArea.getType(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热区域缓存
|
||||
*
|
||||
* @param allAreas 所有区域列表
|
||||
*/
|
||||
public void warmupCache(List<AirportArea> allAreas) {
|
||||
if (!CacheConstants.ENABLE_CACHE_WARMUP || allAreas == null || allAreas.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("开始预热机场区域缓存: 区域数量={}", allAreas.size());
|
||||
|
||||
// 批量缓存所有区域
|
||||
batchCacheAreas(allAreas);
|
||||
|
||||
// 按类型分组并缓存
|
||||
Map<String, List<AirportArea>> areasByType = allAreas.stream()
|
||||
.collect(Collectors.groupingBy(AirportArea::getType));
|
||||
|
||||
for (Map.Entry<String, List<AirportArea>> entry : areasByType.entrySet()) {
|
||||
cacheAreasByType(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
log.info("机场区域缓存预热完成: 区域数量={}, 类型数量={}", allAreas.size(), areasByType.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("机场区域缓存预热失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*
|
||||
* @return 缓存统计信息
|
||||
*/
|
||||
public Map<String, Object> getCacheStatistics() {
|
||||
try {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// 区域配置缓存数量
|
||||
String areaPattern = CacheConstants.AIRPORT_AREA_PREFIX + "*";
|
||||
Set<String> areaKeys = stringRedisTemplate.keys(areaPattern);
|
||||
stats.put("areaCacheCount", areaKeys != null ? areaKeys.size() : 0);
|
||||
|
||||
// 类型索引缓存数量
|
||||
String typePattern = CacheConstants.AREA_TYPE_PREFIX + "*";
|
||||
Set<String> typeKeys = stringRedisTemplate.keys(typePattern);
|
||||
stats.put("typeCacheCount", typeKeys != null ? typeKeys.size() : 0);
|
||||
|
||||
// 空间查询缓存数量
|
||||
String spatialPattern = CacheConstants.POINT_CONTAINS_PREFIX + "*";
|
||||
Set<String> spatialKeys = stringRedisTemplate.keys(spatialPattern);
|
||||
stats.put("spatialQueryCacheCount", spatialKeys != null ? spatialKeys.size() : 0);
|
||||
|
||||
return stats;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取区域缓存统计信息失败", e);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,390 @@
|
||||
package com.dongni.collisionavoidance.common.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.AirportArea;
|
||||
import com.dongni.collisionavoidance.common.model.repository.AirportAreaRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.io.WKTReader;
|
||||
import org.locationtech.jts.io.ParseException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 机场区域服务类 - 基于PostGIS的空间数据管理
|
||||
* 提供机场区域的配置管理、空间查询、冲突检测等功能
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AirportAreaService {
|
||||
|
||||
private final AirportAreaRepository airportAreaRepository;
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
private final WKTReader wktReader = new WKTReader(geometryFactory);
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 保存机场区域配置
|
||||
*/
|
||||
@Transactional
|
||||
public AirportArea saveAirportArea(AirportArea airportArea) {
|
||||
try {
|
||||
AirportArea saved = airportAreaRepository.save(airportArea);
|
||||
log.info("保存机场区域: areaName={}, areaType={}, priority={}",
|
||||
saved.getName(), saved.getType(), saved.getPriority());
|
||||
return saved;
|
||||
} catch (Exception e) {
|
||||
log.error("保存机场区域失败: areaName={}", airportArea.getName(), e);
|
||||
throw new RuntimeException("保存机场区域失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存机场区域配置
|
||||
*/
|
||||
@Transactional
|
||||
public List<AirportArea> saveAirportAreas(List<AirportArea> airportAreas) {
|
||||
try {
|
||||
List<AirportArea> saved = airportAreaRepository.saveAll(airportAreas);
|
||||
log.info("批量保存机场区域: 数量={}", saved.size());
|
||||
return saved;
|
||||
} catch (Exception e) {
|
||||
log.error("批量保存机场区域失败: 数量={}", airportAreas.size(), e);
|
||||
throw new RuntimeException("批量保存机场区域失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建机场区域
|
||||
*/
|
||||
public AirportArea createAirportArea(String areaName, String areaType, String description,
|
||||
String geometryWkt, Integer priority,
|
||||
Map<String, Object> restrictions) {
|
||||
try {
|
||||
Geometry geometry = wktReader.read(geometryWkt);
|
||||
geometry.setSRID(4326); // 设置WGS84坐标系
|
||||
|
||||
AirportArea airportArea = new AirportArea();
|
||||
airportArea.setName(areaName);
|
||||
airportArea.setType(areaType);
|
||||
airportArea.setDescription(description);
|
||||
airportArea.setBoundary((Polygon) geometry);
|
||||
airportArea.setPriority(priority != null ? priority : 1);
|
||||
airportArea.setEnabled(true);
|
||||
|
||||
if (restrictions != null && !restrictions.isEmpty()) {
|
||||
airportArea.setAllowedVehicleTypes(objectMapper.writeValueAsString(restrictions));
|
||||
}
|
||||
|
||||
return airportArea;
|
||||
} catch (ParseException e) {
|
||||
log.error("解析几何形状失败: geometryWkt={}", geometryWkt, e);
|
||||
throw new RuntimeException("无效的几何形状格式", e);
|
||||
} catch (Exception e) {
|
||||
log.error("创建机场区域失败: areaName={}", areaName, e);
|
||||
throw new RuntimeException("创建机场区域失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建矩形区域
|
||||
*/
|
||||
public AirportArea createRectangularArea(String areaName, String areaType, String description,
|
||||
double minLon, double minLat, double maxLon, double maxLat,
|
||||
Integer priority, Map<String, Object> restrictions) {
|
||||
try {
|
||||
// 创建矩形多边形
|
||||
Coordinate[] coordinates = new Coordinate[]{
|
||||
new Coordinate(minLon, minLat),
|
||||
new Coordinate(maxLon, minLat),
|
||||
new Coordinate(maxLon, maxLat),
|
||||
new Coordinate(minLon, maxLat),
|
||||
new Coordinate(minLon, minLat) // 闭合多边形
|
||||
};
|
||||
|
||||
Polygon polygon = geometryFactory.createPolygon(coordinates);
|
||||
polygon.setSRID(4326);
|
||||
|
||||
AirportArea airportArea = new AirportArea();
|
||||
airportArea.setName(areaName);
|
||||
airportArea.setType(areaType);
|
||||
airportArea.setDescription(description);
|
||||
airportArea.setBoundary((Polygon) polygon);
|
||||
airportArea.setPriority(priority != null ? priority : 1);
|
||||
airportArea.setEnabled(true);
|
||||
|
||||
if (restrictions != null && !restrictions.isEmpty()) {
|
||||
airportArea.setAllowedVehicleTypes(objectMapper.writeValueAsString(restrictions));
|
||||
}
|
||||
|
||||
return airportArea;
|
||||
} catch (Exception e) {
|
||||
log.error("创建矩形区域失败: areaName={}", areaName, e);
|
||||
throw new RuntimeException("创建矩形区域失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据区域名称获取区域配置
|
||||
*/
|
||||
public Optional<AirportArea> getAreaByName(String areaName) {
|
||||
try {
|
||||
return airportAreaRepository.findByAreaName(areaName);
|
||||
} catch (Exception e) {
|
||||
log.error("获取区域失败: areaName={}", areaName, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据区域类型获取所有活跃区域
|
||||
*/
|
||||
public List<AirportArea> getActiveAreasByType(String areaType) {
|
||||
try {
|
||||
return airportAreaRepository.findActiveAreasByType(areaType);
|
||||
} catch (Exception e) {
|
||||
log.error("获取活跃区域失败: areaType={}", areaType, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空间查询:查找包含指定点的所有区域
|
||||
*/
|
||||
public List<AirportArea> getAreasContainingPoint(double longitude, double latitude) {
|
||||
Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude));
|
||||
point.setSRID(4326);
|
||||
|
||||
try {
|
||||
return airportAreaRepository.findAreasContainingPoint(point);
|
||||
} catch (Exception e) {
|
||||
log.error("点包含查询失败: point=({}, {})", longitude, latitude, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空间查询:查找与指定几何形状相交的区域
|
||||
*/
|
||||
public List<AirportArea> getAreasIntersectingGeometry(String geometryWkt) {
|
||||
try {
|
||||
Geometry geometry = wktReader.read(geometryWkt);
|
||||
geometry.setSRID(4326);
|
||||
return airportAreaRepository.findAreasIntersectingGeometry(geometry);
|
||||
} catch (ParseException e) {
|
||||
log.error("解析几何形状失败: geometryWkt={}", geometryWkt, e);
|
||||
return List.of();
|
||||
} catch (Exception e) {
|
||||
log.error("相交查询失败: geometryWkt={}", geometryWkt, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空间查询:查找指定距离范围内的区域
|
||||
*/
|
||||
public List<AirportArea> getAreasWithinRadius(double longitude, double latitude, double radiusMeters) {
|
||||
Point centerPoint = geometryFactory.createPoint(new Coordinate(longitude, latitude));
|
||||
centerPoint.setSRID(4326);
|
||||
|
||||
try {
|
||||
return airportAreaRepository.findAreasWithinRadius(centerPoint, radiusMeters);
|
||||
} catch (Exception e) {
|
||||
log.error("半径查询失败: center=({}, {}), radius={}m", longitude, latitude, radiusMeters, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找最高优先级的包含指定点的区域
|
||||
*/
|
||||
public Optional<AirportArea> getHighestPriorityAreaContainingPoint(double longitude, double latitude) {
|
||||
Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude));
|
||||
point.setSRID(4326);
|
||||
|
||||
try {
|
||||
return airportAreaRepository.findHighestPriorityAreaContainingPoint(point);
|
||||
} catch (Exception e) {
|
||||
log.error("最高优先级区域查询失败: point=({}, {})", longitude, latitude, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测区域重叠冲突
|
||||
*/
|
||||
public List<AirportArea> detectAreaOverlaps(Long areaId, String geometryWkt) {
|
||||
try {
|
||||
Geometry geometry = wktReader.read(geometryWkt);
|
||||
geometry.setSRID(4326);
|
||||
return airportAreaRepository.findOverlappingAreas(geometry, areaId);
|
||||
} catch (ParseException e) {
|
||||
log.error("解析几何形状失败: geometryWkt={}", geometryWkt, e);
|
||||
return List.of();
|
||||
} catch (Exception e) {
|
||||
log.error("重叠检测失败: areaId={}, geometryWkt={}", areaId, geometryWkt, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据优先级范围获取区域
|
||||
*/
|
||||
public List<AirportArea> getAreasByPriorityRange(int minPriority, int maxPriority) {
|
||||
try {
|
||||
return airportAreaRepository.findAreasByPriorityRange(minPriority, maxPriority);
|
||||
} catch (Exception e) {
|
||||
log.error("优先级范围查询失败: 优先级范围=[{}, {}]", minPriority, maxPriority, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据限制类型查找区域
|
||||
*/
|
||||
public List<AirportArea> getAreasByRestrictionType(String restrictionType) {
|
||||
try {
|
||||
return airportAreaRepository.findAreasByRestrictionType(restrictionType);
|
||||
} catch (Exception e) {
|
||||
log.error("限制类型查询失败: restrictionType={}", restrictionType, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找与车辆轨迹相交的所有区域
|
||||
*/
|
||||
public List<AirportArea> getAreasIntersectingTrajectory(String trajectoryWkt) {
|
||||
try {
|
||||
return airportAreaRepository.findAreasIntersectingTrajectory(trajectoryWkt);
|
||||
} catch (Exception e) {
|
||||
log.error("轨迹相交查询失败: trajectoryWkt={}", trajectoryWkt, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找距离指定点最近的N个区域
|
||||
*/
|
||||
public List<AirportArea> getNearestAreas(double longitude, double latitude, int limit) {
|
||||
Point referencePoint = geometryFactory.createPoint(new Coordinate(longitude, latitude));
|
||||
referencePoint.setSRID(4326);
|
||||
|
||||
try {
|
||||
return airportAreaRepository.findNearestAreas(referencePoint, limit);
|
||||
} catch (Exception e) {
|
||||
log.error("最近区域查询失败: reference=({}, {}), limit={}", longitude, latitude, limit, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计活跃区域数量(按类型分组)
|
||||
*/
|
||||
public Map<String, Long> countActiveAreasByType() {
|
||||
try {
|
||||
List<Object[]> results = airportAreaRepository.countActiveAreasByType();
|
||||
return results.stream()
|
||||
.collect(java.util.stream.Collectors.toMap(
|
||||
row -> (String) row[0],
|
||||
row -> (Long) row[1]
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("区域统计失败", e);
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新区域激活状态
|
||||
*/
|
||||
@Transactional
|
||||
public void updateAreaActiveStatus(String areaName, boolean isActive) {
|
||||
try {
|
||||
Optional<AirportArea> areaOpt = airportAreaRepository.findByAreaName(areaName);
|
||||
if (areaOpt.isPresent()) {
|
||||
AirportArea area = areaOpt.get();
|
||||
area.setEnabled(isActive);
|
||||
airportAreaRepository.save(area);
|
||||
log.info("更新区域激活状态: areaName={}, isActive={}", areaName, isActive);
|
||||
} else {
|
||||
log.warn("区域不存在: areaName={}", areaName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("更新区域激活状态失败: areaName={}", areaName, e);
|
||||
throw new RuntimeException("更新区域激活状态失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除区域
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteArea(String areaName) {
|
||||
try {
|
||||
Optional<AirportArea> areaOpt = airportAreaRepository.findByAreaName(areaName);
|
||||
if (areaOpt.isPresent()) {
|
||||
airportAreaRepository.delete(areaOpt.get());
|
||||
log.info("删除区域: areaName={}", areaName);
|
||||
} else {
|
||||
log.warn("区域不存在: areaName={}", areaName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("删除区域失败: areaName={}", areaName, e);
|
||||
throw new RuntimeException("删除区域失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证区域几何形状的有效性
|
||||
*/
|
||||
public boolean isValidGeometry(String geometryWkt) {
|
||||
try {
|
||||
Geometry geometry = wktReader.read(geometryWkt);
|
||||
return geometry.isValid();
|
||||
} catch (ParseException e) {
|
||||
log.warn("无效的几何形状格式: {}", geometryWkt, e);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.warn("几何形状验证失败: {}", geometryWkt, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取区域的边界框
|
||||
*/
|
||||
public Optional<Geometry> getAreaBoundingBox(Long areaId) {
|
||||
try {
|
||||
return airportAreaRepository.getAreaBoundingBox(areaId);
|
||||
} catch (Exception e) {
|
||||
log.error("获取区域边界框失败: areaId={}", areaId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有活跃区域
|
||||
*/
|
||||
public List<AirportArea> getAllActiveAreas() {
|
||||
try {
|
||||
return airportAreaRepository.findAll()
|
||||
.stream()
|
||||
.filter(AirportArea::getEnabled)
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
log.error("获取所有活跃区域失败", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,302 @@
|
||||
package com.dongni.collisionavoidance.common.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.VehicleLocation;
|
||||
import com.dongni.collisionavoidance.common.model.base.CacheConstants;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 空间查询缓存服务类
|
||||
* 基于Redis实现空间查询结果的高性能缓存
|
||||
* 采用地理网格策略优化缓存效果
|
||||
*
|
||||
* 主要功能:
|
||||
* 1. 基于地理网格的缓存策略
|
||||
* 2. 车辆空间查询结果缓存
|
||||
* 3. 冲突检测结果缓存
|
||||
* 4. 缓存失效和清理机制
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SpatialQueryCacheService {
|
||||
|
||||
private final RedisTemplate<String, Object> objectRedisTemplate;
|
||||
private final RedisTemplate<String, String> stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 缓存车辆半径查询结果
|
||||
*
|
||||
* @param centerLon 中心点经度
|
||||
* @param centerLat 中心点纬度
|
||||
* @param radius 半径(米)
|
||||
* @param vehicles 查询结果车辆列表
|
||||
*/
|
||||
public void cacheVehicleRadiusQuery(double centerLon, double centerLat, double radius, List<VehicleLocation> vehicles) {
|
||||
try {
|
||||
String key = CacheConstants.buildRadiusQueryKey(centerLon, centerLat, radius);
|
||||
objectRedisTemplate.opsForValue()
|
||||
.set(key, vehicles, CacheConstants.RADIUS_QUERY_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("缓存车辆半径查询结果: center=[{}, {}], radius={}m, 车辆数={}",
|
||||
centerLon, centerLat, radius, vehicles.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存车辆半径查询结果失败: center=[{}, {}], radius={}m", centerLon, centerLat, radius, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆半径查询结果
|
||||
*
|
||||
* @param centerLon 中心点经度
|
||||
* @param centerLat 中心点纬度
|
||||
* @param radius 半径(米)
|
||||
* @return 车辆列表,缓存未命中则返回null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<VehicleLocation> getVehicleRadiusQuery(double centerLon, double centerLat, double radius) {
|
||||
try {
|
||||
String key = CacheConstants.buildRadiusQueryKey(centerLon, centerLat, radius);
|
||||
Object result = objectRedisTemplate.opsForValue().get(key);
|
||||
|
||||
if (result instanceof List) {
|
||||
List<VehicleLocation> vehicles = (List<VehicleLocation>) result;
|
||||
log.debug("获取车辆半径查询缓存: center=[{}, {}], radius={}m, 车辆数={}",
|
||||
centerLon, centerLat, radius, vehicles.size());
|
||||
return vehicles;
|
||||
}
|
||||
|
||||
log.debug("车辆半径查询缓存未命中: center=[{}, {}], radius={}m", centerLon, centerLat, radius);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取车辆半径查询缓存失败: center=[{}, {}], radius={}m", centerLon, centerLat, radius, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存冲突检测结果
|
||||
*
|
||||
* @param vehicleId1 车辆1 ID
|
||||
* @param vehicleId2 车辆2 ID
|
||||
* @param hasCollisionRisk 是否存在冲突风险
|
||||
*/
|
||||
public void cacheCollisionDetection(String vehicleId1, String vehicleId2, boolean hasCollisionRisk) {
|
||||
try {
|
||||
String key = CacheConstants.buildCollisionKey(vehicleId1, vehicleId2);
|
||||
stringRedisTemplate.opsForValue()
|
||||
.set(key, String.valueOf(hasCollisionRisk), CacheConstants.COLLISION_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("缓存冲突检测结果: vehicles=[{}, {}], risk={}", vehicleId1, vehicleId2, hasCollisionRisk);
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存冲突检测结果失败: vehicles=[{}, {}]", vehicleId1, vehicleId2, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冲突检测结果
|
||||
*
|
||||
* @param vehicleId1 车辆1 ID
|
||||
* @param vehicleId2 车辆2 ID
|
||||
* @return 冲突检测结果,缓存未命中则返回null
|
||||
*/
|
||||
public Boolean getCollisionDetection(String vehicleId1, String vehicleId2) {
|
||||
try {
|
||||
String key = CacheConstants.buildCollisionKey(vehicleId1, vehicleId2);
|
||||
String result = stringRedisTemplate.opsForValue().get(key);
|
||||
|
||||
if (result != null) {
|
||||
boolean hasRisk = Boolean.parseBoolean(result);
|
||||
log.debug("获取冲突检测缓存: vehicles=[{}, {}], risk={}", vehicleId1, vehicleId2, hasRisk);
|
||||
return hasRisk;
|
||||
}
|
||||
|
||||
log.debug("冲突检测缓存未命中: vehicles=[{}, {}]", vehicleId1, vehicleId2);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取冲突检测缓存失败: vehicles=[{}, {}]", vehicleId1, vehicleId2, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存地理网格数据
|
||||
*
|
||||
* @param longitude 经度
|
||||
* @param latitude 纬度
|
||||
* @param gridData 网格数据
|
||||
*/
|
||||
public void cacheGeoGrid(double longitude, double latitude, Object gridData) {
|
||||
try {
|
||||
String key = CacheConstants.buildGeoGridKey(longitude, latitude);
|
||||
objectRedisTemplate.opsForValue()
|
||||
.set(key, gridData, CacheConstants.GEO_GRID_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("缓存地理网格数据: grid=[{}, {}]", longitude, latitude);
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存地理网格数据失败: grid=[{}, {}]", longitude, latitude, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地理网格数据
|
||||
*
|
||||
* @param longitude 经度
|
||||
* @param latitude 纬度
|
||||
* @return 网格数据,缓存未命中则返回null
|
||||
*/
|
||||
public Object getGeoGrid(double longitude, double latitude) {
|
||||
try {
|
||||
String key = CacheConstants.buildGeoGridKey(longitude, latitude);
|
||||
Object result = objectRedisTemplate.opsForValue().get(key);
|
||||
|
||||
log.debug("获取地理网格缓存: grid=[{}, {}], found={}", longitude, latitude, result != null);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取地理网格缓存失败: grid=[{}, {}]", longitude, latitude, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除车辆相关的空间查询缓存
|
||||
* 当车辆位置发生变化时调用
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
*/
|
||||
public void evictVehicleSpatialCache(String vehicleId) {
|
||||
try {
|
||||
// 清除冲突检测缓存
|
||||
String collisionPattern = CacheConstants.COLLISION_PREFIX + "*" + vehicleId + "*";
|
||||
Set<String> collisionKeys = stringRedisTemplate.keys(collisionPattern);
|
||||
if (collisionKeys != null && !collisionKeys.isEmpty()) {
|
||||
stringRedisTemplate.delete(collisionKeys);
|
||||
}
|
||||
|
||||
// 清除相关的半径查询缓存(这里可以进一步优化为只清除相关网格)
|
||||
String radiusPattern = CacheConstants.RADIUS_QUERY_PREFIX + "*";
|
||||
Set<String> radiusKeys = stringRedisTemplate.keys(radiusPattern);
|
||||
if (radiusKeys != null && !radiusKeys.isEmpty()) {
|
||||
objectRedisTemplate.delete(radiusKeys);
|
||||
}
|
||||
|
||||
log.debug("清除车辆空间查询缓存: vehicleId={}, 冲突缓存={}个, 半径缓存={}个",
|
||||
vehicleId,
|
||||
collisionKeys != null ? collisionKeys.size() : 0,
|
||||
radiusKeys != null ? radiusKeys.size() : 0);
|
||||
} catch (Exception e) {
|
||||
log.warn("清除车辆空间查询缓存失败: vehicleId={}", vehicleId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除区域相关的空间查询缓存
|
||||
* 当区域配置发生变化时调用
|
||||
*/
|
||||
public void evictAreaSpatialCache() {
|
||||
try {
|
||||
// 清除所有空间查询缓存
|
||||
String spatialPattern = CacheConstants.SPATIAL_QUERY_PREFIX + "*";
|
||||
Set<String> spatialKeys = stringRedisTemplate.keys(spatialPattern);
|
||||
if (spatialKeys != null && !spatialKeys.isEmpty()) {
|
||||
objectRedisTemplate.delete(spatialKeys);
|
||||
}
|
||||
|
||||
// 清除地理网格缓存
|
||||
String gridPattern = CacheConstants.GEO_GRID_PREFIX + "*";
|
||||
Set<String> gridKeys = stringRedisTemplate.keys(gridPattern);
|
||||
if (gridKeys != null && !gridKeys.isEmpty()) {
|
||||
objectRedisTemplate.delete(gridKeys);
|
||||
}
|
||||
|
||||
log.debug("清除区域空间查询缓存: 空间查询={}个, 网格缓存={}个",
|
||||
spatialKeys != null ? spatialKeys.size() : 0,
|
||||
gridKeys != null ? gridKeys.size() : 0);
|
||||
} catch (Exception e) {
|
||||
log.warn("清除区域空间查询缓存失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的空间查询缓存
|
||||
* 定期清理任务
|
||||
*/
|
||||
public void cleanupExpiredSpatialCache() {
|
||||
try {
|
||||
int cleanedCount = 0;
|
||||
|
||||
// 清理过期的冲突检测缓存
|
||||
String collisionPattern = CacheConstants.COLLISION_PREFIX + "*";
|
||||
Set<String> collisionKeys = stringRedisTemplate.keys(collisionPattern);
|
||||
if (collisionKeys != null) {
|
||||
for (String key : collisionKeys) {
|
||||
Long ttl = stringRedisTemplate.getExpire(key);
|
||||
if (ttl != null && ttl <= 0) {
|
||||
stringRedisTemplate.delete(key);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("清理过期空间查询缓存: 清理数量={}", cleanedCount);
|
||||
} catch (Exception e) {
|
||||
log.warn("清理过期空间查询缓存失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取空间查询缓存统计信息
|
||||
*
|
||||
* @return 缓存统计信息
|
||||
*/
|
||||
public Map<String, Object> getSpatialCacheStatistics() {
|
||||
try {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// 冲突检测缓存数量
|
||||
String collisionPattern = CacheConstants.COLLISION_PREFIX + "*";
|
||||
Set<String> collisionKeys = stringRedisTemplate.keys(collisionPattern);
|
||||
stats.put("collisionCacheCount", collisionKeys != null ? collisionKeys.size() : 0);
|
||||
|
||||
// 半径查询缓存数量
|
||||
String radiusPattern = CacheConstants.RADIUS_QUERY_PREFIX + "*";
|
||||
Set<String> radiusKeys = stringRedisTemplate.keys(radiusPattern);
|
||||
stats.put("radiusQueryCacheCount", radiusKeys != null ? radiusKeys.size() : 0);
|
||||
|
||||
// 地理网格缓存数量
|
||||
String gridPattern = CacheConstants.GEO_GRID_PREFIX + "*";
|
||||
Set<String> gridKeys = stringRedisTemplate.keys(gridPattern);
|
||||
stats.put("geoGridCacheCount", gridKeys != null ? gridKeys.size() : 0);
|
||||
|
||||
return stats;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取空间查询缓存统计信息失败", e);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除匹配模式的缓存键
|
||||
*
|
||||
* @param pattern 键模式
|
||||
* @return 删除的键数量
|
||||
*/
|
||||
private int deleteByPattern(String pattern) {
|
||||
try {
|
||||
Set<String> keys = stringRedisTemplate.keys(pattern);
|
||||
if (keys != null && !keys.isEmpty()) {
|
||||
objectRedisTemplate.delete(keys);
|
||||
return keys.size();
|
||||
}
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
log.warn("批量删除缓存键失败: pattern={}", pattern, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,428 @@
|
||||
package com.dongni.collisionavoidance.common.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.AirportArea;
|
||||
import com.dongni.collisionavoidance.common.model.spatial.VehicleLocation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.*;
|
||||
import org.locationtech.jts.io.WKTReader;
|
||||
import org.locationtech.jts.io.ParseException;
|
||||
import org.locationtech.jts.algorithm.distance.DistanceToPoint;
|
||||
import org.locationtech.jts.algorithm.distance.PointPairDistance;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 空间查询服务类 - 基于PostGIS的综合空间分析
|
||||
* 提供车辆位置与机场区域的空间关系分析、碰撞检测、路径规划等功能
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SpatialQueryService {
|
||||
|
||||
private final VehicleLocationService vehicleLocationService;
|
||||
private final AirportAreaService airportAreaService;
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
private final WKTReader wktReader = new WKTReader(geometryFactory);
|
||||
|
||||
/**
|
||||
* 综合空间查询结果
|
||||
*/
|
||||
public static class SpatialQueryResult {
|
||||
private List<VehicleLocation> vehiclesInArea;
|
||||
private List<AirportArea> areasContainingVehicles;
|
||||
private Map<String, Object> spatialStatistics;
|
||||
private List<ConflictDetection> potentialConflicts;
|
||||
|
||||
// 构造函数和getter/setter
|
||||
public SpatialQueryResult() {
|
||||
this.vehiclesInArea = new ArrayList<>();
|
||||
this.areasContainingVehicles = new ArrayList<>();
|
||||
this.spatialStatistics = new HashMap<>();
|
||||
this.potentialConflicts = new ArrayList<>();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public List<VehicleLocation> getVehiclesInArea() { return vehiclesInArea; }
|
||||
public void setVehiclesInArea(List<VehicleLocation> vehiclesInArea) { this.vehiclesInArea = vehiclesInArea; }
|
||||
|
||||
public List<AirportArea> getAreasContainingVehicles() { return areasContainingVehicles; }
|
||||
public void setAreasContainingVehicles(List<AirportArea> areasContainingVehicles) { this.areasContainingVehicles = areasContainingVehicles; }
|
||||
|
||||
public Map<String, Object> getSpatialStatistics() { return spatialStatistics; }
|
||||
public void setSpatialStatistics(Map<String, Object> spatialStatistics) { this.spatialStatistics = spatialStatistics; }
|
||||
|
||||
public List<ConflictDetection> getPotentialConflicts() { return potentialConflicts; }
|
||||
public void setPotentialConflicts(List<ConflictDetection> potentialConflicts) { this.potentialConflicts = potentialConflicts; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 冲突检测结果
|
||||
*/
|
||||
public static class ConflictDetection {
|
||||
private String conflictType;
|
||||
private VehicleLocation vehicle1;
|
||||
private VehicleLocation vehicle2;
|
||||
private AirportArea affectedArea;
|
||||
private double distance;
|
||||
private String severity;
|
||||
private String description;
|
||||
|
||||
public ConflictDetection(String conflictType, String severity, String description) {
|
||||
this.conflictType = conflictType;
|
||||
this.severity = severity;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getConflictType() { return conflictType; }
|
||||
public void setConflictType(String conflictType) { this.conflictType = conflictType; }
|
||||
|
||||
public VehicleLocation getVehicle1() { return vehicle1; }
|
||||
public void setVehicle1(VehicleLocation vehicle1) { this.vehicle1 = vehicle1; }
|
||||
|
||||
public VehicleLocation getVehicle2() { return vehicle2; }
|
||||
public void setVehicle2(VehicleLocation vehicle2) { this.vehicle2 = vehicle2; }
|
||||
|
||||
public AirportArea getAffectedArea() { return affectedArea; }
|
||||
public void setAffectedArea(AirportArea affectedArea) { this.affectedArea = affectedArea; }
|
||||
|
||||
public double getDistance() { return distance; }
|
||||
public void setDistance(double distance) { this.distance = distance; }
|
||||
|
||||
public String getSeverity() { return severity; }
|
||||
public void setSeverity(String severity) { this.severity = severity; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行综合空间分析
|
||||
*/
|
||||
public SpatialQueryResult performSpatialAnalysis(double longitude, double latitude, double radiusMeters, int minutesBack) {
|
||||
SpatialQueryResult result = new SpatialQueryResult();
|
||||
|
||||
try {
|
||||
// 1. 查找半径范围内的车辆
|
||||
List<VehicleLocation> nearbyVehicles = vehicleLocationService.getVehiclesWithinRadius(
|
||||
longitude, latitude, radiusMeters, minutesBack);
|
||||
result.setVehiclesInArea(nearbyVehicles);
|
||||
|
||||
// 2. 查找包含指定点的区域
|
||||
List<AirportArea> containingAreas = airportAreaService.getAreasContainingPoint(longitude, latitude);
|
||||
result.setAreasContainingVehicles(containingAreas);
|
||||
|
||||
// 3. 执行冲突检测
|
||||
List<ConflictDetection> conflicts = detectVehicleConflicts(nearbyVehicles, radiusMeters);
|
||||
result.setPotentialConflicts(conflicts);
|
||||
|
||||
// 4. 计算空间统计信息
|
||||
Map<String, Object> statistics = calculateSpatialStatistics(nearbyVehicles, containingAreas);
|
||||
result.setSpatialStatistics(statistics);
|
||||
|
||||
log.info("空间分析完成: 中心点=({}, {}), 半径={}m, 发现车辆={}, 发现区域={}, 冲突数={}",
|
||||
longitude, latitude, radiusMeters, nearbyVehicles.size(), containingAreas.size(), conflicts.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("空间分析失败: 中心点=({}, {}), 半径={}m", longitude, latitude, radiusMeters, e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测车辆间的潜在冲突
|
||||
*/
|
||||
public List<ConflictDetection> detectVehicleConflicts(List<VehicleLocation> vehicles, double alertRadius) {
|
||||
List<ConflictDetection> conflicts = new ArrayList<>();
|
||||
|
||||
try {
|
||||
for (int i = 0; i < vehicles.size(); i++) {
|
||||
for (int j = i + 1; j < vehicles.size(); j++) {
|
||||
VehicleLocation vehicle1 = vehicles.get(i);
|
||||
VehicleLocation vehicle2 = vehicles.get(j);
|
||||
|
||||
double distance = calculateDistance(vehicle1.getLocation(), vehicle2.getLocation());
|
||||
|
||||
// 距离冲突检测
|
||||
if (distance < alertRadius) {
|
||||
ConflictDetection conflict = new ConflictDetection(
|
||||
"PROXIMITY_CONFLICT",
|
||||
distance < alertRadius * 0.5 ? "HIGH" : "MEDIUM",
|
||||
String.format("车辆 %s 和 %s 距离过近: %.2fm",
|
||||
vehicle1.getVehicleId(), vehicle2.getVehicleId(), distance)
|
||||
);
|
||||
conflict.setVehicle1(vehicle1);
|
||||
conflict.setVehicle2(vehicle2);
|
||||
conflict.setDistance(distance);
|
||||
conflicts.add(conflict);
|
||||
}
|
||||
|
||||
// 相向移动冲突检测
|
||||
if (isHeadOnConflict(vehicle1, vehicle2, distance)) {
|
||||
ConflictDetection conflict = new ConflictDetection(
|
||||
"HEAD_ON_CONFLICT",
|
||||
"HIGH",
|
||||
String.format("车辆 %s 和 %s 可能发生正面冲突",
|
||||
vehicle1.getVehicleId(), vehicle2.getVehicleId())
|
||||
);
|
||||
conflict.setVehicle1(vehicle1);
|
||||
conflict.setVehicle2(vehicle2);
|
||||
conflict.setDistance(distance);
|
||||
conflicts.add(conflict);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("冲突检测失败", e);
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测车辆违反区域限制
|
||||
*/
|
||||
public List<ConflictDetection> detectAreaViolations(String vehicleId, int minutesBack) {
|
||||
List<ConflictDetection> violations = new ArrayList<>();
|
||||
|
||||
try {
|
||||
Optional<VehicleLocation> vehicleOpt = vehicleLocationService.getLatestVehicleLocation(vehicleId);
|
||||
if (vehicleOpt.isEmpty()) {
|
||||
return violations;
|
||||
}
|
||||
|
||||
VehicleLocation vehicle = vehicleOpt.get();
|
||||
Point vehiclePoint = vehicle.getLocation();
|
||||
|
||||
// 检查车辆所在的所有区域
|
||||
List<AirportArea> containingAreas = airportAreaService.getAreasContainingPoint(
|
||||
vehiclePoint.getX(), vehiclePoint.getY());
|
||||
|
||||
for (AirportArea area : containingAreas) {
|
||||
// 检查速度限制
|
||||
if (isSpeedViolation(vehicle, area)) {
|
||||
ConflictDetection violation = new ConflictDetection(
|
||||
"SPEED_VIOLATION",
|
||||
"MEDIUM",
|
||||
String.format("车辆 %s 在区域 %s 超速", vehicleId, area.getName())
|
||||
);
|
||||
violation.setVehicle1(vehicle);
|
||||
violation.setAffectedArea(area);
|
||||
violations.add(violation);
|
||||
}
|
||||
|
||||
// 检查车辆类型限制
|
||||
if (isVehicleTypeViolation(vehicle, area)) {
|
||||
ConflictDetection violation = new ConflictDetection(
|
||||
"ACCESS_VIOLATION",
|
||||
"HIGH",
|
||||
String.format("车辆 %s 不允许进入区域 %s", vehicleId, area.getName())
|
||||
);
|
||||
violation.setVehicle1(vehicle);
|
||||
violation.setAffectedArea(area);
|
||||
violations.add(violation);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("区域违规检测失败: vehicleId={}", vehicleId, e);
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找车辆轨迹与区域的交互历史
|
||||
*/
|
||||
public Map<String, Object> analyzeVehicleTrajectoryIntersections(String vehicleId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
Map<String, Object> analysis = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 获取车辆轨迹
|
||||
List<VehicleLocation> trajectory = vehicleLocationService.getVehicleTrajectory(vehicleId, startTime, endTime);
|
||||
|
||||
if (trajectory.isEmpty()) {
|
||||
analysis.put("error", "未找到轨迹数据");
|
||||
return analysis;
|
||||
}
|
||||
|
||||
// 创建轨迹线
|
||||
String trajectoryWkt = createTrajectoryLineString(trajectory);
|
||||
|
||||
// 查找与轨迹相交的区域
|
||||
List<AirportArea> intersectingAreas = airportAreaService.getAreasIntersectingTrajectory(trajectoryWkt);
|
||||
|
||||
// 分析结果
|
||||
analysis.put("vehicleId", vehicleId);
|
||||
analysis.put("trajectoryPointCount", trajectory.size());
|
||||
analysis.put("intersectingAreaCount", intersectingAreas.size());
|
||||
analysis.put("intersectingAreas", intersectingAreas.stream()
|
||||
.map(area -> Map.of(
|
||||
"areaName", area.getName(),
|
||||
"areaType", area.getType(),
|
||||
"priority", area.getPriority()
|
||||
)).collect(Collectors.toList()));
|
||||
|
||||
// 计算轨迹统计
|
||||
analysis.put("totalDistance", calculateTrajectoryDistance(trajectory));
|
||||
analysis.put("maxSpeed", trajectory.stream()
|
||||
.filter(loc -> loc.getSpeed() != null)
|
||||
.mapToDouble(VehicleLocation::getSpeed)
|
||||
.max().orElse(0.0));
|
||||
analysis.put("averageSpeed", trajectory.stream()
|
||||
.filter(loc -> loc.getSpeed() != null)
|
||||
.mapToDouble(VehicleLocation::getSpeed)
|
||||
.average().orElse(0.0));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("轨迹交互分析失败: vehicleId={}", vehicleId, e);
|
||||
analysis.put("error", "分析失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行区域密度分析
|
||||
*/
|
||||
public Map<String, Object> analyzeAreaDensity(String areaName, int minutesBack) {
|
||||
Map<String, Object> density = new HashMap<>();
|
||||
|
||||
try {
|
||||
Optional<AirportArea> areaOpt = airportAreaService.getAreaByName(areaName);
|
||||
if (areaOpt.isEmpty()) {
|
||||
density.put("error", "区域不存在");
|
||||
return density;
|
||||
}
|
||||
|
||||
AirportArea area = areaOpt.get();
|
||||
|
||||
// 查找区域内的所有车辆
|
||||
String areaWkt = area.getBoundary().toString();
|
||||
List<VehicleLocation> vehiclesInArea = vehicleLocationService.getVehiclesInArea(areaWkt, minutesBack);
|
||||
|
||||
// 计算密度指标
|
||||
double areaSize = area.getArea() != null ? area.getArea() : 0.0; // 平方米
|
||||
double vehicleDensity = areaSize > 0 ? vehiclesInArea.size() / (areaSize / 1000000) : 0; // 车辆/平方公里
|
||||
|
||||
density.put("areaName", areaName);
|
||||
density.put("areaType", area.getType());
|
||||
density.put("areaSize", areaSize);
|
||||
density.put("vehicleCount", vehiclesInArea.size());
|
||||
density.put("vehicleDensity", vehicleDensity);
|
||||
|
||||
// 按车辆类型分组
|
||||
Map<String, Long> vehicleTypeCount = vehiclesInArea.stream()
|
||||
.collect(Collectors.groupingBy(v -> v.getVehicleType().toString(), Collectors.counting()));
|
||||
density.put("vehicleTypeDistribution", vehicleTypeCount);
|
||||
|
||||
// 速度统计
|
||||
density.put("averageSpeed", vehiclesInArea.stream()
|
||||
.filter(v -> v.getSpeed() != null)
|
||||
.mapToDouble(VehicleLocation::getSpeed)
|
||||
.average().orElse(0.0));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("区域密度分析失败: areaName={}", areaName, e);
|
||||
density.put("error", "分析失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return density;
|
||||
}
|
||||
|
||||
// ===== 私有辅助方法 =====
|
||||
|
||||
private double calculateDistance(Point point1, Point point2) {
|
||||
return point1.distance(point2) * 111320; // 近似转换为米(在赤道附近)
|
||||
}
|
||||
|
||||
private boolean isHeadOnConflict(VehicleLocation vehicle1, VehicleLocation vehicle2, double distance) {
|
||||
if (vehicle1.getHeading() == null || vehicle2.getHeading() == null || distance > 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查航向是否相反(差异在150-210度之间表示相向)
|
||||
double headingDiff = Math.abs(vehicle1.getHeading() - vehicle2.getHeading());
|
||||
headingDiff = Math.min(headingDiff, 360 - headingDiff); // 处理跨0度的情况
|
||||
|
||||
return headingDiff > 150 && headingDiff < 210 && distance < 500; // 500米内相向移动
|
||||
}
|
||||
|
||||
private boolean isSpeedViolation(VehicleLocation vehicle, AirportArea area) {
|
||||
if (vehicle.getSpeed() == null || area.getRestricted() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 简化的速度限制检查(实际应解析JSON)
|
||||
return vehicle.getSpeed() > 25.0; // 假设默认限速25m/s
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isVehicleTypeViolation(VehicleLocation vehicle, AirportArea area) {
|
||||
// 简化的车辆类型限制检查
|
||||
return false; // 实际应根据区域限制和车辆类型判断
|
||||
}
|
||||
|
||||
private String createTrajectoryLineString(List<VehicleLocation> trajectory) {
|
||||
if (trajectory.size() < 2) {
|
||||
return "LINESTRING EMPTY";
|
||||
}
|
||||
|
||||
StringBuilder wkt = new StringBuilder("LINESTRING(");
|
||||
for (int i = 0; i < trajectory.size(); i++) {
|
||||
Point location = trajectory.get(i).getLocation();
|
||||
wkt.append(location.getX()).append(" ").append(location.getY());
|
||||
if (i < trajectory.size() - 1) {
|
||||
wkt.append(", ");
|
||||
}
|
||||
}
|
||||
wkt.append(")");
|
||||
return wkt.toString();
|
||||
}
|
||||
|
||||
private double calculateTrajectoryDistance(List<VehicleLocation> trajectory) {
|
||||
double totalDistance = 0.0;
|
||||
for (int i = 1; i < trajectory.size(); i++) {
|
||||
totalDistance += calculateDistance(
|
||||
trajectory.get(i-1).getLocation(),
|
||||
trajectory.get(i).getLocation()
|
||||
);
|
||||
}
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
private Map<String, Object> calculateSpatialStatistics(List<VehicleLocation> vehicles, List<AirportArea> areas) {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
stats.put("vehicleCount", vehicles.size());
|
||||
stats.put("areaCount", areas.size());
|
||||
|
||||
if (!vehicles.isEmpty()) {
|
||||
stats.put("averageSpeed", vehicles.stream()
|
||||
.filter(v -> v.getSpeed() != null)
|
||||
.mapToDouble(VehicleLocation::getSpeed)
|
||||
.average().orElse(0.0));
|
||||
|
||||
Map<String, Long> typeDistribution = vehicles.stream()
|
||||
.collect(Collectors.groupingBy(v -> v.getVehicleType().toString(), Collectors.counting()));
|
||||
stats.put("vehicleTypeDistribution", typeDistribution);
|
||||
}
|
||||
|
||||
if (!areas.isEmpty()) {
|
||||
Map<String, Long> areaTypeDistribution = areas.stream()
|
||||
.collect(Collectors.groupingBy(AirportArea::getType, Collectors.counting()));
|
||||
stats.put("areaTypeDistribution", areaTypeDistribution);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,312 @@
|
||||
package com.dongni.collisionavoidance.common.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.VehicleLocation;
|
||||
import com.dongni.collisionavoidance.common.model.base.CacheConstants;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 车辆位置缓存服务类
|
||||
* 基于Redis实现车辆位置数据的高性能缓存
|
||||
*
|
||||
* 主要功能:
|
||||
* 1. 车辆最新位置缓存
|
||||
* 2. 车辆轨迹数据缓存
|
||||
* 3. 批量操作支持
|
||||
* 4. 缓存穿透保护
|
||||
* 5. 缓存预热
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleLocationCacheService {
|
||||
|
||||
private final RedisTemplate<String, VehicleLocation> vehicleLocationRedisTemplate;
|
||||
private final RedisTemplate<String, Object> objectRedisTemplate;
|
||||
private final RedisTemplate<String, String> stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* 缓存车辆最新位置
|
||||
*
|
||||
* @param vehicleLocation 车辆位置信息
|
||||
*/
|
||||
public void cacheLatestLocation(VehicleLocation vehicleLocation) {
|
||||
try {
|
||||
String key = CacheConstants.buildVehicleLatestKey(vehicleLocation.getVehicleId());
|
||||
vehicleLocationRedisTemplate.opsForValue()
|
||||
.set(key, vehicleLocation, CacheConstants.VEHICLE_LATEST_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("缓存车辆最新位置: vehicleId={}, timestamp={}",
|
||||
vehicleLocation.getVehicleId(), vehicleLocation.getTimestamp());
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存车辆最新位置失败: vehicleId={}", vehicleLocation.getVehicleId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆最新位置
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
* @return 车辆最新位置,不存在则返回null
|
||||
*/
|
||||
public VehicleLocation getLatestLocation(String vehicleId) {
|
||||
try {
|
||||
String key = CacheConstants.buildVehicleLatestKey(vehicleId);
|
||||
VehicleLocation location = vehicleLocationRedisTemplate.opsForValue().get(key);
|
||||
|
||||
log.debug("获取车辆最新位置缓存: vehicleId={}, found={}", vehicleId, location != null);
|
||||
return location;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取车辆最新位置缓存失败: vehicleId={}", vehicleId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量缓存车辆最新位置
|
||||
*
|
||||
* @param vehicleLocations 车辆位置列表
|
||||
*/
|
||||
public void batchCacheLatestLocations(List<VehicleLocation> vehicleLocations) {
|
||||
if (vehicleLocations == null || vehicleLocations.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, VehicleLocation> cacheMap = new HashMap<>();
|
||||
for (VehicleLocation location : vehicleLocations) {
|
||||
String key = CacheConstants.buildVehicleLatestKey(location.getVehicleId());
|
||||
cacheMap.put(key, location);
|
||||
}
|
||||
|
||||
vehicleLocationRedisTemplate.opsForValue().multiSet(cacheMap);
|
||||
|
||||
// 批量设置过期时间
|
||||
for (String key : cacheMap.keySet()) {
|
||||
vehicleLocationRedisTemplate.expire(key, CacheConstants.VEHICLE_LATEST_EXPIRE, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
log.debug("批量缓存车辆最新位置: 数量={}", vehicleLocations.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("批量缓存车辆最新位置失败: 数量={}", vehicleLocations.size(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取车辆最新位置
|
||||
*
|
||||
* @param vehicleIds 车辆ID列表
|
||||
* @return 车辆位置映射
|
||||
*/
|
||||
public Map<String, VehicleLocation> batchGetLatestLocations(List<String> vehicleIds) {
|
||||
if (vehicleIds == null || vehicleIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> keys = vehicleIds.stream()
|
||||
.map(CacheConstants::buildVehicleLatestKey)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<VehicleLocation> locations = vehicleLocationRedisTemplate.opsForValue().multiGet(keys);
|
||||
|
||||
Map<String, VehicleLocation> resultMap = new HashMap<>();
|
||||
for (int i = 0; i < vehicleIds.size(); i++) {
|
||||
VehicleLocation location = locations.get(i);
|
||||
if (location != null) {
|
||||
resultMap.put(vehicleIds.get(i), location);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("批量获取车辆最新位置缓存: 请求数量={}, 命中数量={}",
|
||||
vehicleIds.size(), resultMap.size());
|
||||
return resultMap;
|
||||
} catch (Exception e) {
|
||||
log.warn("批量获取车辆最新位置缓存失败: 数量={}", vehicleIds.size(), e);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存车辆轨迹数据
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @param trajectory 轨迹数据
|
||||
*/
|
||||
public void cacheTrajectory(String vehicleId, LocalDateTime startTime, LocalDateTime endTime,
|
||||
List<VehicleLocation> trajectory) {
|
||||
try {
|
||||
long startTimestamp = startTime.toEpochSecond(ZoneOffset.UTC);
|
||||
long endTimestamp = endTime.toEpochSecond(ZoneOffset.UTC);
|
||||
String key = CacheConstants.buildVehicleTrajectoryKey(vehicleId, startTimestamp, endTimestamp);
|
||||
|
||||
objectRedisTemplate.opsForValue()
|
||||
.set(key, trajectory, CacheConstants.VEHICLE_TRAJECTORY_EXPIRE, TimeUnit.SECONDS);
|
||||
|
||||
log.debug("缓存车辆轨迹: vehicleId={}, 轨迹点数={}, 时间范围=[{}, {}]",
|
||||
vehicleId, trajectory.size(), startTime, endTime);
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存车辆轨迹失败: vehicleId={}", vehicleId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆轨迹数据
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 轨迹数据,不存在则返回null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<VehicleLocation> getTrajectory(String vehicleId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
try {
|
||||
long startTimestamp = startTime.toEpochSecond(ZoneOffset.UTC);
|
||||
long endTimestamp = endTime.toEpochSecond(ZoneOffset.UTC);
|
||||
String key = CacheConstants.buildVehicleTrajectoryKey(vehicleId, startTimestamp, endTimestamp);
|
||||
|
||||
Object result = objectRedisTemplate.opsForValue().get(key);
|
||||
if (result instanceof List) {
|
||||
List<VehicleLocation> trajectory = (List<VehicleLocation>) result;
|
||||
log.debug("获取车辆轨迹缓存: vehicleId={}, 轨迹点数={}", vehicleId, trajectory.size());
|
||||
return trajectory;
|
||||
}
|
||||
|
||||
log.debug("车辆轨迹缓存未命中: vehicleId={}", vehicleId);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取车辆轨迹缓存失败: vehicleId={}", vehicleId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除车辆相关的所有缓存
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
*/
|
||||
public void evictVehicleCache(String vehicleId) {
|
||||
try {
|
||||
// 删除最新位置缓存
|
||||
String latestKey = CacheConstants.buildVehicleLatestKey(vehicleId);
|
||||
vehicleLocationRedisTemplate.delete(latestKey);
|
||||
|
||||
// 删除轨迹缓存(使用模式匹配)
|
||||
String trajectoryPattern = CacheConstants.VEHICLE_TRAJECTORY_PREFIX + vehicleId + ":*";
|
||||
Set<String> trajectoryKeys = stringRedisTemplate.keys(trajectoryPattern);
|
||||
if (trajectoryKeys != null && !trajectoryKeys.isEmpty()) {
|
||||
objectRedisTemplate.delete(trajectoryKeys);
|
||||
}
|
||||
|
||||
log.debug("清除车辆缓存: vehicleId={}, 清除轨迹缓存数量={}",
|
||||
vehicleId, trajectoryKeys != null ? trajectoryKeys.size() : 0);
|
||||
} catch (Exception e) {
|
||||
log.warn("清除车辆缓存失败: vehicleId={}", vehicleId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查车辆最新位置缓存是否存在
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
* @return 是否存在
|
||||
*/
|
||||
public boolean hasLatestLocationCache(String vehicleId) {
|
||||
try {
|
||||
String key = CacheConstants.buildVehicleLatestKey(vehicleId);
|
||||
Boolean exists = vehicleLocationRedisTemplate.hasKey(key);
|
||||
return exists != null && exists;
|
||||
} catch (Exception e) {
|
||||
log.warn("检查车辆最新位置缓存失败: vehicleId={}", vehicleId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存中的活跃车辆列表
|
||||
*
|
||||
* @return 活跃车辆ID列表
|
||||
*/
|
||||
public List<String> getActiveVehicleIds() {
|
||||
try {
|
||||
String pattern = CacheConstants.VEHICLE_LATEST_PREFIX + "*";
|
||||
Set<String> keys = stringRedisTemplate.keys(pattern);
|
||||
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<String> vehicleIds = keys.stream()
|
||||
.map(key -> key.substring(CacheConstants.VEHICLE_LATEST_PREFIX.length()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.debug("获取活跃车辆列表: 数量={}", vehicleIds.size());
|
||||
return vehicleIds;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取活跃车辆列表失败", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的车辆位置缓存
|
||||
*
|
||||
* @param beforeTime 清理此时间之前的缓存
|
||||
*/
|
||||
public void cleanupExpiredCache(LocalDateTime beforeTime) {
|
||||
try {
|
||||
List<String> activeVehicleIds = getActiveVehicleIds();
|
||||
int cleanedCount = 0;
|
||||
|
||||
for (String vehicleId : activeVehicleIds) {
|
||||
VehicleLocation location = getLatestLocation(vehicleId);
|
||||
if (location != null && location.getTimestamp().isBefore(beforeTime)) {
|
||||
evictVehicleCache(vehicleId);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("清理过期车辆位置缓存: 清理数量={}, 基准时间={}", cleanedCount, beforeTime);
|
||||
} catch (Exception e) {
|
||||
log.warn("清理过期车辆位置缓存失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*
|
||||
* @return 缓存统计信息
|
||||
*/
|
||||
public Map<String, Object> getCacheStatistics() {
|
||||
try {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// 活跃车辆数量
|
||||
List<String> activeVehicles = getActiveVehicleIds();
|
||||
stats.put("activeVehicleCount", activeVehicles.size());
|
||||
|
||||
// 轨迹缓存数量
|
||||
String trajectoryPattern = CacheConstants.VEHICLE_TRAJECTORY_PREFIX + "*";
|
||||
Set<String> trajectoryKeys = stringRedisTemplate.keys(trajectoryPattern);
|
||||
stats.put("trajectoryCacheCount", trajectoryKeys != null ? trajectoryKeys.size() : 0);
|
||||
|
||||
// 缓存命中率(需要额外的计数器来实现)
|
||||
stats.put("cacheHitRate", "N/A");
|
||||
|
||||
return stats;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取缓存统计信息失败", e);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,288 @@
|
||||
package com.dongni.collisionavoidance.common.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.VehicleLocation;
|
||||
import com.dongni.collisionavoidance.common.model.repository.VehicleLocationRepository;
|
||||
import com.dongni.collisionavoidance.common.model.MovingObjectType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 车辆位置服务类 - 基于PostGIS的空间数据管理
|
||||
* 提供车辆位置的保存、查询、空间分析等功能
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class VehicleLocationService {
|
||||
|
||||
private final VehicleLocationRepository vehicleLocationRepository;
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
|
||||
/**
|
||||
* 保存车辆位置信息
|
||||
*/
|
||||
@Transactional
|
||||
public VehicleLocation saveVehicleLocation(VehicleLocation vehicleLocation) {
|
||||
try {
|
||||
VehicleLocation saved = vehicleLocationRepository.save(vehicleLocation);
|
||||
log.debug("保存车辆位置: vehicleId={}, location=({}, {})",
|
||||
saved.getVehicleId(),
|
||||
saved.getLocation().getX(),
|
||||
saved.getLocation().getY());
|
||||
return saved;
|
||||
} catch (Exception e) {
|
||||
log.error("保存车辆位置失败: vehicleId={}", vehicleLocation.getVehicleId(), e);
|
||||
throw new RuntimeException("保存车辆位置失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存车辆位置信息
|
||||
*/
|
||||
@Transactional
|
||||
public List<VehicleLocation> saveVehicleLocations(List<VehicleLocation> vehicleLocations) {
|
||||
try {
|
||||
List<VehicleLocation> saved = vehicleLocationRepository.saveAll(vehicleLocations);
|
||||
log.info("批量保存车辆位置: 数量={}", saved.size());
|
||||
return saved;
|
||||
} catch (Exception e) {
|
||||
log.error("批量保存车辆位置失败: 数量={}", vehicleLocations.size(), e);
|
||||
throw new RuntimeException("批量保存车辆位置失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建车辆位置记录
|
||||
*/
|
||||
public VehicleLocation createVehicleLocation(String vehicleId, MovingObjectType vehicleType,
|
||||
double longitude, double latitude,
|
||||
Double altitude, Double heading, Double speed) {
|
||||
Point location = geometryFactory.createPoint(new Coordinate(longitude, latitude));
|
||||
location.setSRID(4326); // 设置WGS84坐标系
|
||||
|
||||
VehicleLocation vehicleLocation = new VehicleLocation();
|
||||
vehicleLocation.setVehicleId(vehicleId);
|
||||
vehicleLocation.setVehicleType(vehicleType);
|
||||
vehicleLocation.setLocation(location);
|
||||
vehicleLocation.setAltitude(altitude);
|
||||
vehicleLocation.setHeading(heading);
|
||||
vehicleLocation.setSpeed(speed);
|
||||
vehicleLocation.setTimestamp(LocalDateTime.now());
|
||||
|
||||
return vehicleLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据车辆ID获取最新位置
|
||||
*/
|
||||
public Optional<VehicleLocation> getLatestVehicleLocation(String vehicleId) {
|
||||
try {
|
||||
return vehicleLocationRepository.findLatestByVehicleId(vehicleId);
|
||||
} catch (Exception e) {
|
||||
log.error("获取车辆最新位置失败: vehicleId={}", vehicleId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据车辆类型获取活跃车辆
|
||||
*/
|
||||
public List<VehicleLocation> getActiveVehiclesByType(String vehicleType, int minutesBack) {
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(minutesBack);
|
||||
try {
|
||||
return vehicleLocationRepository.findActiveByVehicleType(vehicleType, since);
|
||||
} catch (Exception e) {
|
||||
log.error("获取活跃车辆失败: vehicleType={}, minutesBack={}", vehicleType, minutesBack, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空间查询:获取指定半径范围内的车辆
|
||||
*/
|
||||
public List<VehicleLocation> getVehiclesWithinRadius(double longitude, double latitude,
|
||||
double radiusMeters, int minutesBack) {
|
||||
Point centerPoint = geometryFactory.createPoint(new Coordinate(longitude, latitude));
|
||||
centerPoint.setSRID(4326);
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(minutesBack);
|
||||
|
||||
try {
|
||||
return vehicleLocationRepository.findVehiclesWithinRadius(centerPoint, radiusMeters, since);
|
||||
} catch (Exception e) {
|
||||
log.error("半径查询失败: center=({}, {}), radius={}m", longitude, latitude, radiusMeters, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空间查询:获取指定区域内的车辆
|
||||
*/
|
||||
public List<VehicleLocation> getVehiclesInArea(String areaWkt, int minutesBack) {
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(minutesBack);
|
||||
try {
|
||||
return vehicleLocationRepository.findVehiclesInArea(areaWkt, since);
|
||||
} catch (Exception e) {
|
||||
log.error("区域查询失败: areaWkt={}", areaWkt, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆轨迹数据
|
||||
*/
|
||||
public List<VehicleLocation> getVehicleTrajectory(String vehicleId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
try {
|
||||
return vehicleLocationRepository.findVehicleTrajectory(vehicleId, startTime, endTime);
|
||||
} catch (Exception e) {
|
||||
log.error("获取车辆轨迹失败: vehicleId={}, 时间范围=[{}, {}]", vehicleId, startTime, endTime, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定高度范围内的车辆
|
||||
*/
|
||||
public List<VehicleLocation> getVehiclesByAltitudeRange(double minAltitude, double maxAltitude, int minutesBack) {
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(minutesBack);
|
||||
try {
|
||||
return vehicleLocationRepository.findVehiclesByAltitudeRange(minAltitude, maxAltitude, since);
|
||||
} catch (Exception e) {
|
||||
log.error("高度范围查询失败: 高度范围=[{}, {}]", minAltitude, maxAltitude, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高速移动的车辆
|
||||
*/
|
||||
public List<VehicleLocation> getHighSpeedVehicles(double speedThreshold, int minutesBack) {
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(minutesBack);
|
||||
try {
|
||||
return vehicleLocationRepository.findHighSpeedVehicles(speedThreshold, since);
|
||||
} catch (Exception e) {
|
||||
log.error("高速车辆查询失败: speedThreshold={}", speedThreshold, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的N个车辆
|
||||
*/
|
||||
public List<VehicleLocation> getNearestVehicles(double longitude, double latitude, int limit, int minutesBack) {
|
||||
Point referencePoint = geometryFactory.createPoint(new Coordinate(longitude, latitude));
|
||||
referencePoint.setSRID(4326);
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(minutesBack);
|
||||
|
||||
try {
|
||||
return vehicleLocationRepository.findNearestVehicles(referencePoint, since, limit);
|
||||
} catch (Exception e) {
|
||||
log.error("最近车辆查询失败: reference=({}, {}), limit={}", longitude, latitude, limit, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间段内的唯一车辆数量
|
||||
*/
|
||||
public long countUniqueVehiclesInTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
try {
|
||||
return vehicleLocationRepository.countUniqueVehiclesInTimeRange(startTime, endTime);
|
||||
} catch (Exception e) {
|
||||
log.error("车辆统计失败: 时间范围=[{}, {}]", startTime, endTime, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理历史数据
|
||||
*/
|
||||
@Transactional
|
||||
public int cleanupHistoricalData(LocalDateTime beforeTime) {
|
||||
try {
|
||||
int deletedCount = vehicleLocationRepository.deleteHistoricalData(beforeTime);
|
||||
log.info("清理历史数据完成: 删除记录数={}, 清理时间点={}", deletedCount, beforeTime);
|
||||
return deletedCount;
|
||||
} catch (Exception e) {
|
||||
log.error("清理历史数据失败: beforeTime={}", beforeTime, e);
|
||||
throw new RuntimeException("清理历史数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新车辆位置 (如果存在则更新,否则创建新记录)
|
||||
*/
|
||||
@Transactional
|
||||
public VehicleLocation updateOrCreateVehicleLocation(String vehicleId, MovingObjectType vehicleType,
|
||||
double longitude, double latitude,
|
||||
Double altitude, Double heading, Double speed) {
|
||||
VehicleLocation vehicleLocation = createVehicleLocation(vehicleId, vehicleType,
|
||||
longitude, latitude,
|
||||
altitude, heading, speed);
|
||||
vehicleLocation.setTimestamp(LocalDateTime.now());
|
||||
|
||||
return saveVehicleLocation(vehicleLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证车辆位置数据的有效性
|
||||
*/
|
||||
public boolean isValidVehicleLocation(double longitude, double latitude, Double altitude, Double heading, Double speed) {
|
||||
// 经度范围检查
|
||||
if (longitude < -180 || longitude > 180) {
|
||||
log.warn("无效的经度值: {}", longitude);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 纬度范围检查
|
||||
if (latitude < -90 || latitude > 90) {
|
||||
log.warn("无效的纬度值: {}", latitude);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 高度检查
|
||||
if (altitude != null && (altitude < -1000 || altitude > 50000)) {
|
||||
log.warn("无效的高度值: {}", altitude);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 航向检查
|
||||
if (heading != null && (heading < 0 || heading >= 360)) {
|
||||
log.warn("无效的航向值: {}", heading);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 速度检查
|
||||
if (speed != null && (speed < 0 || speed > 1000)) {
|
||||
log.warn("无效的速度值: {}", speed);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据车辆ID删除所有历史位置记录
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteVehicleLocations(String vehicleId) {
|
||||
try {
|
||||
vehicleLocationRepository.deleteAll(
|
||||
vehicleLocationRepository.findVehicleTrajectory(vehicleId,
|
||||
LocalDateTime.now().minusYears(10), LocalDateTime.now())
|
||||
);
|
||||
log.info("删除车辆位置记录: vehicleId={}", vehicleId);
|
||||
} catch (Exception e) {
|
||||
log.error("删除车辆位置记录失败: vehicleId={}", vehicleId, e);
|
||||
throw new RuntimeException("删除车辆位置记录失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package com.dongni.collisionavoidance.config;
|
||||
|
||||
import com.dongni.collisionavoidance.config.properties.AirportAreasProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(AirportAreasProperties.class)
|
||||
@PropertySource(value = "classpath:config/airport_areas.yaml", factory = YamlPropertySourceFactory.class)
|
||||
public class AirportAreaConfig {
|
||||
}
|
||||
@ -0,0 +1,243 @@
|
||||
package com.dongni.collisionavoidance.config;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.stat.Statistics;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 数据库性能配置和监控
|
||||
*
|
||||
* 功能:
|
||||
* 1. 连接池性能监控
|
||||
* 2. Hibernate统计信息收集
|
||||
* 3. 数据库健康检查
|
||||
* 4. PostGIS查询性能优化
|
||||
* 5. 定期性能报告
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class DatabasePerformanceConfig {
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory entityManagerFactory;
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
|
||||
|
||||
/**
|
||||
* 数据库健康检查指标
|
||||
*/
|
||||
@Bean
|
||||
public HealthIndicator databaseHealthIndicator() {
|
||||
return () -> {
|
||||
try {
|
||||
if (dataSource instanceof HikariDataSource hikariDataSource) {
|
||||
// HikariCP连接池状态检查
|
||||
if (hikariDataSource.isRunning()) {
|
||||
int activeConnections = hikariDataSource.getHikariPoolMXBean().getActiveConnections();
|
||||
int totalConnections = hikariDataSource.getHikariPoolMXBean().getTotalConnections();
|
||||
int idleConnections = hikariDataSource.getHikariPoolMXBean().getIdleConnections();
|
||||
|
||||
return Health.up()
|
||||
.withDetail("database", "PostgreSQL + PostGIS")
|
||||
.withDetail("connectionPool", "HikariCP")
|
||||
.withDetail("activeConnections", activeConnections)
|
||||
.withDetail("totalConnections", totalConnections)
|
||||
.withDetail("idleConnections", idleConnections)
|
||||
.withDetail("maxPoolSize", hikariDataSource.getMaximumPoolSize())
|
||||
.withDetail("isRunning", true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
return Health.down()
|
||||
.withDetail("database", "PostgreSQL + PostGIS")
|
||||
.withDetail("connectionPool", "Unknown or not running")
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("数据库健康检查失败", e);
|
||||
return Health.down()
|
||||
.withDetail("database", "PostgreSQL + PostGIS")
|
||||
.withDetail("error", e.getMessage())
|
||||
.build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用启动后初始化性能监控
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void initializePerformanceMonitoring() {
|
||||
log.info("初始化数据库性能监控...");
|
||||
|
||||
// 启用Hibernate统计
|
||||
enableHibernateStatistics();
|
||||
|
||||
// 启动连接池监控
|
||||
startConnectionPoolMonitoring();
|
||||
|
||||
// 启动定期性能报告
|
||||
startPerformanceReporting();
|
||||
|
||||
log.info("数据库性能监控初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用Hibernate统计信息收集
|
||||
*/
|
||||
private void enableHibernateStatistics() {
|
||||
try {
|
||||
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
|
||||
Statistics statistics = sessionFactory.getStatistics();
|
||||
statistics.setStatisticsEnabled(true);
|
||||
|
||||
log.info("Hibernate统计信息收集已启用");
|
||||
} catch (Exception e) {
|
||||
log.warn("启用Hibernate统计信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动连接池监控
|
||||
*/
|
||||
private void startConnectionPoolMonitoring() {
|
||||
if (dataSource instanceof HikariDataSource hikariDataSource) {
|
||||
scheduler.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
var poolMXBean = hikariDataSource.getHikariPoolMXBean();
|
||||
|
||||
int activeConnections = poolMXBean.getActiveConnections();
|
||||
int totalConnections = poolMXBean.getTotalConnections();
|
||||
int idleConnections = poolMXBean.getIdleConnections();
|
||||
int threadsAwaitingConnection = poolMXBean.getThreadsAwaitingConnection();
|
||||
|
||||
// 连接池使用率警告
|
||||
double usageRate = (double) activeConnections / hikariDataSource.getMaximumPoolSize();
|
||||
if (usageRate > 0.8) {
|
||||
log.warn("连接池使用率过高: {:.1%} ({}/{})",
|
||||
usageRate, activeConnections, hikariDataSource.getMaximumPoolSize());
|
||||
}
|
||||
|
||||
// 等待连接的线程警告
|
||||
if (threadsAwaitingConnection > 0) {
|
||||
log.warn("有 {} 个线程正在等待数据库连接", threadsAwaitingConnection);
|
||||
}
|
||||
|
||||
log.debug("连接池状态 - 活跃: {}, 总计: {}, 空闲: {}, 等待: {}",
|
||||
activeConnections, totalConnections, idleConnections, threadsAwaitingConnection);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("连接池监控失败", e);
|
||||
}
|
||||
}, 30, 30, TimeUnit.SECONDS);
|
||||
|
||||
log.info("HikariCP连接池监控已启动,监控间隔: 30秒");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定期性能报告
|
||||
*/
|
||||
private void startPerformanceReporting() {
|
||||
scheduler.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
generatePerformanceReport();
|
||||
} catch (Exception e) {
|
||||
log.error("生成性能报告失败", e);
|
||||
}
|
||||
}, 5, 5, TimeUnit.MINUTES);
|
||||
|
||||
log.info("数据库性能报告已启动,报告间隔: 5分钟");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成性能报告
|
||||
*/
|
||||
private void generatePerformanceReport() {
|
||||
StringBuilder report = new StringBuilder("\n=== 数据库性能报告 ===\n");
|
||||
|
||||
try {
|
||||
// Hibernate统计信息
|
||||
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
|
||||
Statistics stats = sessionFactory.getStatistics();
|
||||
|
||||
if (stats.isStatisticsEnabled()) {
|
||||
report.append("Hibernate统计信息:\n");
|
||||
report.append(" 查询执行次数: ").append(stats.getQueryExecutionCount()).append("\n");
|
||||
report.append(" 查询缓存命中次数: ").append(stats.getQueryCacheHitCount()).append("\n");
|
||||
report.append(" 查询缓存未命中次数: ").append(stats.getQueryCacheMissCount()).append("\n");
|
||||
report.append(" 二级缓存命中次数: ").append(stats.getSecondLevelCacheHitCount()).append("\n");
|
||||
report.append(" 二级缓存未命中次数: ").append(stats.getSecondLevelCacheMissCount()).append("\n");
|
||||
report.append(" 事务次数: ").append(stats.getTransactionCount()).append("\n");
|
||||
report.append(" 成功事务次数: ").append(stats.getSuccessfulTransactionCount()).append("\n");
|
||||
|
||||
// 计算缓存命中率
|
||||
long totalQueryCache = stats.getQueryCacheHitCount() + stats.getQueryCacheMissCount();
|
||||
if (totalQueryCache > 0) {
|
||||
double hitRate = (double) stats.getQueryCacheHitCount() / totalQueryCache * 100;
|
||||
report.append(" 查询缓存命中率: ").append(String.format("%.1f%%", hitRate)).append("\n");
|
||||
}
|
||||
|
||||
long totalSecondLevel = stats.getSecondLevelCacheHitCount() + stats.getSecondLevelCacheMissCount();
|
||||
if (totalSecondLevel > 0) {
|
||||
double hitRate = (double) stats.getSecondLevelCacheHitCount() / totalSecondLevel * 100;
|
||||
report.append(" 二级缓存命中率: ").append(String.format("%.1f%%", hitRate)).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
// 连接池信息
|
||||
if (dataSource instanceof HikariDataSource hikariDataSource) {
|
||||
var poolMXBean = hikariDataSource.getHikariPoolMXBean();
|
||||
report.append("HikariCP连接池信息:\n");
|
||||
report.append(" 活跃连接: ").append(poolMXBean.getActiveConnections()).append("\n");
|
||||
report.append(" 总连接数: ").append(poolMXBean.getTotalConnections()).append("\n");
|
||||
report.append(" 空闲连接: ").append(poolMXBean.getIdleConnections()).append("\n");
|
||||
report.append(" 等待连接的线程: ").append(poolMXBean.getThreadsAwaitingConnection()).append("\n");
|
||||
report.append(" 最大池大小: ").append(hikariDataSource.getMaximumPoolSize()).append("\n");
|
||||
}
|
||||
|
||||
report.append("======================\n");
|
||||
|
||||
log.info(report.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成性能报告时出错", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用关闭时清理资源
|
||||
*/
|
||||
public void destroy() {
|
||||
if (scheduler != null && !scheduler.isShutdown()) {
|
||||
scheduler.shutdown();
|
||||
try {
|
||||
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
log.info("性能监控线程池已关闭");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,34 +1,140 @@
|
||||
package com.dongni.collisionavoidance.config;
|
||||
|
||||
import com.dongni.collisionavoidance.dataCollector.model.VehicleLocationInfo;
|
||||
import com.dongni.collisionavoidance.common.model.spatial.VehicleLocation;
|
||||
import com.dongni.collisionavoidance.common.model.spatial.AirportArea;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis缓存配置类 - 支持PostGIS实体序列化
|
||||
* 提供多种RedisTemplate以支持不同的数据类型缓存
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* 通用ObjectMapper配置
|
||||
* 支持Java 8时间类型和PostGIS空间类型
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, VehicleLocationInfo> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
public ObjectMapper redisObjectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认的字符串RedisTemplate
|
||||
* 用于简单的字符串缓存
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public RedisTemplate<String, String> stringRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, String> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||
template.setKeySerializer(stringSerializer);
|
||||
template.setValueSerializer(stringSerializer);
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
template.setHashValueSerializer(stringSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 原有的VehicleLocationInfo RedisTemplate
|
||||
* 保持向后兼容性
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, VehicleLocationInfo> vehicleLocationInfoRedisTemplate(
|
||||
RedisConnectionFactory connectionFactory, ObjectMapper redisObjectMapper) {
|
||||
RedisTemplate<String, VehicleLocationInfo> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
|
||||
Jackson2JsonRedisSerializer<VehicleLocationInfo> serializer =
|
||||
new Jackson2JsonRedisSerializer<>(mapper, VehicleLocationInfo.class);
|
||||
new Jackson2JsonRedisSerializer<>(redisObjectMapper, VehicleLocationInfo.class);
|
||||
|
||||
template.setValueSerializer(serializer);
|
||||
template.setHashValueSerializer(serializer);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* PostGIS车辆位置实体RedisTemplate
|
||||
* 用于缓存VehicleLocation实体
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, VehicleLocation> vehicleLocationRedisTemplate(
|
||||
RedisConnectionFactory connectionFactory, ObjectMapper redisObjectMapper) {
|
||||
RedisTemplate<String, VehicleLocation> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
Jackson2JsonRedisSerializer<VehicleLocation> serializer =
|
||||
new Jackson2JsonRedisSerializer<>(redisObjectMapper, VehicleLocation.class);
|
||||
|
||||
template.setValueSerializer(serializer);
|
||||
template.setHashValueSerializer(serializer);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 机场区域实体RedisTemplate
|
||||
* 用于缓存AirportArea实体
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, AirportArea> airportAreaRedisTemplate(
|
||||
RedisConnectionFactory connectionFactory, ObjectMapper redisObjectMapper) {
|
||||
RedisTemplate<String, AirportArea> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
Jackson2JsonRedisSerializer<AirportArea> serializer =
|
||||
new Jackson2JsonRedisSerializer<>(redisObjectMapper, AirportArea.class);
|
||||
|
||||
template.setValueSerializer(serializer);
|
||||
template.setHashValueSerializer(serializer);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用对象RedisTemplate
|
||||
* 用于缓存查询结果、统计数据等通用对象
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> objectRedisTemplate(
|
||||
RedisConnectionFactory connectionFactory, ObjectMapper redisObjectMapper) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
Jackson2JsonRedisSerializer<Object> serializer =
|
||||
new Jackson2JsonRedisSerializer<>(redisObjectMapper, Object.class);
|
||||
|
||||
template.setValueSerializer(serializer);
|
||||
template.setHashValueSerializer(serializer);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
package com.dongni.collisionavoidance.config.properties;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ConfigurationProperties(prefix = "airport")
|
||||
public class AirportAreasProperties {
|
||||
private List<AreaProperties> areas;
|
||||
|
||||
public List<AreaProperties> getAreas() {
|
||||
return areas;
|
||||
}
|
||||
|
||||
public void setAreas(List<AreaProperties> areas) {
|
||||
this.areas = areas;
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package com.dongni.collisionavoidance.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AreaProperties {
|
||||
private String id;
|
||||
private String name;
|
||||
private String type;
|
||||
private Double speedLimit;
|
||||
private String purpose;
|
||||
private List<String> restrictions;
|
||||
private List<String> allowedVehicleTypes;
|
||||
private List<String> allowedAircraftTypes;
|
||||
private Double maxHeight;
|
||||
private Double maxWeight;
|
||||
private GeometryProperties geometry;
|
||||
private String activeTime;
|
||||
private String expiryTime;
|
||||
}
|
||||
@ -5,18 +5,19 @@ import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the geometry part of a road in the YAML configuration.
|
||||
* 几何属性配置类
|
||||
* 用于YAML配置文件中的几何数据解析
|
||||
*/
|
||||
@Data
|
||||
public class GeometryProperties {
|
||||
/**
|
||||
* The type of geometry, expected to be "LineString".
|
||||
* 几何类型(如 "LineString", "Polygon" 等)
|
||||
*/
|
||||
private String type;
|
||||
|
||||
|
||||
/**
|
||||
* List of coordinates defining the geometry.
|
||||
* For LineString, it's a list of [longitude, latitude] pairs.
|
||||
* 坐标点列表
|
||||
* 格式:[[经度1, 纬度1], [经度2, 纬度2], ...]
|
||||
*/
|
||||
private List<List<Double>> coordinates;
|
||||
}
|
||||
@ -1,22 +1,28 @@
|
||||
package com.dongni.collisionavoidance.dataCollector.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.*;
|
||||
import com.dongni.collisionavoidance.common.model.repository.MovingObjectRepository;
|
||||
import com.dongni.collisionavoidance.common.service.VehicleLocationService;
|
||||
import com.dongni.collisionavoidance.dataCollector.dao.DataCollectorDao;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* 数据采集服务 - 重构版本
|
||||
*
|
||||
* 主要变更:
|
||||
* 1. 移除内存存储依赖(MovingObjectRepository)
|
||||
* 2. 改为使用PostGIS的VehicleLocationService进行数据持久化
|
||||
* 3. 移除dataMap内存缓存
|
||||
* 4. 简化数据流程,直接存储到PostGIS数据库
|
||||
* 5. 保留定时采集机制和异常处理
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DataCollectorService {
|
||||
@ -34,17 +40,20 @@ public class DataCollectorService {
|
||||
@Value("${data.collector.disabled:false}")
|
||||
private boolean collectorDisabled;
|
||||
|
||||
// 线程安全队列(用于暂存原始数据)
|
||||
@Getter
|
||||
ConcurrentHashMap<String, List<Object>> dataMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private DataCollectorDao dataCollectorDao;
|
||||
|
||||
@Autowired
|
||||
private MovingObjectRepository movingObjectRepository;
|
||||
|
||||
private VehicleLocationService vehicleLocationService;
|
||||
|
||||
/**
|
||||
* 定时采集航空器数据
|
||||
*
|
||||
* 重构说明:
|
||||
* - 移除内存状态历史管理
|
||||
* - 直接使用VehicleLocationService保存到PostGIS
|
||||
* - 简化数据处理流程
|
||||
*/
|
||||
@Scheduled(fixedRateString = "${data.collector.interval}")
|
||||
public void collectAircraftData() {
|
||||
if (collectorDisabled) {
|
||||
@ -54,44 +63,53 @@ public class DataCollectorService {
|
||||
try {
|
||||
List<Aircraft> newAircrafts = dataCollectorDao.collectAircraftData(airportAircraftEndpoint, airportBaseUrl);
|
||||
if (newAircrafts.isEmpty()) {
|
||||
log.debug("未获取到航空器数据");
|
||||
return;
|
||||
}
|
||||
|
||||
for (Aircraft newAircraft : newAircrafts) {
|
||||
String flightNo = newAircraft.getFlightNo();
|
||||
Map<String, MovingObject> snapshot = movingObjectRepository.getTypeMapDirect(MovingObjectType.AIRCRAFT);
|
||||
Aircraft existingAircraft = (Aircraft) snapshot.get(flightNo);
|
||||
// 获取已存在的航空器(如果存在)
|
||||
if (existingAircraft != null) {
|
||||
// 更新现有航空器的状态
|
||||
MovementState currentState = new MovementState(
|
||||
existingAircraft.getCurrentPosition(),
|
||||
existingAircraft.getVelocity(),
|
||||
existingAircraft.getHeading(),
|
||||
existingAircraft.getTimestamp());
|
||||
|
||||
// 控制历史记录长度
|
||||
if (existingAircraft.getStateHistory().size() + 1 > existingAircraft.MAX_HISTORY) {
|
||||
existingAircraft.getStateHistory().removeLast();
|
||||
}
|
||||
existingAircraft.getStateHistory().addFirst(currentState);
|
||||
newAircraft.setStateHistory(existingAircraft.getStateHistory());
|
||||
log.info("开始处理 {} 条航空器数据", newAircrafts.size());
|
||||
|
||||
int savedCount = 0;
|
||||
for (Aircraft aircraft : newAircrafts) {
|
||||
try {
|
||||
// 提取基本位置信息
|
||||
String vehicleId = aircraft.getFlightNo();
|
||||
MovingObjectType vehicleType = MovingObjectType.AIRCRAFT;
|
||||
double longitude = aircraft.getCurrentPosition().getLongitude();
|
||||
double latitude = aircraft.getCurrentPosition().getLatitude();
|
||||
Double altitude = aircraft.getCurrentPosition().getAltitude();
|
||||
Double heading = aircraft.getHeading();
|
||||
Double speed = aircraft.getVelocity() != null ? aircraft.getVelocity().getSpeed() : null;
|
||||
|
||||
// 直接保存到PostGIS数据库
|
||||
vehicleLocationService.updateOrCreateVehicleLocation(
|
||||
vehicleId, vehicleType, longitude, latitude,
|
||||
altitude, heading, speed
|
||||
);
|
||||
savedCount++;
|
||||
|
||||
log.debug("成功保存航空器位置: {} (航班号: {})", vehicleId, aircraft.getFlightNo());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("保存航空器数据失败: {}", aircraft.getFlightNo(), e);
|
||||
}
|
||||
// 将List转为Map(flightNo -> Aircraft)
|
||||
Map<String, MovingObject> aircraftBatch = newAircrafts.stream()
|
||||
.collect(Collectors.toMap(
|
||||
Aircraft::getFlightNo,
|
||||
aircraft -> aircraft,
|
||||
(existing, replacement) -> existing,
|
||||
ConcurrentHashMap::new
|
||||
));
|
||||
movingObjectRepository.updateTypeAll(MovingObjectType.AIRCRAFT, aircraftBatch);
|
||||
}
|
||||
|
||||
log.info("航空器数据采集完成,成功保存 {}/{} 条记录", savedCount, newAircrafts.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集航空器数据异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时采集特种车辆数据
|
||||
*
|
||||
* 重构说明:
|
||||
* - 移除内存状态历史管理
|
||||
* - 直接使用VehicleLocationService保存到PostGIS
|
||||
* - 简化数据处理流程
|
||||
*/
|
||||
@Scheduled(fixedRateString = "${data.collector.interval}")
|
||||
@Async // 异步执行
|
||||
public void collectVehicleData() {
|
||||
@ -102,50 +120,137 @@ public class DataCollectorService {
|
||||
try {
|
||||
List<SpecialVehicle> vehicles = dataCollectorDao.collectVehicleData(airportVehicleEndpoint, airportBaseUrl);
|
||||
if (vehicles.isEmpty()) {
|
||||
log.debug("未获取到特种车辆数据");
|
||||
return;
|
||||
}
|
||||
|
||||
for (SpecialVehicle newVehicle : vehicles) {
|
||||
String vehicleNo = newVehicle.getVehicleNo();
|
||||
// 获取已存在的航空器(如果存在)
|
||||
Map<String, MovingObject> snapshot = movingObjectRepository.getSnapshot(MovingObjectType.SPECIAL_VEHICLE);
|
||||
SpecialVehicle specialVehicle = (SpecialVehicle) snapshot.get(vehicleNo);
|
||||
if (specialVehicle != null) {
|
||||
// 更新现有航空器的状态
|
||||
MovementState currentState = new MovementState(
|
||||
specialVehicle.getCurrentPosition(),
|
||||
specialVehicle.getVelocity(),
|
||||
specialVehicle.getHeading(),
|
||||
specialVehicle.getTimestamp()
|
||||
log.info("开始处理 {} 条特种车辆数据", vehicles.size());
|
||||
|
||||
int savedCount = 0;
|
||||
for (SpecialVehicle vehicle : vehicles) {
|
||||
try {
|
||||
// 提取基本位置信息
|
||||
String vehicleId = vehicle.getVehicleNo();
|
||||
MovingObjectType vehicleType = MovingObjectType.SPECIAL_VEHICLE;
|
||||
double longitude = vehicle.getCurrentPosition().getLongitude();
|
||||
double latitude = vehicle.getCurrentPosition().getLatitude();
|
||||
Double altitude = vehicle.getCurrentPosition().getAltitude();
|
||||
Double heading = vehicle.getHeading();
|
||||
Double speed = vehicle.getVelocity() != null ? vehicle.getVelocity().getSpeed() : null;
|
||||
|
||||
// 直接保存到PostGIS数据库
|
||||
vehicleLocationService.updateOrCreateVehicleLocation(
|
||||
vehicleId, vehicleType, longitude, latitude,
|
||||
altitude, heading, speed
|
||||
);
|
||||
|
||||
// 控制历史记录长度
|
||||
if (specialVehicle.getStateHistory().size() > specialVehicle.MAX_HISTORY) {
|
||||
specialVehicle.getStateHistory().removeLast();
|
||||
}
|
||||
specialVehicle.getStateHistory().addFirst(currentState);
|
||||
newVehicle.setStateHistory(specialVehicle.getStateHistory());
|
||||
savedCount++;
|
||||
|
||||
log.debug("成功保存特种车辆位置: {} (车牌号: {})", vehicleId, vehicle.getVehicleNo());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("保存特种车辆数据失败: {}", vehicle.getVehicleNo(), e);
|
||||
}
|
||||
}
|
||||
// 将List转为Map(vehicleNo -> SpecialVehicle)
|
||||
Map<String, MovingObject> vehicleBatch = vehicles.stream()
|
||||
.collect(Collectors.toMap(
|
||||
SpecialVehicle::getVehicleNo,
|
||||
vehicle -> vehicle,
|
||||
(existing, replacement) -> existing,
|
||||
ConcurrentHashMap::new
|
||||
));
|
||||
movingObjectRepository.updateTypeAll(MovingObjectType.SPECIAL_VEHICLE, vehicleBatch);
|
||||
|
||||
log.info("特种车辆数据采集完成,成功保存 {}/{} 条记录", savedCount, vehicles.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集车辆数据异常", e);
|
||||
log.error("采集特种车辆数据异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时采集无人车数据
|
||||
*
|
||||
* 新增方法:支持无人车数据采集
|
||||
*/
|
||||
@Scheduled(fixedRateString = "${data.collector.interval}")
|
||||
@Async // 异步执行
|
||||
public void collectUnmannedVehicleData() {
|
||||
if (collectorDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<UnmannedVehicle> unmannedVehicles = dataCollectorDao.getVehicleLocationInfo();
|
||||
if (unmannedVehicles.isEmpty()) {
|
||||
log.debug("未获取到无人车数据");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("开始处理 {} 条无人车数据", unmannedVehicles.size());
|
||||
|
||||
int savedCount = 0;
|
||||
for (UnmannedVehicle vehicle : unmannedVehicles) {
|
||||
try {
|
||||
// 提取基本位置信息
|
||||
String vehicleId = vehicle.getVehicleId();
|
||||
MovingObjectType vehicleType = MovingObjectType.UNMANNED_VEHICLE;
|
||||
double longitude = vehicle.getCurrentPosition().getLongitude();
|
||||
double latitude = vehicle.getCurrentPosition().getLatitude();
|
||||
Double altitude = vehicle.getCurrentPosition().getAltitude();
|
||||
Double heading = vehicle.getHeading();
|
||||
Double speed = vehicle.getVelocity() != null ? vehicle.getVelocity().getSpeed() : null;
|
||||
|
||||
// 直接保存到PostGIS数据库
|
||||
vehicleLocationService.updateOrCreateVehicleLocation(
|
||||
vehicleId, vehicleType, longitude, latitude,
|
||||
altitude, heading, speed
|
||||
);
|
||||
savedCount++;
|
||||
|
||||
log.debug("成功保存无人车位置: {} (车辆ID: {})", vehicleId, vehicle.getVehicleId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("保存无人车数据失败: {}", vehicle.getVehicleId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("无人车数据采集完成,成功保存 {}/{} 条记录", savedCount, unmannedVehicles.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集无人车数据异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据采集统计信息
|
||||
*
|
||||
* 提供数据采集的运行状态和统计信息
|
||||
*/
|
||||
public String getCollectionStats() {
|
||||
if (collectorDisabled) {
|
||||
return "数据采集服务已禁用";
|
||||
}
|
||||
|
||||
StringBuilder stats = new StringBuilder();
|
||||
stats.append("数据采集服务状态: 运行中\n");
|
||||
stats.append("数据源配置:\n");
|
||||
stats.append(" - 航空器API: ").append(airportBaseUrl).append(airportAircraftEndpoint).append("\n");
|
||||
stats.append(" - 特种车辆API: ").append(airportBaseUrl).append(airportVehicleEndpoint).append("\n");
|
||||
|
||||
try {
|
||||
// 获取数据库中的数据统计
|
||||
long aircraftCount = vehicleLocationService.getActiveVehiclesByType("AIRCRAFT", 60).size();
|
||||
long vehicleCount = vehicleLocationService.getActiveVehiclesByType("SPECIAL_VEHICLE", 60).size();
|
||||
long unmannedCount = vehicleLocationService.getActiveVehiclesByType("UNMANNED_VEHICLE", 60).size();
|
||||
|
||||
stats.append("最近1小时活跃车辆统计:\n");
|
||||
stats.append(" - 航空器: ").append(aircraftCount).append(" 条记录\n");
|
||||
stats.append(" - 特种车辆: ").append(vehicleCount).append(" 条记录\n");
|
||||
stats.append(" - 无人车: ").append(unmannedCount).append(" 条记录\n");
|
||||
|
||||
} catch (Exception e) {
|
||||
stats.append("获取统计信息失败: ").append(e.getMessage()).append("\n");
|
||||
}
|
||||
|
||||
return stats.toString();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
log.info("正在关闭数据采集服务...");
|
||||
// 清理资源
|
||||
dataMap.clear();
|
||||
log.info("数据采集服务已关闭");
|
||||
// 重构说明:移除内存缓存清理,因为已经不使用内存存储
|
||||
log.info("数据采集服务已关闭 - PostGIS存储模式");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,164 +0,0 @@
|
||||
package com.dongni.collisionavoidance.dataProcessing.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.MovingObject;
|
||||
import com.dongni.collisionavoidance.common.model.MovingObjectType;
|
||||
import com.dongni.collisionavoidance.common.model.repository.MovingObjectRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DataProcessor {
|
||||
|
||||
@Autowired
|
||||
private MovingObjectRepository movingObjectRepository;
|
||||
@Autowired
|
||||
private CoordinateSystemService coordinateSystemService;
|
||||
@Autowired
|
||||
private SpeedCalculationService speedCalculationService;
|
||||
@Resource
|
||||
private Executor processingExecutor;
|
||||
|
||||
@Value("${data.processor.enabled:true}")
|
||||
private boolean processorEnabled;
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
if (!processorEnabled) {
|
||||
log.info("数据处理器已禁用,不会启动处理线程");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("启动数据处理线程...");
|
||||
running.set(true);
|
||||
processingExecutor.execute(() -> {
|
||||
try {
|
||||
processLoop();
|
||||
} catch (Exception e) {
|
||||
log.error("数据处理线程异常终止", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
log.info("正在关闭数据处理器...");
|
||||
// 标记处理循环应该停止
|
||||
running.set(false);
|
||||
log.info("数据处理器已关闭");
|
||||
}
|
||||
|
||||
private void processLoop() {
|
||||
log.debug("进入数据处理循环");
|
||||
try {
|
||||
while (running.get() && !Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
// 添加超时机制和空值判断
|
||||
Map<MovingObjectType, Set<String>> delta = movingObjectRepository.pollUpdate(500, TimeUnit.MILLISECONDS);
|
||||
if (delta == null || delta.isEmpty()) {
|
||||
log.debug("未获取到数据更新,等待下一轮");
|
||||
continue;
|
||||
}
|
||||
|
||||
delta.forEach((objectType, ids) -> {
|
||||
// 添加集合空值检查
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
log.warn("接收到空ID集合,类型:{}", objectType);
|
||||
return;
|
||||
}
|
||||
// 获取该类型全量数据快照
|
||||
Map<String, MovingObject> snapshot = movingObjectRepository.getTypeMapDirect(objectType);
|
||||
|
||||
// 转换为待处理的数据列表
|
||||
List<MovingObject> dataList = new CopyOnWriteArrayList<>(snapshot.values());
|
||||
|
||||
log.debug("正在处理 {} 类型的 {} 条数据", objectType, dataList.size());
|
||||
|
||||
try {
|
||||
switch (objectType) {
|
||||
case AIRCRAFT -> processAircraftData(dataList);
|
||||
case SPECIAL_VEHICLE -> processVehicleData(dataList);
|
||||
case UNMANNED_VEHICLE -> processLocationData(dataList);
|
||||
default -> log.warn("未支持的数据类型: {}", objectType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("数据处理异常 [类型: {}]", objectType, e);
|
||||
}
|
||||
});
|
||||
log.debug("本次处理完成,共处理{}种类型更新", delta.size());
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("数据处理线程被中断");
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
log.error("数据处理过程发生错误", e);
|
||||
// 避免因错误导致CPU使用率飙升
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("数据处理循环发生未预期错误", e);
|
||||
}
|
||||
log.warn("退出数据处理循环");
|
||||
}
|
||||
|
||||
private void processAircraftData(List<MovingObject> dataList) {
|
||||
try {
|
||||
log.info("开始处理航空器数据,共{}条", dataList.size());
|
||||
convertToLocalCoordinate(dataList);
|
||||
speedCalculationService.preprocessData(dataList);
|
||||
} catch (Exception e) {
|
||||
log.error("航空器数据处理异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void processVehicleData(List<MovingObject> dataList) {
|
||||
try {
|
||||
log.info("开始处理特种车辆数据,共{}条", dataList.size());
|
||||
convertToLocalCoordinate(dataList);
|
||||
speedCalculationService.preprocessData(dataList);
|
||||
} catch (Exception e) {
|
||||
log.error("车辆数据处理异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void processLocationData(List<MovingObject> dataList) {
|
||||
try {
|
||||
log.info("开始处理无人载具数据,共{}条", dataList.size());
|
||||
convertToLocalCoordinate(dataList);
|
||||
speedCalculationService.preprocessData(dataList);
|
||||
} catch (Exception e) {
|
||||
log.error("位置数据处理异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 在convertToLocalCoordinate方法中添加调试日志
|
||||
private void convertToLocalCoordinate(List<MovingObject> rawData) throws Exception {
|
||||
log.debug("开始坐标转换,数据量:{}", rawData.size());
|
||||
for (MovingObject item : rawData) {
|
||||
double[] doubles = coordinateSystemService.convertToLocalCoordinate(item.getCurrentPosition().getLongitude(), item.getCurrentPosition().getLatitude());
|
||||
item.getVelocity().setX(doubles[0]);
|
||||
item.getVelocity().setY(doubles[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,27 +1,124 @@
|
||||
package com.dongni.collisionavoidance.webSocket.controller;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.MovingObject;
|
||||
import com.dongni.collisionavoidance.common.model.MovingObjectType;
|
||||
import com.dongni.collisionavoidance.common.model.repository.MovingObjectRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.dongni.collisionavoidance.common.model.spatial.VehicleLocation;
|
||||
import com.dongni.collisionavoidance.common.service.VehicleLocationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.SendTo;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* WebSocket地理位置控制器
|
||||
* 提供实时车辆位置数据的WebSocket接口,基于PostGIS数据库
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class GeopositionController {
|
||||
|
||||
private final VehicleLocationService vehicleLocationService;
|
||||
|
||||
@Autowired
|
||||
MovingObjectRepository movingObjectRepository;
|
||||
/**
|
||||
* 获取所有航空器的最新位置信息
|
||||
*
|
||||
* @return 航空器位置数据映射 (vehicleId -> VehicleLocation)
|
||||
*/
|
||||
@MessageMapping("/getGeoposition")
|
||||
@SendTo("/topic/geoSition")
|
||||
public ConcurrentHashMap<String, MovingObject> getGeosition() {
|
||||
System.out.println("接收到地理坐标请求");
|
||||
ConcurrentHashMap<String, MovingObject> typeMapDirect = movingObjectRepository.getTypeMapDirect(MovingObjectType.AIRCRAFT);
|
||||
return typeMapDirect;
|
||||
public Map<String, VehicleLocation> getGeosition() {
|
||||
log.debug("接收到地理坐标请求 - 获取航空器位置数据");
|
||||
|
||||
try {
|
||||
// 获取最近5分钟内的航空器数据
|
||||
List<VehicleLocation> aircraftLocations = vehicleLocationService
|
||||
.getActiveVehiclesByType(MovingObjectType.AIRCRAFT.name(), 5);
|
||||
|
||||
// 转换为Map格式以保持兼容性
|
||||
Map<String, VehicleLocation> resultMap = aircraftLocations.stream()
|
||||
.collect(Collectors.toMap(
|
||||
VehicleLocation::getVehicleId,
|
||||
location -> location,
|
||||
(existing, replacement) -> replacement // 保留最新的记录
|
||||
));
|
||||
|
||||
log.debug("返回 {} 个航空器位置信息", resultMap.size());
|
||||
return resultMap;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取航空器地理位置数据时发生异常", e);
|
||||
return Map.of(); // 返回空映射而不是null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有类型车辆的最新位置信息
|
||||
*
|
||||
* @return 所有车辆位置数据映射 (vehicleId -> VehicleLocation)
|
||||
*/
|
||||
@MessageMapping("/getAllVehiclePositions")
|
||||
@SendTo("/topic/allVehiclePositions")
|
||||
public Map<String, VehicleLocation> getAllVehiclePositions() {
|
||||
log.debug("接收到全部车辆位置请求");
|
||||
|
||||
try {
|
||||
// 获取最近5分钟内的所有车辆数据(通过组合所有类型)
|
||||
List<VehicleLocation> allVehicles = java.util.stream.Stream.of(
|
||||
vehicleLocationService.getActiveVehiclesByType(MovingObjectType.AIRCRAFT.name(), 5),
|
||||
vehicleLocationService.getActiveVehiclesByType(MovingObjectType.SPECIAL_VEHICLE.name(), 5),
|
||||
vehicleLocationService.getActiveVehiclesByType(MovingObjectType.UNMANNED_VEHICLE.name(), 5)
|
||||
).flatMap(List::stream).collect(Collectors.toList());
|
||||
|
||||
// 转换为Map格式
|
||||
Map<String, VehicleLocation> resultMap = allVehicles.stream()
|
||||
.collect(Collectors.toMap(
|
||||
VehicleLocation::getVehicleId,
|
||||
location -> location,
|
||||
(existing, replacement) -> replacement
|
||||
));
|
||||
|
||||
log.debug("返回 {} 个车辆位置信息", resultMap.size());
|
||||
return resultMap;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取全部车辆地理位置数据时发生异常", e);
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据车辆类型获取位置信息
|
||||
*
|
||||
* @param vehicleType 车辆类型
|
||||
* @return 指定类型车辆位置数据
|
||||
*/
|
||||
@MessageMapping("/getVehiclesByType")
|
||||
@SendTo("/topic/vehiclesByType")
|
||||
public Map<String, VehicleLocation> getVehiclesByType(MovingObjectType vehicleType) {
|
||||
log.debug("接收到按类型查询车辆位置请求,类型: {}", vehicleType);
|
||||
|
||||
try {
|
||||
List<VehicleLocation> vehicles = vehicleLocationService
|
||||
.getActiveVehiclesByType(vehicleType.name(), 5);
|
||||
|
||||
Map<String, VehicleLocation> resultMap = vehicles.stream()
|
||||
.collect(Collectors.toMap(
|
||||
VehicleLocation::getVehicleId,
|
||||
location -> location,
|
||||
(existing, replacement) -> replacement
|
||||
));
|
||||
|
||||
log.debug("返回 {} 个 {} 类型车辆位置信息", resultMap.size(), vehicleType);
|
||||
return resultMap;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("根据类型获取车辆地理位置数据时发生异常,类型: {}", vehicleType, e);
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,44 @@ server:
|
||||
spring:
|
||||
application:
|
||||
name: CollisionAvoidance
|
||||
|
||||
# PostgreSQL数据源配置 - HikariCP连接池
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/collision_avoidance
|
||||
username: postgres
|
||||
password: 123456
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
# HikariCP连接池配置
|
||||
hikari:
|
||||
# 连接池基础配置
|
||||
maximum-pool-size: 20 # 最大连接数,适合PostGIS空间查询
|
||||
minimum-idle: 5 # 最小空闲连接数
|
||||
idle-timeout: 300000 # 空闲连接超时时间(5分钟)
|
||||
max-lifetime: 1200000 # 连接最大生命周期(20分钟)
|
||||
connection-timeout: 30000 # 获取连接超时时间(30秒)
|
||||
|
||||
# PostGIS空间查询优化
|
||||
leak-detection-threshold: 60000 # 连接泄漏检测阈值(1分钟)
|
||||
pool-name: "PostGIS-HikariPool" # 连接池名称
|
||||
|
||||
# 数据库连接优化
|
||||
data-source-properties:
|
||||
# PostgreSQL性能优化参数
|
||||
cachePrepStmts: true # 启用预编译语句缓存
|
||||
prepStmtCacheSize: 250 # 预编译语句缓存大小
|
||||
prepStmtCacheSqlLimit: 2048 # 缓存SQL长度限制
|
||||
useServerPrepStmts: true # 使用服务器端预编译语句
|
||||
|
||||
# PostGIS空间数据优化
|
||||
reWriteBatchedInserts: true # 重写批量插入SQL
|
||||
logUnclosedConnections: true # 记录未关闭的连接
|
||||
connectTimeout: 10 # TCP连接超时(秒)
|
||||
socketTimeout: 60 # Socket读取超时(秒)
|
||||
|
||||
# 应用程序标识
|
||||
ApplicationName: "CollisionAvoidanceSystem"
|
||||
|
||||
# Kafka配置
|
||||
kafka:
|
||||
bootstrap-servers: 192.168.42.128:9092
|
||||
@ -36,6 +74,48 @@ spring:
|
||||
value-serialization: org.springframework.data.redis.serialization.Jackson2JsonRedisSerializer
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
|
||||
# JPA/Hibernate配置优化
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false # 生产环境建议关闭
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.spatial.dialect.postgis.PostgisPG95Dialect
|
||||
format_sql: false # 生产环境建议关闭
|
||||
|
||||
# PostGIS空间数据处理优化
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
batch_size: 50 # 批量操作大小
|
||||
fetch_size: 50 # 查询获取大小
|
||||
|
||||
# 缓存配置
|
||||
cache:
|
||||
use_second_level_cache: true # 启用二级缓存
|
||||
use_query_cache: true # 启用查询缓存
|
||||
region:
|
||||
factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
|
||||
|
||||
# 性能优化
|
||||
order_inserts: true # 排序插入语句
|
||||
order_updates: true # 排序更新语句
|
||||
batch_versioned_data: true # 批量版本化数据处理
|
||||
|
||||
# 空间数据特定优化
|
||||
spatial:
|
||||
connection_finder: org.hibernate.spatial.dialect.postgis.PostgisConnectionFinder
|
||||
|
||||
# 性能监控和统计
|
||||
generate_statistics: true # 启用Hibernate统计
|
||||
log_slow_query: true # 记录慢查询
|
||||
|
||||
# 连接管理优化
|
||||
connection:
|
||||
provider_disables_autocommit: true # 优化连接池性能
|
||||
autocommit: false # 手动控制事务提交
|
||||
|
||||
# 数据采集配置
|
||||
data:
|
||||
@ -63,12 +143,10 @@ data:
|
||||
vehicle-location: /api/VehicleLocationInfo
|
||||
vehicle-state: /api/VehicleStateInfo
|
||||
vehicle-command: /api/VehicleCommandInfo
|
||||
# MongoDB配置
|
||||
mongodb:
|
||||
uri: mongodb://localhost:27017/vehicle_tracking
|
||||
|
||||
retention:
|
||||
redis-expire-seconds: 60
|
||||
mongodb-days: 30
|
||||
postgresql-days: 30
|
||||
# 坐标系统配置
|
||||
coordinate-system:
|
||||
airport:
|
||||
@ -77,7 +155,7 @@ coordinate-system:
|
||||
center-latitude: 36.35406879
|
||||
|
||||
# 数据保留策略配置
|
||||
# Actuator配置
|
||||
# 性能监控和管理配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
@ -86,6 +164,20 @@ management:
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
# 数据库连接池监控
|
||||
metrics:
|
||||
export:
|
||||
simple:
|
||||
enabled: true
|
||||
enable:
|
||||
hikari: true
|
||||
jvm: true
|
||||
|
||||
# JMX监控
|
||||
jmx:
|
||||
exposure:
|
||||
include: "*"
|
||||
logging:
|
||||
level:
|
||||
org:
|
||||
@ -93,9 +185,36 @@ logging:
|
||||
web:
|
||||
client:
|
||||
RestTemplate: DEBUG
|
||||
|
||||
http:
|
||||
converter:
|
||||
json: TRACE
|
||||
|
||||
# 数据库和连接池日志配置
|
||||
com:
|
||||
zaxxer:
|
||||
hikari: DEBUG # HikariCP连接池日志
|
||||
org:
|
||||
hibernate:
|
||||
SQL: DEBUG # SQL语句日志
|
||||
type:
|
||||
descriptor:
|
||||
sql:
|
||||
BasicBinder: TRACE # SQL参数绑定日志
|
||||
stat: DEBUG # Hibernate统计日志
|
||||
engine:
|
||||
transaction: DEBUG # 事务日志
|
||||
spatial: DEBUG # PostGIS空间查询日志
|
||||
|
||||
# PostGIS特定日志
|
||||
net:
|
||||
postgis: DEBUG # PostGIS JDBC日志
|
||||
|
||||
# JPA查询性能日志
|
||||
org.springframework.orm.jpa: DEBUG
|
||||
|
||||
# 日志输出格式优化
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{36}] - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{36}] - %msg%n"
|
||||
|
||||
|
||||
|
||||
19
src/main/resources/db/migration/V001__Enable_PostGIS.sql
Normal file
19
src/main/resources/db/migration/V001__Enable_PostGIS.sql
Normal file
@ -0,0 +1,19 @@
|
||||
-- 启用PostGIS扩展
|
||||
-- 需要在collision_avoidance数据库中手动执行或通过管理员权限执行
|
||||
|
||||
-- 创建PostGIS扩展(需要超级用户权限)
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
||||
|
||||
-- 验证PostGIS版本
|
||||
SELECT PostGIS_Version();
|
||||
|
||||
-- 创建空间参考系统(如果需要特定的坐标系统)
|
||||
-- 例如:中国CGCS2000坐标系统
|
||||
-- INSERT INTO spatial_ref_sys (srid, auth_name, auth_srid, proj4text, srtext) VALUES ...
|
||||
|
||||
-- 说明:
|
||||
-- 1. postgis: 核心PostGIS功能
|
||||
-- 2. postgis_topology: 拓扑功能支持
|
||||
-- 3. 执行此脚本需要数据库超级用户权限
|
||||
-- 4. 验证命令将显示PostGIS版本信息
|
||||
6
src/main/resources/sql/check_current_tables.sql
Normal file
6
src/main/resources/sql/check_current_tables.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- 检查当前表结构
|
||||
SELECT table_name, column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name IN ('vehicle_locations', 'airport_areas', 'vehicle_trajectories')
|
||||
ORDER BY table_name, ordinal_position;
|
||||
237
src/main/resources/sql/create_tables.sql
Normal file
237
src/main/resources/sql/create_tables.sql
Normal file
@ -0,0 +1,237 @@
|
||||
-- ============================================
|
||||
-- PostGIS空间数据表结构创建脚本
|
||||
-- 适用于车辆碰撞避免系统
|
||||
-- PostgreSQL 17 + PostGIS扩展
|
||||
-- ============================================
|
||||
|
||||
-- 确保PostGIS扩展已启用
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 车辆位置表 (vehicle_locations)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS vehicle_locations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vehicle_id VARCHAR(50) NOT NULL,
|
||||
vehicle_type VARCHAR(20) NOT NULL CHECK (vehicle_type IN ('AIRCRAFT', 'SPECIAL_VEHICLE', 'UNMANNED_VEHICLE')),
|
||||
|
||||
-- PostGIS空间字段 (WGS84坐标系)
|
||||
location GEOMETRY(POINT, 4326) NOT NULL,
|
||||
|
||||
-- 位置相关属性
|
||||
altitude DOUBLE PRECISION,
|
||||
heading DOUBLE PRECISION CHECK (heading >= 0 AND heading < 360),
|
||||
speed DOUBLE PRECISION CHECK (speed >= 0),
|
||||
|
||||
-- 时间戳
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 数据质量指标
|
||||
data_quality VARCHAR(20),
|
||||
|
||||
-- 审计字段
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 车辆位置表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle_id ON vehicle_locations(vehicle_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle_type ON vehicle_locations(vehicle_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_timestamp ON vehicle_locations(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_created_at ON vehicle_locations(created_at);
|
||||
|
||||
-- PostGIS空间索引 (GIST索引)
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_location_gist ON vehicle_locations USING GIST(location);
|
||||
|
||||
-- 复合索引优化查询性能
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_compound ON vehicle_locations(vehicle_id, timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_type_time ON vehicle_locations(vehicle_type, timestamp DESC);
|
||||
|
||||
-- ============================================
|
||||
-- 2. 机场区域表 (airport_areas)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS airport_areas (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
area_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(30) NOT NULL,
|
||||
description VARCHAR(500),
|
||||
|
||||
-- PostGIS空间字段 (支持POLYGON/MULTIPOLYGON)
|
||||
boundary GEOMETRY(POLYGON, 4326) NOT NULL,
|
||||
|
||||
-- 速度和限制
|
||||
speed_limit_kph DOUBLE PRECISION,
|
||||
restricted BOOLEAN DEFAULT false,
|
||||
allowed_vehicle_types JSONB,
|
||||
allowed_aircraft_types JSONB,
|
||||
max_height DOUBLE PRECISION,
|
||||
max_weight DOUBLE PRECISION,
|
||||
|
||||
-- 时间控制
|
||||
active_time TIMESTAMP WITH TIME ZONE,
|
||||
expiry_time TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- 区域属性
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
priority INTEGER NOT NULL DEFAULT 1 CHECK (priority >= 1),
|
||||
|
||||
-- 版本控制
|
||||
version BIGINT NOT NULL DEFAULT 1,
|
||||
|
||||
-- 审计字段
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 机场区域表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_area_id ON airport_areas(area_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_name ON airport_areas(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_type ON airport_areas(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_priority ON airport_areas(priority DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_enabled ON airport_areas(enabled);
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_version ON airport_areas(version);
|
||||
|
||||
-- PostGIS空间索引
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_boundary_gist ON airport_areas USING GIST(boundary);
|
||||
|
||||
-- JSONB索引 (用于车辆类型查询)
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_allowed_vehicles_gin ON airport_areas USING GIN(allowed_vehicle_types);
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_allowed_aircraft_gin ON airport_areas USING GIN(allowed_aircraft_types);
|
||||
|
||||
-- 复合索引
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_type_enabled ON airport_areas(type, enabled);
|
||||
CREATE INDEX IF NOT EXISTS idx_airport_areas_enabled_priority ON airport_areas(enabled, priority DESC);
|
||||
|
||||
-- ============================================
|
||||
-- 3. 车辆轨迹表 (vehicle_trajectories) - 可选的历史轨迹存储
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS vehicle_trajectories (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vehicle_id VARCHAR(50) NOT NULL,
|
||||
trajectory_date DATE NOT NULL,
|
||||
|
||||
-- PostGIS轨迹线 (LINESTRING)
|
||||
trajectory_line GEOMETRY(LINESTRING, 4326),
|
||||
|
||||
-- 统计信息
|
||||
total_distance DOUBLE PRECISION, -- 总距离 (米)
|
||||
max_speed DOUBLE PRECISION, -- 最大速度
|
||||
avg_speed DOUBLE PRECISION, -- 平均速度
|
||||
duration_seconds INTEGER, -- 持续时间 (秒)
|
||||
point_count INTEGER, -- 轨迹点数量
|
||||
|
||||
-- 时间范围
|
||||
start_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
end_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
|
||||
-- 审计字段
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 车辆轨迹表索引
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_trajectories_vehicle_id ON vehicle_trajectories(vehicle_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_trajectories_date ON vehicle_trajectories(trajectory_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_trajectories_start_time ON vehicle_trajectories(start_time DESC);
|
||||
|
||||
-- PostGIS轨迹空间索引
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_trajectories_line_gist ON vehicle_trajectories USING GIST(trajectory_line);
|
||||
|
||||
-- 复合索引
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_trajectories_compound ON vehicle_trajectories(vehicle_id, trajectory_date DESC);
|
||||
|
||||
-- ============================================
|
||||
-- 4. 时间分区设置 (车辆位置表按月分区)
|
||||
-- ============================================
|
||||
|
||||
-- 为车辆位置表创建按月分区
|
||||
-- 注意:这需要在数据库中手动执行,Spring Boot JPA不会自动创建分区
|
||||
|
||||
-- 创建分区表函数 (示例,需要根据实际需求调整)
|
||||
-- CREATE OR REPLACE FUNCTION create_monthly_partitions()
|
||||
-- RETURNS void AS $$
|
||||
-- DECLARE
|
||||
-- start_date date;
|
||||
-- end_date date;
|
||||
-- table_name text;
|
||||
-- BEGIN
|
||||
-- FOR i IN 0..12 LOOP -- 创建未来12个月的分区
|
||||
-- start_date := date_trunc('month', CURRENT_DATE + (i || ' month')::interval);
|
||||
-- end_date := start_date + interval '1 month';
|
||||
-- table_name := 'vehicle_locations_' || to_char(start_date, 'YYYY_MM');
|
||||
--
|
||||
-- EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF vehicle_locations
|
||||
-- FOR VALUES FROM (%L) TO (%L)',
|
||||
-- table_name, start_date, end_date);
|
||||
-- END LOOP;
|
||||
-- END;
|
||||
-- $$ LANGUAGE plpgsql;
|
||||
|
||||
-- ============================================
|
||||
-- 5. 触发器 (自动更新updated_at字段)
|
||||
-- ============================================
|
||||
|
||||
-- 创建更新时间戳函数
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 为车辆位置表创建触发器
|
||||
CREATE TRIGGER trigger_vehicle_locations_updated_at
|
||||
BEFORE UPDATE ON vehicle_locations
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 为机场区域表创建触发器
|
||||
CREATE TRIGGER trigger_airport_areas_updated_at
|
||||
BEFORE UPDATE ON airport_areas
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- ============================================
|
||||
-- 6. 数据清理策略 (可选)
|
||||
-- ============================================
|
||||
|
||||
-- 创建历史数据清理函数 (保留30天数据)
|
||||
CREATE OR REPLACE FUNCTION cleanup_old_vehicle_locations()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
deleted_count INTEGER;
|
||||
BEGIN
|
||||
DELETE FROM vehicle_locations
|
||||
WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '30 days';
|
||||
|
||||
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
||||
|
||||
-- 记录清理信息
|
||||
RAISE NOTICE 'Cleaned up % old vehicle location records', deleted_count;
|
||||
|
||||
RETURN deleted_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ============================================
|
||||
-- 7. 权限设置 (根据实际需求调整)
|
||||
-- ============================================
|
||||
|
||||
-- 为应用用户赋予必要权限
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO collision_avoidance_app;
|
||||
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO collision_avoidance_app;
|
||||
|
||||
-- ============================================
|
||||
-- 8. 示例数据 (可选,用于测试)
|
||||
-- ============================================
|
||||
|
||||
-- 插入示例机场区域
|
||||
-- INSERT INTO airport_areas (area_id, name, type, boundary, priority, speed_limit_kph, enabled, allowed_vehicle_types) VALUES
|
||||
-- ('runway_01', 'Runway 01', 'RUNWAY', ST_GeomFromText('POLYGON((116.5 39.8, 116.52 39.8, 116.52 39.81, 116.5 39.81, 116.5 39.8))', 4326), 10, 20.0, true, '["AIRCRAFT"]'),
|
||||
-- ('taxiway_a', 'Taxiway A', 'TAXIWAY', ST_GeomFromText('POLYGON((116.51 39.79, 116.515 39.79, 116.515 39.795, 116.51 39.795, 116.51 39.79))', 4326), 5, 15.0, true, '["AIRCRAFT", "SPECIAL_VEHICLE"]'),
|
||||
-- ('terminal_area', 'Terminal Area', 'TERMINAL', ST_GeomFromText('POLYGON((116.48 39.78, 116.50 39.78, 116.50 39.80, 116.48 39.80, 116.48 39.78))', 4326), 3, 10.0, true, '["SPECIAL_VEHICLE", "UNMANNED_VEHICLE"]');
|
||||
|
||||
-- 脚本完成
|
||||
-- ============================================
|
||||
174
src/main/resources/sql/drop_and_recreate_tables.sql
Normal file
174
src/main/resources/sql/drop_and_recreate_tables.sql
Normal file
@ -0,0 +1,174 @@
|
||||
-- ============================================
|
||||
-- 删除并重新创建PostGIS表结构脚本
|
||||
-- 注意:此操作将删除所有现有数据!
|
||||
-- 仅在开发环境使用
|
||||
-- ============================================
|
||||
|
||||
-- 1. 删除现有表(注意顺序,先删除有外键依赖的表)
|
||||
DROP TABLE IF EXISTS vehicle_trajectories CASCADE;
|
||||
DROP TABLE IF EXISTS airport_areas CASCADE;
|
||||
DROP TABLE IF EXISTS vehicle_locations CASCADE;
|
||||
|
||||
-- 2. 删除相关函数(如果存在)
|
||||
DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE;
|
||||
DROP FUNCTION IF EXISTS cleanup_old_vehicle_locations() CASCADE;
|
||||
|
||||
-- 3. 确保PostGIS扩展存在
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
||||
|
||||
-- 4. 重新创建车辆位置表
|
||||
CREATE TABLE vehicle_locations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vehicle_id VARCHAR(50) NOT NULL,
|
||||
vehicle_type VARCHAR(20) NOT NULL CHECK (vehicle_type IN ('AIRCRAFT', 'SPECIAL_VEHICLE', 'UNMANNED_VEHICLE')),
|
||||
|
||||
-- PostGIS空间字段 (WGS84坐标系)
|
||||
location GEOMETRY(POINT, 4326) NOT NULL,
|
||||
|
||||
-- 位置相关属性
|
||||
altitude DOUBLE PRECISION,
|
||||
heading DOUBLE PRECISION CHECK (heading >= 0 AND heading < 360),
|
||||
speed DOUBLE PRECISION CHECK (speed >= 0),
|
||||
|
||||
-- 时间戳
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 数据质量指标
|
||||
data_quality VARCHAR(20),
|
||||
|
||||
-- 审计字段
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 5. 重新创建机场区域表(使用新的字段结构)
|
||||
CREATE TABLE airport_areas (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
area_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(30) NOT NULL,
|
||||
description VARCHAR(500),
|
||||
|
||||
-- PostGIS空间字段 (支持POLYGON/MULTIPOLYGON)
|
||||
boundary GEOMETRY(POLYGON, 4326) NOT NULL,
|
||||
|
||||
-- 速度和限制
|
||||
speed_limit_kph DOUBLE PRECISION,
|
||||
restricted BOOLEAN DEFAULT false,
|
||||
allowed_vehicle_types JSONB,
|
||||
allowed_aircraft_types JSONB,
|
||||
max_height DOUBLE PRECISION,
|
||||
max_weight DOUBLE PRECISION,
|
||||
|
||||
-- 时间控制
|
||||
active_time TIMESTAMP WITH TIME ZONE,
|
||||
expiry_time TIMESTAMP WITH TIME ZONE,
|
||||
|
||||
-- 区域属性
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
priority INTEGER NOT NULL DEFAULT 1 CHECK (priority >= 1),
|
||||
|
||||
-- 版本控制
|
||||
version BIGINT NOT NULL DEFAULT 1,
|
||||
|
||||
-- 审计字段
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 6. 重新创建车辆轨迹表
|
||||
CREATE TABLE vehicle_trajectories (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vehicle_id VARCHAR(50) NOT NULL,
|
||||
trajectory_date DATE NOT NULL,
|
||||
|
||||
-- PostGIS轨迹线 (LINESTRING)
|
||||
trajectory_line GEOMETRY(LINESTRING, 4326),
|
||||
|
||||
-- 统计信息
|
||||
total_distance DOUBLE PRECISION, -- 总距离 (米)
|
||||
max_speed DOUBLE PRECISION, -- 最大速度
|
||||
avg_speed DOUBLE PRECISION, -- 平均速度
|
||||
duration_seconds INTEGER, -- 持续时间 (秒)
|
||||
point_count INTEGER, -- 轨迹点数量
|
||||
|
||||
-- 时间范围
|
||||
start_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
end_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
|
||||
-- 审计字段
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 7. 重新创建所有索引
|
||||
|
||||
-- 车辆位置表索引
|
||||
CREATE INDEX idx_vehicle_locations_vehicle_id ON vehicle_locations(vehicle_id);
|
||||
CREATE INDEX idx_vehicle_locations_vehicle_type ON vehicle_locations(vehicle_type);
|
||||
CREATE INDEX idx_vehicle_locations_timestamp ON vehicle_locations(timestamp DESC);
|
||||
CREATE INDEX idx_vehicle_locations_created_at ON vehicle_locations(created_at);
|
||||
CREATE INDEX idx_vehicle_locations_location_gist ON vehicle_locations USING GIST(location);
|
||||
CREATE INDEX idx_vehicle_locations_compound ON vehicle_locations(vehicle_id, timestamp DESC);
|
||||
CREATE INDEX idx_vehicle_locations_type_time ON vehicle_locations(vehicle_type, timestamp DESC);
|
||||
|
||||
-- 机场区域表索引
|
||||
CREATE INDEX idx_airport_areas_area_id ON airport_areas(area_id);
|
||||
CREATE INDEX idx_airport_areas_name ON airport_areas(name);
|
||||
CREATE INDEX idx_airport_areas_type ON airport_areas(type);
|
||||
CREATE INDEX idx_airport_areas_priority ON airport_areas(priority DESC);
|
||||
CREATE INDEX idx_airport_areas_enabled ON airport_areas(enabled);
|
||||
CREATE INDEX idx_airport_areas_version ON airport_areas(version);
|
||||
CREATE INDEX idx_airport_areas_boundary_gist ON airport_areas USING GIST(boundary);
|
||||
CREATE INDEX idx_airport_areas_allowed_vehicles_gin ON airport_areas USING GIN(allowed_vehicle_types);
|
||||
CREATE INDEX idx_airport_areas_allowed_aircraft_gin ON airport_areas USING GIN(allowed_aircraft_types);
|
||||
CREATE INDEX idx_airport_areas_type_enabled ON airport_areas(type, enabled);
|
||||
CREATE INDEX idx_airport_areas_enabled_priority ON airport_areas(enabled, priority DESC);
|
||||
|
||||
-- 车辆轨迹表索引
|
||||
CREATE INDEX idx_vehicle_trajectories_vehicle_id ON vehicle_trajectories(vehicle_id);
|
||||
CREATE INDEX idx_vehicle_trajectories_date ON vehicle_trajectories(trajectory_date DESC);
|
||||
CREATE INDEX idx_vehicle_trajectories_start_time ON vehicle_trajectories(start_time DESC);
|
||||
CREATE INDEX idx_vehicle_trajectories_line_gist ON vehicle_trajectories USING GIST(trajectory_line);
|
||||
CREATE INDEX idx_vehicle_trajectories_compound ON vehicle_trajectories(vehicle_id, trajectory_date DESC);
|
||||
|
||||
-- 8. 重新创建触发器函数
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 9. 重新创建触发器
|
||||
CREATE TRIGGER trigger_vehicle_locations_updated_at
|
||||
BEFORE UPDATE ON vehicle_locations
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER trigger_airport_areas_updated_at
|
||||
BEFORE UPDATE ON airport_areas
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 10. 重新创建数据清理函数
|
||||
CREATE OR REPLACE FUNCTION cleanup_old_vehicle_locations()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
deleted_count INTEGER;
|
||||
BEGIN
|
||||
DELETE FROM vehicle_locations
|
||||
WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '30 days';
|
||||
|
||||
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
||||
|
||||
-- 记录清理信息
|
||||
RAISE NOTICE 'Cleaned up % old vehicle location records', deleted_count;
|
||||
|
||||
RETURN deleted_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 脚本执行完成
|
||||
SELECT 'PostGIS表结构重新创建完成' as result;
|
||||
@ -1,11 +1,11 @@
|
||||
package com.dongni.collisionavoidance.areas.service;
|
||||
|
||||
import com.dongni.collisionavoidance.areas.model.AreaInfo;
|
||||
import com.dongni.collisionavoidance.areas.model.AreaType;
|
||||
import com.dongni.collisionavoidance.common.model.GeoPosition;
|
||||
import com.dongni.collisionavoidance.config.AirportAreaConfig;
|
||||
import com.dongni.collisionavoidance.common.model.spatial.AirportArea;
|
||||
import com.dongni.collisionavoidance.common.model.repository.AirportAreaRepository;
|
||||
import com.dongni.collisionavoidance.common.service.AirportAreaService;
|
||||
import com.dongni.collisionavoidance.config.TestConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -13,182 +13,300 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 机场区域服务的集成测试类
|
||||
* 确保区域配置正确加载,并且服务方法按预期工作
|
||||
* 机场区域服务的PostGIS集成测试类
|
||||
* 测试基于PostGIS数据库的区域管理和空间查询功能
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@Import({AirportAreaConfig.class, TestConfig.class})
|
||||
@TestPropertySource(locations = "classpath:config/airport_areas.yaml")
|
||||
@Import(TestConfig.class)
|
||||
@ActiveProfiles("test")
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
@Transactional
|
||||
class AirportAreaServiceIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private AirportAreaService airportAreaService;
|
||||
|
||||
@Autowired
|
||||
private AirportAreaRepository airportAreaRepository;
|
||||
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 清理测试数据
|
||||
airportAreaRepository.deleteAll();
|
||||
|
||||
// 创建测试区域数据
|
||||
createTestAreas();
|
||||
}
|
||||
|
||||
private void createTestAreas() {
|
||||
// 创建跑道区域
|
||||
AirportArea runway = createTestArea(
|
||||
"跑道区域",
|
||||
"RUNWAY",
|
||||
"用于航空器起降的主要跑道",
|
||||
createRectanglePolygon(116.123, 39.123, 116.124, 39.124),
|
||||
10,
|
||||
true,
|
||||
0.0
|
||||
);
|
||||
|
||||
// 创建滑行道区域
|
||||
AirportArea taxiway = createTestArea(
|
||||
"滑行道区域",
|
||||
"TAXIWAY",
|
||||
"连接跑道与机坪的滑行道",
|
||||
createRectanglePolygon(116.123, 39.1235, 116.1235, 39.124),
|
||||
5,
|
||||
false,
|
||||
25.0
|
||||
);
|
||||
|
||||
// 创建机坪区域
|
||||
AirportArea apron = createTestArea(
|
||||
"机坪区域",
|
||||
"APRON",
|
||||
"航空器停靠和地面服务区域",
|
||||
createRectanglePolygon(116.124, 39.123, 116.125, 39.124),
|
||||
1,
|
||||
false,
|
||||
15.0
|
||||
);
|
||||
|
||||
airportAreaRepository.saveAll(List.of(runway, taxiway, apron));
|
||||
}
|
||||
|
||||
private AirportArea createTestArea(String name, String type, String description,
|
||||
Polygon boundary, int priority, boolean restricted,
|
||||
Double speedLimit) {
|
||||
AirportArea area = new AirportArea();
|
||||
area.setName(name);
|
||||
area.setType(type);
|
||||
area.setDescription(description);
|
||||
area.setBoundary(boundary);
|
||||
area.setPriority(priority);
|
||||
area.setRestricted(restricted);
|
||||
area.setSpeedLimitKph(speedLimit);
|
||||
area.setEnabled(true);
|
||||
area.setCreatedAt(LocalDateTime.now());
|
||||
area.setUpdatedAt(LocalDateTime.now());
|
||||
return area;
|
||||
}
|
||||
|
||||
private Polygon createRectanglePolygon(double minLon, double minLat, double maxLon, double maxLat) {
|
||||
Coordinate[] coordinates = new Coordinate[]{
|
||||
new Coordinate(minLon, minLat),
|
||||
new Coordinate(maxLon, minLat),
|
||||
new Coordinate(maxLon, maxLat),
|
||||
new Coordinate(minLon, maxLat),
|
||||
new Coordinate(minLon, minLat) // 闭合多边形
|
||||
};
|
||||
|
||||
Polygon polygon = geometryFactory.createPolygon(coordinates);
|
||||
polygon.setSRID(4326);
|
||||
return polygon;
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextLoadsAndServiceIsInjected() {
|
||||
assertThat(airportAreaService).isNotNull();
|
||||
System.out.println("AirportAreaService 注入成功");
|
||||
assertThat(airportAreaRepository).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllAreas_shouldReturnAllAreas() {
|
||||
List<AreaInfo> areas = airportAreaService.getAllAreas();
|
||||
void createRectangularArea_shouldCreateArea_whenValidParametersProvided() {
|
||||
AirportArea area = airportAreaService.createRectangularArea(
|
||||
"测试区域", "TEST", "测试区域描述",
|
||||
116.0, 39.0, 116.1, 39.1,
|
||||
1, null
|
||||
);
|
||||
|
||||
assertThat(areas)
|
||||
.isNotNull()
|
||||
.isNotEmpty()
|
||||
.hasSize(2); // 配置文件中有两个区域:跑道区域和滑行道区域
|
||||
assertThat(area).isNotNull();
|
||||
assertThat(area.getName()).isEqualTo("测试区域");
|
||||
assertThat(area.getType()).isEqualTo("TEST");
|
||||
assertThat(area.getBoundary()).isNotNull();
|
||||
assertThat(area.getBoundary().getSRID()).isEqualTo(4326);
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveAirportArea_shouldPersistArea_whenValidArea() {
|
||||
AirportArea testArea = createTestArea(
|
||||
"新区域", "OTHER", "新建测试区域",
|
||||
createRectanglePolygon(116.2, 39.2, 116.3, 39.3),
|
||||
1, false, 30.0
|
||||
);
|
||||
|
||||
System.out.println("获取到 " + areas.size() + " 个区域");
|
||||
AirportArea saved = airportAreaService.saveAirportArea(testArea);
|
||||
|
||||
assertThat(saved).isNotNull();
|
||||
assertThat(saved.getId()).isNotNull();
|
||||
assertThat(saved.getName()).isEqualTo("新区域");
|
||||
|
||||
// 验证数据库中确实保存了
|
||||
Optional<AirportArea> found = airportAreaService.getAreaByName("新区域");
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getName()).isEqualTo("新区域");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAreaById_shouldReturnArea_whenIdExists() {
|
||||
String areaId = "1"; // 跑道区域的ID
|
||||
AreaInfo area = airportAreaService.getAreaById(areaId).orElse(null);
|
||||
|
||||
assertThat(area)
|
||||
void getActiveAreasByType_shouldReturnAreas_whenTypeExists() {
|
||||
List<AirportArea> runways = airportAreaService.getActiveAreasByType("RUNWAY");
|
||||
|
||||
assertThat(runways)
|
||||
.isNotNull()
|
||||
.satisfies(a -> {
|
||||
assertThat(a.getId()).isEqualTo("1");
|
||||
assertThat(a.getName()).isEqualTo("跑道区域");
|
||||
assertThat(a.getType()).isEqualTo(AreaType.RUNWAY);
|
||||
assertThat(a.getSpeedLimitKph()).isEqualTo(0.0);
|
||||
assertThat(a.getDescription()).isEqualTo("用于航空器起降的主要跑道");
|
||||
assertThat(a.isRestricted()).isTrue();
|
||||
assertThat(a.getAllowedVehicleTypes())
|
||||
.containsExactly("AIRCRAFT");
|
||||
assertThat(a.getAllowedAircraftTypes())
|
||||
.containsExactly("A320", "B737", "A330");
|
||||
assertThat(a.getMaxHeight()).isEqualTo(45.0);
|
||||
assertThat(a.getMaxWeight()).isEqualTo(400.0);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAreaById_shouldReturnNull_whenIdDoesNotExist() {
|
||||
String nonExistentId = "non-existent-area-id";
|
||||
AreaInfo area = airportAreaService.getAreaById(nonExistentId).orElse(null);
|
||||
|
||||
assertThat(area).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAreasByType_shouldReturnAreas_whenTypeExists() {
|
||||
AreaType type = AreaType.RUNWAY;
|
||||
List<AreaInfo> areas = airportAreaService.getAreasByType(type);
|
||||
|
||||
assertThat(areas)
|
||||
.isNotNull()
|
||||
.isNotEmpty()
|
||||
.hasSize(1)
|
||||
.allMatch(area -> type == area.getType());
|
||||
|
||||
// 验证返回的是跑道区域
|
||||
AreaInfo runwayArea = areas.get(0);
|
||||
assertThat(runwayArea)
|
||||
.satisfies(area -> {
|
||||
assertThat(area.getName()).isEqualTo("跑道区域");
|
||||
assertThat(area.getSpeedLimitKph()).isEqualTo(0.0);
|
||||
assertThat(area.getDescription()).isEqualTo("用于航空器起降的主要跑道");
|
||||
});
|
||||
.allMatch(area -> "RUNWAY".equals(area.getType()))
|
||||
.allMatch(AirportArea::getEnabled);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAreasByType_shouldReturnEmpty_whenTypeDoesNotExist() {
|
||||
AreaType nonExistentType = AreaType.RESTRICTED;
|
||||
List<AreaInfo> areas = airportAreaService.getAreasByType(nonExistentType);
|
||||
void getAreasContainingPoint_shouldReturnMatchingAreas_whenPointIsInside() {
|
||||
// 测试点在跑道区域内
|
||||
List<AirportArea> areas = airportAreaService.getAreasContainingPoint(116.1235, 39.1235);
|
||||
|
||||
assertThat(areas)
|
||||
.isNotNull()
|
||||
.isNotEmpty()
|
||||
.extracting(AirportArea::getType)
|
||||
.contains("RUNWAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAreasContainingPoint_shouldReturnEmpty_whenPointIsOutside() {
|
||||
// 测试点在所有区域外
|
||||
List<AirportArea> areas = airportAreaService.getAreasContainingPoint(116.5, 39.5);
|
||||
|
||||
assertThat(areas)
|
||||
.isNotNull()
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAreasContainingPoint_shouldReturnAllMatchingAreas_whenPointIsIn() {
|
||||
// 测试点在跑道和滑行道重叠区域
|
||||
GeoPosition position = new GeoPosition(39.123500, 116.123600, 0.0);
|
||||
List<AreaInfo> areas = airportAreaService.findAreasContainingPoint(position);
|
||||
void getHighestPriorityAreaContainingPoint_shouldReturnHighestPriority_whenMultipleAreasContainPoint() {
|
||||
// 在跑道和滑行道重叠区域放置测试点
|
||||
Optional<AirportArea> highestPriority = airportAreaService
|
||||
.getHighestPriorityAreaContainingPoint(116.1232, 39.1237);
|
||||
|
||||
assertThat(areas).isNotEmpty()
|
||||
.extracting(AreaInfo::getType)
|
||||
.contains(AreaType.RUNWAY, AreaType.TAXIWAY);
|
||||
|
||||
// 测试点在区域外
|
||||
position = new GeoPosition(39.124000, 116.124000, 0.0);
|
||||
areas = airportAreaService.findAreasContainingPoint(position);
|
||||
|
||||
assertThat(areas).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findDominantAreaAt_shouldReturnHighestPriorityArea_whenPointIsInMultipleAreas() {
|
||||
// 测试点在跑道和滑行道重叠区域,跑道应该是优先级最高的
|
||||
GeoPosition position = new GeoPosition(39.123500, 116.123600, 0.0);
|
||||
Optional<AreaInfo> dominantArea = airportAreaService.findDominantAreaAt(position);
|
||||
|
||||
assertThat(dominantArea)
|
||||
.isPresent()
|
||||
.map(AreaInfo::getType)
|
||||
.hasValue(AreaType.RUNWAY);
|
||||
|
||||
// 测试点在区域外
|
||||
position = new GeoPosition(39.124000, 116.124000, 0.0);
|
||||
dominantArea = airportAreaService.findDominantAreaAt(position);
|
||||
|
||||
assertThat(dominantArea).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSpeedLimitKphAt_shouldReturnSpeedLimit_whenPointIsInArea() {
|
||||
// 测试点在跑道和滑行道重叠区域,根据优先级应返回跑道的速度限制
|
||||
GeoPosition position = new GeoPosition(39.123500, 116.123600, 0.0);
|
||||
Double speedLimit = airportAreaService.getSpeedLimitKphAt(position);
|
||||
|
||||
assertThat(speedLimit)
|
||||
.isNotNull()
|
||||
.isEqualTo(0.0);
|
||||
|
||||
// 测试点在区域外
|
||||
position = new GeoPosition(39.124000, 116.124000, 0.0);
|
||||
speedLimit = airportAreaService.getSpeedLimitKphAt(position);
|
||||
|
||||
assertThat(speedLimit).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isPositionInRestrictedArea_shouldReturnTrue_whenPointIsInRestrictedArea() {
|
||||
// 测试点在跑道内
|
||||
GeoPosition position = new GeoPosition(39.123600, 116.123600, 0.0);
|
||||
boolean isRestricted = airportAreaService.isPositionInRestrictedArea(position);
|
||||
|
||||
assertThat(isRestricted).isTrue();
|
||||
|
||||
// 测试点在滑行道内
|
||||
position = new GeoPosition(39.123500, 116.123600, 0.0);
|
||||
isRestricted = airportAreaService.isPositionInRestrictedArea(position);
|
||||
|
||||
assertThat(isRestricted).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAreaActive_shouldReturnTrue_whenAreaIsActive() {
|
||||
Optional<AreaInfo> area = airportAreaService.getAreaById("1");
|
||||
|
||||
assertThat(area)
|
||||
assertThat(highestPriority)
|
||||
.isPresent()
|
||||
.hasValueSatisfying(a ->
|
||||
assertThat(airportAreaService.isAreaActive(a)).isTrue());
|
||||
.hasValueSatisfying(area -> {
|
||||
assertThat(area.getType()).isEqualTo("RUNWAY");
|
||||
assertThat(area.getPriority()).isEqualTo(10);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAreasWithinRadius_shouldReturnNearbyAreas_whenWithinRadius() {
|
||||
// 查找半径1000米内的区域
|
||||
List<AirportArea> nearbyAreas = airportAreaService
|
||||
.getAreasWithinRadius(116.123, 39.123, 1000.0);
|
||||
|
||||
assertThat(nearbyAreas)
|
||||
.isNotNull()
|
||||
.isNotEmpty()
|
||||
.hasSizeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNearestAreas_shouldReturnLimitedResults_whenLimitSpecified() {
|
||||
// 查找最近的2个区域
|
||||
List<AirportArea> nearest = airportAreaService
|
||||
.getNearestAreas(116.123, 39.123, 2);
|
||||
|
||||
assertThat(nearest)
|
||||
.isNotNull()
|
||||
.hasSizeLessThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectAreaOverlaps_shouldFindOverlaps_whenAreasOverlap() {
|
||||
// 创建与现有区域重叠的几何形状
|
||||
String overlappingWkt = "POLYGON((116.1235 39.1235, 116.125 39.1235, 116.125 39.125, 116.1235 39.125, 116.1235 39.1235))";
|
||||
|
||||
List<AirportArea> overlaps = airportAreaService.detectAreaOverlaps(1L, overlappingWkt);
|
||||
|
||||
assertThat(overlaps)
|
||||
.isNotNull();
|
||||
// 可能有重叠或没有,取决于具体的几何形状
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValidGeometry_shouldReturnTrue_whenGeometryIsValid() {
|
||||
String validWkt = "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))";
|
||||
|
||||
boolean isValid = airportAreaService.isValidGeometry(validWkt);
|
||||
|
||||
assertThat(isValid).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValidGeometry_shouldReturnFalse_whenGeometryIsInvalid() {
|
||||
String invalidWkt = "INVALID_WKT";
|
||||
|
||||
boolean isValid = airportAreaService.isValidGeometry(invalidWkt);
|
||||
|
||||
assertThat(isValid).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllActiveAreas_shouldReturnAllEnabledAreas() {
|
||||
List<AirportArea> allActive = airportAreaService.getAllActiveAreas();
|
||||
|
||||
assertThat(allActive)
|
||||
.isNotNull()
|
||||
.hasSizeGreaterThanOrEqualTo(3) // 至少有setUp中创建的3个区域
|
||||
.allMatch(AirportArea::getEnabled);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAreasByPriorityRange_shouldReturnAreasInRange() {
|
||||
List<AirportArea> midPriorityAreas = airportAreaService.getAreasByPriorityRange(1, 5);
|
||||
|
||||
assertThat(midPriorityAreas)
|
||||
.isNotNull()
|
||||
.allMatch(area -> area.getPriority() >= 1 && area.getPriority() <= 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAreasIntersectingGeometry_shouldReturnIntersectingAreas_whenGeometryIntersects() {
|
||||
// 创建与测试区域相交的线几何
|
||||
String intersectingLineWkt = "LINESTRING(116.1225 39.1225, 116.1245 39.1245)";
|
||||
|
||||
List<AirportArea> intersecting = airportAreaService
|
||||
.getAreasIntersectingGeometry(intersectingLineWkt);
|
||||
|
||||
assertThat(intersecting)
|
||||
.isNotNull();
|
||||
// 应该找到与线相交的区域
|
||||
}
|
||||
|
||||
@Test
|
||||
void repository_spatialQueries_shouldWork() {
|
||||
// 直接测试Repository的空间查询功能
|
||||
long totalCount = airportAreaRepository.count();
|
||||
assertThat(totalCount).isGreaterThanOrEqualTo(3);
|
||||
|
||||
// 测试按类型查找
|
||||
List<AirportArea> runways = airportAreaRepository.findActiveAreasByType("RUNWAY");
|
||||
assertThat(runways).hasSize(1);
|
||||
|
||||
// 测试按名称查找
|
||||
Optional<AirportArea> runway = airportAreaRepository.findByAreaName("跑道区域");
|
||||
assertThat(runway).isPresent();
|
||||
assertThat(runway.get().getType()).isEqualTo("RUNWAY");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,323 @@
|
||||
package com.dongni.collisionavoidance.common.repository;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.AirportArea;
|
||||
import com.dongni.collisionavoidance.common.model.repository.AirportAreaRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* AirportAreaRepository 单元测试
|
||||
* 测试PostGIS空间查询功能和区域数据持久化
|
||||
*/
|
||||
@DataJpaTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
class AirportAreaRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private AirportAreaRepository repository;
|
||||
|
||||
@Autowired
|
||||
private TestEntityManager entityManager;
|
||||
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
private AirportArea runwayArea;
|
||||
private AirportArea taxiwayArea;
|
||||
private AirportArea apronArea;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 清理测试数据
|
||||
repository.deleteAll();
|
||||
|
||||
// 创建测试区域数据
|
||||
runwayArea = createTestArea(
|
||||
"R01", "跑道01", "RUNWAY", "主跑道区域",
|
||||
createRectanglePolygon(116.123, 39.123, 116.124, 39.124),
|
||||
10, true, 0.0
|
||||
);
|
||||
|
||||
taxiwayArea = createTestArea(
|
||||
"T01", "滑行道A", "TAXIWAY", "连接跑道的滑行道",
|
||||
createRectanglePolygon(116.124, 39.124, 116.125, 39.125),
|
||||
5, false, 25.0
|
||||
);
|
||||
|
||||
apronArea = createTestArea(
|
||||
"A01", "机坪1号", "APRON", "航空器停靠区域",
|
||||
createRectanglePolygon(116.125, 39.125, 116.126, 39.126),
|
||||
1, false, 15.0
|
||||
);
|
||||
|
||||
// 保存测试数据
|
||||
repository.save(runwayArea);
|
||||
repository.save(taxiwayArea);
|
||||
repository.save(apronArea);
|
||||
entityManager.flush();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试根据区域ID查找")
|
||||
void testFindByAreaId() {
|
||||
Optional<AirportArea> found = repository.findByAreaId("R01");
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getAreaId()).isEqualTo("R01");
|
||||
assertThat(found.get().getName()).isEqualTo("跑道01");
|
||||
assertThat(found.get().getType()).isEqualTo("RUNWAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试根据区域名称查找")
|
||||
void testFindByAreaName() {
|
||||
Optional<AirportArea> found = repository.findByAreaName("跑道01");
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getName()).isEqualTo("跑道01");
|
||||
assertThat(found.get().getType()).isEqualTo("RUNWAY");
|
||||
assertThat(found.get().getRestricted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试空间查询 - 查找包含指定点的区域")
|
||||
void testFindAreasContainingPoint() {
|
||||
// 测试点在跑道区域内 (116.1235, 39.1235)
|
||||
Point testPoint = geometryFactory.createPoint(new Coordinate(116.1235, 39.1235));
|
||||
testPoint.setSRID(4326);
|
||||
|
||||
List<AirportArea> areas = repository.findAreasContainingPoint(testPoint);
|
||||
|
||||
assertThat(areas).hasSize(1);
|
||||
assertThat(areas.get(0).getName()).isEqualTo("跑道01");
|
||||
assertThat(areas.get(0).getType()).isEqualTo("RUNWAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试空间查询 - 点在区域外")
|
||||
void testFindAreasContainingPoint_OutsideAllAreas() {
|
||||
// 测试点在所有区域外
|
||||
Point testPoint = geometryFactory.createPoint(new Coordinate(116.5, 39.5));
|
||||
testPoint.setSRID(4326);
|
||||
|
||||
List<AirportArea> areas = repository.findAreasContainingPoint(testPoint);
|
||||
|
||||
assertThat(areas).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试查找最高优先级区域")
|
||||
void testFindHighestPriorityAreaContainingPoint() {
|
||||
// 测试点在跑道区域内
|
||||
Point testPoint = geometryFactory.createPoint(new Coordinate(116.1235, 39.1235));
|
||||
testPoint.setSRID(4326);
|
||||
|
||||
Optional<AirportArea> area = repository.findHighestPriorityAreaContainingPoint(testPoint);
|
||||
|
||||
assertThat(area).isPresent();
|
||||
assertThat(area.get().getName()).isEqualTo("跑道01");
|
||||
assertThat(area.get().getPriority()).isEqualTo(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试距离范围查询")
|
||||
void testFindAreasWithinRadius() {
|
||||
// 创建中心点
|
||||
Point centerPoint = geometryFactory.createPoint(new Coordinate(116.124, 39.124));
|
||||
centerPoint.setSRID(4326);
|
||||
|
||||
// 查找1000米范围内的区域
|
||||
List<AirportArea> nearbyAreas = repository.findAreasWithinRadius(centerPoint, 1000.0);
|
||||
|
||||
assertThat(nearbyAreas).hasSizeGreaterThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试最近区域查询")
|
||||
void testFindNearestAreas() {
|
||||
// 创建参考点
|
||||
Point referencePoint = geometryFactory.createPoint(new Coordinate(116.124, 39.124));
|
||||
referencePoint.setSRID(4326);
|
||||
|
||||
// 查找最近的2个区域
|
||||
List<AirportArea> nearestAreas = repository.findNearestAreas(referencePoint, 2);
|
||||
|
||||
assertThat(nearestAreas).hasSize(2);
|
||||
// 结果应该按距离排序(最近的在前)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试根据类型统计数量")
|
||||
void testCountByType() {
|
||||
long runwayCount = repository.countByType("RUNWAY");
|
||||
long taxiwayCount = repository.countByType("TAXIWAY");
|
||||
long apronCount = repository.countByType("APRON");
|
||||
|
||||
assertThat(runwayCount).isEqualTo(1);
|
||||
assertThat(taxiwayCount).isEqualTo(1);
|
||||
assertThat(apronCount).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试根据启用状态统计数量")
|
||||
void testCountByEnabled() {
|
||||
long enabledCount = repository.countByEnabled(true);
|
||||
long disabledCount = repository.countByEnabled(false);
|
||||
|
||||
assertThat(enabledCount).isEqualTo(3);
|
||||
assertThat(disabledCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试基本的CRUD操作")
|
||||
void testBasicCrudOperations() {
|
||||
// 创建新区域
|
||||
AirportArea newArea = createTestArea(
|
||||
"TEST01", "测试区域", "OTHER", "临时测试区域",
|
||||
createRectanglePolygon(116.130, 39.130, 116.131, 39.131),
|
||||
1, false, 50.0
|
||||
);
|
||||
|
||||
// 保存
|
||||
AirportArea saved = repository.save(newArea);
|
||||
assertThat(saved.getId()).isNotNull();
|
||||
|
||||
// 查询
|
||||
Optional<AirportArea> found = repository.findById(saved.getId());
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getName()).isEqualTo("测试区域");
|
||||
|
||||
// 更新
|
||||
found.get().setSpeedLimitKph(60.0);
|
||||
AirportArea updated = repository.save(found.get());
|
||||
assertThat(updated.getSpeedLimitKph()).isEqualTo(60.0);
|
||||
|
||||
// 删除
|
||||
repository.delete(updated);
|
||||
Optional<AirportArea> deleted = repository.findById(updated.getId());
|
||||
assertThat(deleted).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试根据版本查找区域")
|
||||
void testFindAreasByVersion() {
|
||||
// 所有测试区域都有版本号(由@Version自动生成)
|
||||
List<AirportArea> areas = repository.findAreasByVersion(0L);
|
||||
|
||||
assertThat(areas).hasSizeGreaterThanOrEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试PostGIS几何对象的正确性")
|
||||
void testPostGISGeometryCorrectness() {
|
||||
Optional<AirportArea> area = repository.findByAreaName("跑道01");
|
||||
assertThat(area).isPresent();
|
||||
|
||||
Polygon boundary = area.get().getBoundary();
|
||||
assertThat(boundary).isNotNull();
|
||||
assertThat(boundary.getSRID()).isEqualTo(4326); // WGS84坐标系
|
||||
assertThat(boundary.isValid()).isTrue();
|
||||
assertThat(boundary.getArea()).isGreaterThan(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试区域边界验证")
|
||||
void testAreaBoundaryValidation() {
|
||||
// 测试区域边界是否正确闭合
|
||||
Optional<AirportArea> area = repository.findByAreaName("跑道01");
|
||||
assertThat(area).isPresent();
|
||||
|
||||
Polygon boundary = area.get().getBoundary();
|
||||
Coordinate[] coordinates = boundary.getCoordinates();
|
||||
|
||||
// 第一个和最后一个坐标应该相同(闭合多边形)
|
||||
assertThat(coordinates[0]).isEqualTo(coordinates[coordinates.length - 1]);
|
||||
assertThat(coordinates.length).isGreaterThanOrEqualTo(4); // 至少4个点(矩形+闭合点)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试时间戳字段")
|
||||
void testTimestampFields() {
|
||||
Optional<AirportArea> area = repository.findByAreaName("跑道01");
|
||||
assertThat(area).isPresent();
|
||||
|
||||
AirportArea aa = area.get();
|
||||
assertThat(aa.getCreatedAt()).isNotNull();
|
||||
assertThat(aa.getUpdatedAt()).isNotNull();
|
||||
assertThat(aa.getUpdatedAt()).isAfterOrEqualTo(aa.getCreatedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试区域实体类的计算方法")
|
||||
void testAirportAreaComputedMethods() {
|
||||
Optional<AirportArea> area = repository.findByAreaName("跑道01");
|
||||
assertThat(area).isPresent();
|
||||
|
||||
AirportArea aa = area.get();
|
||||
|
||||
// 测试中心点计算
|
||||
Point centroid = aa.getCentroid();
|
||||
assertThat(centroid).isNotNull();
|
||||
assertThat(centroid.getX()).isBetween(116.0, 117.0);
|
||||
assertThat(centroid.getY()).isBetween(39.0, 40.0);
|
||||
|
||||
// 测试面积计算
|
||||
Double areaSize = aa.getArea();
|
||||
assertThat(areaSize).isNotNull();
|
||||
assertThat(areaSize).isGreaterThan(0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试用的机场区域对象
|
||||
*/
|
||||
private AirportArea createTestArea(String areaId, String name, String type, String description,
|
||||
Polygon boundary, int priority, boolean restricted,
|
||||
Double speedLimit) {
|
||||
AirportArea area = new AirportArea();
|
||||
area.setAreaId(areaId);
|
||||
area.setName(name);
|
||||
area.setType(type);
|
||||
area.setDescription(description);
|
||||
area.setBoundary(boundary);
|
||||
area.setPriority(priority);
|
||||
area.setRestricted(restricted);
|
||||
area.setSpeedLimitKph(speedLimit);
|
||||
area.setEnabled(true);
|
||||
area.setCreatedAt(LocalDateTime.now());
|
||||
area.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return area;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建矩形多边形
|
||||
*/
|
||||
private Polygon createRectanglePolygon(double minLon, double minLat, double maxLon, double maxLat) {
|
||||
Coordinate[] coordinates = new Coordinate[]{
|
||||
new Coordinate(minLon, minLat),
|
||||
new Coordinate(maxLon, minLat),
|
||||
new Coordinate(maxLon, maxLat),
|
||||
new Coordinate(minLon, maxLat),
|
||||
new Coordinate(minLon, minLat) // 闭合多边形
|
||||
};
|
||||
|
||||
Polygon polygon = geometryFactory.createPolygon(coordinates);
|
||||
polygon.setSRID(4326);
|
||||
return polygon;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,295 @@
|
||||
package com.dongni.collisionavoidance.common.repository;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.spatial.VehicleLocation;
|
||||
import com.dongni.collisionavoidance.common.model.repository.VehicleLocationRepository;
|
||||
import com.dongni.collisionavoidance.common.model.MovingObjectType;
|
||||
import com.dongni.collisionavoidance.common.model.MovementState;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.within;
|
||||
|
||||
/**
|
||||
* VehicleLocationRepository 单元测试
|
||||
* 测试PostGIS空间查询功能和数据持久化
|
||||
*/
|
||||
@DataJpaTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
class VehicleLocationRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private VehicleLocationRepository repository;
|
||||
|
||||
@Autowired
|
||||
private TestEntityManager entityManager;
|
||||
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
private VehicleLocation testVehicle1;
|
||||
private VehicleLocation testVehicle2;
|
||||
private VehicleLocation testVehicle3;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 清理测试数据
|
||||
repository.deleteAll();
|
||||
|
||||
// 创建测试车辆位置数据
|
||||
testVehicle1 = createTestVehicleLocation(
|
||||
"V001", MovingObjectType.AIRCRAFT,
|
||||
116.3974, 39.9042, 500.0, // 北京首都机场附近
|
||||
90.0, 250.0, LocalDateTime.now().minusMinutes(10)
|
||||
);
|
||||
|
||||
testVehicle2 = createTestVehicleLocation(
|
||||
"V002", MovingObjectType.SPECIAL_VEHICLE,
|
||||
116.3980, 39.9050, 0.0, // 附近位置
|
||||
180.0, 30.0, LocalDateTime.now().minusMinutes(5)
|
||||
);
|
||||
|
||||
testVehicle3 = createTestVehicleLocation(
|
||||
"V001", MovingObjectType.AIRCRAFT,
|
||||
116.4000, 39.9060, 520.0, // 同一车辆的新位置
|
||||
95.0, 260.0, LocalDateTime.now()
|
||||
);
|
||||
|
||||
// 保存测试数据
|
||||
repository.save(testVehicle1);
|
||||
repository.save(testVehicle2);
|
||||
repository.save(testVehicle3);
|
||||
entityManager.flush();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试根据车辆ID查找最新位置")
|
||||
void testFindLatestByVehicleId() {
|
||||
// 查找车辆V001的最新位置
|
||||
Optional<VehicleLocation> latest = repository.findLatestByVehicleId("V001");
|
||||
|
||||
assertThat(latest).isPresent();
|
||||
assertThat(latest.get().getVehicleId()).isEqualTo("V001");
|
||||
assertThat(latest.get().getTimestamp()).isEqualTo(testVehicle3.getTimestamp());
|
||||
assertThat(latest.get().getLocation().getX()).isCloseTo(116.4000, within(0.0001));
|
||||
assertThat(latest.get().getLocation().getY()).isCloseTo(39.9060, within(0.0001));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试空间范围查询 - 查找指定距离内的车辆")
|
||||
void testFindVehiclesWithinRadius() {
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(30);
|
||||
|
||||
// 创建中心点
|
||||
Point centerPoint = geometryFactory.createPoint(new Coordinate(116.3974, 39.9042));
|
||||
centerPoint.setSRID(4326);
|
||||
|
||||
// 查找北京首都机场附近1000米范围内的车辆
|
||||
List<VehicleLocation> nearbyVehicles = repository.findVehiclesWithinRadius(
|
||||
centerPoint, 1000.0, since
|
||||
);
|
||||
|
||||
assertThat(nearbyVehicles).isNotEmpty();
|
||||
assertThat(nearbyVehicles.size()).isGreaterThanOrEqualTo(2);
|
||||
|
||||
// 验证结果中包含预期的车辆
|
||||
boolean hasV001 = nearbyVehicles.stream()
|
||||
.anyMatch(v -> "V001".equals(v.getVehicleId()));
|
||||
boolean hasV002 = nearbyVehicles.stream()
|
||||
.anyMatch(v -> "V002".equals(v.getVehicleId()));
|
||||
|
||||
assertThat(hasV001).isTrue();
|
||||
assertThat(hasV002).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试车辆轨迹查询 - 根据时间范围查找轨迹")
|
||||
void testFindVehicleTrajectory() {
|
||||
LocalDateTime start = LocalDateTime.now().minusMinutes(20);
|
||||
LocalDateTime end = LocalDateTime.now().plusMinutes(5);
|
||||
|
||||
List<VehicleLocation> trajectory = repository.findVehicleTrajectory(
|
||||
"V001", start, end
|
||||
);
|
||||
|
||||
assertThat(trajectory).hasSize(2); // 车辆V001有两个位置记录
|
||||
assertThat(trajectory.get(0).getTimestamp())
|
||||
.isBefore(trajectory.get(1).getTimestamp()); // 按时间排序
|
||||
|
||||
// 验证轨迹数据的完整性
|
||||
assertThat(trajectory.get(0).getVehicleId()).isEqualTo("V001");
|
||||
assertThat(trajectory.get(1).getVehicleId()).isEqualTo("V001");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试基本的CRUD操作")
|
||||
void testBasicCrudOperations() {
|
||||
// 创建新的测试车辆
|
||||
VehicleLocation newVehicle = createTestVehicleLocation(
|
||||
"V999", MovingObjectType.UNMANNED_VEHICLE,
|
||||
116.3900, 39.9000, 0.0,
|
||||
45.0, 80.0, LocalDateTime.now()
|
||||
);
|
||||
|
||||
// 保存
|
||||
VehicleLocation saved = repository.save(newVehicle);
|
||||
assertThat(saved.getId()).isNotNull();
|
||||
|
||||
// 查询
|
||||
Optional<VehicleLocation> found = repository.findById(saved.getId());
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getVehicleId()).isEqualTo("V999");
|
||||
|
||||
// 更新
|
||||
found.get().setSpeed(90.0);
|
||||
VehicleLocation updated = repository.save(found.get());
|
||||
assertThat(updated.getSpeed()).isEqualTo(90.0);
|
||||
|
||||
// 删除
|
||||
repository.delete(updated);
|
||||
Optional<VehicleLocation> deleted = repository.findById(updated.getId());
|
||||
assertThat(deleted).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试根据车辆类型和时间查询活跃车辆")
|
||||
void testFindActiveByVehicleType() {
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(30);
|
||||
|
||||
// 查询活跃的航空器
|
||||
List<VehicleLocation> aircrafts = repository.findActiveByVehicleType("AIRCRAFT", since);
|
||||
assertThat(aircrafts).hasSizeGreaterThanOrEqualTo(2);
|
||||
|
||||
// 查询活跃的特勤车辆
|
||||
List<VehicleLocation> specialVehicles = repository.findActiveByVehicleType("SPECIAL_VEHICLE", since);
|
||||
assertThat(specialVehicles).hasSizeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试高度范围查询")
|
||||
void testFindVehiclesByAltitudeRange() {
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(30);
|
||||
|
||||
// 查询高度在400-600米之间的车辆
|
||||
List<VehicleLocation> highAltitudeVehicles = repository.findVehiclesByAltitudeRange(
|
||||
400.0, 600.0, since
|
||||
);
|
||||
|
||||
assertThat(highAltitudeVehicles).hasSizeGreaterThanOrEqualTo(2);
|
||||
assertThat(highAltitudeVehicles).allMatch(v ->
|
||||
v.getAltitude() >= 400.0 && v.getAltitude() <= 600.0
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试高速车辆查询")
|
||||
void testFindHighSpeedVehicles() {
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(30);
|
||||
|
||||
// 查询速度超过100的车辆
|
||||
List<VehicleLocation> highSpeedVehicles = repository.findHighSpeedVehicles(100.0, since);
|
||||
|
||||
assertThat(highSpeedVehicles).hasSizeGreaterThanOrEqualTo(2);
|
||||
assertThat(highSpeedVehicles).allMatch(v -> v.getSpeed() > 100.0);
|
||||
|
||||
// 验证按速度降序排列
|
||||
for (int i = 0; i < highSpeedVehicles.size() - 1; i++) {
|
||||
assertThat(highSpeedVehicles.get(i).getSpeed())
|
||||
.isGreaterThanOrEqualTo(highSpeedVehicles.get(i + 1).getSpeed());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试最近车辆查询")
|
||||
void testFindNearestVehicles() {
|
||||
LocalDateTime since = LocalDateTime.now().minusMinutes(30);
|
||||
|
||||
// 创建参考点
|
||||
Point referencePoint = geometryFactory.createPoint(new Coordinate(116.3974, 39.9042));
|
||||
referencePoint.setSRID(4326);
|
||||
|
||||
// 查找最近的2个车辆
|
||||
List<VehicleLocation> nearestVehicles = repository.findNearestVehicles(
|
||||
referencePoint, since, 2
|
||||
);
|
||||
|
||||
assertThat(nearestVehicles).hasSize(2);
|
||||
// 注意:由于我们设置了LIMIT,结果应该按距离排序
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试统计查询")
|
||||
void testCountUniqueVehiclesInTimeRange() {
|
||||
LocalDateTime start = LocalDateTime.now().minusMinutes(20);
|
||||
LocalDateTime end = LocalDateTime.now().plusMinutes(5);
|
||||
|
||||
long uniqueVehicleCount = repository.countUniqueVehiclesInTimeRange(start, end);
|
||||
|
||||
// 应该有2个不同的车辆:V001和V002
|
||||
assertThat(uniqueVehicleCount).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试PostGIS几何对象的正确性")
|
||||
void testPostGISGeometryCorrectness() {
|
||||
Optional<VehicleLocation> vehicle = repository.findLatestByVehicleId("V001");
|
||||
assertThat(vehicle).isPresent();
|
||||
|
||||
Point location = vehicle.get().getLocation();
|
||||
assertThat(location).isNotNull();
|
||||
assertThat(location.getSRID()).isEqualTo(4326); // WGS84坐标系
|
||||
assertThat(location.getX()).isBetween(116.0, 117.0);
|
||||
assertThat(location.getY()).isBetween(39.0, 40.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试数据质量和时间戳字段")
|
||||
void testDataQualityAndTimestamp() {
|
||||
Optional<VehicleLocation> vehicle = repository.findLatestByVehicleId("V001");
|
||||
assertThat(vehicle).isPresent();
|
||||
|
||||
VehicleLocation vl = vehicle.get();
|
||||
assertThat(vl.getDataQuality()).isEqualTo(MovementState.DataQuality.GOOD);
|
||||
assertThat(vl.getTimestamp()).isNotNull();
|
||||
assertThat(vl.getCreatedAt()).isNotNull();
|
||||
assertThat(vl.getUpdatedAt()).isNotNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试用的车辆位置对象
|
||||
*/
|
||||
private VehicleLocation createTestVehicleLocation(String vehicleId, MovingObjectType type,
|
||||
double longitude, double latitude, double altitude,
|
||||
double heading, double speed, LocalDateTime timestamp) {
|
||||
VehicleLocation location = new VehicleLocation();
|
||||
location.setVehicleId(vehicleId);
|
||||
location.setVehicleType(type);
|
||||
|
||||
// 创建PostGIS Point对象
|
||||
Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude));
|
||||
point.setSRID(4326);
|
||||
location.setLocation(point);
|
||||
|
||||
location.setAltitude(altitude);
|
||||
location.setHeading(heading);
|
||||
location.setSpeed(speed);
|
||||
location.setTimestamp(timestamp);
|
||||
location.setDataQuality(MovementState.DataQuality.GOOD);
|
||||
location.setCreatedAt(LocalDateTime.now());
|
||||
location.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return location;
|
||||
}
|
||||
}
|
||||
@ -7,12 +7,9 @@ import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.repository.MovingObjectRepository;
|
||||
// import com.dongni.collisionavoidance.common.model.repository.MovingObjectRepository; // 已删除
|
||||
import com.dongni.collisionavoidance.dataCollector.service.AuthService;
|
||||
import com.dongni.collisionavoidance.dataCollector.service.DataCollectorService;
|
||||
import com.dongni.collisionavoidance.dataProcessing.service.CoordinateSystemService;
|
||||
import com.dongni.collisionavoidance.dataProcessing.service.DataProcessor;
|
||||
import com.dongni.collisionavoidance.dataProcessing.service.SpeedCalculationService;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
@ -55,17 +52,17 @@ public class TestConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供一个不执行任何操作的数据处理器
|
||||
* 数据处理器已删除,不再需要
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public DataProcessor noOpDataProcessor(MovingObjectRepository movingObjectRepository,
|
||||
CoordinateSystemService coordinateSystemService,
|
||||
SpeedCalculationService speedCalculationService,
|
||||
Executor processingExecutor) {
|
||||
return new TestDataProcessor(movingObjectRepository, coordinateSystemService,
|
||||
speedCalculationService, processingExecutor);
|
||||
}
|
||||
// @Bean
|
||||
// @Primary
|
||||
// public DataProcessor noOpDataProcessor(MovingObjectRepository movingObjectRepository,
|
||||
// CoordinateSystemService coordinateSystemService,
|
||||
// SpeedCalculationService speedCalculationService,
|
||||
// Executor processingExecutor) {
|
||||
// return new TestDataProcessor(movingObjectRepository, coordinateSystemService,
|
||||
// speedCalculationService, processingExecutor);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 提供一个不进行HTTP请求的测试用认证服务
|
||||
@ -86,22 +83,22 @@ public class TestConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用数据处理器实现,不会启动实际的处理线程
|
||||
* 测试用数据处理器实现已删除,不再需要
|
||||
*/
|
||||
static class TestDataProcessor extends DataProcessor {
|
||||
public TestDataProcessor(MovingObjectRepository movingObjectRepository,
|
||||
CoordinateSystemService coordinateSystemService,
|
||||
SpeedCalculationService speedCalculationService,
|
||||
Executor processingExecutor) {
|
||||
super();
|
||||
// 注入依赖,但不启动后台线程
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
// 不启动处理线程
|
||||
}
|
||||
}
|
||||
// static class TestDataProcessor extends DataProcessor {
|
||||
// public TestDataProcessor(MovingObjectRepository movingObjectRepository,
|
||||
// CoordinateSystemService coordinateSystemService,
|
||||
// SpeedCalculationService speedCalculationService,
|
||||
// Executor processingExecutor) {
|
||||
// super();
|
||||
// // 注入依赖,但不启动后台线程
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void init() {
|
||||
// // 不启动处理线程
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 测试用认证服务实现,返回固定的测试令牌
|
||||
|
||||
@ -2,7 +2,7 @@ package com.dongni.collisionavoidance.roads.service;
|
||||
|
||||
import com.dongni.collisionavoidance.common.model.GeoPosition;
|
||||
import com.dongni.collisionavoidance.dataCollector.service.DataCollectorService;
|
||||
import com.dongni.collisionavoidance.dataProcessing.service.DataProcessor;
|
||||
// import com.dongni.collisionavoidance.dataProcessing.service.DataProcessor; // 已删除
|
||||
import com.dongni.collisionavoidance.roads.model.RoadInfo;
|
||||
import com.dongni.collisionavoidance.config.TestConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -43,8 +43,8 @@ class RoadNetworkServiceIntegrationTest {
|
||||
@Mock
|
||||
private DataCollectorService dataCollectorService;
|
||||
|
||||
@Mock
|
||||
private DataProcessor dataProcessor;
|
||||
// @Mock
|
||||
// private DataProcessor dataProcessor; // 已删除
|
||||
|
||||
@Test
|
||||
void contextLoadsAndServiceIsInjected() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user