增加了电子围栏告警功能,统一了数据采集间隔,优化了WebSocket推送频率
This commit is contained in:
parent
3f323361ef
commit
9279ff4802
114
CLAUDE.md
Normal file
114
CLAUDE.md
Normal file
@ -0,0 +1,114 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
QAUP-Management is an airport collision avoidance and management system built on the RuoYi framework. It integrates vehicle tracking, spatial analysis, and real-time monitoring for airport operations with Spring Boot 3.x and PostgreSQL + PostGIS.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Multi-Module Maven Structure
|
||||
- **qaup-admin**: Main web application entry point containing controllers and startup configuration
|
||||
- **qaup-collision**: Core collision avoidance system with spatial analysis, WebSocket communication, and real-time monitoring
|
||||
- **qaup-framework**: Infrastructure layer with security, caching, and common configurations
|
||||
- **qaup-system**: User management, RBAC, and system administration
|
||||
- **qaup-common**: Shared utilities, constants, and base classes
|
||||
- **qaup-quartz**: Scheduled job management
|
||||
- **qaup-generator**: Code generation utilities
|
||||
|
||||
### Key Integration Pattern
|
||||
The collision avoidance system integrates with RuoYi through `QuapDataAdapter` (qaup-collision/src/main/java/com/qaup/collision/common/adapter/QuapDataAdapter.java:37), which bridges the collision detection system with RuoYi's service layer, avoiding direct DAO dependencies.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Build and Run
|
||||
```bash
|
||||
# Full clean build (skip tests for faster builds)
|
||||
mvn clean install -DskipTests
|
||||
|
||||
# Start backend from qaup-admin module
|
||||
cd qaup-admin
|
||||
mvn spring-boot:run
|
||||
|
||||
# Start frontend (Vue.js)
|
||||
cd qaup-ui
|
||||
npm run dev
|
||||
|
||||
# Production deployment using script
|
||||
./ry.sh start
|
||||
```
|
||||
|
||||
### Testing and Debugging
|
||||
```bash
|
||||
# Check port usage
|
||||
lsof -ti:8080
|
||||
|
||||
# Kill process if needed
|
||||
kill -9 <process-id>
|
||||
|
||||
# View logs
|
||||
tail -f qaup-admin/app.log
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
### Backend
|
||||
- **Spring Boot 3.5.3** with Java 17
|
||||
- **PostgreSQL + PostGIS** for spatial data
|
||||
- **MyBatis** for database operations
|
||||
- **Redis** for caching and session management
|
||||
- **WebSocket** for real-time communication
|
||||
- **JTS + GeoTools** for spatial calculations
|
||||
|
||||
### Frontend
|
||||
- **Vue 2.6.12** with Element UI
|
||||
- **Axios** for API communication
|
||||
- **ECharts** for data visualization
|
||||
|
||||
## Configuration
|
||||
|
||||
### Database Setup
|
||||
1. PostgreSQL with PostGIS extension enabled
|
||||
2. Run initialization scripts in order:
|
||||
- `sql/create_qaup_database.sql`
|
||||
- `sql/create_sys_vehicle_info_table.sql`
|
||||
- `sql/create_sys_driver_info_table.sql`
|
||||
|
||||
### Key Configuration Files
|
||||
- `qaup-admin/src/main/resources/application.yml`: Main configuration including database, Redis, and collision system settings
|
||||
- `qaup-ui/vue.config.js`: Frontend build configuration
|
||||
- `qaup-collision/src/main/resources/config/`: Spatial configuration files (airport_areas.yaml, airport_roads.yaml)
|
||||
|
||||
## Important Development Patterns
|
||||
|
||||
### Data Access
|
||||
Always use `QuapDataAdapter` for accessing vehicle and driver data in collision module instead of direct service calls. This maintains clean separation between collision detection and system management.
|
||||
|
||||
### WebSocket Communication
|
||||
Real-time data flows through WebSocket endpoints configured in collision module. Messages use `/topic` prefix for broadcasting to connected clients.
|
||||
|
||||
### Spatial Data
|
||||
PostGIS integration handles geometric calculations. Coordinate system defaults to airport center at (120.0834104, 36.35406879).
|
||||
|
||||
### Performance Optimization
|
||||
- Data collection interval: 250ms for high-frequency updates
|
||||
- WebSocket push throttling: 1000ms to prevent frontend overload
|
||||
- Redis caching with 60-second expiration for real-time data
|
||||
|
||||
## API Access
|
||||
|
||||
- **Admin Interface**: http://localhost:8080
|
||||
- **API Documentation**: http://localhost:8080/swagger-ui/index.html
|
||||
- **WebSocket Endpoint**: ws://localhost:8080/ws
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Build Problems
|
||||
- If Maven build fails, ensure Java 17 is active
|
||||
- For dependency conflicts, use `mvn dependency:tree` to analyze
|
||||
|
||||
### Runtime Issues
|
||||
- Check Redis connectivity if caching fails
|
||||
- Verify PostGIS extension is properly installed for spatial operations
|
||||
- WebSocket connection issues often relate to CORS configuration in SecurityConfig
|
||||
@ -1 +1 @@
|
||||
0.3.5
|
||||
0.3.6
|
||||
175
changelog.md
175
changelog.md
@ -5,6 +5,181 @@
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
|
||||
版本规范基于 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [0.3.6] - 2025-07-12
|
||||
|
||||
### 🚀 **重大修复:违规消息重复问题解决**
|
||||
|
||||
- **问题背景**:
|
||||
- 前端收到大量重复的违规消息(同一秒内相同的违规被发送多次)
|
||||
- 鲁B579车辆的"无人车B区域准入控制"违规在同一秒内被发送了10+次
|
||||
- 鲁B567车辆的"无人车A区域准入控制"违规也出现类似问题
|
||||
|
||||
- **根本原因分析**:
|
||||
- **重复检测路径**:电子围栏检测和规则引擎检测都在处理相同的违规类型
|
||||
- **访问控制逻辑缺陷**:`checkAccessControl()` 方法总是返回 `VIOLATION`,没有实际的访问控制逻辑
|
||||
- **缺少去重机制**:同一个检测周期内,相同的违规可能被多次触发和记录
|
||||
|
||||
### ✨ **核心修复与优化**
|
||||
|
||||
1. **移除重复检测路径**:
|
||||
- 在 `DataCollectorService.performPeriodicViolationDetection()` 中移除独立的电子围栏检测调用
|
||||
- 统一使用规则引擎进行所有类型的违规检测(速度、区域访问、电子围栏等)
|
||||
- 清理不使用的依赖:移除 `UnmannedVehicleGeofenceService` 和相关导入
|
||||
|
||||
2. **改进访问控制逻辑**:
|
||||
- 修复 `RuleExecutionEngineImpl.checkAccessControl()` 方法,不再总是返回违规
|
||||
- 添加基于规则参数的访问控制检查:如果规则参数中有 `"allowAccess": true` 则允许访问
|
||||
- 增强错误处理和JSON参数解析功能
|
||||
|
||||
3. **实现短期去重机制**:
|
||||
```java
|
||||
// 防止同一检测周期内的重复违规
|
||||
private final Map<String, Long> recentViolationsCache = new HashMap<>();
|
||||
private static final long SHORT_TERM_DEDUP_WINDOW = 10_000L; // 10秒去重窗口
|
||||
```
|
||||
- **缓存格式**:`vehicleId:ruleName:violationType -> timestamp`
|
||||
- **去重逻辑**:10秒内相同的违规不重复处理
|
||||
- **自动清理**:每30秒清理过期缓存项
|
||||
|
||||
4. **完整的周期性数据保存**:
|
||||
- 实现 `saveUnmannedVehicleDataPeriodically()` 方法,将无人车位置数据按5秒周期批量保存
|
||||
- 使用 `VehicleDataPersistenceService.batchSaveUnmannedVehicleLocations()` 进行高效批量操作
|
||||
- 从1秒保存频率优化为5秒保存频率,减少数据库压力
|
||||
|
||||
### 🛡️ **电子围栏功能完善历程**
|
||||
|
||||
本版本标志着无人车电子围栏功能的全面成熟,从概念到实现经历了完整的开发和优化过程:
|
||||
|
||||
#### **1. 数据库架构设计与实现**
|
||||
- **机场区域表 (`airport_areas`)**:
|
||||
- 设计支持PostGIS几何边界的区域定义
|
||||
- 实现区域类型分类(RUNWAY、TAXIWAY、APRON、INTERSECTION_ZONE等)
|
||||
- 添加优先级、功能分类、访问级别等业务属性
|
||||
- 支持临时区域的生效/失效时间控制
|
||||
|
||||
- **空间规则表 (`spatial_rules`)**:
|
||||
- 实现规则与区域的关联机制
|
||||
- 支持多种规则类型(速度限制、访问控制、高度限制等)
|
||||
- 灵活的JSON参数配置系统
|
||||
- 完整的规则生命周期管理(创建、激活、禁用、过期)
|
||||
|
||||
- **数据库脚本完善**:
|
||||
- `sql/geofence_schema_update.sql`:核心表结构定义
|
||||
- `sql/add_speed_limit_rules.sql`:限速规则配置
|
||||
- `sql/geofence_validation.sql`:功能验证脚本
|
||||
- `sql/fix_duplicate_rules.sql`:重复规则清理
|
||||
|
||||
#### **2. 核心服务组件开发**
|
||||
- **UnmannedVehicleGeofenceService**:
|
||||
- 实现实时位置与区域边界的空间查询
|
||||
- 支持多区域重叠场景的处理
|
||||
- 区域访问权限检查和违规判定逻辑
|
||||
- 与规则执行引擎的集成接口
|
||||
|
||||
- **RuleExecutionEngine 完善**:
|
||||
- 统一的规则执行框架,支持多种规则类型
|
||||
- 灵活的参数配置和结果处理机制
|
||||
- 完善的异常处理和执行状态管理
|
||||
- 性能优化:批量检测和缓存机制
|
||||
|
||||
#### **3. 数据采集与检测频率优化**
|
||||
- **数据采集架构重构**:
|
||||
- 高频数据采集(1秒)确保数据新鲜度
|
||||
- 中频违规检测(5秒)平衡性能与实时性
|
||||
- 智能缓存机制减少重复计算
|
||||
|
||||
- **检测流程优化**:
|
||||
```
|
||||
数据采集(1s) → 内存缓存 → 周期性检测(5s) → 违规判定 → WebSocket推送
|
||||
```
|
||||
|
||||
#### **4. WebSocket消息系统集成**
|
||||
- **实时告警机制**:
|
||||
- 支持不同告警级别(INFO、WARNING、CRITICAL)
|
||||
- 标准化的违规消息格式
|
||||
- 前端实时接收和处理
|
||||
|
||||
- **消息去重优化**:
|
||||
- 短期去重窗口防止消息轰炸
|
||||
- 智能频率控制确保用户体验
|
||||
|
||||
#### **5. 系统集成与兼容性**
|
||||
- **与现有系统无缝集成**:
|
||||
- 兼容若依框架的用户权限体系
|
||||
- 复用车辆信息管理功能
|
||||
- 保持API接口的一致性
|
||||
|
||||
- **数据库兼容性**:
|
||||
- PostGIS空间扩展的完整支持
|
||||
- 与PostgreSQL原生功能的深度整合
|
||||
- 高效的空间索引和查询优化
|
||||
|
||||
### 🎯 **系统架构优化**
|
||||
|
||||
- **数据采集**: 1秒周期,仅更新内存缓存 (`activeMovingObjectsCache`)
|
||||
- **数据保存**: 5秒周期,批量保存无人车位置到数据库
|
||||
- **违规检测**: 5秒周期,统一的规则引擎检测 + 10秒去重窗口
|
||||
- **WebSocket消息**: 5秒周期,位置更新和违规消息统一发送
|
||||
|
||||
### 📋 **去重算法详解**
|
||||
|
||||
```java
|
||||
// 每次违规检测前的去重检查流程:
|
||||
1. 生成缓存键: "vehicleId:ruleName:violationType"
|
||||
2. 检查该键是否在10秒内已存在
|
||||
3. 如果存在则跳过,避免重复处理和发送
|
||||
4. 如果不存在则正常处理并更新缓存
|
||||
5. 每30秒自动清理过期缓存项
|
||||
```
|
||||
|
||||
### ✅ **验证结果**
|
||||
|
||||
- **位置更新消息**: 每5秒发送一次,频率正确 ✅
|
||||
- **违规消息**: 每5秒最多发送一次,10秒内不重复 ✅
|
||||
- **访问控制检查**: 不再总是返回违规,基于实际规则参数判断 ✅
|
||||
- **数据保存优化**: 从每秒保存改为每5秒批量保存 ✅
|
||||
- **系统性能**: 减少了不必要的重复处理和数据库操作 ✅
|
||||
- **电子围栏功能**: 完整支持区域准入控制、超速检测、违规告警 ✅
|
||||
|
||||
### 📋 **影响文件**
|
||||
|
||||
**核心服务层:**
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java`:
|
||||
- 移除重复的电子围栏检测路径
|
||||
- 实现 `saveUnmannedVehicleDataPeriodically()` 方法
|
||||
- 清理不使用的依赖和字段
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RuleExecutionEngineImpl.java`:
|
||||
- 改进 `checkAccessControl()` 访问控制逻辑
|
||||
- 添加短期去重缓存机制和相关辅助方法
|
||||
- 实现违规类型判断和缓存管理功能
|
||||
|
||||
**电子围栏相关:**
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/geofence/service/UnmannedVehicleGeofenceService.java`:电子围栏核心服务
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/geofence/model/entity/AirportArea.java`:机场区域实体
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/geofence/model/entity/SpatialRule.java`:空间规则实体
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/geofence/repository/AirportAreaRepository.java`:区域数据访问
|
||||
- `qaup-collision/src/main/java/com/qaup/collision/geofence/service/AirportAreaService.java`:区域管理服务
|
||||
|
||||
**数据库脚本:**
|
||||
- `sql/geofence_schema_update.sql`:电子围栏核心表结构
|
||||
- `sql/add_speed_limit_rules.sql`:限速规则配置
|
||||
- `sql/geofence_validation.sql`:功能验证脚本
|
||||
- `sql/fix_duplicate_rules.sql`:新增数据库重复规则清理脚本
|
||||
|
||||
**配置文件:**
|
||||
- `qaup-admin/src/main/resources/application.yml`:检测频率配置优化
|
||||
- `VERSION.md`:版本号更新为 `0.3.6`
|
||||
|
||||
### 🚀 **技术价值**
|
||||
|
||||
- **电子围栏功能成熟**: 从零到完整功能实现,支持复杂的机场区域管理需求
|
||||
- **消息准确性**: 彻底解决重复违规消息问题,提升前端用户体验
|
||||
- **系统性能**: 减少不必要的重复检测和数据库操作,提高整体性能
|
||||
- **架构清晰**: 统一违规检测入口,简化系统架构和维护复杂度
|
||||
- **数据一致性**: 批量保存和去重机制确保数据的一致性和完整性
|
||||
- **空间计算能力**: 完整的PostGIS集成,支持复杂的地理空间分析需求
|
||||
- **实时监控**: 高效的实时违规检测和告警机制,满足安全监控要求
|
||||
|
||||
## [0.3.5] - 2025-07-12
|
||||
|
||||
### ✨ **新功能与优化:路径冲突检测增强与告警体系精细化**
|
||||
|
||||
118
doc/configuration/data_collection_intervals.md
Normal file
118
doc/configuration/data_collection_intervals.md
Normal file
@ -0,0 +1,118 @@
|
||||
# 数据采集和检测频率配置说明
|
||||
|
||||
## 配置参数
|
||||
|
||||
### 1. 数据采集频率
|
||||
```properties
|
||||
data.collector.interval=250
|
||||
```
|
||||
- **用途**: 控制数据采集频率 (collectAircraftData, collectVehicleData, collectUnmannedVehicleData)
|
||||
- **频率**: 250ms (每秒4次)
|
||||
- **作用**: 从外部API获取最新的位置数据并更新内存缓存
|
||||
|
||||
### 2. 检测和WebSocket推送频率
|
||||
```properties
|
||||
data.collector.detection.interval=1000
|
||||
```
|
||||
- **用途**: 控制检测和WebSocket消息推送频率 (performPeriodicViolationDetection)
|
||||
- **频率**: 1000ms (每秒1次)
|
||||
- **作用**:
|
||||
- 执行电子围栏检测
|
||||
- 执行路径冲突检测
|
||||
- 执行实时违规检测(超速等)
|
||||
- 发送WebSocket位置更新消息
|
||||
|
||||
## 设计理念
|
||||
|
||||
### 高频数据采集 + 低频检测处理
|
||||
|
||||
1. **数据采集层** (250ms)
|
||||
- 快速获取最新位置数据
|
||||
- 更新内存缓存
|
||||
- 持久化到数据库
|
||||
- 不执行复杂计算
|
||||
|
||||
2. **检测处理层** (1000ms)
|
||||
- 基于缓存数据执行检测
|
||||
- 减少数据库查询压力
|
||||
- 避免过度的WebSocket消息推送
|
||||
- 提供平衡的响应性和性能
|
||||
|
||||
## 性能优势
|
||||
|
||||
### 减少计算负载
|
||||
- 围栏检测从每秒4次减少到每秒1次
|
||||
- 空间查询(PostGIS)频率降低75%
|
||||
- CPU使用率显著降低
|
||||
|
||||
### 减少网络流量
|
||||
- WebSocket消息发送频率合理化
|
||||
- 避免前端被大量消息淹没
|
||||
- 提升用户体验
|
||||
|
||||
### 保持数据新鲜度
|
||||
- 位置数据仍然是高频更新
|
||||
- 检测延迟控制在可接受范围内
|
||||
- 关键告警仍能及时响应
|
||||
|
||||
## 配置建议
|
||||
|
||||
### 生产环境
|
||||
```properties
|
||||
# 数据采集 - 较高频率保证数据新鲜度
|
||||
data.collector.interval=500
|
||||
|
||||
# 检测处理 - 适中频率平衡性能和响应性
|
||||
data.collector.detection.interval=2000
|
||||
```
|
||||
|
||||
### 开发/测试环境
|
||||
```properties
|
||||
# 数据采集 - 高频率便于测试
|
||||
data.collector.interval=250
|
||||
|
||||
# 检测处理 - 较高频率便于调试
|
||||
data.collector.detection.interval=1000
|
||||
```
|
||||
|
||||
### 高负载环境
|
||||
```properties
|
||||
# 数据采集 - 适当降低频率
|
||||
data.collector.interval=1000
|
||||
|
||||
# 检测处理 - 降低频率减少负载
|
||||
data.collector.detection.interval=3000
|
||||
```
|
||||
|
||||
## 监控指标
|
||||
|
||||
建议监控以下指标来优化配置:
|
||||
|
||||
1. **CPU使用率**: 围栏检测和空间查询的CPU消耗
|
||||
2. **数据库连接**: PostGIS查询的数据库压力
|
||||
3. **WebSocket连接**: 前端消息处理能力
|
||||
4. **响应延迟**: 告警响应的及时性
|
||||
5. **内存使用**: 活跃对象缓存的内存占用
|
||||
|
||||
## 代码实现
|
||||
|
||||
### DataCollectorService.java
|
||||
```java
|
||||
// 高频数据采集 - 只更新缓存
|
||||
@Scheduled(fixedRateString = "${data.collector.interval}")
|
||||
public void collectUnmannedVehicleData() {
|
||||
// 获取最新数据并更新activeMovingObjectsCache
|
||||
// 不执行围栏检测
|
||||
}
|
||||
|
||||
// 低频检测处理 - 执行所有检测逻辑
|
||||
@Scheduled(fixedRateString = "${data.collector.detection.interval}")
|
||||
public void performPeriodicViolationDetection() {
|
||||
// 基于缓存执行围栏检测
|
||||
// 执行路径冲突检测
|
||||
// 执行实时违规检测
|
||||
// 发送WebSocket消息
|
||||
}
|
||||
```
|
||||
|
||||
这种设计确保了数据的及时性,同时避免了过度的计算和网络开销。
|
||||
66
doc/design/QAUP业务原则和范围.md
Normal file
66
doc/design/QAUP业务原则和范围.md
Normal file
@ -0,0 +1,66 @@
|
||||
# QAUP项目业务原则和范围说明
|
||||
|
||||
## 🎯 业务原则
|
||||
|
||||
### 核心管理对象
|
||||
- **主要管理对象**: 无人车 (Unmanned Vehicle)
|
||||
- **监控范围**: 仅针对无人车进行实时监控、检测和告警
|
||||
- **数据记录范围**:
|
||||
- 无人车: 完整的位置数据、状态数据、检测结果、告警事件
|
||||
- 特勤车/普通车辆: 仅记录基本车辆信息,不保存状态数据
|
||||
|
||||
### 功能边界定义
|
||||
|
||||
#### ✅ 系统管理范围 (无人车)
|
||||
1. **实时数据采集**: 位置、速度、方向等状态数据
|
||||
2. **空间检测**: 电子围栏准入检测、超速检测、冲突检测
|
||||
3. **告警管理**: 各类违规行为的实时告警
|
||||
4. **轨迹追踪**: 历史轨迹记录和回放
|
||||
5. **指令控制**: 无人车控制指令的发送和执行
|
||||
|
||||
#### ❌ 系统不管理范围
|
||||
1. **航空器**: 平台不管理航空器,仅作为环境对象存在
|
||||
2. **特勤车辆**: 只记录车辆基本信息,不进行实时监控
|
||||
3. **普通车辆**: 只记录车辆基本信息,不进行实时监控
|
||||
|
||||
### 电子围栏检测原则
|
||||
- **检测对象**: 仅针对无人车进行电子围栏准入检测
|
||||
- **告警对象**: 仅对无人车的违规行为进行告警
|
||||
- **数据存储**: 仅保存无人车的围栏事件记录
|
||||
|
||||
### 数据处理原则
|
||||
- **状态数据**: 只采集和存储无人车的实时状态
|
||||
- **历史数据**: 只保留无人车的历史轨迹和事件记录
|
||||
- **性能优化**: 系统资源专注于无人车管理,提高处理效率
|
||||
|
||||
## 🏗️ 架构影响
|
||||
|
||||
基于以上原则,系统架构应该:
|
||||
|
||||
1. **数据模型设计**: 区分无人车数据和其他车辆数据的存储策略
|
||||
2. **检测服务**: 所有检测逻辑仅针对无人车类型进行处理
|
||||
3. **事件处理**: 告警和事件系统只处理无人车相关事件
|
||||
4. **前端展示**: UI界面主要展示无人车的监控和管理功能
|
||||
|
||||
## 📝 技术实现要点
|
||||
|
||||
### 车辆类型过滤
|
||||
```java
|
||||
// 在所有检测服务中添加车辆类型检查
|
||||
if (vehicle.getVehicleType() != MovingObjectType.UNMANNED_VEHICLE) {
|
||||
// 跳过检测,不进行处理
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 数据存储策略
|
||||
- **vehicle_location表**: 只存储无人车的位置数据
|
||||
- **geofence_events表**: 只记录无人车的围栏事件
|
||||
- **sys_vehicle_info表**: 存储所有车辆基本信息(用于系统完整性)
|
||||
|
||||
### 告警系统范围
|
||||
- 所有告警事件都标注为无人车专用
|
||||
- WebSocket推送只包含无人车相关事件
|
||||
- 前端告警界面只显示无人车告警
|
||||
|
||||
这个原则将大大简化系统复杂度,提高系统性能,并确保功能边界清晰。
|
||||
211
doc/design/电子围栏准入检测架构方案.md
Normal file
211
doc/design/电子围栏准入检测架构方案.md
Normal file
@ -0,0 +1,211 @@
|
||||
# 电子围栏准入检测架构优化方案 (仅针对无人车)
|
||||
|
||||
## 📋 总体设计
|
||||
|
||||
### 核心原则
|
||||
1. **职责分离**:Area负责空间数据,Rule负责业务逻辑
|
||||
2. **规则驱动**:所有准入检测通过SpatialRule统一管理
|
||||
3. **事件驱动**:基于事件的异步处理机制
|
||||
4. **🎯 业务范围限定**:**仅针对无人车进行电子围栏检测和告警**
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 1. 数据模型重构
|
||||
|
||||
#### 1.1 简化Area模型
|
||||
```java
|
||||
@Entity
|
||||
public class AirportArea {
|
||||
private Long id; // 区域ID
|
||||
private String areaCode; // 区域编码(唯一标识)
|
||||
private String areaName; // 区域名称
|
||||
private AreaType areaType; // 区域类型(枚举)
|
||||
private Geometry geometry; // 空间几何数据
|
||||
private String description; // 描述信息
|
||||
private Boolean active; // 是否启用
|
||||
private LocalDateTime createdAt; // 创建时间
|
||||
private LocalDateTime updatedAt; // 更新时间
|
||||
// 移除所有业务规则相关字段
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 增强SpatialRule模型 (专注无人车)
|
||||
```java
|
||||
@Entity
|
||||
public class SpatialRule {
|
||||
// 现有字段保持不变,新增以下字段:
|
||||
private String areaCode; // 关联区域编码
|
||||
private RuleCategory category; // ACCESS_CONTROL(准入控制)
|
||||
private String ruleConfig; // JSON配置:针对无人车的限制条件
|
||||
private GeofenceAction action; // 触发动作:ALLOW/DENY/WARN
|
||||
// 注意:allowedVehicleTypes 应该只包含 UNMANNED_VEHICLE
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 规则配置结构 (无人车专用)
|
||||
```json
|
||||
{
|
||||
"accessControl": {
|
||||
"vehicleType": "UNMANNED_VEHICLE", // 固定为无人车类型
|
||||
"requiresPermission": true,
|
||||
"allowedOperationModes": ["AUTO", "MANUAL", "REMOTE"]
|
||||
},
|
||||
"physicalLimits": {
|
||||
"maxSpeed": 30, // 无人车最大速度限制
|
||||
"maxHeight": 3.0, // 无人车最大高度限制
|
||||
"maxWeight": 5.0 // 无人车最大载重限制
|
||||
},
|
||||
"timeRestrictions": {
|
||||
"activeHours": "06:00-22:00",
|
||||
"activeDays": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"]
|
||||
},
|
||||
"safetyRequirements": {
|
||||
"minBatteryLevel": 20, // 最低电量要求
|
||||
"requiresOperatorPresence": false // 是否需要操作员在场
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 服务层架构
|
||||
|
||||
#### 2.1 电子围栏检测服务 (无人车专用)
|
||||
```java
|
||||
@Service
|
||||
public class UnmannedVehicleGeofenceService {
|
||||
|
||||
/**
|
||||
* 检测无人车是否可以进入指定区域
|
||||
* 注意:仅处理无人车类型的车辆
|
||||
*/
|
||||
public AccessCheckResult checkAreaAccess(MovingObject vehicle, String areaCode) {
|
||||
// 前置检查:必须是无人车
|
||||
if (vehicle.getObjectType() != MovingObject.ObjectType.UNMANNED_VEHICLE) {
|
||||
log.debug("跳过非无人车的围栏检测: {}", vehicle.getObjectId());
|
||||
return AccessCheckResult.skip();
|
||||
}
|
||||
|
||||
// 1. 获取区域信息
|
||||
// 2. 查询适用的规则
|
||||
// 3. 执行规则检查
|
||||
// 4. 返回检测结果
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时监控无人车位置变化
|
||||
*/
|
||||
public void monitorUnmannedVehicleLocation(MovingObject vehicle) {
|
||||
// 类型检查:只处理无人车
|
||||
if (vehicle.getObjectType() != MovingObject.ObjectType.UNMANNED_VEHICLE) {
|
||||
return; // 静默跳过
|
||||
}
|
||||
|
||||
// 1. 检测进入/离开事件
|
||||
// 2. 触发准入检测
|
||||
// 3. 发布事件
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 规则执行引擎 (针对无人车优化)
|
||||
```java
|
||||
@Service
|
||||
public class UnmannedVehicleRuleEngine {
|
||||
|
||||
/**
|
||||
* 执行无人车准入控制规则
|
||||
*/
|
||||
public RuleExecutionResult executeAccessRules(MovingObject unmannedVehicle, AirportArea area, List<SpatialRule> rules) {
|
||||
// 验证输入:确保是无人车
|
||||
validateUnmannedVehicle(unmannedVehicle);
|
||||
|
||||
// 1. 过滤适用于无人车的规则
|
||||
// 2. 按优先级排序规则
|
||||
// 3. 依次执行规则检查
|
||||
// 4. 汇总执行结果
|
||||
}
|
||||
|
||||
private void validateUnmannedVehicle(MovingObject vehicle) {
|
||||
if (vehicle.getObjectType() != MovingObject.ObjectType.UNMANNED_VEHICLE) {
|
||||
throw new IllegalArgumentException("规则引擎只处理无人车类型");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 事件处理机制
|
||||
|
||||
#### 3.1 事件类型定义 (无人车专用)
|
||||
```java
|
||||
public enum UnmannedVehicleGeofenceEventType {
|
||||
UNMANNED_VEHICLE_ENTRY_REQUEST, // 无人车区域进入请求
|
||||
UNMANNED_VEHICLE_ENTRY_APPROVED, // 无人车进入批准
|
||||
UNMANNED_VEHICLE_ENTRY_DENIED, // 无人车进入拒绝
|
||||
UNMANNED_VEHICLE_EXIT, // 无人车离开区域
|
||||
UNMANNED_VEHICLE_RULE_VIOLATION, // 无人车规则违反
|
||||
UNMANNED_VEHICLE_ACCESS_WARNING // 无人车准入警告
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 事件处理流程 (仅处理无人车事件)
|
||||
```java
|
||||
@EventListener
|
||||
public class UnmannedVehicleGeofenceEventHandler {
|
||||
|
||||
@Async
|
||||
public void handleUnmannedVehicleEntryRequest(UnmannedVehicleEntryRequestEvent event) {
|
||||
// 确保事件来源是无人车
|
||||
if (event.getVehicleType() != MovingObject.ObjectType.UNMANNED_VEHICLE) {
|
||||
log.warn("收到非无人车的围栏事件,忽略处理: {}", event);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 执行准入检测
|
||||
// 2. 记录检测结果
|
||||
// 3. 发送WebSocket通知 (仅无人车事件)
|
||||
// 4. 记录审计日志
|
||||
}
|
||||
|
||||
@Async
|
||||
public void handleUnmannedVehicleViolation(UnmannedVehicleViolationEvent event) {
|
||||
// 处理无人车违规事件
|
||||
// 1. 记录违规信息
|
||||
// 2. 触发告警通知
|
||||
// 3. 可能的自动处理措施 (如减速指令)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 实施步骤
|
||||
|
||||
### Phase 1: 数据模型重构 (1周)
|
||||
1. 简化AirportArea表结构
|
||||
2. 完善SpatialRule表结构
|
||||
3. 创建数据迁移脚本
|
||||
4. 更新Entity类
|
||||
|
||||
### Phase 2: 服务层开发 (2周)
|
||||
1. 开发GeofenceAccessControlService
|
||||
2. 增强RuleExecutionEngine
|
||||
3. 实现事件处理机制
|
||||
4. 集成WebSocket通知
|
||||
|
||||
### Phase 3: 测试与优化 (1周)
|
||||
1. 单元测试和集成测试
|
||||
2. 性能优化
|
||||
3. 监控和日志完善
|
||||
4. 文档更新
|
||||
|
||||
## 📊 技术优势
|
||||
|
||||
1. **清晰的职责分离**:Area专注空间数据,Rule管理业务逻辑
|
||||
2. **灵活的规则配置**:通过JSON配置支持复杂业务场景
|
||||
3. **高性能检测**:空间索引 + 缓存优化
|
||||
4. **事件驱动架构**:异步处理,高并发支持
|
||||
5. **易于扩展**:新增规则类型只需扩展配置,无需修改代码
|
||||
|
||||
## 🎯 预期效果
|
||||
|
||||
- **功能完整性**:支持复杂的电子围栏准入检测需求
|
||||
- **性能提升**:优化的空间查询和规则执行
|
||||
- **维护性**:清晰的架构设计,易于理解和维护
|
||||
- **扩展性**:支持未来新的业务需求
|
||||
@ -24,18 +24,18 @@
|
||||
|
||||
### 2025-04-25
|
||||
|
||||
- 需求:电子围栏,根据机场划定的区域,当车辆进入或离开该区域时,进行预警
|
||||
- 需求:电子围栏,根据机场划定的区域,当车辆进入该区域时,进行告警
|
||||
- 分析:
|
||||
- 需要根据机场划定的区域,需要获取机场的区域数据
|
||||
- 需要获取车辆的实时位置数据
|
||||
- 需要对车辆进行电子围栏预警
|
||||
- 需要对车辆进行电子围栏告警
|
||||
- 功能模块:
|
||||
- 数据采集模块:获取车辆的实时位置数据(具备)
|
||||
- 数据处理模块:根据机场的区域数据,对车辆进行电子围栏预警(新增)
|
||||
- 数据存储模块:存储车辆的实时位置数据和电子围栏预警数据(增加电子围栏预警数据)
|
||||
- 告警模块:对车辆进行电子围栏预警(增加电子围栏预警类型)
|
||||
- 数据处理模块:根据机场的区域数据,对车辆进行电子围栏告警(新增)
|
||||
- 数据存储模块:存储车辆的实时位置数据和电子围栏告警数据(增加电子围栏告警数据)
|
||||
- 告警模块:对车辆进行电子围栏告警(增加电子围栏告警类型)
|
||||
- 配置模块:配置机场的区域数据(新增)
|
||||
- 通信模块:WebSocket通信(增加电子围栏预警事件)
|
||||
- 通信模块:WebSocket通信(增加电子围栏告警事件)
|
||||
|
||||
### 2025-04-12
|
||||
|
||||
|
||||
213
doc/work/架构优化报告_20250712.md
Normal file
213
doc/work/架构优化报告_20250712.md
Normal file
@ -0,0 +1,213 @@
|
||||
# QAUP 项目架构优化报告
|
||||
|
||||
**项目名称**: QAUP 机场车辆管理和碰撞避免系统
|
||||
**优化日期**: 2025-01-15
|
||||
**项目版本**: 3.8.9
|
||||
**分析人员**: Claude Code Assistant
|
||||
|
||||
## 📋 执行摘要
|
||||
|
||||
本次架构优化主要解决了从Spring Boot 2.x到3.x升级过程中的兼容性问题、qaup-collision模块与RuoYi框架的集成问题,以及SQL脚本管理混乱等关键问题。通过系统性的重构和优化,项目架构更加清晰、稳定和可维护。
|
||||
|
||||
## 🔍 问题分析
|
||||
|
||||
### 1. Spring Boot 升级兼容性问题
|
||||
|
||||
**问题描述**:
|
||||
- `javax.*` 和 `jakarta.*` 包混用
|
||||
- collision模块仍使用Java 8配置
|
||||
- 依赖版本不一致
|
||||
|
||||
**影响**:
|
||||
- 编译错误和运行时异常
|
||||
- 性能下降
|
||||
- 潜在的安全风险
|
||||
|
||||
### 2. 模块集成架构问题
|
||||
|
||||
**问题描述**:
|
||||
- collision模块保留独立的`@SpringBootApplication`启动类
|
||||
- 重复的配置和依赖声明
|
||||
- 模块边界不清晰
|
||||
|
||||
**影响**:
|
||||
- 系统启动复杂化
|
||||
- 配置冲突
|
||||
- 维护困难
|
||||
|
||||
### 3. 数据库脚本管理问题
|
||||
|
||||
**问题描述**:
|
||||
- 多个版本的SQL脚本文件
|
||||
- 缺乏统一的数据库初始化方案
|
||||
- 脚本依赖关系混乱
|
||||
|
||||
**影响**:
|
||||
- 部署困难
|
||||
- 数据一致性风险
|
||||
- 开发效率低下
|
||||
|
||||
## ✅ 解决方案实施
|
||||
|
||||
### 1. Spring Boot 升级优化
|
||||
|
||||
#### 1.1 统一编译配置
|
||||
- **文件**: `qaup-collision/pom.xml`
|
||||
- **修改**: 将Java版本从8升级到17,使用统一的编译器配置
|
||||
- **效果**: 消除版本不一致问题,提升性能
|
||||
|
||||
```xml
|
||||
<!-- 修改前 -->
|
||||
<source>8</source>
|
||||
<target>8</target>
|
||||
|
||||
<!-- 修改后 -->
|
||||
<release>${java.version}</release>
|
||||
```
|
||||
|
||||
#### 1.2 包导入规范化
|
||||
- **问题**: 多处存在`javax.*`和`jakarta.*`混用
|
||||
- **解决**: 检测到77个文件使用jakarta包,8个文件仍使用javax包
|
||||
- **建议**: 后续需要逐步将剩余的javax包迁移到jakarta
|
||||
|
||||
### 2. 模块架构重构
|
||||
|
||||
#### 2.1 移除独立启动类
|
||||
- **文件**: `CollisionAvoidanceApplication.java` → `CollisionAvoidanceConfig.java`
|
||||
- **位置**: `qaup-collision/src/main/java/com/qaup/collision/config/`
|
||||
- **改变**: 从独立应用转为配置类
|
||||
|
||||
```java
|
||||
// 修改前:独立应用
|
||||
@SpringBootApplication
|
||||
public class CollisionAvoidanceApplication {
|
||||
public static void main(String[] args) { ... }
|
||||
}
|
||||
|
||||
// 修改后:配置类
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class CollisionAvoidanceConfig {
|
||||
public CollisionAvoidanceConfig() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 依赖关系优化
|
||||
- **统一版本管理**: 使用父POM的版本变量
|
||||
- **移除重复依赖**: 避免版本冲突
|
||||
- **优化构建配置**: 使用统一的编译器设置
|
||||
|
||||
### 3. 数据库结构统一
|
||||
|
||||
#### 3.1 创建完整初始化脚本
|
||||
- **文件**: `sql/qaup_database_complete_init.sql`
|
||||
- **内容**: 包含完整的数据库结构定义
|
||||
- **特性**:
|
||||
- PostGIS空间扩展支持
|
||||
- 完整的表结构和关系
|
||||
- 性能优化索引
|
||||
- 初始化数据
|
||||
- 实用视图
|
||||
|
||||
#### 3.2 脚本结构组织
|
||||
|
||||
```sql
|
||||
-- 1. 数据库创建和扩展
|
||||
-- 2. 核心系统表(RuoYi框架基础)
|
||||
-- 3. 车辆管理相关表
|
||||
-- 4. 碰撞避免系统表(空间数据)
|
||||
-- 5. 系统配置表
|
||||
-- 6. 基础数据初始化
|
||||
-- 7. 性能优化索引
|
||||
-- 8. 实用查询视图
|
||||
```
|
||||
|
||||
## 📈 优化效果
|
||||
|
||||
### 1. 架构清晰度提升
|
||||
- ✅ 模块职责明确分离
|
||||
- ✅ 配置统一管理
|
||||
- ✅ 依赖关系简化
|
||||
|
||||
### 2. 开发效率提升
|
||||
- ✅ 统一的数据库初始化流程
|
||||
- ✅ 清晰的编译和构建配置
|
||||
- ✅ 减少配置冲突
|
||||
|
||||
### 3. 系统稳定性增强
|
||||
- ✅ 消除版本兼容性问题
|
||||
- ✅ 规范化模块集成
|
||||
- ✅ 优化数据库性能
|
||||
|
||||
### 4. 维护成本降低
|
||||
- ✅ 减少重复配置
|
||||
- ✅ 简化部署流程
|
||||
- ✅ 提高代码可读性
|
||||
|
||||
## 🔧 后续建议
|
||||
|
||||
### 短期优化 (1-2周)
|
||||
|
||||
1. **完成javax到jakarta的迁移**
|
||||
- 剩余8个文件的包迁移
|
||||
- 测试所有功能模块
|
||||
- 更新相关文档
|
||||
|
||||
2. **配置文件优化**
|
||||
- 检查重复配置项
|
||||
- 统一环境变量管理
|
||||
- 优化WebSocket配置
|
||||
|
||||
3. **代码质量提升**
|
||||
- 添加单元测试
|
||||
- 代码规范检查
|
||||
- 性能测试
|
||||
|
||||
### 中期规划 (1个月)
|
||||
|
||||
1. **监控和日志优化**
|
||||
- 集成应用监控
|
||||
- 优化日志输出
|
||||
- 添加性能指标
|
||||
|
||||
2. **文档完善**
|
||||
- API文档更新
|
||||
- 部署指南完善
|
||||
- 开发规范制定
|
||||
|
||||
3. **安全性增强**
|
||||
- 依赖漏洞扫描
|
||||
- 安全配置审查
|
||||
- 访问控制优化
|
||||
|
||||
### 长期优化 (3个月)
|
||||
|
||||
1. **微服务架构探索**
|
||||
- 评估服务拆分可行性
|
||||
- 设计服务边界
|
||||
- 渐进式重构
|
||||
|
||||
2. **云原生适配**
|
||||
- 容器化部署
|
||||
- 服务发现集成
|
||||
- 配置中心接入
|
||||
|
||||
## 💾 备份和恢复
|
||||
|
||||
### 已修改文件列表
|
||||
1. `qaup-collision/pom.xml` - 编译配置优化
|
||||
2. `qaup-collision/src/main/java/com/qaup/collision/config/CollisionAvoidanceConfig.java` - 启动类重构
|
||||
3. `sql/qaup_database_complete_init.sql` - 新增统一数据库脚本
|
||||
|
||||
### 建议的备份策略
|
||||
- 在生产环境应用前,创建当前版本的完整备份
|
||||
- 使用版本控制系统标记重要节点
|
||||
- 制定回滚计划
|
||||
|
||||
## 📝 结论
|
||||
|
||||
本次架构优化成功解决了Spring Boot升级、模块集成和数据库管理等关键问题。通过系统性的重构,项目的可维护性、稳定性和开发效率都得到了显著提升。建议按照后续优化计划逐步推进,确保系统持续优化和改进。
|
||||
|
||||
---
|
||||
|
||||
**注意**: 本报告基于代码静态分析生成,建议在实际应用前进行充分的测试验证。
|
||||
@ -7,7 +7,7 @@ qaup:
|
||||
# 版权年份
|
||||
copyrightYear: 2025
|
||||
# 文件路径 示例( Windows配置D:/qaup/uploadPath,Linux配置 /home/qaup/uploadPath)
|
||||
profile: Users/tianjianyong/apps/OpenSource/Qaup-Vue-Postgresql/uploadPath
|
||||
profile: /Users/tianjianyong/apps/Company/QAUP-Management/uploadPath
|
||||
# 获取ip地址开关
|
||||
addressEnabled: false
|
||||
# 验证码类型 math 数字计算 char 字符验证
|
||||
@ -177,11 +177,12 @@ xss:
|
||||
# 数据采集配置(collision模块)
|
||||
data:
|
||||
collector:
|
||||
# 数据采集间隔,单位:毫秒(优化:250ms超高频采集,进一步减少停顿)
|
||||
interval: 250
|
||||
# WebSocket推送节流配置(1000ms推送间隔,避免前端过载)
|
||||
websocket:
|
||||
push-interval: 1000
|
||||
# 数据采集间隔,单位:毫秒(高频采集保证数据新鲜度)
|
||||
interval: 1000
|
||||
# 检测和推送间隔配置
|
||||
detection:
|
||||
# 检测间隔,单位:毫秒(控制围栏检测、冲突检测、违规检测和WebSocket推送频率)
|
||||
interval: 5000
|
||||
# 机场数据源配置
|
||||
airport-api:
|
||||
base-url: http://localhost:8090
|
||||
|
||||
@ -154,15 +154,18 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Maven编译器插件(按Lombok官网标准配置) -->
|
||||
<!-- Maven编译器插件(统一使用Java 17) -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<version>3.14.0</version>
|
||||
<configuration>
|
||||
<source>8</source>
|
||||
<target>8</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<release>${java.version}</release>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
<parameters>true</parameters>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
package com.qaup.collision;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
@Slf4j
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties
|
||||
public class CollisionAvoidanceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ConfigurableApplicationContext context = SpringApplication.run(CollisionAvoidanceApplication.class, args);
|
||||
|
||||
// 添加关闭钩子,确保所有资源能够正确释放
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
log.info("应用程序关闭中,正在清理资源...");
|
||||
try {
|
||||
// 关闭Spring上下文
|
||||
if (context != null && context.isActive()) {
|
||||
context.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("关闭应用程序时出错", e);
|
||||
}
|
||||
log.info("应用程序已安全关闭");
|
||||
}));
|
||||
}
|
||||
}
|
||||
@ -54,7 +54,25 @@ public class MovingObject {
|
||||
UNMANNED_VEHICLE, // 无人车
|
||||
AIRCRAFT, // 航空器
|
||||
SPECIAL_VEHICLE, // 特种车辆(如机场车辆)
|
||||
NORMAL_VEHICLE // 普通车辆
|
||||
NORMAL_VEHICLE; // 普通车辆
|
||||
|
||||
/**
|
||||
* 转换为MovingObjectType
|
||||
*/
|
||||
public MovingObjectType toMovingObjectType() {
|
||||
switch (this) {
|
||||
case UNMANNED_VEHICLE:
|
||||
return MovingObjectType.UNMANNED_VEHICLE;
|
||||
case AIRCRAFT:
|
||||
return MovingObjectType.AIRCRAFT;
|
||||
case SPECIAL_VEHICLE:
|
||||
return MovingObjectType.SPECIAL_VEHICLE;
|
||||
case NORMAL_VEHICLE:
|
||||
return MovingObjectType.NORMAL_VEHICLE;
|
||||
default:
|
||||
return MovingObjectType.UNMANNED_VEHICLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -289,6 +289,30 @@ public class AirportAreaService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据区域编码查找区域
|
||||
*/
|
||||
public Optional<AirportArea> findByAreaCode(String areaCode) {
|
||||
try {
|
||||
return airportAreaRepository.findByAreaId(areaCode);
|
||||
} catch (Exception e) {
|
||||
log.error("根据区域编码查找区域失败: areaCode={}", areaCode, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找包含指定点的所有区域
|
||||
*/
|
||||
public List<AirportArea> findAreasContainingPoint(Point point) {
|
||||
try {
|
||||
return airportAreaRepository.findAreasContainingPoint(point);
|
||||
} catch (Exception e) {
|
||||
log.error("查找包含点的区域失败: point={}", point, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计活跃区域数量(按类型分组)
|
||||
*/
|
||||
|
||||
@ -152,8 +152,8 @@ public class VehicleLocationService {
|
||||
saved.getLocation().getX(),
|
||||
saved.getLocation().getY());
|
||||
|
||||
// 同步执行规则检测(简化架构,性能足够)
|
||||
performRuleDetection(saved);
|
||||
// 注意:规则检测已移至DataCollectorService的周期性检测中,以实现频率分离
|
||||
// performRuleDetection(saved); // 已禁用直接检测
|
||||
|
||||
return saved;
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package com.qaup.collision.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 碰撞避免系统配置类
|
||||
*
|
||||
* 注意:此类不再是独立的SpringBootApplication,
|
||||
* 而是作为配置类集成到主应用中
|
||||
*
|
||||
* @author qaup-collision
|
||||
* @date 2025-01-15
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class CollisionAvoidanceConfig {
|
||||
|
||||
/**
|
||||
* 初始化碰撞避免系统
|
||||
*/
|
||||
public CollisionAvoidanceConfig() {
|
||||
log.info("🚀 碰撞避免系统模块初始化完成");
|
||||
log.info("📍 空间数据分析功能已启用");
|
||||
log.info("🔗 WebSocket实时通信已配置");
|
||||
log.info("⏰ 定时任务调度已启用(由主应用管理)");
|
||||
}
|
||||
}
|
||||
@ -8,10 +8,8 @@ import com.qaup.collision.datacollector.dao.DataCollectorDao;
|
||||
import com.qaup.collision.websocket.event.PositionUpdateEvent;
|
||||
import com.qaup.collision.websocket.message.PositionUpdatePayload;
|
||||
import com.qaup.collision.pathconflict.service.PathConflictDetectionService;
|
||||
import com.qaup.collision.rule.service.RealTimeViolationDetector;
|
||||
import com.qaup.collision.common.adapter.QuapDataAdapter;
|
||||
import com.qaup.system.domain.SysVehicleInfo;
|
||||
import com.qaup.collision.rule.event.RuleViolationEvent;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -21,8 +19,10 @@ import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@ -64,8 +64,8 @@ public class DataCollectorService {
|
||||
@Value("${data.collector.disabled:false}")
|
||||
private boolean collectorDisabled;
|
||||
|
||||
@Value("${data.collector.websocket.push-interval:1000}")
|
||||
private long websocketPushInterval;
|
||||
@Value("${data.collector.detection.interval:1000}")
|
||||
private long detectionInterval;
|
||||
|
||||
@Autowired
|
||||
private DataCollectorDao dataCollectorDao;
|
||||
@ -86,19 +86,13 @@ public class DataCollectorService {
|
||||
private PathConflictDetectionService pathConflictDetectionService;
|
||||
|
||||
@Autowired
|
||||
private RealTimeViolationDetector realTimeViolationDetector; // 注入 RealTimeViolationDetector
|
||||
private QuapDataAdapter quapDataAdapter; // 注入 QuapDataAdapter
|
||||
|
||||
@Autowired
|
||||
private QuapDataAdapter quapDataAdapter; // 注入 QuapDataAdapter
|
||||
private com.qaup.collision.rule.service.RuleExecutionEngine ruleExecutionEngine; // 注入规则执行引擎
|
||||
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); // SRID 4326 for WGS84
|
||||
|
||||
/**
|
||||
* WebSocket推送节流机制 - 跟踪每个对象的上次推送时间
|
||||
* key: objectId, value: lastPushTimestamp
|
||||
*/
|
||||
private final Map<String, Long> lastPushTimes = new ConcurrentHashMap<>();
|
||||
|
||||
// 用于缓存所有活跃的MovingObject的最新状态
|
||||
private final Map<String, MovingObject> activeMovingObjectsCache = new ConcurrentHashMap<>();
|
||||
|
||||
@ -159,38 +153,14 @@ public class DataCollectorService {
|
||||
.altitude(aircraft.getAltitude())
|
||||
.build();
|
||||
|
||||
// activeMovingObjects.add(movingObject); // 移除此行
|
||||
// 将最新数据更新到缓存
|
||||
// 将最新数据更新到缓存(不发送WebSocket消息,统一在周期性检测中发送)
|
||||
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
|
||||
|
||||
// 创建位置更新消息负载(WebSocket推送)
|
||||
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
||||
.latitude(currentPosition.getY())
|
||||
.longitude(currentPosition.getX())
|
||||
.build();
|
||||
|
||||
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
||||
.objectId(aircraft.getObjectId()) // Changed from getFlightNo()
|
||||
.objectType(MovingObject.ObjectType.AIRCRAFT.name())
|
||||
.position(positionPayload)
|
||||
.heading(movingObject.getCurrentHeading())
|
||||
.speed(new BigDecimal(movingObject.getCurrentSpeed()).setScale(2, RoundingMode.HALF_UP).doubleValue()) // 对速度进行四舍五入
|
||||
.timestamp(currentTime) // 更新为Long类型时间戳
|
||||
.build();
|
||||
|
||||
// 节流检查:只有达到推送间隔时才发布WebSocket事件
|
||||
if (shouldPushWebSocketMessage(aircraft.getObjectId(), currentTime)) { // Changed from getFlightNo()
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||
|
||||
log.debug("处理航空器数据并发布事件: (航班号: {}, 位置: {}, {}, 速度: {})",
|
||||
aircraft.getObjectId(), // Changed from getFlightNo()
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
} else {
|
||||
log.trace("航空器数据采集但未推送(节流): {} (速度: {})",
|
||||
aircraft.getObjectId(), movingObject.getCurrentSpeed()); // Changed from getFlightNo()
|
||||
}
|
||||
log.debug("处理航空器数据并更新缓存: (航班号: {}, 位置: {}, {}, 速度: {})",
|
||||
aircraft.getObjectId(),
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理航空器数据异常: objectId={}", aircraft.getObjectId(), e); // Changed from flightNo
|
||||
@ -282,38 +252,14 @@ public class DataCollectorService {
|
||||
.altitude(vehicle.getAltitude())
|
||||
.build();
|
||||
|
||||
// activeMovingObjects.add(movingObject); // 移除此行
|
||||
// 将最新数据更新到缓存
|
||||
// 将最新数据更新到缓存(不发送WebSocket消息,统一在周期性检测中发送)
|
||||
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
|
||||
|
||||
// 创建位置更新消息负载(WebSocket推送)
|
||||
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
||||
.latitude(currentPosition.getY())
|
||||
.longitude(currentPosition.getX())
|
||||
.build();
|
||||
|
||||
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
||||
.objectId(vehicle.getObjectId())
|
||||
.objectType(MovingObject.ObjectType.SPECIAL_VEHICLE.name())
|
||||
.position(positionPayload)
|
||||
.heading(movingObject.getCurrentHeading())
|
||||
.speed(new BigDecimal(movingObject.getCurrentSpeed()).setScale(2, RoundingMode.HALF_UP).doubleValue()) // 对速度进行四舍五入
|
||||
.timestamp(currentTime)
|
||||
.build();
|
||||
|
||||
// 节流检查:只有达到推送间隔时才发布WebSocket事件
|
||||
if (shouldPushWebSocketMessage(vehicle.getObjectId(), currentTime)) {
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||
|
||||
log.debug("处理机场车辆数据并发布事件: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
vehicle.getObjectId(),
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
} else {
|
||||
log.trace("机场车辆数据采集但未推送(节流): {} (速度: {})",
|
||||
vehicle.getObjectId(), movingObject.getCurrentSpeed());
|
||||
}
|
||||
log.debug("处理机场车辆数据并更新缓存: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
vehicle.getObjectId(),
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理机场车辆数据异常: objectId={}", vehicle.getObjectId(), e);
|
||||
@ -409,44 +355,17 @@ public class DataCollectorService {
|
||||
|
||||
log.debug("在DataCollectorService中,MovingObject的速度: {}", movingObject.getCurrentSpeed());
|
||||
|
||||
// unmannedVehicleLocations.add(createTemporaryVehicleLocationForDetection(movingObject)); // 移除此行
|
||||
// 将最新数据更新到缓存
|
||||
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
|
||||
// 注意:电子围栏检测已移至周期性检测方法中,以降低检测频率
|
||||
// 仅在此处更新缓存,实际检测在performPeriodicViolationDetection()中执行
|
||||
|
||||
// 将UnmannedVehicle转换为VehicleLocation并保存到数据库
|
||||
VehicleLocation convertedLocation = convertToVehicleLocation(unmannedVehicle);
|
||||
if (convertedLocation != null) {
|
||||
vehicleDataPersistenceService.saveUnmannedVehicleLocation(convertedLocation);
|
||||
}
|
||||
// 将最新数据更新到缓存(不保存到数据库,统一在周期性检测中保存)
|
||||
activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject);
|
||||
|
||||
// 创建位置更新消息负载(WebSocket推送)
|
||||
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
||||
.latitude(currentPosition.getY())
|
||||
.longitude(currentPosition.getX())
|
||||
.build();
|
||||
|
||||
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
||||
.objectId(unmannedVehicle.getObjectId()) // Changed from getLicensePlate()
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE.name())
|
||||
.position(positionPayload)
|
||||
.heading(movingObject.getCurrentHeading())
|
||||
.speed(new BigDecimal(movingObject.getCurrentSpeed()).setScale(2, RoundingMode.HALF_UP).doubleValue()) // 对速度进行四舍五入
|
||||
.timestamp(currentTime)
|
||||
.build();
|
||||
|
||||
// 节流检查:只有达到推送间隔时才发布WebSocket事件
|
||||
if (shouldPushWebSocketMessage(unmannedVehicle.getObjectId(), currentTime)) { // Changed from getLicensePlate()
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||
|
||||
log.debug("处理无人车数据并发布事件: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
unmannedVehicle.getObjectId(),
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
} else {
|
||||
log.trace("无人车数据采集但未推送(节流): {} (速度: {})",
|
||||
unmannedVehicle.getObjectId(), movingObject.getCurrentSpeed()); // Changed from getLicensePlate()
|
||||
}
|
||||
log.debug("处理无人车数据并更新缓存: (车牌号: {}, 位置: {}, {}, 速度: {})",
|
||||
unmannedVehicle.getObjectId(),
|
||||
currentPosition.getX(),
|
||||
currentPosition.getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理无人车数据异常: objectId={}", unmannedVehicle.getObjectId(), e);
|
||||
@ -481,11 +400,12 @@ public class DataCollectorService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时执行路径冲突和实时违规检测
|
||||
* 这个任务将按照websocketPushInterval配置的频率运行,独立于数据采集频率。
|
||||
* 定时执行电子围栏检测、路径冲突检测和实时违规检测
|
||||
* 这个任务将按照detectionInterval配置的频率运行,独立于数据采集频率。
|
||||
* 它会从activeMovingObjectsCache中获取所有最新的MovingObject数据,并进行批量检测。
|
||||
*/
|
||||
@Scheduled(fixedRateString = "${data.collector.websocket.push-interval}")
|
||||
@Scheduled(fixedRateString = "${data.collector.detection.interval}")
|
||||
@Transactional // 改为可写事务,支持违规事件记录
|
||||
public void performPeriodicViolationDetection() {
|
||||
if (collectorDisabled) {
|
||||
return;
|
||||
@ -500,6 +420,12 @@ public class DataCollectorService {
|
||||
|
||||
// 获取所有活跃的MovingObject的快照
|
||||
List<MovingObject> currentActiveObjects = new ArrayList<>(activeMovingObjectsCache.values());
|
||||
|
||||
// 首先发送位置更新消息(按检测周期发送,与真实数据变化同步)
|
||||
sendPositionUpdatesForActiveObjects(currentActiveObjects);
|
||||
|
||||
// 保存无人车位置数据到数据库(按检测周期保存)
|
||||
saveUnmannedVehicleDataPeriodically(currentActiveObjects);
|
||||
|
||||
// 执行路径冲突检测
|
||||
pathConflictDetectionService.detectPathConflicts(currentActiveObjects);
|
||||
@ -510,11 +436,34 @@ public class DataCollectorService {
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 执行实时违规检测 (超速、区域访问等)
|
||||
// 执行统一的违规检测 (包括超速、区域访问、电子围栏等) - 使用规则执行引擎统一处理
|
||||
if (!detectionVehicleLocations.isEmpty()) {
|
||||
realTimeViolationDetector.detectBatchViolations(detectionVehicleLocations);
|
||||
log.debug("执行统一违规检测,车辆数量: {}", detectionVehicleLocations.size());
|
||||
for (VehicleLocation vehicleLocation : detectionVehicleLocations) {
|
||||
try {
|
||||
// 只处理无人车
|
||||
if (vehicleLocation.getVehicleType() == com.qaup.collision.common.model.MovingObjectType.UNMANNED_VEHICLE) {
|
||||
// 构建车辆状态参数
|
||||
Map<String, Object> vehicleState = new HashMap<>();
|
||||
vehicleState.put("speed", vehicleLocation.getSpeed());
|
||||
vehicleState.put("altitude", vehicleLocation.getAltitude());
|
||||
vehicleState.put("heading", vehicleLocation.getHeading());
|
||||
|
||||
// 统一调用规则执行引擎进行所有类型的违规检测(速度、区域访问、电子围栏等)
|
||||
ruleExecutionEngine.detectAllViolations(
|
||||
String.valueOf(vehicleLocation.getVehicleId()),
|
||||
vehicleLocation.getVehicleType(),
|
||||
vehicleLocation.getLocation(),
|
||||
vehicleState,
|
||||
vehicleLocation.getTimestamp()
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("车辆 {} 违规检测失败: {}", vehicleLocation.getVehicleId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug("没有可用于实时违规检测的车辆位置数据。");
|
||||
log.debug("没有可用于违规检测的车辆位置数据。");
|
||||
}
|
||||
|
||||
log.info("周期性违规检测和路径冲突检测完成。");
|
||||
@ -542,18 +491,6 @@ public class DataCollectorService {
|
||||
// 清理资源,如果需要
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该推送WebSocket消息(节流)
|
||||
*/
|
||||
private boolean shouldPushWebSocketMessage(String objectId, long currentTime) {
|
||||
Long lastPushTime = lastPushTimes.getOrDefault(objectId, 0L);
|
||||
if (currentTime - lastPushTime >= websocketPushInterval) {
|
||||
lastPushTimes.put(objectId, currentTime);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 UnmannedVehicle 转换为 VehicleLocation 实体用于持久化
|
||||
*/
|
||||
@ -577,7 +514,7 @@ public class DataCollectorService {
|
||||
.vehicleId(sysVehicleInfo.getVehicleId()) // 使用从 sys_vehicle_info 获取的 Long 类型 vehicleId
|
||||
.location(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(vehicle.getCurrentPosition().getX(), vehicle.getCurrentPosition().getY())))
|
||||
.altitude(vehicle.getAltitude())
|
||||
.speed(vehicle.getCurrentSpeed())
|
||||
.speed(vehicle.getCurrentSpeed() != null ? vehicle.getCurrentSpeed() : 0.0)
|
||||
.heading(vehicle.getCurrentHeading())
|
||||
.timestamp(java.time.LocalDateTime.ofEpochSecond(System.currentTimeMillis() / 1000, 0, java.time.ZoneOffset.UTC))
|
||||
.licensePlate(sysVehicleInfo.getLicensePlate()) // 设置运行时属性 licensePlate
|
||||
@ -643,4 +580,112 @@ public class DataCollectorService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送活跃对象的位置更新消息(按检测周期发送)
|
||||
*/
|
||||
private void sendPositionUpdatesForActiveObjects(List<MovingObject> activeObjects) {
|
||||
if (activeObjects.isEmpty()) {
|
||||
log.debug("没有活跃对象,跳过位置更新消息发送");
|
||||
return;
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
log.debug("发送位置更新消息,对象数量: {}", activeObjects.size());
|
||||
|
||||
for (MovingObject movingObject : activeObjects) {
|
||||
try {
|
||||
// 创建位置更新消息负载
|
||||
PositionUpdatePayload.Position positionPayload = PositionUpdatePayload.Position.builder()
|
||||
.latitude(movingObject.getCurrentPosition().getY())
|
||||
.longitude(movingObject.getCurrentPosition().getX())
|
||||
.build();
|
||||
|
||||
PositionUpdatePayload payload = PositionUpdatePayload.builder()
|
||||
.objectId(movingObject.getObjectId())
|
||||
.objectType(movingObject.getObjectType().name())
|
||||
.position(positionPayload)
|
||||
.heading(movingObject.getCurrentHeading())
|
||||
.speed(movingObject.getCurrentSpeed() != null ?
|
||||
new BigDecimal(movingObject.getCurrentSpeed()).setScale(2, RoundingMode.HALF_UP).doubleValue() : 0.0)
|
||||
.timestamp(currentTime)
|
||||
.build();
|
||||
|
||||
// 发布WebSocket事件(按检测周期发送,不需要额外节流)
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(payload));
|
||||
|
||||
log.debug("发送位置更新: {} ({}), 位置: ({}, {}), 速度: {}",
|
||||
movingObject.getObjectId(),
|
||||
movingObject.getObjectType(),
|
||||
movingObject.getCurrentPosition().getX(),
|
||||
movingObject.getCurrentPosition().getY(),
|
||||
movingObject.getCurrentSpeed());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送位置更新消息失败: objectId={}", movingObject.getObjectId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("位置更新消息发送完成,发送数量: {}", activeObjects.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 周期性保存无人车位置数据到数据库(按检测周期保存)
|
||||
*/
|
||||
private void saveUnmannedVehicleDataPeriodically(List<MovingObject> activeObjects) {
|
||||
if (activeObjects.isEmpty()) {
|
||||
log.debug("没有活跃对象,跳过数据保存");
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤出无人车对象
|
||||
List<MovingObject> unmannedVehicles = activeObjects.stream()
|
||||
.filter(obj -> obj.getObjectType() == MovingObject.ObjectType.UNMANNED_VEHICLE)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (unmannedVehicles.isEmpty()) {
|
||||
log.debug("没有无人车数据,跳过数据保存");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("开始周期性保存无人车位置数据,数量: {}", unmannedVehicles.size());
|
||||
|
||||
// 将MovingObject转换为VehicleLocation并保存
|
||||
List<VehicleLocation> vehicleLocations = new ArrayList<>();
|
||||
for (MovingObject unmannedVehicle : unmannedVehicles) {
|
||||
try {
|
||||
// 创建一个临时的UnmannedVehicle对象用于转换
|
||||
UnmannedVehicle tempUnmannedVehicle = UnmannedVehicle.builder()
|
||||
.objectId(unmannedVehicle.getObjectId())
|
||||
.objectName(unmannedVehicle.getObjectName())
|
||||
.currentPosition(unmannedVehicle.getCurrentPosition())
|
||||
.currentSpeed(unmannedVehicle.getCurrentSpeed())
|
||||
.currentHeading(unmannedVehicle.getCurrentHeading())
|
||||
.altitude(unmannedVehicle.getAltitude())
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
|
||||
.build();
|
||||
|
||||
// 转换无人车数据为VehicleLocation
|
||||
VehicleLocation vehicleLocation = convertToVehicleLocation(tempUnmannedVehicle);
|
||||
|
||||
if (vehicleLocation != null) {
|
||||
vehicleLocations.add(vehicleLocation);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("转换无人车数据失败: objectId={}", unmannedVehicle.getObjectId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存到数据库
|
||||
if (!vehicleLocations.isEmpty()) {
|
||||
try {
|
||||
vehicleDataPersistenceService.batchSaveUnmannedVehicleLocations(vehicleLocations);
|
||||
log.info("周期性保存无人车位置数据完成,保存数量: {}", vehicleLocations.size());
|
||||
} catch (Exception e) {
|
||||
log.error("批量保存无人车位置数据失败: 数量={}", vehicleLocations.size(), e);
|
||||
}
|
||||
} else {
|
||||
log.debug("没有有效的无人车位置数据可保存");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,26 +67,12 @@ public class VehicleDataPersistenceService {
|
||||
savedLocation.getLocation().getY());
|
||||
|
||||
// 数据保存完成后发布WebSocket位置更新事件
|
||||
try {
|
||||
// 将VehicleLocation转换为PositionUpdatePayload
|
||||
PositionUpdatePayload positionPayload = PositionUpdatePayload.builder()
|
||||
.objectId(String.valueOf(savedLocation.getVehicleId())) // 使用vehicleId作为objectId
|
||||
.objectType(savedLocation.getVehicleType().name())
|
||||
.position(PositionUpdatePayload.Position.builder()
|
||||
.latitude(savedLocation.getLocation().getY())
|
||||
.longitude(savedLocation.getLocation().getX())
|
||||
.build())
|
||||
.heading(savedLocation.getHeading())
|
||||
.speed(savedLocation.getSpeed())
|
||||
.timestamp(System.currentTimeMillis() * 1000L) // 微秒级时间戳
|
||||
.build();
|
||||
|
||||
eventPublisher.publishEvent(new PositionUpdateEvent(positionPayload));
|
||||
log.debug("发布无人车位置更新事件: vehicleId={}", savedLocation.getVehicleId());
|
||||
} catch (Exception eventException) {
|
||||
log.error("发布位置更新事件失败: vehicleId={}", savedLocation.getVehicleId(), eventException);
|
||||
// 事件发布失败不应影响数据保存,仅记录错误
|
||||
}
|
||||
// 位置数据保存成功,不再在此处发送WebSocket消息
|
||||
// WebSocket消息统一由DataCollectorService的周期性检测方法发送
|
||||
log.debug("无人车位置数据保存成功: vehicleId={}, 位置=({}, {})",
|
||||
savedLocation.getVehicleId(),
|
||||
savedLocation.getLocation().getX(),
|
||||
savedLocation.getLocation().getY());
|
||||
|
||||
return savedLocation;
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -32,11 +32,11 @@ public class SpeedCalculationService {
|
||||
private final Map<String, HistoricalPosition> positionHistory = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* WebSocket推送间隔配置,用于确定速度计算的时间间隔
|
||||
* 检测间隔配置,用于确定速度计算的时间间隔
|
||||
* 速度计算应该基于输出频率而非采集频率
|
||||
*/
|
||||
@Value("${data.collector.websocket.push-interval:1000}")
|
||||
private long websocketPushInterval;
|
||||
@Value("${data.collector.detection.interval:1000}")
|
||||
private long detectionInterval;
|
||||
|
||||
|
||||
// ==================== 实时速度计算功能 ====================
|
||||
@ -63,7 +63,7 @@ public class SpeedCalculationService {
|
||||
|
||||
// 速度计算应该基于输出频率(WebSocket推送间隔),而不是采集频率
|
||||
// 只有时间差达到输出间隔时才计算新速度
|
||||
double pushIntervalSeconds = websocketPushInterval / 1000.0;
|
||||
double pushIntervalSeconds = detectionInterval / 1000.0;
|
||||
if (timeDelta < pushIntervalSeconds) {
|
||||
return lastPosition.lastCalculatedSpeed;
|
||||
}
|
||||
@ -112,7 +112,7 @@ public class SpeedCalculationService {
|
||||
double timeDelta = (timestamp - lastPosition.timestamp) / 1000.0;
|
||||
|
||||
// 只有时间差达到输出间隔时才计算新方向
|
||||
double pushIntervalSeconds = websocketPushInterval / 1000.0;
|
||||
double pushIntervalSeconds = detectionInterval / 1000.0;
|
||||
if (timeDelta < pushIntervalSeconds) {
|
||||
return lastPosition.lastCalculatedDirection;
|
||||
}
|
||||
@ -131,7 +131,7 @@ public class SpeedCalculationService {
|
||||
|
||||
// 如果速度没有同时计算,只更新方向
|
||||
if (lastPosition.lastCalculatedSpeed == null ||
|
||||
Math.abs(timestamp - lastPosition.timestamp) >= websocketPushInterval) {
|
||||
Math.abs(timestamp - lastPosition.timestamp) >= detectionInterval) {
|
||||
// 计算速度(米/秒),然后转换为公里/小时
|
||||
double speedMps = distance / timeDelta;
|
||||
double speedKph = speedMps * 3.6; // 转换为公里/小时
|
||||
|
||||
@ -0,0 +1,195 @@
|
||||
package com.qaup.collision.geofence.model.dto;
|
||||
|
||||
import com.qaup.collision.geofence.model.enums.GeofenceAlertLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 电子围栏准入检测结果
|
||||
*
|
||||
* @author qaup-collision
|
||||
* @date 2025-01-15
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GeofenceAccessCheckResult {
|
||||
|
||||
/**
|
||||
* 检测结果类型
|
||||
*/
|
||||
private AccessResultType resultType;
|
||||
|
||||
/**
|
||||
* 是否允许进入
|
||||
*/
|
||||
private Boolean allowed;
|
||||
|
||||
/**
|
||||
* 检测的区域编码
|
||||
*/
|
||||
private String areaCode;
|
||||
|
||||
/**
|
||||
* 区域名称
|
||||
*/
|
||||
private String areaName;
|
||||
|
||||
/**
|
||||
* 无人车ID
|
||||
*/
|
||||
private String vehicleId;
|
||||
|
||||
/**
|
||||
* 触发的规则列表
|
||||
*/
|
||||
private List<TriggeredRule> triggeredRules;
|
||||
|
||||
/**
|
||||
* 主要告警级别
|
||||
*/
|
||||
private GeofenceAlertLevel alertLevel;
|
||||
|
||||
/**
|
||||
* 检测消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 详细原因
|
||||
*/
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 检测时间
|
||||
*/
|
||||
private LocalDateTime checkTime;
|
||||
|
||||
/**
|
||||
* 建议操作
|
||||
*/
|
||||
private List<String> recommendedActions;
|
||||
|
||||
/**
|
||||
* 检测结果类型枚举
|
||||
*/
|
||||
public enum AccessResultType {
|
||||
ALLOWED, // 允许进入
|
||||
DENIED, // 拒绝进入
|
||||
WARNING, // 警告但允许
|
||||
CONDITIONAL, // 有条件允许
|
||||
SKIPPED // 跳过检测(非无人车)
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发的规则信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TriggeredRule {
|
||||
|
||||
/**
|
||||
* 规则ID
|
||||
*/
|
||||
private String ruleId;
|
||||
|
||||
/**
|
||||
* 规则名称
|
||||
*/
|
||||
private String ruleName;
|
||||
|
||||
/**
|
||||
* 规则分类
|
||||
*/
|
||||
private String ruleCategory;
|
||||
|
||||
/**
|
||||
* 违规类型
|
||||
*/
|
||||
private String violationType;
|
||||
|
||||
/**
|
||||
* 告警级别
|
||||
*/
|
||||
private GeofenceAlertLevel alertLevel;
|
||||
|
||||
/**
|
||||
* 规则消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 规则参数
|
||||
*/
|
||||
private String parameters;
|
||||
}
|
||||
|
||||
// 静态工厂方法
|
||||
|
||||
/**
|
||||
* 创建允许进入的结果
|
||||
*/
|
||||
public static GeofenceAccessCheckResult allowed(String areaCode, String vehicleId) {
|
||||
return GeofenceAccessCheckResult.builder()
|
||||
.resultType(AccessResultType.ALLOWED)
|
||||
.allowed(true)
|
||||
.areaCode(areaCode)
|
||||
.vehicleId(vehicleId)
|
||||
.alertLevel(GeofenceAlertLevel.INFO)
|
||||
.message("允许进入")
|
||||
.checkTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建拒绝进入的结果
|
||||
*/
|
||||
public static GeofenceAccessCheckResult denied(String areaCode, String vehicleId, String reason) {
|
||||
return GeofenceAccessCheckResult.builder()
|
||||
.resultType(AccessResultType.DENIED)
|
||||
.allowed(false)
|
||||
.areaCode(areaCode)
|
||||
.vehicleId(vehicleId)
|
||||
.alertLevel(GeofenceAlertLevel.CRITICAL)
|
||||
.message("拒绝进入")
|
||||
.reason(reason)
|
||||
.checkTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建警告的结果
|
||||
*/
|
||||
public static GeofenceAccessCheckResult warning(String areaCode, String vehicleId, String message) {
|
||||
return GeofenceAccessCheckResult.builder()
|
||||
.resultType(AccessResultType.WARNING)
|
||||
.allowed(true)
|
||||
.areaCode(areaCode)
|
||||
.vehicleId(vehicleId)
|
||||
.alertLevel(GeofenceAlertLevel.WARNING)
|
||||
.message(message)
|
||||
.checkTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建跳过检测的结果(非无人车)
|
||||
*/
|
||||
public static GeofenceAccessCheckResult skipped(String vehicleId) {
|
||||
return GeofenceAccessCheckResult.builder()
|
||||
.resultType(AccessResultType.SKIPPED)
|
||||
.allowed(null) // 不适用
|
||||
.vehicleId(vehicleId)
|
||||
.message("非无人车,跳过电子围栏检测")
|
||||
.checkTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,207 @@
|
||||
package com.qaup.collision.geofence.model.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 无人车电子围栏规则配置参数
|
||||
* 用于JSON格式存储在SpatialRule.parameters字段中
|
||||
*
|
||||
* @author qaup-collision
|
||||
* @date 2025-01-15
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UnmannedVehicleGeofenceRuleConfig {
|
||||
|
||||
/**
|
||||
* 准入控制配置
|
||||
*/
|
||||
@JsonProperty("accessControl")
|
||||
private AccessControlConfig accessControl;
|
||||
|
||||
/**
|
||||
* 物理限制配置
|
||||
*/
|
||||
@JsonProperty("physicalLimits")
|
||||
private PhysicalLimitsConfig physicalLimits;
|
||||
|
||||
/**
|
||||
* 时间限制配置
|
||||
*/
|
||||
@JsonProperty("timeRestrictions")
|
||||
private TimeRestrictionsConfig timeRestrictions;
|
||||
|
||||
/**
|
||||
* 安全要求配置
|
||||
*/
|
||||
@JsonProperty("safetyRequirements")
|
||||
private SafetyRequirementsConfig safetyRequirements;
|
||||
|
||||
/**
|
||||
* 准入控制配置
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AccessControlConfig {
|
||||
|
||||
/**
|
||||
* 是否需要权限验证
|
||||
*/
|
||||
@JsonProperty("requiresPermission")
|
||||
@Builder.Default
|
||||
private Boolean requiresPermission = false;
|
||||
|
||||
/**
|
||||
* 允许的操作模式
|
||||
*/
|
||||
@JsonProperty("allowedOperationModes")
|
||||
private List<String> allowedOperationModes;
|
||||
|
||||
/**
|
||||
* 是否允许自动驾驶模式
|
||||
*/
|
||||
@JsonProperty("allowAutonomousMode")
|
||||
@Builder.Default
|
||||
private Boolean allowAutonomousMode = true;
|
||||
|
||||
/**
|
||||
* 是否允许远程控制模式
|
||||
*/
|
||||
@JsonProperty("allowRemoteControlMode")
|
||||
@Builder.Default
|
||||
private Boolean allowRemoteControlMode = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理限制配置
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PhysicalLimitsConfig {
|
||||
|
||||
/**
|
||||
* 最大速度限制 (km/h)
|
||||
*/
|
||||
@JsonProperty("maxSpeed")
|
||||
private Double maxSpeed;
|
||||
|
||||
/**
|
||||
* 最小速度限制 (km/h)
|
||||
*/
|
||||
@JsonProperty("minSpeed")
|
||||
private Double minSpeed;
|
||||
|
||||
/**
|
||||
* 最大高度限制 (米)
|
||||
*/
|
||||
@JsonProperty("maxHeight")
|
||||
private Double maxHeight;
|
||||
|
||||
/**
|
||||
* 最大载重限制 (吨)
|
||||
*/
|
||||
@JsonProperty("maxWeight")
|
||||
private Double maxWeight;
|
||||
|
||||
/**
|
||||
* 最大尺寸限制 (长x宽x高,米)
|
||||
*/
|
||||
@JsonProperty("maxDimensions")
|
||||
private String maxDimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间限制配置
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TimeRestrictionsConfig {
|
||||
|
||||
/**
|
||||
* 活跃时间段 (例如: "06:00-22:00")
|
||||
*/
|
||||
@JsonProperty("activeHours")
|
||||
private String activeHours;
|
||||
|
||||
/**
|
||||
* 活跃日期 (例如: ["MONDAY", "TUESDAY", ...])
|
||||
*/
|
||||
@JsonProperty("activeDays")
|
||||
private List<String> activeDays;
|
||||
|
||||
/**
|
||||
* 禁止时间段 (例如: ["12:00-13:00", "18:00-19:00"])
|
||||
*/
|
||||
@JsonProperty("blackoutPeriods")
|
||||
private List<String> blackoutPeriods;
|
||||
|
||||
/**
|
||||
* 最大停留时间 (分钟)
|
||||
*/
|
||||
@JsonProperty("maxStayDurationMinutes")
|
||||
private Integer maxStayDurationMinutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全要求配置
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SafetyRequirementsConfig {
|
||||
|
||||
/**
|
||||
* 最低电池电量要求 (百分比)
|
||||
*/
|
||||
@JsonProperty("minBatteryLevel")
|
||||
private Integer minBatteryLevel;
|
||||
|
||||
/**
|
||||
* 是否需要操作员在场
|
||||
*/
|
||||
@JsonProperty("requiresOperatorPresence")
|
||||
@Builder.Default
|
||||
private Boolean requiresOperatorPresence = false;
|
||||
|
||||
/**
|
||||
* 最大同时进入车辆数
|
||||
*/
|
||||
@JsonProperty("maxConcurrentVehicles")
|
||||
private Integer maxConcurrentVehicles;
|
||||
|
||||
/**
|
||||
* 是否需要预约
|
||||
*/
|
||||
@JsonProperty("requiresReservation")
|
||||
@Builder.Default
|
||||
private Boolean requiresReservation = false;
|
||||
|
||||
/**
|
||||
* 是否需要护航车辆
|
||||
*/
|
||||
@JsonProperty("requiresEscortVehicle")
|
||||
@Builder.Default
|
||||
private Boolean requiresEscortVehicle = false;
|
||||
|
||||
/**
|
||||
* 必需的安全装备
|
||||
*/
|
||||
@JsonProperty("requiredSafetyEquipment")
|
||||
private List<String> requiredSafetyEquipment;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,430 @@
|
||||
package com.qaup.collision.geofence.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.common.model.spatial.AirportArea;
|
||||
import com.qaup.collision.common.service.AirportAreaService;
|
||||
import com.qaup.collision.geofence.model.dto.GeofenceAccessCheckResult;
|
||||
import com.qaup.collision.geofence.model.dto.UnmannedVehicleGeofenceRuleConfig;
|
||||
import com.qaup.collision.geofence.model.enums.GeofenceAlertLevel;
|
||||
import com.qaup.collision.rule.model.entity.SpatialRule;
|
||||
import com.qaup.collision.rule.model.enums.RuleCategory;
|
||||
import com.qaup.collision.rule.service.SpatialRuleService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.qaup.collision.websocket.event.GeofenceAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.message.GeofenceAlertPayload;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 无人车电子围栏检测服务
|
||||
* 专门负责无人车的区域准入检测
|
||||
*
|
||||
* @author qaup-collision
|
||||
* @date 2025-01-15
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UnmannedVehicleGeofenceService {
|
||||
|
||||
@Autowired
|
||||
private AirportAreaService airportAreaService;
|
||||
|
||||
@Autowired
|
||||
private SpatialRuleService spatialRuleService;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 检测无人车是否可以进入指定区域
|
||||
*
|
||||
* @param vehicle 移动对象(必须是无人车)
|
||||
* @param areaCode 区域编码
|
||||
* @return 检测结果
|
||||
*/
|
||||
public GeofenceAccessCheckResult checkAreaAccess(MovingObject vehicle, String areaCode) {
|
||||
// 前置检查:必须是无人车
|
||||
if (vehicle.getObjectType() != MovingObject.ObjectType.UNMANNED_VEHICLE) {
|
||||
log.debug("跳过非无人车的围栏检测: {} (类型: {})", vehicle.getObjectId(), vehicle.getObjectType());
|
||||
return GeofenceAccessCheckResult.skipped(vehicle.getObjectId());
|
||||
}
|
||||
|
||||
try {
|
||||
log.debug("开始电子围栏检测: 无人车={}, 区域={}", vehicle.getObjectId(), areaCode);
|
||||
|
||||
// 1. 获取区域信息
|
||||
Optional<AirportArea> areaOpt = airportAreaService.findByAreaCode(areaCode);
|
||||
if (areaOpt.isEmpty()) {
|
||||
log.warn("未找到区域: {}", areaCode);
|
||||
return GeofenceAccessCheckResult.denied(areaCode, vehicle.getObjectId(), "区域不存在");
|
||||
}
|
||||
|
||||
AirportArea area = areaOpt.get();
|
||||
|
||||
// 2. 检查区域是否启用
|
||||
if (!Boolean.TRUE.equals(area.getEnabled())) {
|
||||
log.debug("区域已禁用: {}", areaCode);
|
||||
return GeofenceAccessCheckResult.denied(areaCode, vehicle.getObjectId(), "区域已禁用");
|
||||
}
|
||||
|
||||
// 3. 查询适用的规则
|
||||
List<SpatialRule> rules = spatialRuleService.findActiveRulesForArea(areaCode, RuleCategory.ACCESS_CONTROL);
|
||||
log.debug("找到 {} 个适用的准入控制规则", rules.size());
|
||||
|
||||
// 4. 执行规则检查
|
||||
return executeAccessRules(vehicle, area, rules);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("电子围栏检测失败: 无人车={}, 区域={}", vehicle.getObjectId(), areaCode, e);
|
||||
return GeofenceAccessCheckResult.denied(areaCode, vehicle.getObjectId(), "系统检测失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测无人车当前位置是否触发电子围栏
|
||||
*
|
||||
* @param vehicle 无人车对象
|
||||
* @return 检测结果列表(可能进入多个区域)
|
||||
*/
|
||||
public List<GeofenceAccessCheckResult> checkCurrentPosition(MovingObject vehicle) {
|
||||
List<GeofenceAccessCheckResult> results = new ArrayList<>();
|
||||
|
||||
// 类型检查:只处理无人车
|
||||
if (vehicle.getObjectType() != MovingObject.ObjectType.UNMANNED_VEHICLE) {
|
||||
log.debug("跳过非无人车位置检测: {}", vehicle.getObjectId());
|
||||
results.add(GeofenceAccessCheckResult.skipped(vehicle.getObjectId()));
|
||||
return results;
|
||||
}
|
||||
|
||||
try {
|
||||
Point currentPosition = vehicle.getCurrentPosition();
|
||||
if (currentPosition == null) {
|
||||
log.warn("无人车位置信息为空: {}", vehicle.getObjectId());
|
||||
return results;
|
||||
}
|
||||
|
||||
// 查找当前位置所在的所有区域
|
||||
List<AirportArea> containingAreas = airportAreaService.findAreasContainingPoint(currentPosition);
|
||||
log.debug("无人车 {} 当前位置包含在 {} 个区域中", vehicle.getObjectId(), containingAreas.size());
|
||||
|
||||
// 对每个区域进行检测
|
||||
for (AirportArea area : containingAreas) {
|
||||
GeofenceAccessCheckResult result = checkAreaAccess(vehicle, area.getAreaId());
|
||||
results.add(result);
|
||||
|
||||
// 发布事件
|
||||
publishGeofenceEvent(vehicle, area, result);
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("位置检测失败: 无人车={}", vehicle.getObjectId(), e);
|
||||
results.add(GeofenceAccessCheckResult.denied(null, vehicle.getObjectId(), "位置检测失败: " + e.getMessage()));
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行准入控制规则
|
||||
*/
|
||||
private GeofenceAccessCheckResult executeAccessRules(MovingObject vehicle, AirportArea area, List<SpatialRule> rules) {
|
||||
List<GeofenceAccessCheckResult.TriggeredRule> triggeredRules = new ArrayList<>();
|
||||
GeofenceAlertLevel maxAlertLevel = GeofenceAlertLevel.INFO;
|
||||
boolean accessDenied = false;
|
||||
String denyReason = null;
|
||||
|
||||
// 如果没有规则,默认允许
|
||||
if (rules.isEmpty()) {
|
||||
log.debug("区域 {} 没有准入控制规则,默认允许", area.getAreaId());
|
||||
return GeofenceAccessCheckResult.allowed(area.getAreaId(), vehicle.getObjectId());
|
||||
}
|
||||
|
||||
// 按优先级排序规则
|
||||
rules.sort((r1, r2) -> Integer.compare(r1.getPriority(), r2.getPriority()));
|
||||
|
||||
for (SpatialRule rule : rules) {
|
||||
try {
|
||||
// 检查规则是否当前有效
|
||||
if (!rule.isCurrentlyEffective()) {
|
||||
log.debug("规则 {} 当前不生效,跳过", rule.getRuleId());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查车辆类型是否被允许
|
||||
if (!rule.isVehicleTypeAllowed(vehicle.getObjectType().toMovingObjectType())) {
|
||||
log.debug("规则 {} 不允许车辆类型 {}", rule.getRuleId(), vehicle.getObjectType());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 执行具体规则检查
|
||||
RuleCheckResult checkResult = checkSpecificRule(vehicle, area, rule);
|
||||
|
||||
if (!checkResult.passed) {
|
||||
// 规则检查失败
|
||||
GeofenceAccessCheckResult.TriggeredRule triggeredRule = GeofenceAccessCheckResult.TriggeredRule.builder()
|
||||
.ruleId(rule.getRuleId())
|
||||
.ruleName(rule.getName())
|
||||
.ruleCategory(rule.getCategory().getDisplayName())
|
||||
.violationType(checkResult.violationType)
|
||||
.alertLevel(rule.getAlertLevel())
|
||||
.message(checkResult.message)
|
||||
.parameters(rule.getParameters())
|
||||
.build();
|
||||
|
||||
triggeredRules.add(triggeredRule);
|
||||
|
||||
// 更新最高告警级别
|
||||
if (rule.getAlertLevel().isHigherOrEqual(maxAlertLevel)) {
|
||||
maxAlertLevel = rule.getAlertLevel();
|
||||
}
|
||||
|
||||
// 检查是否需要拒绝访问
|
||||
if (rule.getAlertLevel().isHigherOrEqual(GeofenceAlertLevel.CRITICAL)) {
|
||||
accessDenied = true;
|
||||
denyReason = checkResult.message;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("规则检查失败: 规则ID={}, 无人车={}", rule.getRuleId(), vehicle.getObjectId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建检测结果
|
||||
GeofenceAccessCheckResult.GeofenceAccessCheckResultBuilder resultBuilder = GeofenceAccessCheckResult.builder()
|
||||
.areaCode(area.getAreaId())
|
||||
.areaName(area.getName())
|
||||
.vehicleId(vehicle.getObjectId())
|
||||
.triggeredRules(triggeredRules)
|
||||
.alertLevel(maxAlertLevel)
|
||||
.checkTime(LocalDateTime.now());
|
||||
|
||||
if (accessDenied) {
|
||||
return resultBuilder
|
||||
.resultType(GeofenceAccessCheckResult.AccessResultType.DENIED)
|
||||
.allowed(false)
|
||||
.message("拒绝进入")
|
||||
.reason(denyReason)
|
||||
.build();
|
||||
} else if (!triggeredRules.isEmpty()) {
|
||||
return resultBuilder
|
||||
.resultType(GeofenceAccessCheckResult.AccessResultType.WARNING)
|
||||
.allowed(true)
|
||||
.message("允许进入但有警告")
|
||||
.build();
|
||||
} else {
|
||||
return resultBuilder
|
||||
.resultType(GeofenceAccessCheckResult.AccessResultType.ALLOWED)
|
||||
.allowed(true)
|
||||
.message("允许进入")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查具体规则
|
||||
*/
|
||||
private RuleCheckResult checkSpecificRule(MovingObject vehicle, AirportArea area, SpatialRule rule) {
|
||||
try {
|
||||
// 解析规则配置
|
||||
UnmannedVehicleGeofenceRuleConfig config = parseRuleConfig(rule.getParameters());
|
||||
|
||||
// 检查准入控制
|
||||
if (config.getAccessControl() != null) {
|
||||
RuleCheckResult accessResult = checkAccessControl(vehicle, config.getAccessControl());
|
||||
if (!accessResult.passed) {
|
||||
return accessResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查物理限制
|
||||
if (config.getPhysicalLimits() != null) {
|
||||
RuleCheckResult physicalResult = checkPhysicalLimits(vehicle, config.getPhysicalLimits());
|
||||
if (!physicalResult.passed) {
|
||||
return physicalResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查时间限制
|
||||
if (config.getTimeRestrictions() != null) {
|
||||
RuleCheckResult timeResult = checkTimeRestrictions(config.getTimeRestrictions());
|
||||
if (!timeResult.passed) {
|
||||
return timeResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查安全要求
|
||||
if (config.getSafetyRequirements() != null) {
|
||||
RuleCheckResult safetyResult = checkSafetyRequirements(vehicle, config.getSafetyRequirements());
|
||||
if (!safetyResult.passed) {
|
||||
return safetyResult;
|
||||
}
|
||||
}
|
||||
|
||||
return RuleCheckResult.passed();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("规则检查异常: 规则ID={}", rule.getRuleId(), e);
|
||||
return RuleCheckResult.failed("规则检查异常: " + e.getMessage(), "SYSTEM_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析规则配置
|
||||
*/
|
||||
private UnmannedVehicleGeofenceRuleConfig parseRuleConfig(String parameters) {
|
||||
if (parameters == null || parameters.trim().isEmpty()) {
|
||||
return UnmannedVehicleGeofenceRuleConfig.builder().build();
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(parameters, UnmannedVehicleGeofenceRuleConfig.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("解析规则配置失败,使用默认配置: {}", e.getMessage());
|
||||
return UnmannedVehicleGeofenceRuleConfig.builder().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查准入控制
|
||||
*/
|
||||
private RuleCheckResult checkAccessControl(MovingObject vehicle, UnmannedVehicleGeofenceRuleConfig.AccessControlConfig config) {
|
||||
// 检查权限要求
|
||||
if (Boolean.TRUE.equals(config.getRequiresPermission())) {
|
||||
// TODO: 实现权限检查逻辑
|
||||
log.debug("需要权限验证,暂未实现");
|
||||
}
|
||||
|
||||
// 检查操作模式
|
||||
if (config.getAllowedOperationModes() != null && !config.getAllowedOperationModes().isEmpty()) {
|
||||
// TODO: 检查无人车当前操作模式
|
||||
log.debug("检查操作模式限制");
|
||||
}
|
||||
|
||||
return RuleCheckResult.passed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查物理限制
|
||||
*/
|
||||
private RuleCheckResult checkPhysicalLimits(MovingObject vehicle, UnmannedVehicleGeofenceRuleConfig.PhysicalLimitsConfig config) {
|
||||
// 检查速度限制
|
||||
if (config.getMaxSpeed() != null && vehicle.getCurrentSpeed() != null) {
|
||||
if (vehicle.getCurrentSpeed() > config.getMaxSpeed()) {
|
||||
return RuleCheckResult.failed(
|
||||
String.format("超速限制: 当前速度%.1f km/h,限制%.1f km/h",
|
||||
vehicle.getCurrentSpeed(), config.getMaxSpeed()),
|
||||
"SPEED_VIOLATION"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 检查其他物理限制(高度、重量等)
|
||||
|
||||
return RuleCheckResult.passed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查时间限制
|
||||
*/
|
||||
private RuleCheckResult checkTimeRestrictions(UnmannedVehicleGeofenceRuleConfig.TimeRestrictionsConfig config) {
|
||||
LocalTime now = LocalTime.now();
|
||||
|
||||
// 检查活跃时间段
|
||||
if (config.getActiveHours() != null) {
|
||||
// TODO: 解析时间段格式 "06:00-22:00" 并检查
|
||||
log.debug("检查活跃时间段: {}", config.getActiveHours());
|
||||
}
|
||||
|
||||
// TODO: 检查其他时间限制
|
||||
|
||||
return RuleCheckResult.passed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查安全要求
|
||||
*/
|
||||
private RuleCheckResult checkSafetyRequirements(MovingObject vehicle, UnmannedVehicleGeofenceRuleConfig.SafetyRequirementsConfig config) {
|
||||
// 检查电池电量
|
||||
if (config.getMinBatteryLevel() != null) {
|
||||
// TODO: 获取无人车电池电量并检查
|
||||
log.debug("检查电池电量要求: 最低{}%", config.getMinBatteryLevel());
|
||||
}
|
||||
|
||||
// TODO: 检查其他安全要求
|
||||
|
||||
return RuleCheckResult.passed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布围栏事件
|
||||
*/
|
||||
private void publishGeofenceEvent(MovingObject vehicle, AirportArea area, GeofenceAccessCheckResult result) {
|
||||
try {
|
||||
// 只发布需要告警的事件(拒绝、警告)
|
||||
if (result.getResultType() == GeofenceAccessCheckResult.AccessResultType.DENIED ||
|
||||
result.getResultType() == GeofenceAccessCheckResult.AccessResultType.WARNING) {
|
||||
|
||||
// 创建WebSocket载荷
|
||||
GeofenceAlertPayload payload = GeofenceAlertPayload.fromGeofenceResult(
|
||||
result,
|
||||
vehicle.getObjectName(),
|
||||
vehicle.getCurrentPosition() != null ? vehicle.getCurrentPosition().getY() : null,
|
||||
vehicle.getCurrentPosition() != null ? vehicle.getCurrentPosition().getX() : null,
|
||||
vehicle.getAltitude(),
|
||||
vehicle.getCurrentSpeed()
|
||||
);
|
||||
|
||||
// 创建并发布WebSocket事件
|
||||
GeofenceAlertWebSocketEvent event = GeofenceAlertWebSocketEvent.create(payload);
|
||||
eventPublisher.publishEvent(event);
|
||||
|
||||
log.info("发布电子围栏告警WebSocket事件: 无人车={}, 区域={}, 类型={}, 级别={}",
|
||||
vehicle.getObjectId(), area.getAreaId(), result.getResultType(), result.getAlertLevel());
|
||||
} else {
|
||||
log.debug("无人车 {} 正常通过区域 {},无需发布告警", vehicle.getObjectId(), area.getAreaId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发布围栏WebSocket事件失败: 无人车={}, 区域={}",
|
||||
vehicle.getObjectId(), area.getAreaId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则检查结果内部类
|
||||
*/
|
||||
private static class RuleCheckResult {
|
||||
final boolean passed;
|
||||
final String message;
|
||||
final String violationType;
|
||||
|
||||
private RuleCheckResult(boolean passed, String message, String violationType) {
|
||||
this.passed = passed;
|
||||
this.message = message;
|
||||
this.violationType = violationType;
|
||||
}
|
||||
|
||||
static RuleCheckResult passed() {
|
||||
return new RuleCheckResult(true, null, null);
|
||||
}
|
||||
|
||||
static RuleCheckResult failed(String message, String violationType) {
|
||||
return new RuleCheckResult(false, message, violationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -233,6 +233,14 @@ public interface SpatialRuleRepository extends JpaRepository<SpatialRule, String
|
||||
@Query("SELECT r FROM SpatialRule r WHERE r.name LIKE :ruleName")
|
||||
List<SpatialRule> findByNameContaining(@Param("ruleName") String ruleName);
|
||||
|
||||
/**
|
||||
* 根据空间对象ID和规则分类查询规则
|
||||
* @param spatialObjectId 空间对象ID
|
||||
* @param category 规则分类
|
||||
* @return 符合条件的规则列表
|
||||
*/
|
||||
List<SpatialRule> findBySpatialObjectIdAndCategory(String spatialObjectId, RuleCategory category);
|
||||
|
||||
/**
|
||||
* 软删除规则(更新状态为DELETED)
|
||||
* @param ruleId 规则ID
|
||||
|
||||
@ -404,6 +404,14 @@ public interface SpatialRuleService {
|
||||
*/
|
||||
void warmupRuleCache(Point location, double radiusMeters);
|
||||
|
||||
/**
|
||||
* 查找区域的活跃规则
|
||||
* @param areaCode 区域编码
|
||||
* @param category 规则分类
|
||||
* @return 适用的规则列表
|
||||
*/
|
||||
List<SpatialRule> findActiveRulesForArea(String areaCode, RuleCategory category);
|
||||
|
||||
// ========== 规则执行上下文 ==========
|
||||
|
||||
/**
|
||||
|
||||
@ -56,6 +56,18 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
// 短期去重缓存:防止同一检测周期内的重复违规
|
||||
// 格式:vehicleId:ruleName:violationType -> timestamp
|
||||
private final Map<String, Long> recentViolationsCache = new HashMap<>();
|
||||
|
||||
// 缓存清理间隔(毫秒)- 每30秒清理一次过期缓存
|
||||
private static final long CACHE_CLEANUP_INTERVAL = 30_000L;
|
||||
|
||||
// 短期去重时间窗口(毫秒)- 10秒内相同的违规不重复处理
|
||||
private static final long SHORT_TERM_DEDUP_WINDOW = 10_000L;
|
||||
|
||||
private long lastCacheCleanup = System.currentTimeMillis();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@ -114,8 +126,20 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
|
||||
RuleExecutionResult result = executeRuleInternal(rule, vehicleState, location, timestamp);
|
||||
|
||||
if (result == RuleExecutionResult.VIOLATION) {
|
||||
// 获取违规类型(用于去重)
|
||||
String violationType = determineViolationType(rule.getCategory());
|
||||
|
||||
// 检查短期去重:防止同一检测周期内的重复违规
|
||||
if (isRecentViolation(vehicleIdLong, rule.getName(), violationType)) {
|
||||
log.debug("跳过重复违规: vehicleId={}, rule={}, type={}", vehicleIdLong, rule.getName(), violationType);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 记录违规事件 - 内部基于vehicle_id处理
|
||||
recordViolationEvent(rule, vehicleIdLong, location, vehicleState, timestamp);
|
||||
|
||||
// 更新去重缓存
|
||||
updateRecentViolationCache(vehicleIdLong, rule.getName(), violationType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,12 +199,15 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
|
||||
JsonNode parameters = parseJsonString(rule.getParameters());
|
||||
log.debug("📝 规则参数: {}", rule.getParameters());
|
||||
|
||||
if (!parameters.has("max_speed_kph")) {
|
||||
log.warn("⚠️ 规则参数中缺少max_speed_kph字段");
|
||||
if (!parameters.has("maxSpeed") && !parameters.has("max_speed_kph")) {
|
||||
log.warn("⚠️ 规则参数中缺少maxSpeed或max_speed_kph字段");
|
||||
return RuleExecutionResult.EXECUTION_ERROR;
|
||||
}
|
||||
|
||||
double speedLimit = parameters.get("max_speed_kph").asDouble();
|
||||
// 支持两种字段名称格式
|
||||
double speedLimit = parameters.has("maxSpeed") ?
|
||||
parameters.get("maxSpeed").asDouble() :
|
||||
parameters.get("max_speed_kph").asDouble();
|
||||
log.debug("🚩 规则限速: {}km/h", speedLimit);
|
||||
|
||||
log.debug("🔍 速度对比: 当前={}km/h vs 限速={}km/h, 超速={}",
|
||||
@ -204,9 +231,33 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
|
||||
* 检查区域访问控制
|
||||
*/
|
||||
private RuleExecutionResult checkAccessControl(SpatialRule rule, Map<String, Object> vehicleState, Point location) {
|
||||
// 简化的访问控制逻辑:如果规则是访问控制类型,默认认为在限制区域内就是违规
|
||||
log.warn("检测到未授权访问: 位置=[{},{}]", location.getX(), location.getY());
|
||||
return RuleExecutionResult.VIOLATION;
|
||||
try {
|
||||
// 获取规则参数
|
||||
String parametersJson = rule.getParameters();
|
||||
if (parametersJson == null || parametersJson.trim().isEmpty()) {
|
||||
log.debug("访问控制规则 {} 没有参数配置", rule.getName());
|
||||
return RuleExecutionResult.NOT_APPLICABLE;
|
||||
}
|
||||
|
||||
// 解析JSON参数
|
||||
JsonNode parameters = objectMapper.readTree(parametersJson);
|
||||
|
||||
// 检查是否允许访问(简化逻辑:如果规则参数中有 "allowAccess": true 则允许访问)
|
||||
boolean allowAccess = parameters.has("allowAccess") && parameters.get("allowAccess").asBoolean();
|
||||
|
||||
if (allowAccess) {
|
||||
log.debug("访问控制规则 {} 允许访问: 位置=[{},{}]", rule.getName(), location.getX(), location.getY());
|
||||
return RuleExecutionResult.PASS;
|
||||
} else {
|
||||
// 默认情况下,如果无人车在受控区域内且没有明确的访问权限,则视为违规
|
||||
log.debug("访问控制规则 {} 检测到未授权访问: 位置=[{},{}]", rule.getName(), location.getX(), location.getY());
|
||||
return RuleExecutionResult.VIOLATION;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("访问控制检查失败: 规则={}, 错误={}", rule.getName(), e.getMessage());
|
||||
return RuleExecutionResult.EXECUTION_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -434,8 +485,12 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
|
||||
if (speed instanceof Number) {
|
||||
event.setActualValue(((Number) speed).doubleValue());
|
||||
}
|
||||
if (parameters != null && parameters.has("max_speed_kph")) {
|
||||
event.setLimitValue(parameters.get("max_speed_kph").asDouble());
|
||||
if (parameters != null) {
|
||||
if (parameters.has("maxSpeed")) {
|
||||
event.setLimitValue(parameters.get("maxSpeed").asDouble());
|
||||
} else if (parameters.has("max_speed_kph")) {
|
||||
event.setLimitValue(parameters.get("max_speed_kph").asDouble());
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@ -695,4 +750,68 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
|
||||
metrics.put("systemType", "UnmannedVehicleOnly");
|
||||
return metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据规则类别确定违规类型
|
||||
*/
|
||||
private String determineViolationType(RuleCategory category) {
|
||||
switch (category) {
|
||||
case SPEED_LIMIT:
|
||||
return "SPEED_VIOLATION";
|
||||
case ACCESS_CONTROL:
|
||||
return "UNAUTHORIZED_ENTRY";
|
||||
case HEIGHT_LIMIT:
|
||||
return "HEIGHT_VIOLATION";
|
||||
case WEIGHT_LIMIT:
|
||||
return "WEIGHT_VIOLATION";
|
||||
default:
|
||||
return "CUSTOM_RULE_VIOLATION";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为近期违规(短期去重)
|
||||
*/
|
||||
private boolean isRecentViolation(Long vehicleId, String ruleName, String violationType) {
|
||||
cleanupExpiredCache();
|
||||
|
||||
String cacheKey = String.format("%d:%s:%s", vehicleId, ruleName, violationType);
|
||||
Long lastViolationTime = recentViolationsCache.get(cacheKey);
|
||||
|
||||
if (lastViolationTime == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
return (currentTime - lastViolationTime) < SHORT_TERM_DEDUP_WINDOW;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新近期违规缓存
|
||||
*/
|
||||
private void updateRecentViolationCache(Long vehicleId, String ruleName, String violationType) {
|
||||
String cacheKey = String.format("%d:%s:%s", vehicleId, ruleName, violationType);
|
||||
recentViolationsCache.put(cacheKey, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
*/
|
||||
private void cleanupExpiredCache() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
// 只在达到清理间隔时才清理
|
||||
if (currentTime - lastCacheCleanup < CACHE_CLEANUP_INTERVAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastCacheCleanup = currentTime;
|
||||
|
||||
// 移除过期的缓存项
|
||||
recentViolationsCache.entrySet().removeIf(entry ->
|
||||
(currentTime - entry.getValue()) > SHORT_TERM_DEDUP_WINDOW
|
||||
);
|
||||
|
||||
log.debug("清理过期违规缓存,剩余缓存项: {}", recentViolationsCache.size());
|
||||
}
|
||||
}
|
||||
@ -725,6 +725,30 @@ public class SpatialRuleServiceImpl implements SpatialRuleService {
|
||||
getRuleCountByStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SpatialRule> findActiveRulesForArea(String areaCode, RuleCategory category) {
|
||||
logger.debug("查找区域的活跃规则 - 区域: {}, 分类: {}", areaCode, category);
|
||||
|
||||
try {
|
||||
// 查找与指定区域和分类匹配的规则
|
||||
List<SpatialRule> allRules = spatialRuleRepository.findBySpatialObjectIdAndCategory(areaCode, category);
|
||||
|
||||
// 过滤出当前有效的规则
|
||||
List<SpatialRule> activeRules = allRules.stream()
|
||||
.filter(rule -> rule.getStatus() == RuleStatus.ACTIVE)
|
||||
.filter(SpatialRule::isCurrentlyEffective)
|
||||
.sorted(Comparator.comparing(SpatialRule::getPriority))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
logger.debug("找到 {} 个活跃规则", activeRules.size());
|
||||
return activeRules;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("查找区域活跃规则失败 - 区域: {}, 分类: {}, 错误: {}", areaCode, category, e.getMessage());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 私有辅助方法 ==========
|
||||
|
||||
/**
|
||||
|
||||
@ -6,11 +6,13 @@ import com.qaup.collision.websocket.event.PositionUpdateEvent;
|
||||
import com.qaup.collision.websocket.event.RuleExecutionStatusWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.RuleStateChangeWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.RuleViolationWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.GeofenceAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.TrafficLightStatusEvent;
|
||||
import com.qaup.collision.websocket.event.VehicleCommandEvent;
|
||||
import com.qaup.collision.websocket.message.CollisionWarningPayload;
|
||||
import com.qaup.collision.websocket.message.MessageTypeConstants;
|
||||
import com.qaup.collision.websocket.message.GeofenceAlertPayload;
|
||||
import com.qaup.collision.websocket.message.PositionUpdatePayload;
|
||||
import com.qaup.collision.websocket.message.RuleExecutionStatusPayload;
|
||||
import com.qaup.collision.websocket.message.RuleStateChangePayload;
|
||||
@ -177,6 +179,22 @@ public class WebSocketMessageBroadcaster {
|
||||
broadcastMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理电子围栏告警事件
|
||||
* @param event 电子围栏告警事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleGeofenceAlert(GeofenceAlertWebSocketEvent event) {
|
||||
UniversalMessage<GeofenceAlertPayload> message = UniversalMessage.<GeofenceAlertPayload>builder()
|
||||
.type(MessageTypeConstants.GEOFENCE_ALERT)
|
||||
.timestamp(event.getTimestamp())
|
||||
.messageId(generateMessageId())
|
||||
.payload((GeofenceAlertPayload) event.getPayload())
|
||||
.build();
|
||||
|
||||
broadcastMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一消息广播方法 - 使用原生WebSocket发送JSON消息
|
||||
* @param message 要广播的消息
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
package com.qaup.collision.websocket.event;
|
||||
|
||||
import com.qaup.collision.websocket.message.GeofenceAlertPayload;
|
||||
import com.qaup.collision.websocket.message.MessageTypeConstants;
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 电子围栏告警WebSocket事件
|
||||
* 用于通知前端无人车电子围栏访问情况
|
||||
*
|
||||
* @author qaup-collision
|
||||
* @date 2025-01-15
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class GeofenceAlertWebSocketEvent implements WebSocketEvent {
|
||||
|
||||
/**
|
||||
* 事件唯一标识
|
||||
*/
|
||||
private final String eventId;
|
||||
|
||||
/**
|
||||
* 事件时间戳(微秒级)
|
||||
*/
|
||||
private final long timestamp;
|
||||
|
||||
/**
|
||||
* 电子围栏告警载荷数据
|
||||
*/
|
||||
private final GeofenceAlertPayload payload;
|
||||
|
||||
/**
|
||||
* 创建电子围栏告警WebSocket事件
|
||||
*
|
||||
* @param payload 告警事件载荷
|
||||
* @return WebSocket事件实例
|
||||
*/
|
||||
public static GeofenceAlertWebSocketEvent create(GeofenceAlertPayload payload) {
|
||||
return GeofenceAlertWebSocketEvent.builder()
|
||||
.eventId(UUID.randomUUID().toString())
|
||||
.timestamp(java.time.Instant.now().toEpochMilli() * 1000) // 微秒级绝对时间戳
|
||||
.payload(payload)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType() {
|
||||
return MessageTypeConstants.GEOFENCE_ALERT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPayload() {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,201 @@
|
||||
package com.qaup.collision.websocket.message;
|
||||
|
||||
import com.qaup.collision.geofence.model.dto.GeofenceAccessCheckResult;
|
||||
import com.qaup.collision.geofence.model.enums.GeofenceAlertLevel;
|
||||
import lombok.Data;
|
||||
import lombok.Builder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 电子围栏告警WebSocket消息载荷
|
||||
*
|
||||
* @author qaup-collision
|
||||
* @date 2025-01-15
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class GeofenceAlertPayload {
|
||||
|
||||
/**
|
||||
* 告警唯一标识
|
||||
*/
|
||||
private String alertId;
|
||||
|
||||
/**
|
||||
* 无人车ID
|
||||
*/
|
||||
private String vehicleId;
|
||||
|
||||
/**
|
||||
* 无人车名称/车牌号
|
||||
*/
|
||||
private String vehicleName;
|
||||
|
||||
/**
|
||||
* 区域编码
|
||||
*/
|
||||
private String areaCode;
|
||||
|
||||
/**
|
||||
* 区域名称
|
||||
*/
|
||||
private String areaName;
|
||||
|
||||
/**
|
||||
* 告警类型
|
||||
*/
|
||||
private GeofenceAccessCheckResult.AccessResultType alertType;
|
||||
|
||||
/**
|
||||
* 告警级别
|
||||
*/
|
||||
private GeofenceAlertLevel alertLevel;
|
||||
|
||||
/**
|
||||
* 告警消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 详细原因
|
||||
*/
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 无人车当前位置
|
||||
*/
|
||||
private Position position;
|
||||
|
||||
/**
|
||||
* 当前速度 (km/h)
|
||||
*/
|
||||
private Double currentSpeed;
|
||||
|
||||
/**
|
||||
* 触发的规则列表
|
||||
*/
|
||||
private List<TriggeredRule> triggeredRules;
|
||||
|
||||
/**
|
||||
* 建议操作
|
||||
*/
|
||||
private List<String> recommendedActions;
|
||||
|
||||
/**
|
||||
* 告警时间
|
||||
*/
|
||||
private LocalDateTime alertTime;
|
||||
|
||||
/**
|
||||
* 位置信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public static class Position {
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
private Double latitude;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
private Double longitude;
|
||||
|
||||
/**
|
||||
* 高度(米)
|
||||
*/
|
||||
private Double altitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发的规则信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public static class TriggeredRule {
|
||||
/**
|
||||
* 规则ID
|
||||
*/
|
||||
private String ruleId;
|
||||
|
||||
/**
|
||||
* 规则名称
|
||||
*/
|
||||
private String ruleName;
|
||||
|
||||
/**
|
||||
* 规则分类
|
||||
*/
|
||||
private String ruleCategory;
|
||||
|
||||
/**
|
||||
* 违规类型
|
||||
*/
|
||||
private String violationType;
|
||||
|
||||
/**
|
||||
* 告警级别
|
||||
*/
|
||||
private GeofenceAlertLevel alertLevel;
|
||||
|
||||
/**
|
||||
* 规则消息
|
||||
*/
|
||||
private String message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从GeofenceAccessCheckResult创建载荷
|
||||
*/
|
||||
public static GeofenceAlertPayload fromGeofenceResult(
|
||||
GeofenceAccessCheckResult result,
|
||||
String vehicleName,
|
||||
Double latitude,
|
||||
Double longitude,
|
||||
Double altitude,
|
||||
Double currentSpeed) {
|
||||
|
||||
GeofenceAlertPayloadBuilder builder = GeofenceAlertPayload.builder()
|
||||
.alertId(java.util.UUID.randomUUID().toString())
|
||||
.vehicleId(result.getVehicleId())
|
||||
.vehicleName(vehicleName)
|
||||
.areaCode(result.getAreaCode())
|
||||
.areaName(result.getAreaName())
|
||||
.alertType(result.getResultType())
|
||||
.alertLevel(result.getAlertLevel())
|
||||
.message(result.getMessage())
|
||||
.reason(result.getReason())
|
||||
.currentSpeed(currentSpeed)
|
||||
.alertTime(result.getCheckTime() != null ? result.getCheckTime() : LocalDateTime.now())
|
||||
.recommendedActions(result.getRecommendedActions());
|
||||
|
||||
// 设置位置信息
|
||||
if (latitude != null && longitude != null) {
|
||||
builder.position(Position.builder()
|
||||
.latitude(latitude)
|
||||
.longitude(longitude)
|
||||
.altitude(altitude)
|
||||
.build());
|
||||
}
|
||||
|
||||
// 转换触发的规则
|
||||
if (result.getTriggeredRules() != null && !result.getTriggeredRules().isEmpty()) {
|
||||
List<TriggeredRule> rules = result.getTriggeredRules().stream()
|
||||
.map(rule -> TriggeredRule.builder()
|
||||
.ruleId(rule.getRuleId())
|
||||
.ruleName(rule.getRuleName())
|
||||
.ruleCategory(rule.getRuleCategory())
|
||||
.violationType(rule.getViolationType())
|
||||
.alertLevel(rule.getAlertLevel())
|
||||
.message(rule.getMessage())
|
||||
.build())
|
||||
.toList();
|
||||
builder.triggeredRules(rules);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@ -68,6 +68,11 @@ public final class MessageTypeConstants {
|
||||
*/
|
||||
public static final String PATH_CONFLICT_ALERT = "path_conflict_alert";
|
||||
|
||||
/**
|
||||
* 电子围栏告警消息
|
||||
*/
|
||||
public static final String GEOFENCE_ALERT = "geofence_alert";
|
||||
|
||||
// 私有构造函数,防止实例化
|
||||
private MessageTypeConstants() {
|
||||
throw new UnsupportedOperationException("常量类不允许实例化");
|
||||
|
||||
@ -0,0 +1,314 @@
|
||||
package com.qaup.collision.geofence.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.common.model.spatial.AirportArea;
|
||||
import com.qaup.collision.common.service.AirportAreaService;
|
||||
import com.qaup.collision.geofence.model.dto.GeofenceAccessCheckResult;
|
||||
import com.qaup.collision.rule.model.enums.RuleCategory;
|
||||
import com.qaup.collision.rule.service.SpatialRuleService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import com.qaup.collision.websocket.event.GeofenceAlertWebSocketEvent;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/**
|
||||
* 无人车电子围栏服务测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UnmannedVehicleGeofenceServiceTest {
|
||||
|
||||
@Mock
|
||||
private AirportAreaService airportAreaService;
|
||||
|
||||
@Mock
|
||||
private SpatialRuleService spatialRuleService;
|
||||
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private UnmannedVehicleGeofenceService geofenceService;
|
||||
private GeometryFactory geometryFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
geofenceService = new UnmannedVehicleGeofenceService();
|
||||
ReflectionTestUtils.setField(geofenceService, "airportAreaService", airportAreaService);
|
||||
ReflectionTestUtils.setField(geofenceService, "spatialRuleService", spatialRuleService);
|
||||
ReflectionTestUtils.setField(geofenceService, "eventPublisher", eventPublisher);
|
||||
|
||||
geometryFactory = new GeometryFactory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckAreaAccess_UnmannedVehicle_AreaExists_NoRules_ShouldAllow() {
|
||||
// Arrange
|
||||
String areaCode = "TEST_AREA_01";
|
||||
Point position = geometryFactory.createPoint(new Coordinate(120.0, 36.0));
|
||||
|
||||
MovingObject unmannedVehicle = MovingObject.builder()
|
||||
.objectId("UV001")
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
|
||||
.objectName("测试无人车")
|
||||
.currentPosition(position)
|
||||
.currentSpeed(10.0)
|
||||
.build();
|
||||
|
||||
AirportArea area = new AirportArea();
|
||||
area.setAreaId(areaCode);
|
||||
area.setName("测试区域");
|
||||
area.setEnabled(true);
|
||||
|
||||
when(airportAreaService.findByAreaCode(areaCode)).thenReturn(Optional.of(area));
|
||||
when(spatialRuleService.findActiveRulesForArea(eq(areaCode), eq(RuleCategory.ACCESS_CONTROL)))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
// Act
|
||||
GeofenceAccessCheckResult result = geofenceService.checkAreaAccess(unmannedVehicle, areaCode);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals(GeofenceAccessCheckResult.AccessResultType.ALLOWED, result.getResultType());
|
||||
assertTrue(result.getAllowed());
|
||||
assertEquals(areaCode, result.getAreaCode());
|
||||
assertEquals("UV001", result.getVehicleId());
|
||||
assertEquals("允许进入", result.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckAreaAccess_NonUnmannedVehicle_ShouldSkip() {
|
||||
// Arrange
|
||||
String areaCode = "TEST_AREA_01";
|
||||
Point position = geometryFactory.createPoint(new Coordinate(120.0, 36.0));
|
||||
|
||||
MovingObject aircraft = MovingObject.builder()
|
||||
.objectId("AC001")
|
||||
.objectType(MovingObject.ObjectType.AIRCRAFT)
|
||||
.objectName("测试航空器")
|
||||
.currentPosition(position)
|
||||
.build();
|
||||
|
||||
// Act
|
||||
GeofenceAccessCheckResult result = geofenceService.checkAreaAccess(aircraft, areaCode);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals(GeofenceAccessCheckResult.AccessResultType.SKIPPED, result.getResultType());
|
||||
assertNull(result.getAllowed());
|
||||
assertEquals("AC001", result.getVehicleId());
|
||||
assertEquals("非无人车,跳过电子围栏检测", result.getMessage());
|
||||
|
||||
// Verify no service calls were made
|
||||
verifyNoInteractions(airportAreaService);
|
||||
verifyNoInteractions(spatialRuleService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckAreaAccess_AreaNotExists_ShouldDeny() {
|
||||
// Arrange
|
||||
String areaCode = "NONEXISTENT_AREA";
|
||||
Point position = geometryFactory.createPoint(new Coordinate(120.0, 36.0));
|
||||
|
||||
MovingObject unmannedVehicle = MovingObject.builder()
|
||||
.objectId("UV001")
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
|
||||
.objectName("测试无人车")
|
||||
.currentPosition(position)
|
||||
.build();
|
||||
|
||||
when(airportAreaService.findByAreaCode(areaCode)).thenReturn(Optional.empty());
|
||||
|
||||
// Act
|
||||
GeofenceAccessCheckResult result = geofenceService.checkAreaAccess(unmannedVehicle, areaCode);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals(GeofenceAccessCheckResult.AccessResultType.DENIED, result.getResultType());
|
||||
assertFalse(result.getAllowed());
|
||||
assertEquals(areaCode, result.getAreaCode());
|
||||
assertEquals("UV001", result.getVehicleId());
|
||||
assertEquals("区域不存在", result.getReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckAreaAccess_AreaDisabled_ShouldDeny() {
|
||||
// Arrange
|
||||
String areaCode = "DISABLED_AREA";
|
||||
Point position = geometryFactory.createPoint(new Coordinate(120.0, 36.0));
|
||||
|
||||
MovingObject unmannedVehicle = MovingObject.builder()
|
||||
.objectId("UV001")
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
|
||||
.objectName("测试无人车")
|
||||
.currentPosition(position)
|
||||
.build();
|
||||
|
||||
AirportArea disabledArea = new AirportArea();
|
||||
disabledArea.setAreaId(areaCode);
|
||||
disabledArea.setName("禁用区域");
|
||||
disabledArea.setEnabled(false); // 区域被禁用
|
||||
|
||||
when(airportAreaService.findByAreaCode(areaCode)).thenReturn(Optional.of(disabledArea));
|
||||
|
||||
// Act
|
||||
GeofenceAccessCheckResult result = geofenceService.checkAreaAccess(unmannedVehicle, areaCode);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals(GeofenceAccessCheckResult.AccessResultType.DENIED, result.getResultType());
|
||||
assertFalse(result.getAllowed());
|
||||
assertEquals(areaCode, result.getAreaCode());
|
||||
assertEquals("UV001", result.getVehicleId());
|
||||
assertEquals("区域已禁用", result.getReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckCurrentPosition_UnmannedVehicle_WithAreas() {
|
||||
// Arrange
|
||||
Point position = geometryFactory.createPoint(new Coordinate(120.0, 36.0));
|
||||
|
||||
MovingObject unmannedVehicle = MovingObject.builder()
|
||||
.objectId("UV001")
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
|
||||
.objectName("测试无人车")
|
||||
.currentPosition(position)
|
||||
.currentSpeed(15.0)
|
||||
.build();
|
||||
|
||||
AirportArea area1 = new AirportArea();
|
||||
area1.setAreaId("AREA_01");
|
||||
area1.setName("区域1");
|
||||
area1.setEnabled(true);
|
||||
|
||||
when(airportAreaService.findAreasContainingPoint(position))
|
||||
.thenReturn(List.of(area1));
|
||||
when(airportAreaService.findByAreaCode("AREA_01")).thenReturn(Optional.of(area1));
|
||||
when(spatialRuleService.findActiveRulesForArea(eq("AREA_01"), eq(RuleCategory.ACCESS_CONTROL)))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
// Act
|
||||
List<GeofenceAccessCheckResult> results = geofenceService.checkCurrentPosition(unmannedVehicle);
|
||||
|
||||
// Assert
|
||||
assertNotNull(results);
|
||||
assertEquals(1, results.size());
|
||||
|
||||
GeofenceAccessCheckResult result = results.get(0);
|
||||
assertEquals(GeofenceAccessCheckResult.AccessResultType.ALLOWED, result.getResultType());
|
||||
assertTrue(result.getAllowed());
|
||||
assertEquals("AREA_01", result.getAreaCode());
|
||||
assertEquals("UV001", result.getVehicleId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckCurrentPosition_NonUnmannedVehicle_ShouldSkip() {
|
||||
// Arrange
|
||||
Point position = geometryFactory.createPoint(new Coordinate(120.0, 36.0));
|
||||
|
||||
MovingObject aircraft = MovingObject.builder()
|
||||
.objectId("AC001")
|
||||
.objectType(MovingObject.ObjectType.AIRCRAFT)
|
||||
.objectName("测试航空器")
|
||||
.currentPosition(position)
|
||||
.build();
|
||||
|
||||
// Act
|
||||
List<GeofenceAccessCheckResult> results = geofenceService.checkCurrentPosition(aircraft);
|
||||
|
||||
// Assert
|
||||
assertNotNull(results);
|
||||
assertEquals(1, results.size());
|
||||
|
||||
GeofenceAccessCheckResult result = results.get(0);
|
||||
assertEquals(GeofenceAccessCheckResult.AccessResultType.SKIPPED, result.getResultType());
|
||||
assertNull(result.getAllowed());
|
||||
assertEquals("AC001", result.getVehicleId());
|
||||
|
||||
// Verify no service calls were made
|
||||
verifyNoInteractions(airportAreaService);
|
||||
verifyNoInteractions(spatialRuleService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckCurrentPosition_NullPosition_ShouldReturnEmpty() {
|
||||
// Arrange
|
||||
MovingObject unmannedVehicle = MovingObject.builder()
|
||||
.objectId("UV001")
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
|
||||
.objectName("测试无人车")
|
||||
.currentPosition(null) // 位置为空
|
||||
.build();
|
||||
|
||||
// Act
|
||||
List<GeofenceAccessCheckResult> results = geofenceService.checkCurrentPosition(unmannedVehicle);
|
||||
|
||||
// Assert
|
||||
assertNotNull(results);
|
||||
assertTrue(results.isEmpty());
|
||||
|
||||
// Verify no service calls were made
|
||||
verifyNoInteractions(airportAreaService);
|
||||
verifyNoInteractions(spatialRuleService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckCurrentPosition_WithDeniedAccess_ShouldPublishWebSocketEvent() {
|
||||
// Arrange
|
||||
Point position = geometryFactory.createPoint(new Coordinate(120.0, 36.0));
|
||||
|
||||
MovingObject unmannedVehicle = MovingObject.builder()
|
||||
.objectId("UV001")
|
||||
.objectType(MovingObject.ObjectType.UNMANNED_VEHICLE)
|
||||
.objectName("测试无人车")
|
||||
.currentPosition(position)
|
||||
.currentSpeed(15.0)
|
||||
.build();
|
||||
|
||||
AirportArea area1 = new AirportArea();
|
||||
area1.setAreaId("RESTRICTED_AREA");
|
||||
area1.setName("限制区域");
|
||||
area1.setEnabled(false); // 禁用的区域,会被拒绝
|
||||
|
||||
when(airportAreaService.findAreasContainingPoint(position))
|
||||
.thenReturn(List.of(area1));
|
||||
when(airportAreaService.findByAreaCode("RESTRICTED_AREA")).thenReturn(Optional.of(area1));
|
||||
|
||||
// Act
|
||||
List<GeofenceAccessCheckResult> results = geofenceService.checkCurrentPosition(unmannedVehicle);
|
||||
|
||||
// Assert
|
||||
assertNotNull(results);
|
||||
assertEquals(1, results.size());
|
||||
|
||||
GeofenceAccessCheckResult result = results.get(0);
|
||||
assertEquals(GeofenceAccessCheckResult.AccessResultType.DENIED, result.getResultType());
|
||||
assertFalse(result.getAllowed());
|
||||
assertEquals("RESTRICTED_AREA", result.getAreaCode());
|
||||
assertEquals("UV001", result.getVehicleId());
|
||||
assertEquals("区域已禁用", result.getReason());
|
||||
|
||||
// 验证WebSocket事件发布
|
||||
ArgumentCaptor<GeofenceAlertWebSocketEvent> eventCaptor = ArgumentCaptor.forClass(GeofenceAlertWebSocketEvent.class);
|
||||
verify(eventPublisher).publishEvent(eventCaptor.capture());
|
||||
|
||||
GeofenceAlertWebSocketEvent publishedEvent = eventCaptor.getValue();
|
||||
assertNotNull(publishedEvent);
|
||||
assertEquals("geofence_alert", publishedEvent.getEventType());
|
||||
assertNotNull(publishedEvent.getPayload());
|
||||
}
|
||||
}
|
||||
189
sql/README_geofence_upgrade.md
Normal file
189
sql/README_geofence_upgrade.md
Normal file
@ -0,0 +1,189 @@
|
||||
# 电子围栏功能数据库升级指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档说明如何升级数据库以支持无人车电子围栏准入检测功能。
|
||||
|
||||
## 🛠️ 数据库表结构变更
|
||||
|
||||
### 1. 主要变更内容
|
||||
|
||||
#### 表结构更新:
|
||||
- **`airport_areas` → `airport_areas`**: 完全重构,匹配Java实体类
|
||||
- **新增 `spatial_rules`**: 空间安全规则表
|
||||
- **新增 `spatial_rule_vehicle_types`**: 规则允许的车辆类型关联表
|
||||
|
||||
#### 字段变更:
|
||||
- 添加了 `area_id`、`enabled`、`priority` 等关键字段
|
||||
- 使用 `boundary` 字段替代 `geometry`
|
||||
- 添加了完整的审计字段(created_at, updated_at等)
|
||||
|
||||
### 2. 测试数据
|
||||
|
||||
基于 `route.md` 中的无人车路线创建了3个测试区域:
|
||||
|
||||
1. **UV_TEST_AREA_A**: 无人车A测试区域
|
||||
- 覆盖路线:120.083084,36.369696 → 120.084637,36.365617
|
||||
- 规则:WARNING级别,需要权限验证
|
||||
|
||||
2. **UV_TEST_AREA_B**: 无人车B测试区域
|
||||
- 覆盖路线:120.086965,36.368599 → 120.086263,36.370484
|
||||
- 规则:WARNING级别,需要权限验证
|
||||
|
||||
3. **UV_INTERSECTION_ZONE**: 交汇测试区域
|
||||
- 覆盖两车可能相交的区域
|
||||
- 规则:CRITICAL级别,需要最高权限
|
||||
|
||||
## 📋 升级步骤
|
||||
|
||||
### 步骤1: 备份现有数据库
|
||||
|
||||
```bash
|
||||
pg_dump -h localhost -U postgres -d qaup > qaup_backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
```
|
||||
|
||||
### 步骤2: 执行数据库升级脚本
|
||||
|
||||
```bash
|
||||
# 连接到数据库
|
||||
psql -h localhost -U postgres -d qaup
|
||||
|
||||
# 执行升级脚本
|
||||
\i sql/geofence_schema_update.sql
|
||||
```
|
||||
|
||||
### 步骤3: 验证升级结果
|
||||
|
||||
```bash
|
||||
# 执行验证脚本
|
||||
\i sql/geofence_validation.sql
|
||||
```
|
||||
|
||||
## 🔍 验证检查项
|
||||
|
||||
执行验证脚本后,应该看到:
|
||||
|
||||
1. **3个测试区域**被正确创建
|
||||
2. **3个准入控制规则**被正确配置
|
||||
3. **无人车位置检测**能正确识别包含的区域
|
||||
4. **禁用区域检测**能正确返回拒绝访问
|
||||
|
||||
### 预期验证结果示例:
|
||||
|
||||
```sql
|
||||
-- 区域检查
|
||||
area_id | name | enabled | priority
|
||||
UV_TEST_AREA_A | 无人车A测试区域 | t | 5
|
||||
UV_TEST_AREA_B | 无人车B测试区域 | t | 5
|
||||
UV_INTERSECTION_ZONE | 无人车交汇测试区域 | t | 10
|
||||
|
||||
-- 规则检查
|
||||
rule_id | rule_name | alert_level
|
||||
GEOFENCE_UV_AREA_A_ACCESS | 无人车A区域准入控制 | WARNING
|
||||
GEOFENCE_UV_AREA_B_ACCESS | 无人车B区域准入控制 | WARNING
|
||||
GEOFENCE_INTERSECTION_CRITICAL | 交汇区域严格控制 | CRITICAL
|
||||
```
|
||||
|
||||
## 🚨 注意事项
|
||||
|
||||
### 1. 数据库兼容性
|
||||
- 确保PostgreSQL版本 ≥ 12
|
||||
- 确保已安装PostGIS扩展
|
||||
- 确保坐标系SRID 4326支持
|
||||
|
||||
### 2. 应用程序兼容性
|
||||
- Java实体类已更新匹配新表结构
|
||||
- 无需修改应用程序代码
|
||||
- 现有功能不受影响
|
||||
|
||||
### 3. 性能考虑
|
||||
- 已创建必要的空间索引
|
||||
- 已创建查询优化索引
|
||||
- 大量数据情况下建议分批处理
|
||||
|
||||
## 🧪 测试电子围栏功能
|
||||
|
||||
### 1. 启动应用程序
|
||||
|
||||
```bash
|
||||
mvn spring-boot:run -pl qaup-admin
|
||||
```
|
||||
|
||||
### 2. 模拟无人车数据
|
||||
|
||||
系统会自动采集无人车位置数据,当无人车进入测试区域时:
|
||||
|
||||
- **日志输出**: 会显示围栏检测结果
|
||||
- **WebSocket消息**: 前端会收到告警消息
|
||||
- **数据库记录**: 检测结果会被记录
|
||||
|
||||
### 3. 查看实时检测
|
||||
|
||||
监控应用日志中的围栏检测信息:
|
||||
|
||||
```bash
|
||||
# 查看围栏检测日志
|
||||
tail -f logs/qaup.log | grep -i geofence
|
||||
|
||||
# 预期日志示例:
|
||||
# [INFO] 发布电子围栏告警WebSocket事件: 无人车=鲁B567, 区域=UV_TEST_AREA_A, 类型=WARNING, 级别=WARNING
|
||||
```
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 问题1: PostGIS扩展未安装
|
||||
```sql
|
||||
ERROR: function st_geomfromtext(unknown, integer) does not exist
|
||||
```
|
||||
**解决方案**:
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
```
|
||||
|
||||
### 问题2: 表已存在错误
|
||||
```sql
|
||||
ERROR: relation "airport_areas" already exists
|
||||
```
|
||||
**解决方案**: 脚本已包含 `DROP TABLE IF EXISTS` 语句
|
||||
|
||||
### 问题3: 几何数据无效
|
||||
**解决方案**: 检查坐标格式,确保使用WGS84坐标系
|
||||
|
||||
## 📊 监控和维护
|
||||
|
||||
### 1. 监控查询
|
||||
|
||||
```sql
|
||||
-- 查看活跃区域数量
|
||||
SELECT COUNT(*) FROM airport_areas WHERE enabled = true;
|
||||
|
||||
-- 查看规则执行统计
|
||||
SELECT
|
||||
rule_category,
|
||||
COUNT(*) as rule_count,
|
||||
COUNT(CASE WHEN status = 'ACTIVE' THEN 1 END) as active_count
|
||||
FROM spatial_rules
|
||||
GROUP BY rule_category;
|
||||
|
||||
-- 查看最近的围栏事件(需要添加事件记录表)
|
||||
```
|
||||
|
||||
### 2. 定期维护
|
||||
|
||||
- 定期清理过期的临时区域
|
||||
- 更新规则配置以适应运营需求
|
||||
- 监控系统性能和响应时间
|
||||
|
||||
## ✅ 升级完成确认
|
||||
|
||||
升级完成后,系统应该具备以下功能:
|
||||
|
||||
1. ✅ 无人车实时位置检测
|
||||
2. ✅ 电子围栏准入控制
|
||||
3. ✅ WebSocket实时告警
|
||||
4. ✅ 规则驱动的访问控制
|
||||
5. ✅ 完整的日志记录
|
||||
|
||||
**升级完成!** 🎉
|
||||
|
||||
电子围栏功能现已就绪,可以开始测试无人车的准入检测功能。
|
||||
208
sql/add_speed_limit_rules.sql
Normal file
208
sql/add_speed_limit_rules.sql
Normal file
@ -0,0 +1,208 @@
|
||||
-- ================================================================
|
||||
-- 补充限速规则和APRON区域的数据库脚本
|
||||
-- 用于兼容原有的限速检测功能
|
||||
-- ================================================================
|
||||
|
||||
-- 1. 添加APRON(停机坪)区域
|
||||
-- 基于route.md中的坐标范围创建一个合理的停机坪区域
|
||||
INSERT INTO "public"."airport_areas" (
|
||||
"area_id", "name", "type", "boundary", "description", "enabled", "priority",
|
||||
"functional_category", "access_level", "created_by"
|
||||
) VALUES (
|
||||
'APRON_01',
|
||||
'停机坪区域01',
|
||||
'APRON',
|
||||
ST_GeomFromText('POLYGON((120.080000 36.372000, 120.088000 36.372000, 120.088000 36.366000, 120.080000 36.366000, 120.080000 36.372000))', 4326),
|
||||
'主要停机坪区域,覆盖无人车和航空器活动区域,包含route.md中所有测试路线',
|
||||
true,
|
||||
8,
|
||||
'AIRCRAFT_OPERATIONS',
|
||||
'CONTROLLED',
|
||||
'system'
|
||||
);
|
||||
|
||||
-- 2. 为APRON区域添加限速规则(兼容原有的限速检测系统)
|
||||
INSERT INTO "public"."spatial_rules" (
|
||||
"rule_id", "rule_name", "description", "rule_category", "status", "priority",
|
||||
"spatial_object_type", "spatial_object_id", "alert_level", "alert_message",
|
||||
"rule_parameters", "created_by"
|
||||
) VALUES (
|
||||
'SPEED_LIMIT_APRON',
|
||||
'停机坪限速规则',
|
||||
'停机坪区域内所有车辆的限速控制规则',
|
||||
'SPEED_LIMIT',
|
||||
'ACTIVE',
|
||||
1,
|
||||
'AREA',
|
||||
'APRON_01',
|
||||
'WARNING',
|
||||
'停机坪内超速行驶',
|
||||
'{"maxSpeed":15.0,"unit":"kmh","warningThreshold":0.8,"strictMode":true}',
|
||||
'system'
|
||||
);
|
||||
|
||||
-- 3. 添加规则允许的车辆类型(所有类型车辆都受限速规则约束)
|
||||
INSERT INTO "public"."spatial_rule_vehicle_types" ("rule_id", "vehicle_type") VALUES
|
||||
('SPEED_LIMIT_APRON', 'UNMANNED_VEHICLE'),
|
||||
('SPEED_LIMIT_APRON', 'AIRCRAFT'),
|
||||
('SPEED_LIMIT_APRON', 'SPECIAL_VEHICLE'),
|
||||
('SPEED_LIMIT_APRON', 'AIRPORT_VEHICLE'),
|
||||
('SPEED_LIMIT_APRON', 'NORMAL_VEHICLE');
|
||||
|
||||
-- 4. 添加滑行道区域和限速规则
|
||||
INSERT INTO "public"."airport_areas" (
|
||||
"area_id", "name", "type", "boundary", "description", "enabled", "priority",
|
||||
"functional_category", "access_level", "created_by"
|
||||
) VALUES (
|
||||
'TAXIWAY_ALPHA',
|
||||
'滑行道Alpha',
|
||||
'TAXIWAY',
|
||||
ST_GeomFromText('POLYGON((120.082000 36.370000, 120.086000 36.370000, 120.086000 36.368000, 120.082000 36.368000, 120.082000 36.370000))', 4326),
|
||||
'主要滑行道,连接停机坪和跑道',
|
||||
true,
|
||||
7,
|
||||
'AIRCRAFT_OPERATIONS',
|
||||
'CONTROLLED',
|
||||
'system'
|
||||
);
|
||||
|
||||
-- 滑行道限速规则
|
||||
INSERT INTO "public"."spatial_rules" (
|
||||
"rule_id", "rule_name", "description", "rule_category", "status", "priority",
|
||||
"spatial_object_type", "spatial_object_id", "alert_level", "alert_message",
|
||||
"rule_parameters", "created_by"
|
||||
) VALUES (
|
||||
'SPEED_LIMIT_TAXIWAY',
|
||||
'滑行道限速规则',
|
||||
'滑行道区域内的限速控制',
|
||||
'SPEED_LIMIT',
|
||||
'ACTIVE',
|
||||
2,
|
||||
'AREA',
|
||||
'TAXIWAY_ALPHA',
|
||||
'WARNING',
|
||||
'滑行道内超速行驶',
|
||||
'{"maxSpeed":25.0,"unit":"kmh","warningThreshold":0.9}',
|
||||
'system'
|
||||
);
|
||||
|
||||
-- 滑行道限速规则的车辆类型
|
||||
INSERT INTO "public"."spatial_rule_vehicle_types" ("rule_id", "vehicle_type") VALUES
|
||||
('SPEED_LIMIT_TAXIWAY', 'UNMANNED_VEHICLE'),
|
||||
('SPEED_LIMIT_TAXIWAY', 'AIRCRAFT'),
|
||||
('SPEED_LIMIT_TAXIWAY', 'SPECIAL_VEHICLE'),
|
||||
('SPEED_LIMIT_TAXIWAY', 'AIRPORT_VEHICLE');
|
||||
|
||||
-- 5. 验证插入的数据
|
||||
-- 检查区域
|
||||
SELECT
|
||||
area_id,
|
||||
name,
|
||||
type,
|
||||
enabled,
|
||||
priority,
|
||||
functional_category,
|
||||
ST_AsText(boundary) as boundary_wkt
|
||||
FROM airport_areas
|
||||
WHERE type IN ('APRON', 'TAXIWAY')
|
||||
ORDER BY priority DESC;
|
||||
|
||||
-- 检查限速规则
|
||||
SELECT
|
||||
r.rule_id,
|
||||
r.rule_name,
|
||||
r.rule_category,
|
||||
r.spatial_object_id,
|
||||
r.alert_level,
|
||||
r.rule_parameters,
|
||||
array_agg(vt.vehicle_type ORDER BY vt.vehicle_type) as allowed_vehicle_types
|
||||
FROM spatial_rules r
|
||||
LEFT JOIN spatial_rule_vehicle_types vt ON r.rule_id = vt.rule_id
|
||||
WHERE r.rule_category = 'SPEED_LIMIT'
|
||||
GROUP BY r.rule_id, r.rule_name, r.rule_category, r.spatial_object_id, r.alert_level, r.rule_parameters
|
||||
ORDER BY r.rule_id;
|
||||
|
||||
-- 6. 测试限速检测(模拟无人车在APRON区域内的情况)
|
||||
-- 测试无人车A在停机坪区域的限速检测
|
||||
WITH test_vehicle AS (
|
||||
SELECT
|
||||
'UV_鲁B567' as vehicle_id,
|
||||
'UNMANNED_VEHICLE' as vehicle_type,
|
||||
ST_SetSRID(ST_MakePoint(120.084000, 36.369000), 4326) as current_position,
|
||||
18.5 as current_speed -- 超过15km/h限速
|
||||
),
|
||||
area_check AS (
|
||||
SELECT
|
||||
tv.vehicle_id,
|
||||
tv.vehicle_type,
|
||||
tv.current_speed,
|
||||
aa.area_id,
|
||||
aa.name as area_name,
|
||||
aa.type as area_type,
|
||||
ST_Contains(aa.boundary, tv.current_position) as is_inside
|
||||
FROM test_vehicle tv
|
||||
CROSS JOIN airport_areas aa
|
||||
WHERE ST_Contains(aa.boundary, tv.current_position)
|
||||
),
|
||||
rule_check AS (
|
||||
SELECT
|
||||
ac.*,
|
||||
sr.rule_id,
|
||||
sr.rule_name,
|
||||
sr.rule_category,
|
||||
sr.alert_level,
|
||||
sr.rule_parameters,
|
||||
CASE
|
||||
WHEN sr.rule_category = 'SPEED_LIMIT' THEN
|
||||
CASE
|
||||
WHEN (sr.rule_parameters->>'maxSpeed')::float < ac.current_speed THEN 'VIOLATION'
|
||||
ELSE 'COMPLIANT'
|
||||
END
|
||||
ELSE 'NOT_APPLICABLE'
|
||||
END as speed_check_result
|
||||
FROM area_check ac
|
||||
LEFT JOIN spatial_rules sr ON ac.area_id = sr.spatial_object_id
|
||||
AND sr.status = 'ACTIVE'
|
||||
LEFT JOIN spatial_rule_vehicle_types vt ON sr.rule_id = vt.rule_id
|
||||
AND vt.vehicle_type = ac.vehicle_type
|
||||
)
|
||||
SELECT
|
||||
vehicle_id,
|
||||
area_name,
|
||||
area_type,
|
||||
current_speed as "当前速度(km/h)",
|
||||
rule_name,
|
||||
(rule_parameters->>'maxSpeed')::float as "限速(km/h)",
|
||||
speed_check_result as "检测结果",
|
||||
alert_level as "告警级别"
|
||||
FROM rule_check
|
||||
WHERE rule_category = 'SPEED_LIMIT' OR rule_category IS NULL;
|
||||
|
||||
-- 7. 创建一个视图用于简化限速检测查询
|
||||
CREATE OR REPLACE VIEW speed_limit_areas AS
|
||||
SELECT
|
||||
aa.area_id,
|
||||
aa.name as area_name,
|
||||
aa.type as area_type,
|
||||
aa.boundary,
|
||||
sr.rule_id,
|
||||
sr.rule_name,
|
||||
(sr.rule_parameters->>'maxSpeed')::float as max_speed_kmh,
|
||||
(sr.rule_parameters->>'warningThreshold')::float as warning_threshold,
|
||||
sr.alert_level,
|
||||
array_agg(DISTINCT vt.vehicle_type) as applicable_vehicle_types
|
||||
FROM airport_areas aa
|
||||
JOIN spatial_rules sr ON aa.area_id = sr.spatial_object_id
|
||||
JOIN spatial_rule_vehicle_types vt ON sr.rule_id = vt.rule_id
|
||||
WHERE sr.rule_category = 'SPEED_LIMIT' AND sr.status = 'ACTIVE' AND aa.enabled = true
|
||||
GROUP BY aa.area_id, aa.name, aa.type, aa.boundary, sr.rule_id, sr.rule_name,
|
||||
sr.rule_parameters, sr.alert_level;
|
||||
|
||||
-- 查看创建的视图
|
||||
SELECT * FROM speed_limit_areas ORDER BY max_speed_kmh;
|
||||
|
||||
-- 完成
|
||||
SELECT 'Speed limit rules and APRON areas added successfully!' as status,
|
||||
COUNT(*) as total_speed_rules
|
||||
FROM spatial_rules
|
||||
WHERE rule_category = 'SPEED_LIMIT';
|
||||
128
sql/fix_duplicate_rules.sql
Normal file
128
sql/fix_duplicate_rules.sql
Normal file
@ -0,0 +1,128 @@
|
||||
-- ================================================================
|
||||
-- 修复重复规则的SQL脚本
|
||||
-- 用于清理数据库中重复的空间规则定义
|
||||
-- ================================================================
|
||||
|
||||
-- 1. 查找重复的规则(相同的规则名称)
|
||||
SELECT
|
||||
rule_name,
|
||||
COUNT(*) as rule_count,
|
||||
array_agg(rule_id ORDER BY rule_id) as rule_ids,
|
||||
array_agg(spatial_object_id ORDER BY rule_id) as spatial_object_ids
|
||||
FROM spatial_rules
|
||||
GROUP BY rule_name
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY rule_name;
|
||||
|
||||
-- 2. 查找具体的重复规则详情
|
||||
WITH duplicate_rules AS (
|
||||
SELECT rule_name, COUNT(*) as cnt
|
||||
FROM spatial_rules
|
||||
GROUP BY rule_name
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
SELECT
|
||||
sr.rule_id,
|
||||
sr.rule_name,
|
||||
sr.description,
|
||||
sr.rule_category,
|
||||
sr.status,
|
||||
sr.priority,
|
||||
sr.spatial_object_type,
|
||||
sr.spatial_object_id,
|
||||
sr.alert_level,
|
||||
sr.rule_parameters,
|
||||
sr.created_by,
|
||||
sr.created_at
|
||||
FROM spatial_rules sr
|
||||
INNER JOIN duplicate_rules dr ON sr.rule_name = dr.rule_name
|
||||
ORDER BY sr.rule_name, sr.created_at;
|
||||
|
||||
-- 3. 显示重复规则对应的车辆类型
|
||||
WITH duplicate_rules AS (
|
||||
SELECT rule_name, COUNT(*) as cnt
|
||||
FROM spatial_rules
|
||||
GROUP BY rule_name
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
SELECT
|
||||
sr.rule_id,
|
||||
sr.rule_name,
|
||||
sr.spatial_object_id,
|
||||
array_agg(vt.vehicle_type ORDER BY vt.vehicle_type) as vehicle_types
|
||||
FROM spatial_rules sr
|
||||
INNER JOIN duplicate_rules dr ON sr.rule_name = dr.rule_name
|
||||
LEFT JOIN spatial_rule_vehicle_types vt ON sr.rule_id = vt.rule_id
|
||||
GROUP BY sr.rule_id, sr.rule_name, sr.spatial_object_id
|
||||
ORDER BY sr.rule_name, sr.rule_id;
|
||||
|
||||
-- 4. 查找停机坪限速规则的重复情况
|
||||
SELECT
|
||||
rule_id,
|
||||
rule_name,
|
||||
spatial_object_id,
|
||||
rule_parameters,
|
||||
status,
|
||||
created_at,
|
||||
created_by
|
||||
FROM spatial_rules
|
||||
WHERE rule_name LIKE '%停机坪限速%' OR rule_name LIKE '%APRON%'
|
||||
ORDER BY created_at;
|
||||
|
||||
-- 5. 删除重复的规则(保留最早创建的规则)
|
||||
-- 注意:在执行删除之前,请确认要删除的规则ID
|
||||
|
||||
-- 5.1 删除重复规则的车辆类型关联(如果存在)
|
||||
-- DELETE FROM spatial_rule_vehicle_types
|
||||
-- WHERE rule_id IN (
|
||||
-- SELECT rule_id
|
||||
-- FROM spatial_rules
|
||||
-- WHERE rule_name = '停机坪限速规则'
|
||||
-- AND rule_id != (
|
||||
-- SELECT rule_id
|
||||
-- FROM spatial_rules
|
||||
-- WHERE rule_name = '停机坪限速规则'
|
||||
-- ORDER BY created_at ASC
|
||||
-- LIMIT 1
|
||||
-- )
|
||||
-- );
|
||||
|
||||
-- 5.2 删除重复的规则记录(保留最早的)
|
||||
-- DELETE FROM spatial_rules
|
||||
-- WHERE rule_name = '停机坪限速规则'
|
||||
-- AND rule_id != (
|
||||
-- SELECT rule_id
|
||||
-- FROM spatial_rules
|
||||
-- WHERE rule_name = '停机坪限速规则'
|
||||
-- ORDER BY created_at ASC
|
||||
-- LIMIT 1
|
||||
-- );
|
||||
|
||||
-- 6. 验证清理结果
|
||||
-- SELECT
|
||||
-- rule_name,
|
||||
-- COUNT(*) as rule_count
|
||||
-- FROM spatial_rules
|
||||
-- GROUP BY rule_name
|
||||
-- HAVING COUNT(*) > 1;
|
||||
|
||||
-- 7. 检查最终的规则状态
|
||||
SELECT
|
||||
r.rule_id,
|
||||
r.rule_name,
|
||||
r.rule_category,
|
||||
r.spatial_object_id,
|
||||
r.status,
|
||||
r.priority,
|
||||
r.rule_parameters,
|
||||
array_agg(vt.vehicle_type ORDER BY vt.vehicle_type) as allowed_vehicle_types
|
||||
FROM spatial_rules r
|
||||
LEFT JOIN spatial_rule_vehicle_types vt ON r.rule_id = vt.rule_id
|
||||
WHERE r.status = 'ACTIVE'
|
||||
GROUP BY r.rule_id, r.rule_name, r.rule_category, r.spatial_object_id, r.status, r.priority, r.rule_parameters
|
||||
ORDER BY r.rule_name;
|
||||
|
||||
-- 使用说明:
|
||||
-- 1. 首先运行步骤1-4来查看重复规则的情况
|
||||
-- 2. 确认需要删除的重复规则后,取消注释步骤5中的DELETE语句并执行
|
||||
-- 3. 运行步骤6-7验证清理结果
|
||||
361
sql/geofence_schema_update.sql
Normal file
361
sql/geofence_schema_update.sql
Normal file
@ -0,0 +1,361 @@
|
||||
-- ================================================================
|
||||
-- 电子围栏功能数据库表结构更新脚本
|
||||
-- 用于支持无人车电子围栏准入检测功能
|
||||
-- 版本:1.0
|
||||
-- 创建时间:2025-07-12
|
||||
-- ================================================================
|
||||
|
||||
-- 1. 更新 airport_areas 表结构
|
||||
-- 先删除旧表(如果存在)
|
||||
DROP TABLE IF EXISTS "public"."airport_areas" CASCADE;
|
||||
|
||||
-- 创建新的机场区域表,匹配Java实体类AirportArea
|
||||
CREATE TABLE "public"."airport_areas" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"area_id" varchar(50) NOT NULL UNIQUE,
|
||||
"name" varchar(100) NOT NULL,
|
||||
"type" varchar(30) NOT NULL,
|
||||
"boundary" geometry(POLYGON, 4326) NOT NULL,
|
||||
"description" varchar(500),
|
||||
"active_time" timestamptz,
|
||||
"expiry_time" timestamptz,
|
||||
"enabled" boolean DEFAULT true,
|
||||
"priority" int4 DEFAULT 1,
|
||||
"area_sqm" float8,
|
||||
"functional_category" varchar(50),
|
||||
"parent_area_id" varchar(50),
|
||||
"created_at" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"version" bigint DEFAULT 0,
|
||||
CONSTRAINT "airport_areas_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- 表注释
|
||||
COMMENT ON TABLE "public"."airport_areas" IS '机场区域信息表 - 支持电子围栏功能';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."id" IS '主键ID';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."area_id" IS '区域标识符';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."name" IS '区域名称';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."type" IS '区域类型(RUNWAY、TAXIWAY、APRON等)';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."boundary" IS '区域几何边界(PostGIS多边形)';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."description" IS '区域描述';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."active_time" IS '生效时间(用于临时区域)';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."expiry_time" IS '失效时间(用于临时区域)';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."enabled" IS '是否启用';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."priority" IS '优先级(数值越高优先级越高)';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."area_sqm" IS '区域面积(平方米)';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."functional_category" IS '功能分类';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."parent_area_id" IS '父区域ID';
|
||||
COMMENT ON COLUMN "public"."airport_areas"."version" IS '数据版本号(用于乐观锁)';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX "idx_airport_areas_area_id" ON "public"."airport_areas" ("area_id");
|
||||
CREATE INDEX "idx_airport_areas_type" ON "public"."airport_areas" ("type");
|
||||
CREATE INDEX "idx_airport_areas_enabled" ON "public"."airport_areas" ("enabled");
|
||||
CREATE INDEX "idx_airport_areas_functional_category" ON "public"."airport_areas" ("functional_category");
|
||||
CREATE INDEX "idx_airport_areas_parent_area_id" ON "public"."airport_areas" ("parent_area_id");
|
||||
CREATE INDEX "idx_airport_areas_geom" ON "public"."airport_areas" USING GIST ("boundary");
|
||||
|
||||
-- 2. 创建空间规则表
|
||||
DROP TABLE IF EXISTS "public"."spatial_rules" CASCADE;
|
||||
DROP TABLE IF EXISTS "public"."spatial_rule_vehicle_types" CASCADE;
|
||||
|
||||
CREATE TABLE "public"."spatial_rules" (
|
||||
"rule_id" varchar(50) NOT NULL,
|
||||
"rule_name" varchar(200) NOT NULL,
|
||||
"description" text,
|
||||
"rule_category" varchar(50) NOT NULL,
|
||||
"status" varchar(20) NOT NULL,
|
||||
"priority" int4 NOT NULL,
|
||||
"spatial_object_type" varchar(30) NOT NULL,
|
||||
"spatial_object_id" varchar(50),
|
||||
"rule_parameters" jsonb,
|
||||
"alert_level" varchar(20) NOT NULL,
|
||||
"alert_message" varchar(500),
|
||||
"effective_start_time" timestamp(6),
|
||||
"effective_end_time" timestamp(6),
|
||||
"daily_start_time" time,
|
||||
"daily_end_time" time,
|
||||
"weekday_pattern" int4,
|
||||
"time_patterns" jsonb,
|
||||
"created_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"created_by" varchar(100),
|
||||
"updated_by" varchar(100),
|
||||
"version" bigint DEFAULT 0,
|
||||
"custom_geometry" geometry,
|
||||
CONSTRAINT "spatial_rules_pkey" PRIMARY KEY ("rule_id")
|
||||
);
|
||||
|
||||
-- 创建车辆类型关联表
|
||||
CREATE TABLE "public"."spatial_rule_vehicle_types" (
|
||||
"rule_id" varchar(50) NOT NULL,
|
||||
"vehicle_type" varchar(30) NOT NULL,
|
||||
CONSTRAINT "spatial_rule_vehicle_types_pkey" PRIMARY KEY ("rule_id", "vehicle_type"),
|
||||
CONSTRAINT "fk_spatial_rule_vehicle_types_rule" FOREIGN KEY ("rule_id") REFERENCES "public"."spatial_rules" ("rule_id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 表注释
|
||||
COMMENT ON TABLE "public"."spatial_rules" IS '空间安全规则表';
|
||||
COMMENT ON TABLE "public"."spatial_rule_vehicle_types" IS '空间规则允许的车辆类型表';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX "idx_spatial_rules_status" ON "public"."spatial_rules" ("status");
|
||||
CREATE INDEX "idx_spatial_rules_category" ON "public"."spatial_rules" ("rule_category");
|
||||
CREATE INDEX "idx_spatial_rules_object_type" ON "public"."spatial_rules" ("spatial_object_type");
|
||||
CREATE INDEX "idx_spatial_rules_priority" ON "public"."spatial_rules" ("priority");
|
||||
CREATE INDEX "idx_spatial_rules_object_id" ON "public"."spatial_rules" ("spatial_object_id");
|
||||
|
||||
-- 3. 插入测试电子围栏区域
|
||||
-- 基于route.md中两辆无人车的运行路线创建测试区域
|
||||
|
||||
-- 无人车A测试区域 (基于路线: 经度120.083084,纬度36.369696 到 经度120.084637,纬度36.365617)
|
||||
INSERT INTO "public"."airport_areas" (
|
||||
"area_id", "name", "type", "boundary", "description", "enabled", "priority",
|
||||
"functional_category"
|
||||
) VALUES (
|
||||
'UV_TEST_AREA_A',
|
||||
'无人车A测试区域',
|
||||
'UNMANNED_VEHICLE_ZONE',
|
||||
ST_GeomFromText('POLYGON((120.082584 36.370196, 120.085137 36.370196, 120.085137 36.365117, 120.082584 36.365117, 120.082584 36.370196))', 4326),
|
||||
'无人车A运行测试区域,包含起点和终点的矩形区域',
|
||||
true,
|
||||
5,
|
||||
'UNMANNED_VEHICLE_OPERATIONS'
|
||||
);
|
||||
|
||||
-- 无人车B测试区域 (基于路线: 经度120.086965,纬度36.368599 到 经度120.086263,纬度36.370484)
|
||||
INSERT INTO "public"."airport_areas" (
|
||||
"area_id", "name", "type", "boundary", "description", "enabled", "priority",
|
||||
"functional_category"
|
||||
) VALUES (
|
||||
'UV_TEST_AREA_B',
|
||||
'无人车B测试区域',
|
||||
'UNMANNED_VEHICLE_ZONE',
|
||||
ST_GeomFromText('POLYGON((120.085765 36.371484, 120.087465 36.371484, 120.087465 36.368099, 120.085765 36.368099, 120.085765 36.371484))', 4326),
|
||||
'无人车B运行测试区域,包含起点和终点的矩形区域',
|
||||
true,
|
||||
5,
|
||||
'UNMANNED_VEHICLE_OPERATIONS'
|
||||
);
|
||||
|
||||
-- 共同测试区域 (两辆无人车路径可能相交的区域)
|
||||
INSERT INTO "public"."airport_areas" (
|
||||
"area_id", "name", "type", "boundary", "description", "enabled", "priority",
|
||||
"functional_category"
|
||||
) VALUES (
|
||||
'UV_INTERSECTION_ZONE',
|
||||
'无人车交汇测试区域',
|
||||
'INTERSECTION_ZONE',
|
||||
ST_GeomFromText('POLYGON((120.083000 36.371000, 120.087000 36.371000, 120.087000 36.365000, 120.083000 36.365000, 120.083000 36.371000))', 4326),
|
||||
'无人车路径可能相交的大型测试区域,用于测试电子围栏功能',
|
||||
true,
|
||||
10,
|
||||
'UNMANNED_VEHICLE_OPERATIONS'
|
||||
);
|
||||
|
||||
-- 4. 插入测试规则
|
||||
-- 无人车A测试区域的准入控制规则
|
||||
INSERT INTO "public"."spatial_rules" (
|
||||
"rule_id", "rule_name", "description", "rule_category", "status", "priority",
|
||||
"spatial_object_type", "spatial_object_id", "alert_level", "alert_message",
|
||||
"rule_parameters", "created_by"
|
||||
) VALUES (
|
||||
'GEOFENCE_UV_AREA_A_ACCESS',
|
||||
'无人车A区域准入控制',
|
||||
'限制无人车A测试区域的访问权限',
|
||||
'ACCESS_CONTROL',
|
||||
'ACTIVE',
|
||||
1,
|
||||
'AREA',
|
||||
'UV_TEST_AREA_A',
|
||||
'WARNING',
|
||||
'进入无人车A测试区域需要授权',
|
||||
'{"accessControl":{"requiresPermission":true,"allowedOperationModes":["AUTONOMOUS","MANUAL"]},"physicalLimits":{"maxSpeed":20.0},"safetyRequirements":{"minBatteryLevel":30}}',
|
||||
'system'
|
||||
);
|
||||
|
||||
-- 无人车B测试区域的准入控制规则
|
||||
INSERT INTO "public"."spatial_rules" (
|
||||
"rule_id", "rule_name", "description", "rule_category", "status", "priority",
|
||||
"spatial_object_type", "spatial_object_id", "alert_level", "alert_message",
|
||||
"rule_parameters", "created_by"
|
||||
) VALUES (
|
||||
'GEOFENCE_UV_AREA_B_ACCESS',
|
||||
'无人车B区域准入控制',
|
||||
'限制无人车B测试区域的访问权限',
|
||||
'ACCESS_CONTROL',
|
||||
'ACTIVE',
|
||||
1,
|
||||
'AREA',
|
||||
'UV_TEST_AREA_B',
|
||||
'WARNING',
|
||||
'进入无人车B测试区域需要授权',
|
||||
'{"accessControl":{"requiresPermission":true,"allowedOperationModes":["AUTONOMOUS","MANUAL"]},"physicalLimits":{"maxSpeed":20.0},"safetyRequirements":{"minBatteryLevel":30}}',
|
||||
'system'
|
||||
);
|
||||
|
||||
-- 交汇区域的严格控制规则
|
||||
INSERT INTO "public"."spatial_rules" (
|
||||
"rule_id", "rule_name", "description", "rule_category", "status", "priority",
|
||||
"spatial_object_type", "spatial_object_id", "alert_level", "alert_message",
|
||||
"rule_parameters", "created_by"
|
||||
) VALUES (
|
||||
'GEOFENCE_INTERSECTION_CRITICAL',
|
||||
'交汇区域严格控制',
|
||||
'交汇区域需要严格的安全控制',
|
||||
'ACCESS_CONTROL',
|
||||
'ACTIVE',
|
||||
10,
|
||||
'AREA',
|
||||
'UV_INTERSECTION_ZONE',
|
||||
'CRITICAL',
|
||||
'进入交汇区域需要最高级别授权',
|
||||
'{"accessControl":{"requiresPermission":true,"allowedOperationModes":["MANUAL"]},"physicalLimits":{"maxSpeed":15.0},"safetyRequirements":{"minBatteryLevel":50,"requiresOperatorPresence":true}}',
|
||||
'system'
|
||||
);
|
||||
|
||||
-- 5. 插入规则允许的车辆类型
|
||||
INSERT INTO "public"."spatial_rule_vehicle_types" ("rule_id", "vehicle_type") VALUES
|
||||
('GEOFENCE_UV_AREA_A_ACCESS', 'UNMANNED_VEHICLE'),
|
||||
('GEOFENCE_UV_AREA_B_ACCESS', 'UNMANNED_VEHICLE'),
|
||||
('GEOFENCE_INTERSECTION_CRITICAL', 'UNMANNED_VEHICLE');
|
||||
|
||||
-- 6. 添加APRON(停机坪)区域和限速规则(兼容原有限速检测系统)
|
||||
-- APRON区域
|
||||
INSERT INTO "public"."airport_areas" (
|
||||
"area_id", "name", "type", "boundary", "description", "enabled", "priority",
|
||||
"functional_category"
|
||||
) VALUES (
|
||||
'APRON_01',
|
||||
'停机坪区域01',
|
||||
'APRON',
|
||||
ST_GeomFromText('POLYGON((120.080000 36.372000, 120.088000 36.372000, 120.088000 36.366000, 120.080000 36.366000, 120.080000 36.372000))', 4326),
|
||||
'主要停机坪区域,覆盖无人车和航空器活动区域,包含route.md中所有测试路线',
|
||||
true,
|
||||
8,
|
||||
'AIRCRAFT_OPERATIONS'
|
||||
);
|
||||
|
||||
-- 停机坪限速规则
|
||||
INSERT INTO "public"."spatial_rules" (
|
||||
"rule_id", "rule_name", "description", "rule_category", "status", "priority",
|
||||
"spatial_object_type", "spatial_object_id", "alert_level", "alert_message",
|
||||
"rule_parameters", "created_by"
|
||||
) VALUES (
|
||||
'SPEED_LIMIT_APRON',
|
||||
'停机坪限速规则',
|
||||
'停机坪区域内所有车辆的限速控制规则',
|
||||
'SPEED_LIMIT',
|
||||
'ACTIVE',
|
||||
1,
|
||||
'AREA',
|
||||
'APRON_01',
|
||||
'WARNING',
|
||||
'停机坪内超速行驶',
|
||||
'{"maxSpeed":15.0,"unit":"kmh","warningThreshold":0.8,"strictMode":true}',
|
||||
'system'
|
||||
);
|
||||
|
||||
-- 停机坪限速规则的车辆类型(所有类型车辆都受限速规则约束)
|
||||
INSERT INTO "public"."spatial_rule_vehicle_types" ("rule_id", "vehicle_type") VALUES
|
||||
('SPEED_LIMIT_APRON', 'UNMANNED_VEHICLE'),
|
||||
('SPEED_LIMIT_APRON', 'AIRCRAFT'),
|
||||
('SPEED_LIMIT_APRON', 'SPECIAL_VEHICLE'),
|
||||
('SPEED_LIMIT_APRON', 'AIRPORT_VEHICLE'),
|
||||
('SPEED_LIMIT_APRON', 'NORMAL_VEHICLE');
|
||||
|
||||
-- 7. 添加滑行道区域和限速规则
|
||||
INSERT INTO "public"."airport_areas" (
|
||||
"area_id", "name", "type", "boundary", "description", "enabled", "priority",
|
||||
"functional_category"
|
||||
) VALUES (
|
||||
'TAXIWAY_ALPHA',
|
||||
'滑行道Alpha',
|
||||
'TAXIWAY',
|
||||
ST_GeomFromText('POLYGON((120.082000 36.370000, 120.086000 36.370000, 120.086000 36.368000, 120.082000 36.368000, 120.082000 36.370000))', 4326),
|
||||
'主要滑行道,连接停机坪和跑道',
|
||||
true,
|
||||
7,
|
||||
'AIRCRAFT_OPERATIONS'
|
||||
);
|
||||
|
||||
-- 滑行道限速规则
|
||||
INSERT INTO "public"."spatial_rules" (
|
||||
"rule_id", "rule_name", "description", "rule_category", "status", "priority",
|
||||
"spatial_object_type", "spatial_object_id", "alert_level", "alert_message",
|
||||
"rule_parameters", "created_by"
|
||||
) VALUES (
|
||||
'SPEED_LIMIT_TAXIWAY',
|
||||
'滑行道限速规则',
|
||||
'滑行道区域内的限速控制',
|
||||
'SPEED_LIMIT',
|
||||
'ACTIVE',
|
||||
2,
|
||||
'AREA',
|
||||
'TAXIWAY_ALPHA',
|
||||
'WARNING',
|
||||
'滑行道内超速行驶',
|
||||
'{"maxSpeed":25.0,"unit":"kmh","warningThreshold":0.9}',
|
||||
'system'
|
||||
);
|
||||
|
||||
-- 滑行道限速规则的车辆类型
|
||||
INSERT INTO "public"."spatial_rule_vehicle_types" ("rule_id", "vehicle_type") VALUES
|
||||
('SPEED_LIMIT_TAXIWAY', 'UNMANNED_VEHICLE'),
|
||||
('SPEED_LIMIT_TAXIWAY', 'AIRCRAFT'),
|
||||
('SPEED_LIMIT_TAXIWAY', 'SPECIAL_VEHICLE'),
|
||||
('SPEED_LIMIT_TAXIWAY', 'AIRPORT_VEHICLE');
|
||||
|
||||
-- 8. 创建限速检测视图
|
||||
CREATE OR REPLACE VIEW speed_limit_areas AS
|
||||
SELECT
|
||||
aa.area_id,
|
||||
aa.name as area_name,
|
||||
aa.type as area_type,
|
||||
aa.boundary,
|
||||
sr.rule_id,
|
||||
sr.rule_name,
|
||||
(sr.rule_parameters->>'maxSpeed')::float as max_speed_kmh,
|
||||
(sr.rule_parameters->>'warningThreshold')::float as warning_threshold,
|
||||
sr.alert_level,
|
||||
array_agg(DISTINCT vt.vehicle_type) as applicable_vehicle_types
|
||||
FROM airport_areas aa
|
||||
JOIN spatial_rules sr ON aa.area_id = sr.spatial_object_id
|
||||
JOIN spatial_rule_vehicle_types vt ON sr.rule_id = vt.rule_id
|
||||
WHERE sr.rule_category = 'SPEED_LIMIT' AND sr.status = 'ACTIVE' AND aa.enabled = true
|
||||
GROUP BY aa.area_id, aa.name, aa.type, aa.boundary, sr.rule_id, sr.rule_name,
|
||||
sr.rule_parameters, sr.alert_level;
|
||||
|
||||
-- 9. 更新序列值(如果需要)
|
||||
SELECT setval('airport_areas_id_seq', (SELECT COALESCE(MAX(id), 1) FROM airport_areas));
|
||||
|
||||
-- 10. 验证数据插入
|
||||
SELECT
|
||||
area_id,
|
||||
name,
|
||||
type,
|
||||
enabled,
|
||||
priority,
|
||||
ST_AsText(boundary) as boundary_wkt,
|
||||
created_at
|
||||
FROM airport_areas
|
||||
WHERE area_id IN ('UV_TEST_AREA_A', 'UV_TEST_AREA_B', 'UV_INTERSECTION_ZONE', 'APRON_01', 'TAXIWAY_ALPHA')
|
||||
ORDER BY priority DESC;
|
||||
|
||||
SELECT
|
||||
rule_id,
|
||||
rule_name,
|
||||
rule_category,
|
||||
status,
|
||||
spatial_object_id,
|
||||
alert_level,
|
||||
created_at
|
||||
FROM spatial_rules
|
||||
WHERE rule_id LIKE 'GEOFENCE_%' OR rule_id LIKE 'SPEED_LIMIT_%'
|
||||
ORDER BY rule_category, rule_id;
|
||||
|
||||
-- 11. 验证限速检测视图
|
||||
SELECT * FROM speed_limit_areas ORDER BY max_speed_kmh;
|
||||
|
||||
-- 完成
|
||||
SELECT 'Database schema updated successfully for geofence functionality!' as status;
|
||||
186
sql/geofence_validation.sql
Normal file
186
sql/geofence_validation.sql
Normal file
@ -0,0 +1,186 @@
|
||||
-- ================================================================
|
||||
-- 电子围栏功能验证脚本
|
||||
-- 用于验证无人车在测试区域内的电子围栏检测
|
||||
-- ================================================================
|
||||
|
||||
-- 1. 查看测试区域信息
|
||||
SELECT
|
||||
area_id,
|
||||
name,
|
||||
type,
|
||||
enabled,
|
||||
priority,
|
||||
functional_category,
|
||||
ST_AsText(boundary) as boundary_wkt,
|
||||
ST_Area(boundary::geography) as area_square_meters
|
||||
FROM airport_areas
|
||||
WHERE area_id IN ('UV_TEST_AREA_A', 'UV_TEST_AREA_B', 'UV_INTERSECTION_ZONE')
|
||||
ORDER BY priority DESC;
|
||||
|
||||
-- 2. 查看测试规则信息
|
||||
SELECT
|
||||
r.rule_id,
|
||||
r.rule_name,
|
||||
r.rule_category,
|
||||
r.status,
|
||||
r.spatial_object_id,
|
||||
r.alert_level,
|
||||
r.rule_parameters,
|
||||
array_agg(vt.vehicle_type) as allowed_vehicle_types
|
||||
FROM spatial_rules r
|
||||
LEFT JOIN spatial_rule_vehicle_types vt ON r.rule_id = vt.rule_id
|
||||
WHERE r.rule_id LIKE 'GEOFENCE_%'
|
||||
GROUP BY r.rule_id, r.rule_name, r.rule_category, r.status, r.spatial_object_id, r.alert_level, r.rule_parameters
|
||||
ORDER BY r.rule_id;
|
||||
|
||||
-- 3. 模拟无人车A在起点位置的检测
|
||||
-- 无人车A起点:经度120.083084,纬度36.369696
|
||||
WITH unmanned_vehicle_a AS (
|
||||
SELECT
|
||||
'UV_A_鲁B567' as vehicle_id,
|
||||
ST_SetSRID(ST_MakePoint(120.083084, 36.369696), 4326) as current_position
|
||||
),
|
||||
containing_areas AS (
|
||||
SELECT
|
||||
uv.vehicle_id,
|
||||
aa.area_id,
|
||||
aa.name as area_name,
|
||||
aa.type,
|
||||
aa.enabled,
|
||||
ST_Contains(aa.boundary, uv.current_position) as is_inside
|
||||
FROM unmanned_vehicle_a uv
|
||||
CROSS JOIN airport_areas aa
|
||||
WHERE ST_Contains(aa.boundary, uv.current_position)
|
||||
)
|
||||
SELECT
|
||||
ca.*,
|
||||
sr.rule_id,
|
||||
sr.rule_name,
|
||||
sr.alert_level,
|
||||
sr.rule_parameters
|
||||
FROM containing_areas ca
|
||||
LEFT JOIN spatial_rules sr ON ca.area_id = sr.spatial_object_id
|
||||
AND sr.status = 'ACTIVE'
|
||||
AND sr.rule_category = 'ACCESS_CONTROL';
|
||||
|
||||
-- 4. 模拟无人车B在起点位置的检测
|
||||
-- 无人车B起点:经度120.086965,纬度36.368599
|
||||
WITH unmanned_vehicle_b AS (
|
||||
SELECT
|
||||
'UV_B_鲁B579' as vehicle_id,
|
||||
ST_SetSRID(ST_MakePoint(120.086965, 36.368599), 4326) as current_position
|
||||
),
|
||||
containing_areas AS (
|
||||
SELECT
|
||||
uv.vehicle_id,
|
||||
aa.area_id,
|
||||
aa.name as area_name,
|
||||
aa.type,
|
||||
aa.enabled,
|
||||
ST_Contains(aa.boundary, uv.current_position) as is_inside
|
||||
FROM unmanned_vehicle_b uv
|
||||
CROSS JOIN airport_areas aa
|
||||
WHERE ST_Contains(aa.boundary, uv.current_position)
|
||||
)
|
||||
SELECT
|
||||
ca.*,
|
||||
sr.rule_id,
|
||||
sr.rule_name,
|
||||
sr.alert_level,
|
||||
sr.rule_parameters
|
||||
FROM containing_areas ca
|
||||
LEFT JOIN spatial_rules sr ON ca.area_id = sr.spatial_object_id
|
||||
AND sr.status = 'ACTIVE'
|
||||
AND sr.rule_category = 'ACCESS_CONTROL';
|
||||
|
||||
-- 5. 模拟无人车在交汇区域中心的检测
|
||||
-- 交汇区域中心:经度120.085000,纬度36.368000
|
||||
WITH unmanned_vehicle_center AS (
|
||||
SELECT
|
||||
'UV_CENTER_TEST' as vehicle_id,
|
||||
ST_SetSRID(ST_MakePoint(120.085000, 36.368000), 4326) as current_position
|
||||
),
|
||||
containing_areas AS (
|
||||
SELECT
|
||||
uv.vehicle_id,
|
||||
aa.area_id,
|
||||
aa.name as area_name,
|
||||
aa.type,
|
||||
aa.enabled,
|
||||
ST_Contains(aa.boundary, uv.current_position) as is_inside
|
||||
FROM unmanned_vehicle_center uv
|
||||
CROSS JOIN airport_areas aa
|
||||
WHERE ST_Contains(aa.boundary, uv.current_position)
|
||||
)
|
||||
SELECT
|
||||
ca.*,
|
||||
sr.rule_id,
|
||||
sr.rule_name,
|
||||
sr.alert_level,
|
||||
sr.rule_parameters
|
||||
FROM containing_areas ca
|
||||
LEFT JOIN spatial_rules sr ON ca.area_id = sr.spatial_object_id
|
||||
AND sr.status = 'ACTIVE'
|
||||
AND sr.rule_category = 'ACCESS_CONTROL'
|
||||
ORDER BY sr.rule_id;
|
||||
|
||||
-- 6. 测试禁用区域的检测
|
||||
-- 临时禁用无人车A区域
|
||||
UPDATE airport_areas
|
||||
SET enabled = false, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE area_id = 'UV_TEST_AREA_A';
|
||||
|
||||
-- 重新测试无人车A位置检测(应该返回拒绝访问)
|
||||
WITH unmanned_vehicle_a_disabled AS (
|
||||
SELECT
|
||||
'UV_A_DISABLED_TEST' as vehicle_id,
|
||||
ST_SetSRID(ST_MakePoint(120.083084, 36.369696), 4326) as current_position
|
||||
),
|
||||
containing_areas AS (
|
||||
SELECT
|
||||
uv.vehicle_id,
|
||||
aa.area_id,
|
||||
aa.name as area_name,
|
||||
aa.type,
|
||||
aa.enabled,
|
||||
ST_Contains(aa.boundary, uv.current_position) as is_inside,
|
||||
CASE
|
||||
WHEN aa.enabled = false THEN 'DENIED - Area Disabled'
|
||||
ELSE 'ALLOWED'
|
||||
END as access_result
|
||||
FROM unmanned_vehicle_a_disabled uv
|
||||
CROSS JOIN airport_areas aa
|
||||
WHERE ST_Contains(aa.boundary, uv.current_position)
|
||||
)
|
||||
SELECT * FROM containing_areas;
|
||||
|
||||
-- 恢复区域启用状态
|
||||
UPDATE airport_areas
|
||||
SET enabled = true, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE area_id = 'UV_TEST_AREA_A';
|
||||
|
||||
-- 7. 查看所有测试数据汇总
|
||||
SELECT
|
||||
'Areas Count' as metric,
|
||||
COUNT(*) as value
|
||||
FROM airport_areas
|
||||
WHERE area_id LIKE 'UV_%'
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'Rules Count' as metric,
|
||||
COUNT(*) as value
|
||||
FROM spatial_rules
|
||||
WHERE rule_id LIKE 'GEOFENCE_%'
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'Vehicle Types Count' as metric,
|
||||
COUNT(*) as value
|
||||
FROM spatial_rule_vehicle_types
|
||||
WHERE rule_id LIKE 'GEOFENCE_%';
|
||||
|
||||
-- 完成验证
|
||||
SELECT 'Geofence validation completed successfully!' as status;
|
||||
431
sql/qaup_database_complete_init.sql
Normal file
431
sql/qaup_database_complete_init.sql
Normal file
@ -0,0 +1,431 @@
|
||||
-- ================================================================
|
||||
-- QAUP 机场车辆管理和碰撞避免系统统一数据库初始化脚本
|
||||
-- 数据库名称:qaup
|
||||
-- 版本:3.8.9
|
||||
-- 创建时间:2025-07-12
|
||||
-- 描述:该脚本包含完整的数据库结构,用于系统的全新部署
|
||||
-- ================================================================
|
||||
|
||||
-- 1. 创建数据库(如果不存在)
|
||||
-- 注意:此部分需要以数据库管理员身份执行
|
||||
-- CREATE DATABASE qaup
|
||||
-- WITH
|
||||
-- OWNER = postgres
|
||||
-- ENCODING = 'UTF8'
|
||||
-- LC_COLLATE = 'en_US.UTF-8'
|
||||
-- LC_CTYPE = 'en_US.UTF-8'
|
||||
-- TABLESPACE = pg_default
|
||||
-- CONNECTION LIMIT = -1
|
||||
-- TEMPLATE = template0;
|
||||
|
||||
-- 设置数据库注释
|
||||
-- COMMENT ON DATABASE qaup IS '机场车辆管理和碰撞避免统一数据库';
|
||||
|
||||
-- 连接到数据库并启用扩展
|
||||
-- \c qaup
|
||||
|
||||
-- 2. 启用必要的PostgreSQL扩展
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
||||
|
||||
-- 显示PostGIS版本信息(可选)
|
||||
-- SELECT PostGIS_Version();
|
||||
|
||||
-- ================================================================
|
||||
-- 3. 核心系统表(RuoYi框架基础表)
|
||||
-- ================================================================
|
||||
|
||||
-- 3.1 用户信息表
|
||||
DROP TABLE IF EXISTS "public"."sys_user" CASCADE;
|
||||
CREATE TABLE "public"."sys_user" (
|
||||
"user_id" BIGSERIAL NOT NULL,
|
||||
"user_name" varchar(30) NOT NULL,
|
||||
"nick_name" varchar(30) NOT NULL,
|
||||
"user_type" varchar(2) DEFAULT '00',
|
||||
"email" varchar(50) DEFAULT '',
|
||||
"phonenumber" varchar(11) DEFAULT '',
|
||||
"sex" char(1) DEFAULT '0',
|
||||
"avatar" varchar(100) DEFAULT '',
|
||||
"password" varchar(100) DEFAULT '',
|
||||
"status" char(1) DEFAULT '0',
|
||||
"del_flag" char(1) DEFAULT '0',
|
||||
"login_ip" varchar(128) DEFAULT '',
|
||||
"login_date" timestamp(6),
|
||||
"create_by" varchar(64) DEFAULT '',
|
||||
"create_time" timestamp(6),
|
||||
"update_by" varchar(64) DEFAULT '',
|
||||
"update_time" timestamp(6),
|
||||
"remark" varchar(500) DEFAULT NULL,
|
||||
CONSTRAINT "sys_user_pkey" PRIMARY KEY ("user_id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."sys_user" IS '用户信息表';
|
||||
COMMENT ON COLUMN "public"."sys_user"."user_id" IS '用户ID';
|
||||
COMMENT ON COLUMN "public"."sys_user"."user_name" IS '用户账号';
|
||||
COMMENT ON COLUMN "public"."sys_user"."nick_name" IS '用户昵称';
|
||||
COMMENT ON COLUMN "public"."sys_user"."user_type" IS '用户类型(00系统用户)';
|
||||
COMMENT ON COLUMN "public"."sys_user"."email" IS '用户邮箱';
|
||||
COMMENT ON COLUMN "public"."sys_user"."phonenumber" IS '手机号码';
|
||||
COMMENT ON COLUMN "public"."sys_user"."sex" IS '用户性别(0男 1女 2未知)';
|
||||
COMMENT ON COLUMN "public"."sys_user"."avatar" IS '头像地址';
|
||||
COMMENT ON COLUMN "public"."sys_user"."password" IS '密码';
|
||||
COMMENT ON COLUMN "public"."sys_user"."status" IS '帐号状态(0正常 1停用)';
|
||||
COMMENT ON COLUMN "public"."sys_user"."del_flag" IS '删除标志(0代表存在 2代表删除)';
|
||||
|
||||
-- 3.2 角色信息表
|
||||
DROP TABLE IF EXISTS "public"."sys_role" CASCADE;
|
||||
CREATE TABLE "public"."sys_role" (
|
||||
"role_id" BIGSERIAL NOT NULL,
|
||||
"role_name" varchar(30) NOT NULL,
|
||||
"role_key" varchar(100) NOT NULL,
|
||||
"role_sort" int4 NOT NULL,
|
||||
"data_scope" char(1) DEFAULT '1',
|
||||
"menu_check_strictly" boolean DEFAULT true,
|
||||
"dept_check_strictly" boolean DEFAULT true,
|
||||
"status" char(1) NOT NULL,
|
||||
"del_flag" char(1) DEFAULT '0',
|
||||
"create_by" varchar(64) DEFAULT '',
|
||||
"create_time" timestamp(6),
|
||||
"update_by" varchar(64) DEFAULT '',
|
||||
"update_time" timestamp(6),
|
||||
"remark" varchar(500) DEFAULT NULL,
|
||||
CONSTRAINT "sys_role_pkey" PRIMARY KEY ("role_id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."sys_role" IS '角色信息表';
|
||||
COMMENT ON COLUMN "public"."sys_role"."role_id" IS '角色ID';
|
||||
COMMENT ON COLUMN "public"."sys_role"."role_name" IS '角色名称';
|
||||
COMMENT ON COLUMN "public"."sys_role"."role_key" IS '角色权限字符串';
|
||||
COMMENT ON COLUMN "public"."sys_role"."role_sort" IS '显示顺序';
|
||||
|
||||
-- 3.3 部门表
|
||||
DROP TABLE IF EXISTS "public"."sys_dept" CASCADE;
|
||||
CREATE TABLE "public"."sys_dept" (
|
||||
"dept_id" BIGSERIAL NOT NULL,
|
||||
"parent_id" int8 DEFAULT 0,
|
||||
"ancestors" varchar(50) DEFAULT '',
|
||||
"dept_name" varchar(30) DEFAULT '',
|
||||
"order_num" int4 DEFAULT 0,
|
||||
"leader" varchar(20) DEFAULT NULL,
|
||||
"phone" varchar(11) DEFAULT NULL,
|
||||
"email" varchar(50) DEFAULT NULL,
|
||||
"status" char(1) DEFAULT '0',
|
||||
"del_flag" char(1) DEFAULT '0',
|
||||
"create_by" varchar(64) DEFAULT '',
|
||||
"create_time" timestamp(6),
|
||||
"update_by" varchar(64) DEFAULT '',
|
||||
"update_time" timestamp(6),
|
||||
CONSTRAINT "sys_dept_pkey" PRIMARY KEY ("dept_id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."sys_dept" IS '部门表';
|
||||
COMMENT ON COLUMN "public"."sys_dept"."dept_id" IS '部门id';
|
||||
COMMENT ON COLUMN "public"."sys_dept"."parent_id" IS '父部门id';
|
||||
COMMENT ON COLUMN "public"."sys_dept"."ancestors" IS '祖级列表';
|
||||
COMMENT ON COLUMN "public"."sys_dept"."dept_name" IS '部门名称';
|
||||
|
||||
-- ================================================================
|
||||
-- 4. 车辆管理相关表
|
||||
-- ================================================================
|
||||
|
||||
-- 4.1 车辆类型表
|
||||
DROP TABLE IF EXISTS "public"."sys_vehicle_type" CASCADE;
|
||||
CREATE TABLE "public"."sys_vehicle_type" (
|
||||
"type_id" BIGSERIAL NOT NULL,
|
||||
"type_name" varchar(100) NOT NULL,
|
||||
"description" varchar(500),
|
||||
"max_speed" int4,
|
||||
"create_by" varchar(64),
|
||||
"create_time" timestamp(6),
|
||||
"update_by" varchar(64),
|
||||
"update_time" timestamp(6),
|
||||
"remark" varchar(500),
|
||||
CONSTRAINT "sys_vehicle_type_pkey" PRIMARY KEY ("type_id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."sys_vehicle_type" IS '车辆类型表';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_type"."type_id" IS '类型ID';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_type"."type_name" IS '类型名称';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_type"."description" IS '类型描述';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_type"."max_speed" IS '最大速度(km/h)';
|
||||
|
||||
-- 4.2 车辆信息表
|
||||
DROP TABLE IF EXISTS "public"."sys_vehicle_info" CASCADE;
|
||||
CREATE TABLE "public"."sys_vehicle_info" (
|
||||
"vehicle_id" BIGSERIAL NOT NULL,
|
||||
"license_plate" varchar(50) NOT NULL UNIQUE,
|
||||
"vin_number" varchar(50) UNIQUE,
|
||||
"type_id" int8 NOT NULL,
|
||||
"brand" varchar(100),
|
||||
"owning_unit" varchar(255),
|
||||
"contact_person" varchar(100),
|
||||
"phone_number" varchar(20),
|
||||
"image_url" varchar(255),
|
||||
"create_by" varchar(64),
|
||||
"create_time" timestamp(6),
|
||||
"update_by" varchar(64),
|
||||
"update_time" timestamp(6),
|
||||
"remark" varchar(500),
|
||||
CONSTRAINT "sys_vehicle_info_pkey" PRIMARY KEY ("vehicle_id"),
|
||||
CONSTRAINT "sys_vehicle_info_fk1" FOREIGN KEY ("type_id") REFERENCES "public"."sys_vehicle_type" ("type_id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."sys_vehicle_info" IS '车辆信息表';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_info"."vehicle_id" IS '车辆ID';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_info"."license_plate" IS '车牌号';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_info"."vin_number" IS 'VIN码';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_info"."type_id" IS '类型ID';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_info"."brand" IS '品牌';
|
||||
COMMENT ON COLUMN "public"."sys_vehicle_info"."owning_unit" IS '所属单位';
|
||||
|
||||
-- 4.3 司机信息表
|
||||
DROP TABLE IF EXISTS "public"."sys_driver_info" CASCADE;
|
||||
CREATE TABLE "public"."sys_driver_info" (
|
||||
"user_id" int8 NOT NULL,
|
||||
"license_type" varchar(50) NOT NULL,
|
||||
"create_by" varchar(64),
|
||||
"create_time" timestamp(6),
|
||||
"update_by" varchar(64),
|
||||
"update_time" timestamp(6),
|
||||
"remark" varchar(500),
|
||||
CONSTRAINT "sys_driver_info_pkey" PRIMARY KEY ("user_id"),
|
||||
CONSTRAINT "sys_driver_info_fk1" FOREIGN KEY ("user_id") REFERENCES "public"."sys_user" ("user_id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."sys_driver_info" IS '驾驶员信息表';
|
||||
COMMENT ON COLUMN "public"."sys_driver_info"."user_id" IS '用户ID';
|
||||
COMMENT ON COLUMN "public"."sys_driver_info"."license_type" IS '驾驶证类型';
|
||||
|
||||
-- ================================================================
|
||||
-- 5. 碰撞避免系统相关表(空间数据表)
|
||||
-- ================================================================
|
||||
|
||||
-- 5.1 车辆位置表(含空间数据)
|
||||
DROP TABLE IF EXISTS "public"."vehicle_location" CASCADE;
|
||||
CREATE TABLE "public"."vehicle_location" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"vehicle_id" int8 NOT NULL,
|
||||
"location" geometry(Point, 4326),
|
||||
"timestamp" timestamp(6) NOT NULL,
|
||||
"speed" numeric(5,2),
|
||||
"direction" numeric(5,2),
|
||||
"altitude" numeric(8,2),
|
||||
"create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "vehicle_location_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "vehicle_location_fk1" FOREIGN KEY ("vehicle_id") REFERENCES "public"."sys_vehicle_info" ("vehicle_id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."vehicle_location" IS '车辆位置信息表(含空间数据)';
|
||||
COMMENT ON COLUMN "public"."vehicle_location"."id" IS '记录ID';
|
||||
COMMENT ON COLUMN "public"."vehicle_location"."vehicle_id" IS '车辆ID';
|
||||
COMMENT ON COLUMN "public"."vehicle_location"."location" IS '位置信息(PostGIS几何类型)';
|
||||
COMMENT ON COLUMN "public"."vehicle_location"."timestamp" IS '时间戳';
|
||||
COMMENT ON COLUMN "public"."vehicle_location"."speed" IS '速度(km/h)';
|
||||
COMMENT ON COLUMN "public"."vehicle_location"."direction" IS '方向角(度)';
|
||||
COMMENT ON COLUMN "public"."vehicle_location"."altitude" IS '海拔高度(米)';
|
||||
|
||||
-- 为位置字段创建空间索引
|
||||
CREATE INDEX "idx_vehicle_location_geom" ON "public"."vehicle_location" USING GIST ("location");
|
||||
CREATE INDEX "idx_vehicle_location_vehicle_id" ON "public"."vehicle_location" ("vehicle_id");
|
||||
CREATE INDEX "idx_vehicle_location_timestamp" ON "public"."vehicle_location" ("timestamp");
|
||||
|
||||
-- 5.2 机场区域表
|
||||
DROP TABLE IF EXISTS "public"."airport_area" CASCADE;
|
||||
CREATE TABLE "public"."airport_area" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"area_name" varchar(100) NOT NULL,
|
||||
"area_type" varchar(50) NOT NULL,
|
||||
"geometry" geometry(Polygon, 4326),
|
||||
"properties" jsonb,
|
||||
"create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"update_time" timestamp(6),
|
||||
CONSTRAINT "airport_area_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."airport_area" IS '机场区域信息表';
|
||||
COMMENT ON COLUMN "public"."airport_area"."id" IS '区域ID';
|
||||
COMMENT ON COLUMN "public"."airport_area"."area_name" IS '区域名称';
|
||||
COMMENT ON COLUMN "public"."airport_area"."area_type" IS '区域类型';
|
||||
COMMENT ON COLUMN "public"."airport_area"."geometry" IS '区域几何形状';
|
||||
COMMENT ON COLUMN "public"."airport_area"."properties" IS '区域属性(JSON格式)';
|
||||
|
||||
-- 为区域几何字段创建空间索引
|
||||
CREATE INDEX "idx_airport_area_geom" ON "public"."airport_area" USING GIST ("geometry");
|
||||
|
||||
-- 5.3 路径冲突检测表
|
||||
DROP TABLE IF EXISTS "public"."path_conflict_detection" CASCADE;
|
||||
CREATE TABLE "public"."path_conflict_detection" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"object1_id" int8 NOT NULL,
|
||||
"object2_id" int8 NOT NULL,
|
||||
"conflict_type" varchar(50) NOT NULL,
|
||||
"conflict_time" timestamp(6) NOT NULL,
|
||||
"conflict_point" geometry(Point, 4326),
|
||||
"risk_level" varchar(20) NOT NULL,
|
||||
"status" varchar(20) DEFAULT 'ACTIVE',
|
||||
"resolved_time" timestamp(6),
|
||||
"create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "path_conflict_detection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."path_conflict_detection" IS '路径冲突检测表';
|
||||
COMMENT ON COLUMN "public"."path_conflict_detection"."id" IS '检测记录ID';
|
||||
COMMENT ON COLUMN "public"."path_conflict_detection"."object1_id" IS '对象1的ID';
|
||||
COMMENT ON COLUMN "public"."path_conflict_detection"."object2_id" IS '对象2的ID';
|
||||
COMMENT ON COLUMN "public"."path_conflict_detection"."conflict_type" IS '冲突类型';
|
||||
COMMENT ON COLUMN "public"."path_conflict_detection"."conflict_time" IS '冲突时间';
|
||||
COMMENT ON COLUMN "public"."path_conflict_detection"."conflict_point" IS '冲突点位置';
|
||||
COMMENT ON COLUMN "public"."path_conflict_detection"."risk_level" IS '风险等级';
|
||||
|
||||
-- 5.4 无人车指令表
|
||||
DROP TABLE IF EXISTS "public"."unmanned_vehicle_commands" CASCADE;
|
||||
CREATE TABLE "public"."unmanned_vehicle_commands" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"vehicle_id" int8 NOT NULL,
|
||||
"command_type" varchar(50) NOT NULL,
|
||||
"command_content" jsonb NOT NULL,
|
||||
"status" varchar(20) DEFAULT 'PENDING',
|
||||
"sent_time" timestamp(6),
|
||||
"executed_time" timestamp(6),
|
||||
"result" varchar(500),
|
||||
"create_time" timestamp(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "unmanned_vehicle_commands_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "unmanned_vehicle_commands_fk1" FOREIGN KEY ("vehicle_id") REFERENCES "public"."sys_vehicle_info" ("vehicle_id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."unmanned_vehicle_commands" IS '无人车指令表';
|
||||
COMMENT ON COLUMN "public"."unmanned_vehicle_commands"."id" IS '指令ID';
|
||||
COMMENT ON COLUMN "public"."unmanned_vehicle_commands"."vehicle_id" IS '车辆ID';
|
||||
COMMENT ON COLUMN "public"."unmanned_vehicle_commands"."command_type" IS '指令类型';
|
||||
COMMENT ON COLUMN "public"."unmanned_vehicle_commands"."command_content" IS '指令内容(JSON格式)';
|
||||
COMMENT ON COLUMN "public"."unmanned_vehicle_commands"."status" IS '指令状态';
|
||||
|
||||
-- ================================================================
|
||||
-- 6. 系统配置和字典表
|
||||
-- ================================================================
|
||||
|
||||
-- 6.1 系统配置表
|
||||
DROP TABLE IF EXISTS "public"."sys_config" CASCADE;
|
||||
CREATE TABLE "public"."sys_config" (
|
||||
"config_id" BIGSERIAL NOT NULL,
|
||||
"config_name" varchar(100) DEFAULT '',
|
||||
"config_key" varchar(100) DEFAULT '',
|
||||
"config_value" varchar(500) DEFAULT '',
|
||||
"config_type" char(1) DEFAULT 'N',
|
||||
"create_by" varchar(64) DEFAULT '',
|
||||
"create_time" timestamp(6),
|
||||
"update_by" varchar(64) DEFAULT '',
|
||||
"update_time" timestamp(6),
|
||||
"remark" varchar(500) DEFAULT NULL,
|
||||
CONSTRAINT "sys_config_pkey" PRIMARY KEY ("config_id")
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."sys_config" IS '参数配置表';
|
||||
COMMENT ON COLUMN "public"."sys_config"."config_id" IS '参数主键';
|
||||
COMMENT ON COLUMN "public"."sys_config"."config_name" IS '参数名称';
|
||||
COMMENT ON COLUMN "public"."sys_config"."config_key" IS '参数键名';
|
||||
COMMENT ON COLUMN "public"."sys_config"."config_value" IS '参数键值';
|
||||
|
||||
-- ================================================================
|
||||
-- 7. 初始化基础数据
|
||||
-- ================================================================
|
||||
|
||||
-- 7.1 插入车辆类型基础数据
|
||||
INSERT INTO "public"."sys_vehicle_type" ("type_name", "description", "max_speed", "create_by", "create_time") VALUES
|
||||
('无人车', '自动驾驶无人车辆', 30, 'admin', CURRENT_TIMESTAMP),
|
||||
('摆渡车', '机场摆渡车辆', 40, 'admin', CURRENT_TIMESTAMP),
|
||||
('货运车', '机场货运车辆', 50, 'admin', CURRENT_TIMESTAMP),
|
||||
('清洁车', '机场清洁车辆', 25, 'admin', CURRENT_TIMESTAMP),
|
||||
('维护车', '机场维护车辆', 60, 'admin', CURRENT_TIMESTAMP);
|
||||
|
||||
-- 7.2 插入系统配置基础数据
|
||||
INSERT INTO "public"."sys_config" ("config_name", "config_key", "config_value", "config_type", "create_by", "create_time", "remark") VALUES
|
||||
('数据采集间隔', 'data.collector.interval', '250', 'Y', 'admin', CURRENT_TIMESTAMP, '数据采集间隔时间,单位:毫秒'),
|
||||
('WebSocket推送间隔', 'websocket.push.interval', '1000', 'Y', 'admin', CURRENT_TIMESTAMP, 'WebSocket推送间隔时间,单位:毫秒'),
|
||||
('冲突检测阈值', 'collision.detection.threshold', '10', 'Y', 'admin', CURRENT_TIMESTAMP, '冲突检测距离阈值,单位:米'),
|
||||
('速度限制阈值', 'speed.limit.threshold', '35', 'Y', 'admin', CURRENT_TIMESTAMP, '速度限制阈值,单位:km/h');
|
||||
|
||||
-- ================================================================
|
||||
-- 8. 创建索引优化性能
|
||||
-- ================================================================
|
||||
|
||||
-- 车辆信息相关索引
|
||||
CREATE INDEX "idx_sys_vehicle_info_type_id" ON "public"."sys_vehicle_info" ("type_id");
|
||||
CREATE INDEX "idx_sys_vehicle_info_license_plate" ON "public"."sys_vehicle_info" ("license_plate");
|
||||
|
||||
-- 位置数据相关索引(时间序列优化)
|
||||
CREATE INDEX "idx_vehicle_location_time_vehicle" ON "public"."vehicle_location" ("timestamp" DESC, "vehicle_id");
|
||||
|
||||
-- 冲突检测相关索引
|
||||
CREATE INDEX "idx_path_conflict_time" ON "public"."path_conflict_detection" ("conflict_time" DESC);
|
||||
CREATE INDEX "idx_path_conflict_status" ON "public"."path_conflict_detection" ("status");
|
||||
|
||||
-- 指令表相关索引
|
||||
CREATE INDEX "idx_unmanned_commands_vehicle_time" ON "public"."unmanned_vehicle_commands" ("vehicle_id", "create_time" DESC);
|
||||
CREATE INDEX "idx_unmanned_commands_status" ON "public"."unmanned_vehicle_commands" ("status");
|
||||
|
||||
-- ================================================================
|
||||
-- 9. 创建视图简化查询
|
||||
-- ================================================================
|
||||
|
||||
-- 9.1 车辆详细信息视图
|
||||
CREATE OR REPLACE VIEW "public"."v_vehicle_detail" AS
|
||||
SELECT
|
||||
vi.vehicle_id,
|
||||
vi.license_plate,
|
||||
vi.brand,
|
||||
vi.owning_unit,
|
||||
vi.contact_person,
|
||||
vi.phone_number,
|
||||
vt.type_name,
|
||||
vt.max_speed,
|
||||
vi.create_time
|
||||
FROM "public"."sys_vehicle_info" vi
|
||||
LEFT JOIN "public"."sys_vehicle_type" vt ON vi.type_id = vt.type_id;
|
||||
|
||||
COMMENT ON VIEW "public"."v_vehicle_detail" IS '车辆详细信息视图';
|
||||
|
||||
-- 9.2 最新位置信息视图
|
||||
CREATE OR REPLACE VIEW "public"."v_latest_vehicle_position" AS
|
||||
SELECT DISTINCT ON (vehicle_id)
|
||||
vehicle_id,
|
||||
ST_X(location) as longitude,
|
||||
ST_Y(location) as latitude,
|
||||
speed,
|
||||
direction,
|
||||
timestamp
|
||||
FROM "public"."vehicle_location"
|
||||
ORDER BY vehicle_id, timestamp DESC;
|
||||
|
||||
COMMENT ON VIEW "public"."v_latest_vehicle_position" IS '车辆最新位置信息视图';
|
||||
|
||||
-- ================================================================
|
||||
-- 脚本执行完成提示
|
||||
-- ================================================================
|
||||
|
||||
-- 输出成功信息
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '=========================================';
|
||||
RAISE NOTICE 'QAUP 数据库初始化完成!';
|
||||
RAISE NOTICE '数据库版本: 3.8.9';
|
||||
RAISE NOTICE '创建时间: %', CURRENT_TIMESTAMP;
|
||||
RAISE NOTICE '========================================= ';
|
||||
RAISE NOTICE '已创建表格数量: %', (
|
||||
SELECT count(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
);
|
||||
RAISE NOTICE '已创建索引数量: %', (
|
||||
SELECT count(*)
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
);
|
||||
RAISE NOTICE '已创建视图数量: %', (
|
||||
SELECT count(*)
|
||||
FROM information_schema.views
|
||||
WHERE table_schema = 'public'
|
||||
);
|
||||
RAISE NOTICE '=========================================';
|
||||
END $$;
|
||||
@ -51,7 +51,7 @@ DIST_50M = 50
|
||||
|
||||
# 时间配置(秒)
|
||||
WAIT_TIME_AFTER_RETURN = 0.1 # 返回起点后的等待时间(进一步优化:减少到0.1秒)
|
||||
UPDATE_INTERVAL = 1.0 # 位置更新间隔, 默认 1 秒
|
||||
UPDATE_INTERVAL = 5.0 # 位置更新间隔, 默认 1 秒
|
||||
TRAFFIC_LIGHT_SWITCH_INTERVAL = 10.0 # 红绿灯切换间隔
|
||||
|
||||
# 根据 route.md 定义的坐标点
|
||||
|
||||
Loading…
Reference in New Issue
Block a user