diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..53c5264b --- /dev/null +++ b/CLAUDE.md @@ -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 + +# 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 \ No newline at end of file diff --git a/VERSION.md b/VERSION.md index 09e91570..53b61ecf 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -0.3.5 \ No newline at end of file +0.3.6 \ No newline at end of file diff --git a/changelog.md b/changelog.md index 3d25f45f..5381e201 100644 --- a/changelog.md +++ b/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 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 ### ✨ **新功能与优化:路径冲突检测增强与告警体系精细化** diff --git a/doc/configuration/data_collection_intervals.md b/doc/configuration/data_collection_intervals.md new file mode 100644 index 00000000..1f6ea962 --- /dev/null +++ b/doc/configuration/data_collection_intervals.md @@ -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消息 +} +``` + +这种设计确保了数据的及时性,同时避免了过度的计算和网络开销。 \ No newline at end of file diff --git a/doc/design/QAUP业务原则和范围.md b/doc/design/QAUP业务原则和范围.md new file mode 100644 index 00000000..579e68b7 --- /dev/null +++ b/doc/design/QAUP业务原则和范围.md @@ -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推送只包含无人车相关事件 +- 前端告警界面只显示无人车告警 + +这个原则将大大简化系统复杂度,提高系统性能,并确保功能边界清晰。 \ No newline at end of file diff --git a/doc/design/电子围栏准入检测架构方案.md b/doc/design/电子围栏准入检测架构方案.md new file mode 100644 index 00000000..130b10b3 --- /dev/null +++ b/doc/design/电子围栏准入检测架构方案.md @@ -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 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. **易于扩展**:新增规则类型只需扩展配置,无需修改代码 + +## 🎯 预期效果 + +- **功能完整性**:支持复杂的电子围栏准入检测需求 +- **性能提升**:优化的空间查询和规则执行 +- **维护性**:清晰的架构设计,易于理解和维护 +- **扩展性**:支持未来新的业务需求 \ No newline at end of file diff --git a/doc/requirement/requirements.md b/doc/requirement/requirements.md index b086fbe8..ff7dbecd 100644 --- a/doc/requirement/requirements.md +++ b/doc/requirement/requirements.md @@ -24,18 +24,18 @@ ### 2025-04-25 -- 需求:电子围栏,根据机场划定的区域,当车辆进入或离开该区域时,进行预警 +- 需求:电子围栏,根据机场划定的区域,当车辆进入该区域时,进行告警 - 分析: - 需要根据机场划定的区域,需要获取机场的区域数据 - 需要获取车辆的实时位置数据 - - 需要对车辆进行电子围栏预警 + - 需要对车辆进行电子围栏告警 - 功能模块: - 数据采集模块:获取车辆的实时位置数据(具备) - - 数据处理模块:根据机场的区域数据,对车辆进行电子围栏预警(新增) - - 数据存储模块:存储车辆的实时位置数据和电子围栏预警数据(增加电子围栏预警数据) - - 告警模块:对车辆进行电子围栏预警(增加电子围栏预警类型) + - 数据处理模块:根据机场的区域数据,对车辆进行电子围栏告警(新增) + - 数据存储模块:存储车辆的实时位置数据和电子围栏告警数据(增加电子围栏告警数据) + - 告警模块:对车辆进行电子围栏告警(增加电子围栏告警类型) - 配置模块:配置机场的区域数据(新增) - - 通信模块:WebSocket通信(增加电子围栏预警事件) + - 通信模块:WebSocket通信(增加电子围栏告警事件) ### 2025-04-12 diff --git a/doc/work/架构优化报告_20250712.md b/doc/work/架构优化报告_20250712.md new file mode 100644 index 00000000..48871511 --- /dev/null +++ b/doc/work/架构优化报告_20250712.md @@ -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 + +8 +8 + + +${java.version} +``` + +#### 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升级、模块集成和数据库管理等关键问题。通过系统性的重构,项目的可维护性、稳定性和开发效率都得到了显著提升。建议按照后续优化计划逐步推进,确保系统持续优化和改进。 + +--- + +**注意**: 本报告基于代码静态分析生成,建议在实际应用前进行充分的测试验证。 \ No newline at end of file diff --git a/qaup-admin/src/main/resources/application.yml b/qaup-admin/src/main/resources/application.yml index e080c713..23edc7c3 100644 --- a/qaup-admin/src/main/resources/application.yml +++ b/qaup-admin/src/main/resources/application.yml @@ -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 diff --git a/qaup-collision/pom.xml b/qaup-collision/pom.xml index 3a97dcaf..d8a6c114 100644 --- a/qaup-collision/pom.xml +++ b/qaup-collision/pom.xml @@ -154,15 +154,18 @@ - + org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.14.0 - 8 - 8 - UTF-8 + ${java.version} + ${project.build.sourceEncoding} + true + + -parameters + org.projectlombok diff --git a/qaup-collision/src/main/java/com/qaup/collision/CollisionAvoidanceApplication.java b/qaup-collision/src/main/java/com/qaup/collision/CollisionAvoidanceApplication.java deleted file mode 100644 index 657960b2..00000000 --- a/qaup-collision/src/main/java/com/qaup/collision/CollisionAvoidanceApplication.java +++ /dev/null @@ -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("应用程序已安全关闭"); - })); - } -} diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/MovingObject.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/MovingObject.java index fb9efe15..1a06ae93 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/common/model/MovingObject.java +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/MovingObject.java @@ -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; + } + } } /** diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/service/AirportAreaService.java b/qaup-collision/src/main/java/com/qaup/collision/common/service/AirportAreaService.java index af1307b9..93429d23 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/common/service/AirportAreaService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/common/service/AirportAreaService.java @@ -289,6 +289,30 @@ public class AirportAreaService { } } + /** + * 根据区域编码查找区域 + */ + public Optional findByAreaCode(String areaCode) { + try { + return airportAreaRepository.findByAreaId(areaCode); + } catch (Exception e) { + log.error("根据区域编码查找区域失败: areaCode={}", areaCode, e); + return Optional.empty(); + } + } + + /** + * 查找包含指定点的所有区域 + */ + public List findAreasContainingPoint(Point point) { + try { + return airportAreaRepository.findAreasContainingPoint(point); + } catch (Exception e) { + log.error("查找包含点的区域失败: point={}", point, e); + return List.of(); + } + } + /** * 统计活跃区域数量(按类型分组) */ diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/service/VehicleLocationService.java b/qaup-collision/src/main/java/com/qaup/collision/common/service/VehicleLocationService.java index 5b5cc877..9aceac7f 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/common/service/VehicleLocationService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/common/service/VehicleLocationService.java @@ -152,8 +152,8 @@ public class VehicleLocationService { saved.getLocation().getX(), saved.getLocation().getY()); - // 同步执行规则检测(简化架构,性能足够) - performRuleDetection(saved); + // 注意:规则检测已移至DataCollectorService的周期性检测中,以实现频率分离 + // performRuleDetection(saved); // 已禁用直接检测 return saved; } catch (Exception e) { diff --git a/qaup-collision/src/main/java/com/qaup/collision/config/CollisionAvoidanceConfig.java b/qaup-collision/src/main/java/com/qaup/collision/config/CollisionAvoidanceConfig.java new file mode 100644 index 00000000..74e9cbd8 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/config/CollisionAvoidanceConfig.java @@ -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("⏰ 定时任务调度已启用(由主应用管理)"); + } +} diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java index eac74fa7..6120dbdd 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java @@ -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 lastPushTimes = new ConcurrentHashMap<>(); - // 用于缓存所有活跃的MovingObject的最新状态 private final Map 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 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 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 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 activeObjects) { + if (activeObjects.isEmpty()) { + log.debug("没有活跃对象,跳过数据保存"); + return; + } + + // 过滤出无人车对象 + List 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 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("没有有效的无人车位置数据可保存"); + } + } } diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/VehicleDataPersistenceService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/VehicleDataPersistenceService.java index 24c27b1a..ce4aef86 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/VehicleDataPersistenceService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/VehicleDataPersistenceService.java @@ -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) { diff --git a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/SpeedCalculationService.java b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/SpeedCalculationService.java index d6e7ce1d..8a720b5d 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/SpeedCalculationService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/SpeedCalculationService.java @@ -32,11 +32,11 @@ public class SpeedCalculationService { private final Map 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; // 转换为公里/小时 diff --git a/qaup-collision/src/main/java/com/qaup/collision/geofence/model/dto/GeofenceAccessCheckResult.java b/qaup-collision/src/main/java/com/qaup/collision/geofence/model/dto/GeofenceAccessCheckResult.java new file mode 100644 index 00000000..7e10d910 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/geofence/model/dto/GeofenceAccessCheckResult.java @@ -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 triggeredRules; + + /** + * 主要告警级别 + */ + private GeofenceAlertLevel alertLevel; + + /** + * 检测消息 + */ + private String message; + + /** + * 详细原因 + */ + private String reason; + + /** + * 检测时间 + */ + private LocalDateTime checkTime; + + /** + * 建议操作 + */ + private List 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(); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/geofence/model/dto/UnmannedVehicleGeofenceRuleConfig.java b/qaup-collision/src/main/java/com/qaup/collision/geofence/model/dto/UnmannedVehicleGeofenceRuleConfig.java new file mode 100644 index 00000000..c8263bd7 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/geofence/model/dto/UnmannedVehicleGeofenceRuleConfig.java @@ -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 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 activeDays; + + /** + * 禁止时间段 (例如: ["12:00-13:00", "18:00-19:00"]) + */ + @JsonProperty("blackoutPeriods") + private List 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 requiredSafetyEquipment; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/geofence/service/UnmannedVehicleGeofenceService.java b/qaup-collision/src/main/java/com/qaup/collision/geofence/service/UnmannedVehicleGeofenceService.java new file mode 100644 index 00000000..bc385e7f --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/geofence/service/UnmannedVehicleGeofenceService.java @@ -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 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 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 checkCurrentPosition(MovingObject vehicle) { + List 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 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 rules) { + List 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); + } + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/rule/repository/SpatialRuleRepository.java b/qaup-collision/src/main/java/com/qaup/collision/rule/repository/SpatialRuleRepository.java index b6abb4a2..829eb8fc 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/rule/repository/SpatialRuleRepository.java +++ b/qaup-collision/src/main/java/com/qaup/collision/rule/repository/SpatialRuleRepository.java @@ -233,6 +233,14 @@ public interface SpatialRuleRepository extends JpaRepository findByNameContaining(@Param("ruleName") String ruleName); + /** + * 根据空间对象ID和规则分类查询规则 + * @param spatialObjectId 空间对象ID + * @param category 规则分类 + * @return 符合条件的规则列表 + */ + List findBySpatialObjectIdAndCategory(String spatialObjectId, RuleCategory category); + /** * 软删除规则(更新状态为DELETED) * @param ruleId 规则ID diff --git a/qaup-collision/src/main/java/com/qaup/collision/rule/service/SpatialRuleService.java b/qaup-collision/src/main/java/com/qaup/collision/rule/service/SpatialRuleService.java index df8d77df..56d9ca30 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/rule/service/SpatialRuleService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/rule/service/SpatialRuleService.java @@ -404,6 +404,14 @@ public interface SpatialRuleService { */ void warmupRuleCache(Point location, double radiusMeters); + /** + * 查找区域的活跃规则 + * @param areaCode 区域编码 + * @param category 规则分类 + * @return 适用的规则列表 + */ + List findActiveRulesForArea(String areaCode, RuleCategory category); + // ========== 规则执行上下文 ========== /** diff --git a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RuleExecutionEngineImpl.java b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RuleExecutionEngineImpl.java index 589a4b05..d9943031 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RuleExecutionEngineImpl.java +++ b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/RuleExecutionEngineImpl.java @@ -56,6 +56,18 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine { @PersistenceContext private EntityManager entityManager; + + // 短期去重缓存:防止同一检测周期内的重复违规 + // 格式:vehicleId:ruleName:violationType -> timestamp + private final Map 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 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()); + } } \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/SpatialRuleServiceImpl.java b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/SpatialRuleServiceImpl.java index a9bcb45c..8531d729 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/SpatialRuleServiceImpl.java +++ b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/SpatialRuleServiceImpl.java @@ -725,6 +725,30 @@ public class SpatialRuleServiceImpl implements SpatialRuleService { getRuleCountByStatus(); } + @Override + public List findActiveRulesForArea(String areaCode, RuleCategory category) { + logger.debug("查找区域的活跃规则 - 区域: {}, 分类: {}", areaCode, category); + + try { + // 查找与指定区域和分类匹配的规则 + List allRules = spatialRuleRepository.findBySpatialObjectIdAndCategory(areaCode, category); + + // 过滤出当前有效的规则 + List 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(); + } + } + // ========== 私有辅助方法 ========== /** diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java index af468746..24f9bad3 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java @@ -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 message = UniversalMessage.builder() + .type(MessageTypeConstants.GEOFENCE_ALERT) + .timestamp(event.getTimestamp()) + .messageId(generateMessageId()) + .payload((GeofenceAlertPayload) event.getPayload()) + .build(); + + broadcastMessage(message); + } + /** * 统一消息广播方法 - 使用原生WebSocket发送JSON消息 * @param message 要广播的消息 diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/event/GeofenceAlertWebSocketEvent.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/event/GeofenceAlertWebSocketEvent.java new file mode 100644 index 00000000..722c8815 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/event/GeofenceAlertWebSocketEvent.java @@ -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; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/message/GeofenceAlertPayload.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/GeofenceAlertPayload.java new file mode 100644 index 00000000..54b242eb --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/GeofenceAlertPayload.java @@ -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 triggeredRules; + + /** + * 建议操作 + */ + private List 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 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(); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/message/MessageTypeConstants.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/MessageTypeConstants.java index d1a29b31..a71d68b8 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/websocket/message/MessageTypeConstants.java +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/MessageTypeConstants.java @@ -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("常量类不允许实例化"); diff --git a/qaup-collision/src/test/java/com/qaup/collision/geofence/service/UnmannedVehicleGeofenceServiceTest.java b/qaup-collision/src/test/java/com/qaup/collision/geofence/service/UnmannedVehicleGeofenceServiceTest.java new file mode 100644 index 00000000..f51741af --- /dev/null +++ b/qaup-collision/src/test/java/com/qaup/collision/geofence/service/UnmannedVehicleGeofenceServiceTest.java @@ -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 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 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 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 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 eventCaptor = ArgumentCaptor.forClass(GeofenceAlertWebSocketEvent.class); + verify(eventPublisher).publishEvent(eventCaptor.capture()); + + GeofenceAlertWebSocketEvent publishedEvent = eventCaptor.getValue(); + assertNotNull(publishedEvent); + assertEquals("geofence_alert", publishedEvent.getEventType()); + assertNotNull(publishedEvent.getPayload()); + } +} \ No newline at end of file diff --git a/sql/README_geofence_upgrade.md b/sql/README_geofence_upgrade.md new file mode 100644 index 00000000..c519ea24 --- /dev/null +++ b/sql/README_geofence_upgrade.md @@ -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. ✅ 完整的日志记录 + +**升级完成!** 🎉 + +电子围栏功能现已就绪,可以开始测试无人车的准入检测功能。 \ No newline at end of file diff --git a/sql/add_speed_limit_rules.sql b/sql/add_speed_limit_rules.sql new file mode 100644 index 00000000..9c663b43 --- /dev/null +++ b/sql/add_speed_limit_rules.sql @@ -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'; \ No newline at end of file diff --git a/sql/fix_duplicate_rules.sql b/sql/fix_duplicate_rules.sql new file mode 100644 index 00000000..0bbac276 --- /dev/null +++ b/sql/fix_duplicate_rules.sql @@ -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验证清理结果 \ No newline at end of file diff --git a/sql/geofence_schema_update.sql b/sql/geofence_schema_update.sql new file mode 100644 index 00000000..5b99ce37 --- /dev/null +++ b/sql/geofence_schema_update.sql @@ -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; \ No newline at end of file diff --git a/sql/geofence_validation.sql b/sql/geofence_validation.sql new file mode 100644 index 00000000..615aca2f --- /dev/null +++ b/sql/geofence_validation.sql @@ -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; \ No newline at end of file diff --git a/sql/qaup_database_complete_init.sql b/sql/qaup_database_complete_init.sql new file mode 100644 index 00000000..43df29a6 --- /dev/null +++ b/sql/qaup_database_complete_init.sql @@ -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 $$; \ No newline at end of file diff --git a/tools/mock_server.py b/tools/mock_server.py index acd9247f..44866007 100644 --- a/tools/mock_server.py +++ b/tools/mock_server.py @@ -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 定义的坐标点