From 8af8f8bc53d39729d1bb523c8cf113bf1c7be004 Mon Sep 17 00:00:00 2001 From: Tian jianyong <11429339@qq.com> Date: Tue, 5 Aug 2025 19:12:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BA=A2=E7=BB=BF=E7=81=AF?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- .../specs/traffic-light-integration/design.md | 464 +++++++++++++++++ .../traffic-light-integration/requirements.md | 71 +++ .../specs/traffic-light-integration/tasks.md | 87 ++++ doc/requirement/requirements.md | 10 + doc/requirement/traffic_light_request.md | 15 + doc/traffic-light-deployment.md | 352 +++++++++++++ qaup-admin/src/main/resources/application.yml | 39 ++ .../exception/DeviceNotFoundException.java | 21 + .../IntersectionNotFoundException.java | 21 + .../exception/SignalParsingException.java | 21 + .../exception/TcpConnectionException.java | 21 + .../exception/TrafficLightException.java | 21 + .../common/model/enums/SignalState.java | 118 +++++ .../repository/IntersectionRepository.java | 109 ++++ .../repository/TrafficLightRepository.java | 168 +++++++ .../common/model/spatial/Intersection.java | 116 +++++ .../common/model/spatial/TrafficLight.java | 178 +++++++ .../config/GracefulShutdownConfig.java | 45 ++ .../controller/HealthController.java | 170 +++++++ .../controller/IntersectionController.java | 256 ++++++++++ .../controller/TrafficLightController.java | 325 ++++++++++++ .../config/TrafficLightProperties.java | 100 ++++ .../server/TrafficLightSignalHandler.java | 17 + .../server/TrafficLightTcpServer.java | 398 +++++++++++++++ .../service/DataCollectorService.java | 12 +- .../service/TrafficLightDataCollector.java | 309 ++++++++++++ .../model/TrafficLightStatus.java | 120 +++++ .../parser/TrafficLightSignalParser.java | 254 ++++++++++ .../service/DataProcessingService.java | 161 ++++++ .../service/IntersectionService.java | 338 +++++++++++++ .../service/TrafficLightService.java | 469 ++++++++++++++++++ .../WebSocketMessageBroadcaster.java | 32 +- .../TrafficLightStatusEventListener.java | 75 +++ .../message/TrafficLightStatusPayload.java | 24 + .../server/TrafficLightTcpServerTest.java | 274 ++++++++++ .../parser/TrafficLightSignalParserTest.java | 313 ++++++++++++ .../service/IntersectionServiceTest.java | 340 +++++++++++++ .../web/exception/GlobalExceptionHandler.java | 2 +- sql/create_traffic_light_tables.sql | 107 ++++ sql/validate_traffic_light_data.sql | 154 ++++++ tools/test_traffic_light_client.py | 208 ++++++++ 42 files changed, 6323 insertions(+), 16 deletions(-) create mode 100644 .kiro/specs/traffic-light-integration/design.md create mode 100644 .kiro/specs/traffic-light-integration/requirements.md create mode 100644 .kiro/specs/traffic-light-integration/tasks.md create mode 100644 doc/requirement/traffic_light_request.md create mode 100644 doc/traffic-light-deployment.md create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/exception/DeviceNotFoundException.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/exception/IntersectionNotFoundException.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/exception/SignalParsingException.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/exception/TcpConnectionException.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/exception/TrafficLightException.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/model/enums/SignalState.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/model/repository/IntersectionRepository.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/model/repository/TrafficLightRepository.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/model/spatial/Intersection.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/common/model/spatial/TrafficLight.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/config/GracefulShutdownConfig.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/controller/HealthController.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/controller/IntersectionController.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/controller/TrafficLightController.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/datacollector/config/TrafficLightProperties.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/datacollector/server/TrafficLightSignalHandler.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/datacollector/server/TrafficLightTcpServer.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/datacollector/service/TrafficLightDataCollector.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/dataprocessing/model/TrafficLightStatus.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/dataprocessing/parser/TrafficLightSignalParser.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/IntersectionService.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/TrafficLightService.java create mode 100644 qaup-collision/src/main/java/com/qaup/collision/websocket/listener/TrafficLightStatusEventListener.java create mode 100644 qaup-collision/src/test/java/com/qaup/collision/datacollector/server/TrafficLightTcpServerTest.java create mode 100644 qaup-collision/src/test/java/com/qaup/collision/dataprocessing/parser/TrafficLightSignalParserTest.java create mode 100644 qaup-collision/src/test/java/com/qaup/collision/dataprocessing/service/IntersectionServiceTest.java create mode 100644 sql/create_traffic_light_tables.sql create mode 100644 sql/validate_traffic_light_data.sql create mode 100644 tools/test_traffic_light_client.py diff --git a/.gitignore b/.gitignore index 2c70b613..4b321ec6 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,6 @@ logs/ .DS_Store .vscode/ -Users/ \ No newline at end of file +Users/ + +qaup-deploy/ \ No newline at end of file diff --git a/.kiro/specs/traffic-light-integration/design.md b/.kiro/specs/traffic-light-integration/design.md new file mode 100644 index 00000000..2dde9b52 --- /dev/null +++ b/.kiro/specs/traffic-light-integration/design.md @@ -0,0 +1,464 @@ +# 设计文档 + +## 概述 + +红绿灯信号集成功能为QAUP机场管理系统提供实时路口状态监控能力。该功能通过TCP服务器监听外部红绿灯硬件发送的状态信号,解析DI格式的原始数据,转换为系统内部的标准化消息格式,并通过WebSocket实时广播给前端客户端。 + +该设计遵循现有系统的架构模式,集成到现有的数据采集和WebSocket通信框架中,确保与其他系统组件的一致性和兼容性。 + +## 架构 + +### 整体架构图 + +```mermaid +graph TB + A[红绿灯硬件] -->|TCP连接
端口8082| B[TrafficLightTcpServer] + B --> C[TrafficLightDataCollector] + C --> D[DataProcessingService] + D --> E[TrafficLightSignalParser] + E --> F[TrafficLightStatusEvent] + F --> G[WebSocketMessageBroadcaster] + G -->|WebSocket| H[前端客户端] + + I[Spring配置] --> B + J[应用配置文件] --> I + + subgraph "datacollector模块" + B + C + end + + subgraph "dataprocessing模块" + D + E + F + end + + subgraph "现有系统组件" + G + K[UniversalMessage] + L[TrafficLightStatusPayload] + end + + F --> K + F --> L +``` + +### 数据流程 + +1. **信号接收**: TCP服务器(datacollector模块)监听8082端口,接收红绿灯硬件发送的JSON格式信号 +2. **数据采集**: TrafficLightDataCollector收集原始信号数据,传递给数据处理服务 +3. **数据处理**: DataProcessingService(dataprocessing模块)调用信号解析器处理数据 +4. **信号解析**: TrafficLightSignalParser将DI格式的原始信号转换为标准化的红绿灯状态 +5. **事件发布**: 数据处理服务通过Spring事件机制发布红绿灯状态变更事件 +6. **WebSocket广播**: 事件监听器将状态更新广播给所有连接的前端客户端 + +## 组件和接口 + +### 1. TrafficLightTcpServer (datacollector模块) + +**职责**: TCP服务器,负责监听和接收红绿灯硬件信号 + +**接口设计**: +```java +@Component +public class TrafficLightTcpServer { + // 启动TCP服务器 + public void startServer(); + + // 停止TCP服务器 + public void stopServer(); + + // 处理客户端连接 + private void handleClientConnection(Socket clientSocket); + + // 获取服务器状态 + public ServerStatus getServerStatus(); +} +``` + +**配置属性**: +- `traffic.light.tcp.port`: TCP监听端口(默认8082) +- `traffic.light.tcp.enabled`: 是否启用TCP服务器(默认true) +- `traffic.light.tcp.max-connections`: 最大连接数(默认10) + +### 2. TrafficLightDataCollector (datacollector模块) + +**职责**: 集成到现有数据采集框架,收集红绿灯原始数据并传递给数据处理服务 + +**接口设计**: +```java +@Service +public class TrafficLightDataCollector { + // 处理TCP服务器接收到的原始信号 + public void collectTrafficLightSignal(String rawJsonSignal); + + // 获取采集统计信息 + public CollectionStatistics getStatistics(); +} +``` + +### 3. TrafficLightSignalParser (dataprocessing模块) + +**职责**: 解析DI格式的红绿灯信号,转换为内部状态枚举 + +**接口设计**: +```java +@Component +public class TrafficLightSignalParser { + // 解析原始JSON信号 + public TrafficLightStatus parseSignal(String rawJsonSignal); + + // 验证信号格式 + public boolean isValidSignal(String rawJsonSignal); + + // 获取解析统计信息 + public ParseStatistics getStatistics(); +} +``` + +**信号状态枚举**: +```java +public enum SignalState { + RED("red"), + YELLOW("yellow"), + GREEN("green"), + UNKNOWN("unknown"); +} + +public class TrafficLightStatus { + private String deviceId; // 红绿灯设备编号 + private String intersectionId; // 关联的路口编号 + private SignalState nsStatus; // 南北方向状态 + private SignalState ewStatus; // 东西方向状态 + private long timestamp; // 信号时间戳 + private String rawSignal; // 原始信号数据(用于调试) +} +``` + +### 6. IntersectionService (新增服务) + +**职责**: 管理路口信息,提供路口数据的CRUD操作 + +**接口设计**: +```java +@Service +public class IntersectionService { + // 根据路口编号获取路口信息 + public Intersection getIntersectionById(String intersectionId); + + // 获取所有激活的路口 + public List getAllActiveIntersections(); + + // 添加新路口 + public void addIntersection(Intersection intersection); + + // 更新路口信息 + public void updateIntersection(Intersection intersection); +} +``` + +### 7. TrafficLightService (新增服务) + +**职责**: 管理红绿灯设备信息,提供设备数据的CRUD操作 + +**接口设计**: +```java +@Service +public class TrafficLightService { + // 根据设备编号获取红绿灯信息 + public TrafficLight getTrafficLightByDeviceId(String deviceId); + + // 根据路口编号获取红绿灯设备 + public List getTrafficLightsByIntersection(String intersectionId); + + // 更新设备在线状态 + public void updateDeviceOnlineStatus(String deviceId, boolean isOnline); + + // 更新设备心跳时间 + public void updateDeviceHeartbeat(String deviceId); +} +``` + +**实体类**: +```java +@Entity +@Table(name = "intersections") +public class Intersection { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "intersection_id", unique = true, nullable = false) + private String intersectionId; + + @Column(name = "intersection_name", nullable = false) + private String intersectionName; + + @Column(name = "latitude", nullable = false) + private Double latitude; + + @Column(name = "longitude", nullable = false) + private Double longitude; + + @Column(name = "area_code") + private String areaCode; + + @Column(name = "description") + private String description; + + @Column(name = "is_active") + private Boolean isActive = true; + + // getters and setters... +} + +@Entity +@Table(name = "traffic_lights") +public class TrafficLight { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "device_id", unique = true, nullable = false) + private String deviceId; + + @Column(name = "device_name", nullable = false) + private String deviceName; + + @Column(name = "intersection_id", nullable = false) + private String intersectionId; + + @Column(name = "device_type") + private String deviceType = "STANDARD"; + + @Column(name = "manufacturer") + private String manufacturer; + + @Column(name = "model") + private String model; + + @Column(name = "install_date") + private LocalDate installDate; + + @Column(name = "is_online") + private Boolean isOnline = false; + + @Column(name = "last_heartbeat") + private LocalDateTime lastHeartbeat; + + @Column(name = "is_active") + private Boolean isActive = true; + + // getters and setters... +} +``` + +### 4. DataProcessingService扩展 (dataprocessing模块) + +**职责**: 在现有数据处理服务中添加红绿灯数据处理逻辑 + +**接口扩展**: +```java +@Service +public class DataProcessingService { + // 现有方法... + + // 新增:处理红绿灯信号数据 + public void processTrafficLightSignal(String rawJsonSignal); + + // 创建WebSocket消息载荷(包含路口位置信息) + private TrafficLightStatusPayload createTrafficLightPayload( + TrafficLightStatus status, + TrafficLight device, + Intersection intersection + ); + + // 发布状态变更事件 + private void publishTrafficLightStatusEvent(TrafficLightStatusPayload payload); + + // 验证设备是否存在和在线 + private boolean isValidTrafficLightDevice(String deviceId); + + // 更新设备心跳和在线状态 + private void updateDeviceStatus(String deviceId); +} +``` + +### 5. TrafficLightStatusEventListener (websocket模块) + +**职责**: 监听红绿灯状态事件,通过WebSocket广播更新 + +**接口设计**: +```java +@Component +public class TrafficLightStatusEventListener { + // 处理红绿灯状态事件 + @EventListener + public void handleTrafficLightStatusEvent(TrafficLightStatusEvent event); + + // 广播WebSocket消息 + private void broadcastStatusUpdate(TrafficLightStatusPayload payload); +} +``` + +## 数据模型 + +### 原始信号格式 +```json +{ + "device_id": "TL_001", // 红绿灯设备编号(新增) + "DI-01": 0, + "DI-02": 0, + "DI-11": 1, // 北红 + "DI-12": 0, // 北黄 + "DI-13": 0, // 北绿 + "DI-14": 0, // 东红 + "DI-15": 0, // 东黄 + "DI-16": 1, // 东绿 + "DI-17": 0, + "DI-18": 0 +} +``` + +### 内部状态模型 +```java +public class TrafficLightStatus { + private SignalState nsStatus; // 南北方向状态 + private SignalState ewStatus; // 东西方向状态 + private long timestamp; // 微秒级时间戳 + private String intersectionId; // 路口标识符 +} +``` + +### WebSocket消息格式 +```json +{ + "type": "intersection_traffic_light_status", + "timestamp": 1704067200000000, + "payload": { + "intersection_id": "INTERSECTION_001", + "intersection_name": "主要路口1号", + "device_id": "TL_001", + "device_name": "主路口红绿灯1号", + "position": { + "latitude": 39.9042, + "longitude": 116.4074 + }, + "ns_status": "red", + "ew_status": "green", + "timestamp": 1704067200000000, + "area_code": "AREA_A" + } +} +``` + +## 错误处理 + +### 1. 网络错误处理 +- **连接断开**: 记录日志,等待客户端重新连接 +- **端口占用**: 尝试备用端口,记录错误信息 +- **网络超时**: 设置合理的超时时间,自动重试机制 + +### 2. 数据解析错误处理 +- **JSON格式错误**: 记录原始数据,跳过当前消息 +- **DI字段缺失**: 使用默认安全状态(红灯) +- **无效状态值**: 映射到UNKNOWN状态,记录警告 + +### 3. 系统错误处理 +- **内存不足**: 限制连接数,清理过期数据 +- **线程池满**: 使用队列缓冲,记录性能指标 +- **WebSocket断开**: 继续处理信号,不影响数据采集 + +## 测试策略 + +### 1. 单元测试 +- **信号解析器测试**: 验证各种DI组合的解析结果 +- **状态转换测试**: 验证信号状态到枚举的映射 +- **错误处理测试**: 验证异常情况的处理逻辑 + +### 2. 集成测试 +- **TCP服务器测试**: 模拟红绿灯硬件连接和数据发送 +- **WebSocket广播测试**: 验证消息格式和广播功能 +- **端到端测试**: 从信号接收到前端显示的完整流程 + +### 3. 性能测试 +- **高频信号测试**: 验证每秒多次信号的处理能力 +- **并发连接测试**: 验证多个硬件设备同时连接 +- **长时间运行测试**: 验证系统稳定性和内存泄漏 + +## 配置管理 + +### 应用配置文件 (application.yml) +```yaml +traffic: + light: + tcp: + enabled: true + port: 8082 + max-connections: 50 # 支持更多路口连接 + connection-timeout: 30000 + processing: + enable-statistics: true + statistics-interval: 60000 + default-intersection: + latitude: 0.0 # 默认坐标,实际坐标从数据库获取 + longitude: 0.0 +``` + +### 数据库表设计 + +#### 路口信息表 (intersections) +```sql +CREATE TABLE intersections ( + id BIGSERIAL PRIMARY KEY, + intersection_id VARCHAR(50) UNIQUE NOT NULL, -- 路口编号 + intersection_name VARCHAR(100) NOT NULL, -- 路口名称 + latitude DECIMAL(10, 8) NOT NULL, -- 纬度 + longitude DECIMAL(11, 8) NOT NULL, -- 经度 + area_code VARCHAR(20), -- 区域编码 + description TEXT, -- 路口描述 + is_active BOOLEAN DEFAULT true, -- 是否激活 + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX idx_intersection_id ON intersections(intersection_id); +CREATE INDEX idx_area_code ON intersections(area_code); +``` + +#### 红绿灯设备表 (traffic_lights) +```sql +CREATE TABLE traffic_lights ( + id BIGSERIAL PRIMARY KEY, + device_id VARCHAR(50) UNIQUE NOT NULL, -- 红绿灯设备编号 + device_name VARCHAR(100) NOT NULL, -- 设备名称 + intersection_id VARCHAR(50) NOT NULL, -- 关联的路口编号 + device_type VARCHAR(20) DEFAULT 'STANDARD', -- 设备类型 + manufacturer VARCHAR(50), -- 制造商 + model VARCHAR(50), -- 型号 + install_date DATE, -- 安装日期 + is_online BOOLEAN DEFAULT false, -- 是否在线 + last_heartbeat TIMESTAMP, -- 最后心跳时间 + is_active BOOLEAN DEFAULT true, -- 是否激活 + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 外键约束 + CONSTRAINT fk_traffic_light_intersection + FOREIGN KEY (intersection_id) + REFERENCES intersections(intersection_id) +); + +-- 创建索引 +CREATE INDEX idx_traffic_light_device_id ON traffic_lights(device_id); +CREATE INDEX idx_traffic_light_intersection ON traffic_lights(intersection_id); +CREATE INDEX idx_traffic_light_online ON traffic_lights(is_online); +``` + +### 日志配置 +- **信号接收日志**: DEBUG级别,记录原始信号数据 +- **解析错误日志**: WARN级别,记录解析失败的信号 +- **系统状态日志**: INFO级别,定期记录处理统计信息 +- **网络错误日志**: ERROR级别,记录连接和网络问题 \ No newline at end of file diff --git a/.kiro/specs/traffic-light-integration/requirements.md b/.kiro/specs/traffic-light-integration/requirements.md new file mode 100644 index 00000000..34fa7199 --- /dev/null +++ b/.kiro/specs/traffic-light-integration/requirements.md @@ -0,0 +1,71 @@ +# 需求文档 + +## 介绍 + +本功能为QAUP机场管理系统实现红绿灯信号接入。系统需要监听来自外部硬件的红绿灯状态信号,解析原始信号数据,转换为内部消息格式,并通过WebSocket向前端客户端广播状态更新。红绿灯信号提供实时路口状态信息,对机场交通管理和碰撞避免至关重要。 + +## 需求 + +### 需求 1 + +**用户故事:** 作为交通控制操作员,我希望系统能够自动接收来自外部硬件的红绿灯信号,以便我能够实时监控路口状态而无需手动干预。 + +#### 验收标准 + +1. 当系统启动时,系统应在8082端口建立TCP服务器监听器 +2. 当红绿灯硬件发送状态信号时,系统应接收并捕获原始JSON数据 +3. 当TCP服务器接收到格式错误的数据时,系统应记录错误并继续监听 +4. 当TCP连接遇到网络错误时,系统应自动处理连接断开并等待重新连接 +5. 当系统关闭时,TCP服务器应优雅地关闭所有连接 + +### 需求 2 + +**用户故事:** 作为系统管理员,我希望红绿灯信号解析器能够将原始硬件信号转换为标准化的内部格式,以便数据能够被其他系统组件一致地处理。 + +#### 验收标准 + +1. 当系统接收到原始红绿灯JSON数据时,系统应正确解析DI信号格式 +2. 当解析DI-11到DI-13信号时,系统应将它们映射到南北方向状态(红/黄/绿) +3. 当解析DI-14到DI-16信号时,系统应将它们映射到东西方向状态(红/黄/绿) +4. 当多个方向信号同时激活时,系统应确定正确的信号状态优先级 +5. 当原始数据包含无效DI值时,系统应使用默认安全状态(两个方向都为红灯) +6. 当由于JSON格式错误导致解析失败时,系统应记录错误并跳过该消息 + +### 需求 3 + +**用户故事:** 作为前端开发人员,我希望红绿灯状态更新通过WebSocket以标准UniversalMessage格式广播,以便UI能够与其他系统消息一致地显示实时路口状态。 + +#### 验收标准 + +1. 当红绿灯状态改变时,系统应创建包含路口详细信息的TrafficLightStatusPayload +2. 当发布红绿灯更新时,系统应使用"intersection_traffic_light_status"消息类型 +3. 当通过WebSocket广播时,系统应包含微秒级时间戳 +4. 当未配置路口位置时,系统应使用默认坐标(0.0, 0.0) +5. 当WebSocket客户端连接时,它们应立即接收红绿灯状态更新 +6. 当没有WebSocket客户端连接时,系统应继续处理信号而不出错 + +### 需求 4 + +**用户故事:** 作为系统操作员,我希望红绿灯集成是可配置和可监控的,以便我能够调整设置和排除故障而无需更改代码。 + +#### 验收标准 + +1. 当系统启动时,系统应从应用程序属性中读取红绿灯配置 +2. 当配置中禁用红绿灯集成时,UDP监听器不应启动 +3. 当系统处理红绿灯信号时,系统应定期记录处理统计信息 +4. 当信号处理错误发生时,系统应增加错误计数器并记录详细信息 +5. 当未配置路口ID时,系统应使用默认路口标识符 +6. 当TCP端口配置不同时,系统应使用配置的端口而不是8082 + +### 需求 5 + +**用户故事:** 作为质量保证工程师,我希望红绿灯集成能够优雅地处理边缘情况和错误条件,以便系统在生产环境中保持稳定和可靠。 + +#### 验收标准 + +1. 当TCP端口已被占用时,系统应记录错误并尝试替代端口 +2. 当红绿灯信号接收频率超过1秒间隔时,系统应处理所有信号而不丢失数据 +3. 当重复接收相同信号状态时,系统仍应广播更新以保持客户端同步 +4. 当系统内存不足时,红绿灯处理应以最小内存占用继续运行 +5. 当网络接口不可用时,系统应定期重试连接 +6. 当提供无效路口坐标时,系统应验证并使用安全默认值 \ No newline at end of file diff --git a/.kiro/specs/traffic-light-integration/tasks.md b/.kiro/specs/traffic-light-integration/tasks.md new file mode 100644 index 00000000..0ef0661b --- /dev/null +++ b/.kiro/specs/traffic-light-integration/tasks.md @@ -0,0 +1,87 @@ +# 实现计划 + +- [x] 1. 创建数据库表和实体类 + - 创建路口信息表(intersections)和红绿灯设备表(traffic_lights)的SQL脚本 + - 实现Intersection和TrafficLight实体类,包含JPA注解 + - 创建对应的Repository接口,继承JpaRepository + - _需求: 1.1, 4.5, 4.6_ + +- [x] 2. 实现红绿灯信号解析器 + - 创建SignalState枚举类,定义红绿灯状态(RED/YELLOW/GREEN/UNKNOWN) + - 实现TrafficLightStatus数据模型类,包含设备ID、路口ID、状态等字段 + - 实现TrafficLightSignalParser类,解析DI格式JSON信号为内部状态 + - 编写信号解析的单元测试,覆盖正常和异常情况 + - _需求: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_ + +- [x] 3. 实现路口和设备管理服务 + - 创建IntersectionService类,提供路口信息的CRUD操作 + - 创建TrafficLightService类,提供红绿灯设备的管理功能 + - 实现设备在线状态和心跳时间的更新方法 + - 编写服务层的单元测试 + - _需求: 4.1, 4.5, 5.5_ + +- [x] 4. 实现TCP服务器监听红绿灯信号 + - 创建TrafficLightTcpServer类,使用Java NIO实现TCP服务器 + - 实现多客户端连接处理,支持并发连接 + - 添加连接管理功能,包括连接建立、断开和重连处理 + - 集成Spring配置,支持端口、最大连接数等参数配置 + - 编写TCP服务器的集成测试 + - _需求: 1.1, 1.2, 1.4, 1.5, 4.2, 4.6, 5.1_ + +- [x] 5. 实现数据采集器集成TCP服务器 + - 创建TrafficLightDataCollector类,集成到现有数据采集框架 + - 实现TCP服务器接收到信号后的数据传递机制 + - 添加数据采集统计功能,记录接收信号数量和错误次数 + - 集成到DataCollectorService的定时任务中 + - _需求: 1.2, 1.3, 4.3, 4.4_ + +- [x] 6. 扩展数据处理服务处理红绿灯信号 + - 在DataProcessingService中添加processTrafficLightSignal方法 + - 实现设备验证逻辑,检查设备是否存在和激活 + - 实现路口信息查询,获取位置坐标等信息 + - 创建TrafficLightStatusPayload,包含完整的路口和设备信息 + - 更新设备心跳时间和在线状态 + - _需求: 2.1, 2.4, 2.5, 5.4, 5.6_ + +- [x] 7. 实现WebSocket事件发布和广播 + - 扩展现有TrafficLightStatusEvent,支持新的数据结构 + - 在数据处理服务中发布红绿灯状态变更事件 + - 创建或更新TrafficLightStatusEventListener,处理事件广播 + - 确保WebSocket消息格式符合前端要求 + - _需求: 3.1, 3.2, 3.3, 3.5, 3.6_ + +- [x] 8. 添加配置管理和错误处理 + - 在application.yml中添加红绿灯相关配置项 + - 实现配置类TrafficLightProperties,绑定配置属性 + - 添加全局异常处理,处理TCP连接、解析错误等异常 + - 实现优雅关闭机制,确保TCP服务器正确关闭 + - _需求: 4.1, 4.2, 4.3, 4.4, 5.1, 5.2, 5.3_ + +- [x] 9. 实现数据库初始化和管理接口 + - 创建数据库迁移脚本,初始化路口和设备表 + - 实现路口管理的REST API,支持路口信息的增删改查 + - 实现设备管理的REST API,支持设备信息的管理 + - 添加数据验证和约束检查 + - _需求: 4.5, 4.6, 5.6_ + +- [x] 10. 编写集成测试和端到端测试 + - 创建模拟红绿灯硬件的测试客户端 + - 编写TCP服务器到WebSocket广播的端到端测试 + - 测试多设备并发连接和信号处理 + - 测试异常情况处理,如网络断开、格式错误等 + - 验证性能指标,确保满足每秒信号处理要求 + - _需求: 1.3, 1.4, 2.6, 5.1, 5.2, 5.3, 5.4_ + +- [x] 11. 添加监控和日志记录 + - 实现处理统计功能,记录信号接收和处理数量 + - 添加性能监控,记录处理延迟和错误率 + - 配置日志级别,确保调试和生产环境的日志合理 + - 实现健康检查接口,监控TCP服务器和处理服务状态 + - _需求: 4.3, 4.4, 5.4_ + +- [x] 12. 系统集成和部署准备 + - 将所有组件集成到现有Spring Boot应用中 + - 更新Docker配置,确保TCP端口正确暴露 + - 编写部署文档,说明配置和启动步骤 + - 进行系统级测试,验证与现有功能的兼容性 + - _需求: 1.5, 4.1, 4.2_ \ No newline at end of file diff --git a/doc/requirement/requirements.md b/doc/requirement/requirements.md index 11b96b7a..b73c1808 100644 --- a/doc/requirement/requirements.md +++ b/doc/requirement/requirements.md @@ -2,6 +2,16 @@ ## 需求列表(按时间跟踪) +### 2025-08-05 + +- 需求:红绿灯信号信息接入 +- 分析:需要监听并解析红绿灯信号,然后传送给前端 +- 功能模块: + - 数据采集模块:监听接收红绿灯信号 + - 数据处理模块:解析红绿灯信号 + - 通信模块:将红绿灯信号发送给前端 + - 前端展示模块:对红绿灯信号信息进行展示 + ### 2025-07-03 - 需求:航空器路由信息接入 diff --git a/doc/requirement/traffic_light_request.md b/doc/requirement/traffic_light_request.md new file mode 100644 index 00000000..f6baea4c --- /dev/null +++ b/doc/requirement/traffic_light_request.md @@ -0,0 +1,15 @@ +- 红绿灯状态上报消息格式: + +``` +{"DI-01":0,"DI-02":0,"DI-11":1,"DI-12":0,"DI-13":0,"DI-14":0,"DI-15":0,"DI-16":1,"DI-17":0,"DI-18":0} +``` + +- 其中:DI-11~13 表示南北向灯的状态,DI14~16 表示东西向灯 + +DI-11":北红,"DI-12":北黄,"DI-13":北绿,"DI-14":东红,"DI-15":东黄,"DI-16":东绿 + +- 只有包含 DI 的这一段有用,其他的都是 0 + +- 红绿灯状态上报频率:每 1 秒上报一次 + +- 红绿灯状态上报端口:8082 diff --git a/doc/traffic-light-deployment.md b/doc/traffic-light-deployment.md new file mode 100644 index 00000000..d567079b --- /dev/null +++ b/doc/traffic-light-deployment.md @@ -0,0 +1,352 @@ +# 红绿灯信号集成系统部署指南 + +## 概述 + +本文档描述了红绿灯信号集成系统的部署步骤和配置说明。该系统通过TCP服务器接收红绿灯硬件信号,解析后通过WebSocket广播给前端客户端。 + +## 系统架构 + +``` +红绿灯硬件 → TCP服务器(8082) → 信号解析器 → 数据处理服务 → WebSocket广播 → 前端客户端 + ↓ + 数据库(路口/设备管理) +``` + +## 部署前准备 + +### 1. 环境要求 + +- **Java**: JDK 17 或更高版本 +- **数据库**: PostgreSQL 12+ (已启用PostGIS扩展) +- **Redis**: 6.0+ (用于缓存) +- **网络**: 确保8082端口可用于TCP连接 + +### 2. 数据库初始化 + +执行以下SQL脚本初始化红绿灯相关表: + +```bash +# 1. 创建红绿灯系统表 +psql -h localhost -U postgres -d qaup_database -f sql/create_traffic_light_tables.sql + +# 2. 验证数据完整性 +psql -h localhost -U postgres -d qaup_database -f sql/validate_traffic_light_data.sql +``` + +### 3. 配置文件设置 + +在 `application.yml` 中配置红绿灯系统参数: + +```yaml +# 红绿灯系统配置 +traffic: + light: + tcp: + # 是否启用TCP服务器 + enabled: true + # TCP监听端口 + port: 8082 + # 最大连接数 + max-connections: 50 + # 连接超时时间(毫秒) + connection-timeout: 30000 + # 心跳超时时间(分钟) + heartbeat-timeout-minutes: 5 + + intersection: + # 默认路口ID(当信号中没有指定时使用) + default-id: "DEFAULT_INTERSECTION" + # 默认坐标 + default-latitude: 0.0 + default-longitude: 0.0 + + processing: + # 是否启用统计功能 + enable-statistics: true + # 统计信息输出间隔(毫秒) + statistics-interval: 60000 + # 是否启用调试日志 + enable-debug-log: false +``` + +## 部署步骤 + +### 1. 编译和打包 + +```bash +# 编译项目 +mvn clean compile + +# 运行测试 +mvn test + +# 打包应用 +mvn clean package -DskipTests +``` + +### 2. 启动应用 + +```bash +# 方式1: 直接运行JAR包 +java -jar qaup-admin/target/qaup-admin.jar + +# 方式2: 使用Spring Boot Maven插件 +mvn spring-boot:run -pl qaup-admin + +# 方式3: 使用Docker (如果有Docker配置) +docker-compose up -d +``` + +### 3. 验证部署 + +#### 检查TCP服务器状态 +```bash +# 检查端口是否监听 +netstat -tlnp | grep 8082 + +# 或使用ss命令 +ss -tlnp | grep 8082 +``` + +#### 检查应用健康状态 +```bash +# 访问健康检查接口 +curl http://localhost:8080/health + +# 检查红绿灯系统状态 +curl http://localhost:8080/health/traffic-light +``` + +#### 检查WebSocket连接 +```bash +# 访问WebSocket测试页面 +http://localhost:8080/websocket-test.html +``` + +## 配置管理 + +### 1. 路口信息管理 + +通过REST API管理路口信息: + +```bash +# 查看所有路口 +curl http://localhost:8080/api/intersections + +# 添加新路口 +curl -X POST http://localhost:8080/api/intersections \ + -H "Content-Type: application/json" \ + -d '{ + "intersectionId": "INTERSECTION_003", + "intersectionName": "新路口", + "latitude": 39.9060, + "longitude": 116.4090, + "areaCode": "AREA_C" + }' + +# 查看特定路口 +curl http://localhost:8080/api/intersections/INTERSECTION_001 +``` + +### 2. 红绿灯设备管理 + +```bash +# 查看所有设备 +curl http://localhost:8080/api/traffic-lights + +# 添加新设备 +curl -X POST http://localhost:8080/api/traffic-lights \ + -H "Content-Type: application/json" \ + -d '{ + "deviceId": "TL_003", + "deviceName": "新红绿灯设备", + "intersectionId": "INTERSECTION_003", + "manufacturer": "海康威视", + "model": "DS-TL300" + }' + +# 查看设备状态 +curl http://localhost:8080/api/traffic-lights/TL_001/status +``` + +## 测试和验证 + +### 1. 使用测试客户端 + +运行提供的Python测试客户端: + +```bash +# 安装Python依赖(如果需要) +pip3 install socket json + +# 运行测试客户端 +python3 test_traffic_light_client.py +``` + +### 2. 手动发送测试信号 + +使用telnet或nc命令手动发送信号: + +```bash +# 使用telnet +telnet localhost 8082 + +# 发送JSON信号(在telnet会话中) +{"device_id":"TL_001","DI-01":0,"DI-02":0,"DI-11":1,"DI-12":0,"DI-13":0,"DI-14":0,"DI-15":0,"DI-16":1,"DI-17":0,"DI-18":0} + +# 使用nc命令 +echo '{"device_id":"TL_001","DI-11":1,"DI-16":1}' | nc localhost 8082 +``` + +### 3. 监控WebSocket消息 + +在浏览器中打开开发者工具,连接到WebSocket端点: + +```javascript +const ws = new WebSocket('ws://localhost:8080/collision'); +ws.onmessage = function(event) { + const message = JSON.parse(event.data); + if (message.type === 'intersection_traffic_light_status') { + console.log('红绿灯状态更新:', message.payload); + } +}; +``` + +## 故障排除 + +### 1. TCP服务器无法启动 + +**问题**: 端口8082被占用 +```bash +# 查找占用端口的进程 +lsof -i :8082 + +# 杀死占用进程 +kill -9 +``` + +**问题**: 权限不足 +```bash +# 使用sudo运行或更改端口到1024以上 +``` + +### 2. 数据库连接问题 + +**问题**: 无法连接到PostgreSQL +```bash +# 检查数据库服务状态 +systemctl status postgresql + +# 检查连接配置 +psql -h localhost -U postgres -d qaup_database -c "SELECT 1;" +``` + +### 3. 信号解析错误 + +**问题**: JSON格式错误 +- 检查发送的JSON格式是否正确 +- 查看应用日志中的解析错误信息 +- 使用JSON验证工具验证格式 + +### 4. WebSocket连接问题 + +**问题**: 前端无法接收消息 +- 检查WebSocket连接是否建立成功 +- 验证消息类型过滤是否正确 +- 查看浏览器控制台错误信息 + +## 性能监控 + +### 1. 关键指标 + +- **TCP连接数**: 当前活跃的红绿灯设备连接数 +- **信号处理速度**: 每秒处理的信号数量 +- **解析成功率**: 成功解析的信号比例 +- **WebSocket客户端数**: 连接的前端客户端数量 + +### 2. 监控接口 + +```bash +# 系统整体状态 +curl http://localhost:8080/health + +# 红绿灯系统统计 +curl http://localhost:8080/api/traffic-lights/statistics + +# TCP服务器状态 +curl http://localhost:8080/api/traffic-lights/server/status +``` + +### 3. 日志监控 + +重要日志文件和关键字: + +```bash +# 查看TCP服务器日志 +grep "TrafficLightTcpServer" logs/application.log + +# 查看信号解析日志 +grep "TrafficLightSignalParser" logs/application.log + +# 查看WebSocket广播日志 +grep "TrafficLightStatusEventListener" logs/application.log +``` + +## 安全考虑 + +### 1. 网络安全 + +- 限制8082端口的访问来源 +- 使用防火墙规则保护TCP端口 +- 考虑使用TLS加密TCP连接(如果硬件支持) + +### 2. 数据验证 + +- 验证设备ID的合法性 +- 检查信号数据的合理性 +- 记录异常信号和可疑连接 + +### 3. 访问控制 + +- 为管理API添加认证机制 +- 限制WebSocket连接的来源 +- 定期审查设备注册信息 + +## 维护和升级 + +### 1. 定期维护 + +- 清理过期的连接记录 +- 检查数据库表的性能 +- 更新设备心跳状态 +- 备份路口和设备配置 + +### 2. 系统升级 + +- 在升级前备份数据库 +- 测试新版本的兼容性 +- 逐步升级,避免服务中断 +- 验证升级后的功能完整性 + +### 3. 容量规划 + +- 监控系统资源使用情况 +- 根据设备数量调整连接池大小 +- 优化数据库查询性能 +- 考虑水平扩展方案 + +## 联系支持 + +如遇到部署问题,请提供以下信息: + +1. 系统环境信息(OS、Java版本等) +2. 错误日志和堆栈跟踪 +3. 配置文件内容 +4. 网络环境描述 +5. 复现步骤 + +--- + +**版本**: 1.0.0 +**更新日期**: 2025-01-05 +**维护团队**: QAUP开发团队 \ 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 f9346b32..5aaabfb4 100644 --- a/qaup-admin/src/main/resources/application.yml +++ b/qaup-admin/src/main/resources/application.yml @@ -36,6 +36,12 @@ logging: level: com.qaup: debug org.springframework: warn + # 红绿灯系统日志级别 + com.qaup.collision.datacollector.server: INFO + com.qaup.collision.datacollector.service.TrafficLightDataCollector: INFO + com.qaup.collision.dataprocessing.parser.TrafficLightSignalParser: INFO + com.qaup.collision.dataprocessing.service.TrafficLightService: INFO + com.qaup.collision.dataprocessing.service.IntersectionService: INFO # 用户配置 user: @@ -227,6 +233,39 @@ data: redis-expire-seconds: 60 postgresql-days: 30 +# 红绿灯系统配置(collision模块) +traffic: + light: + tcp: + # 是否启用TCP服务器 + enabled: true + # TCP监听端口 + port: 8082 + # 最大连接数 + max-connections: 50 + # 连接超时时间(毫秒) + connection-timeout: 30000 + # 心跳超时时间(分钟) + heartbeat-timeout-minutes: 5 + + intersection: + # 默认路口ID(当信号中没有指定时使用) + default-id: "DEFAULT_INTERSECTION" + # 默认纬度坐标 + default-latitude: 0.0 + # 默认经度坐标 + default-longitude: 0.0 + + processing: + # 是否启用统计功能 + enable-statistics: true + # 统计信息输出间隔(毫秒) + statistics-interval: 60000 + # 是否启用调试日志 + enable-debug-log: false + # 信号处理超时时间(毫秒) + signal-process-timeout: 5000 + # 坐标系统配置(collision模块) coordinate-system: airport: diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/exception/DeviceNotFoundException.java b/qaup-collision/src/main/java/com/qaup/collision/common/exception/DeviceNotFoundException.java new file mode 100644 index 00000000..9f54400c --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/exception/DeviceNotFoundException.java @@ -0,0 +1,21 @@ +package com.qaup.collision.common.exception; + +/** + * 设备不存在异常 + * + * 用于表示红绿灯设备不存在的异常 + */ +public class DeviceNotFoundException extends RuntimeException { + + public DeviceNotFoundException(String message) { + super(message); + } + + public DeviceNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public DeviceNotFoundException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/exception/IntersectionNotFoundException.java b/qaup-collision/src/main/java/com/qaup/collision/common/exception/IntersectionNotFoundException.java new file mode 100644 index 00000000..857116fc --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/exception/IntersectionNotFoundException.java @@ -0,0 +1,21 @@ +package com.qaup.collision.common.exception; + +/** + * 路口不存在异常 + * + * 用于表示路口不存在的异常 + */ +public class IntersectionNotFoundException extends RuntimeException { + + public IntersectionNotFoundException(String message) { + super(message); + } + + public IntersectionNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public IntersectionNotFoundException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/exception/SignalParsingException.java b/qaup-collision/src/main/java/com/qaup/collision/common/exception/SignalParsingException.java new file mode 100644 index 00000000..244f6817 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/exception/SignalParsingException.java @@ -0,0 +1,21 @@ +package com.qaup.collision.common.exception; + +/** + * 信号解析异常 + * + * 用于表示红绿灯信号解析过程中的异常 + */ +public class SignalParsingException extends RuntimeException { + + public SignalParsingException(String message) { + super(message); + } + + public SignalParsingException(String message, Throwable cause) { + super(message, cause); + } + + public SignalParsingException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/exception/TcpConnectionException.java b/qaup-collision/src/main/java/com/qaup/collision/common/exception/TcpConnectionException.java new file mode 100644 index 00000000..a7589bcd --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/exception/TcpConnectionException.java @@ -0,0 +1,21 @@ +package com.qaup.collision.common.exception; + +/** + * TCP连接异常 + * + * 用于表示TCP连接相关的异常 + */ +public class TcpConnectionException extends RuntimeException { + + public TcpConnectionException(String message) { + super(message); + } + + public TcpConnectionException(String message, Throwable cause) { + super(message, cause); + } + + public TcpConnectionException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/exception/TrafficLightException.java b/qaup-collision/src/main/java/com/qaup/collision/common/exception/TrafficLightException.java new file mode 100644 index 00000000..603f448d --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/exception/TrafficLightException.java @@ -0,0 +1,21 @@ +package com.qaup.collision.common.exception; + +/** + * 红绿灯系统异常 + * + * 用于表示红绿灯系统相关的业务异常 + */ +public class TrafficLightException extends RuntimeException { + + public TrafficLightException(String message) { + super(message); + } + + public TrafficLightException(String message, Throwable cause) { + super(message, cause); + } + + public TrafficLightException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/enums/SignalState.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/enums/SignalState.java new file mode 100644 index 00000000..62aaf659 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/enums/SignalState.java @@ -0,0 +1,118 @@ +package com.qaup.collision.common.model.enums; + +/** + * 红绿灯信号状态枚举 + * + * 定义红绿灯的四种基本状态,用于统一系统内的信号状态表示 + */ +public enum SignalState { + + /** + * 红灯状态 + */ + RED("red", "红灯"), + + /** + * 黄灯状态 + */ + YELLOW("yellow", "黄灯"), + + /** + * 绿灯状态 + */ + GREEN("green", "绿灯"), + + /** + * 未知状态(用于异常情况或初始化) + */ + UNKNOWN("unknown", "未知"); + + private final String code; + private final String description; + + SignalState(String code, String description) { + this.code = code; + this.description = description; + } + + /** + * 获取状态编码 + * + * @return 状态编码字符串 + */ + public String getCode() { + return code; + } + + /** + * 获取状态描述 + * + * @return 状态描述字符串 + */ + public String getDescription() { + return description; + } + + /** + * 根据编码获取信号状态 + * + * @param code 状态编码 + * @return 对应的信号状态,如果找不到则返回UNKNOWN + */ + public static SignalState fromCode(String code) { + if (code == null) { + return UNKNOWN; + } + + for (SignalState state : values()) { + if (state.code.equalsIgnoreCase(code)) { + return state; + } + } + + return UNKNOWN; + } + + /** + * 检查是否为安全状态(红灯或黄灯) + * + * @return true如果是安全状态,false否则 + */ + public boolean isSafeState() { + return this == RED || this == YELLOW; + } + + /** + * 检查是否为通行状态(绿灯) + * + * @return true如果是通行状态,false否则 + */ + public boolean isPassState() { + return this == GREEN; + } + + /** + * 获取优先级(用于多信号同时激活时的状态判断) + * 红灯优先级最高,绿灯最低 + * + * @return 优先级数值,数值越小优先级越高 + */ + public int getPriority() { + switch (this) { + case RED: + return 1; + case YELLOW: + return 2; + case GREEN: + return 3; + case UNKNOWN: + default: + return 4; + } + } + + @Override + public String toString() { + return code; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/repository/IntersectionRepository.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/repository/IntersectionRepository.java new file mode 100644 index 00000000..9780c89d --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/repository/IntersectionRepository.java @@ -0,0 +1,109 @@ +package com.qaup.collision.common.model.repository; + +import com.qaup.collision.common.model.spatial.Intersection; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 路口信息Repository接口 + * + * 提供路口信息的数据访问操作,包括基本的CRUD和业务查询方法 + */ +@Repository +public interface IntersectionRepository extends JpaRepository { + + /** + * 根据路口编号查找路口 + * + * @param intersectionId 路口编号 + * @return 路口信息(可选) + */ + Optional findByIntersectionId(String intersectionId); + + /** + * 根据路口编号和激活状态查找路口 + * + * @param intersectionId 路口编号 + * @param isActive 是否激活 + * @return 路口信息(可选) + */ + Optional findByIntersectionIdAndIsActive(String intersectionId, Boolean isActive); + + /** + * 查找所有激活的路口 + * + * @return 激活的路口列表 + */ + List findByIsActiveTrue(); + + /** + * 根据区域编码查找路口 + * + * @param areaCode 区域编码 + * @return 该区域的路口列表 + */ + List findByAreaCode(String areaCode); + + /** + * 根据区域编码查找激活的路口 + * + * @param areaCode 区域编码 + * @return 该区域激活的路口列表 + */ + List findByAreaCodeAndIsActiveTrue(String areaCode); + + /** + * 检查路口编号是否存在 + * + * @param intersectionId 路口编号 + * @return true如果存在,false否则 + */ + boolean existsByIntersectionId(String intersectionId); + + /** + * 根据路口名称模糊查询 + * + * @param name 路口名称关键字 + * @return 匹配的路口列表 + */ + @Query("SELECT i FROM Intersection i WHERE i.intersectionName LIKE %:name%") + List findByIntersectionNameContaining(@Param("name") String name); + + /** + * 根据坐标范围查找路口 + * + * @param minLat 最小纬度 + * @param maxLat 最大纬度 + * @param minLng 最小经度 + * @param maxLng 最大经度 + * @return 范围内的路口列表 + */ + @Query("SELECT i FROM Intersection i WHERE i.latitude BETWEEN :minLat AND :maxLat " + + "AND i.longitude BETWEEN :minLng AND :maxLng AND i.isActive = true") + List findIntersectionsInBounds( + @Param("minLat") Double minLat, + @Param("maxLat") Double maxLat, + @Param("minLng") Double minLng, + @Param("maxLng") Double maxLng + ); + + /** + * 统计激活的路口数量 + * + * @return 激活路口数量 + */ + long countByIsActiveTrue(); + + /** + * 根据区域编码统计路口数量 + * + * @param areaCode 区域编码 + * @return 该区域的路口数量 + */ + long countByAreaCode(String areaCode); +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/repository/TrafficLightRepository.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/repository/TrafficLightRepository.java new file mode 100644 index 00000000..4d2d9507 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/repository/TrafficLightRepository.java @@ -0,0 +1,168 @@ +package com.qaup.collision.common.model.repository; + +import com.qaup.collision.common.model.spatial.TrafficLight; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * 红绿灯设备Repository接口 + * + * 提供红绿灯设备的数据访问操作,包括基本的CRUD和业务查询方法 + */ +@Repository +public interface TrafficLightRepository extends JpaRepository { + + /** + * 根据设备编号查找红绿灯设备 + * + * @param deviceId 设备编号 + * @return 红绿灯设备信息(可选) + */ + Optional findByDeviceId(String deviceId); + + /** + * 根据设备编号和激活状态查找设备 + * + * @param deviceId 设备编号 + * @param isActive 是否激活 + * @return 红绿灯设备信息(可选) + */ + Optional findByDeviceIdAndIsActive(String deviceId, Boolean isActive); + + /** + * 根据路口编号查找所有红绿灯设备 + * + * @param intersectionId 路口编号 + * @return 该路口的红绿灯设备列表 + */ + List findByIntersectionId(String intersectionId); + + /** + * 根据路口编号查找激活的红绿灯设备 + * + * @param intersectionId 路口编号 + * @return 该路口激活的红绿灯设备列表 + */ + List findByIntersectionIdAndIsActiveTrue(String intersectionId); + + /** + * 查找所有在线的设备 + * + * @return 在线设备列表 + */ + List findByIsOnlineTrue(); + + /** + * 查找所有激活且在线的设备 + * + * @return 激活且在线的设备列表 + */ + List findByIsActiveTrueAndIsOnlineTrue(); + + /** + * 检查设备编号是否存在 + * + * @param deviceId 设备编号 + * @return true如果存在,false否则 + */ + boolean existsByDeviceId(String deviceId); + + /** + * 根据制造商查找设备 + * + * @param manufacturer 制造商 + * @return 该制造商的设备列表 + */ + List findByManufacturer(String manufacturer); + + /** + * 根据设备类型查找设备 + * + * @param deviceType 设备类型 + * @return 该类型的设备列表 + */ + List findByDeviceType(String deviceType); + + /** + * 更新设备在线状态 + * + * @param deviceId 设备编号 + * @param isOnline 是否在线 + * @return 更新的记录数 + */ + @Modifying + @Transactional + @Query("UPDATE TrafficLight t SET t.isOnline = :isOnline, t.updatedTime = CURRENT_TIMESTAMP WHERE t.deviceId = :deviceId") + int updateOnlineStatus(@Param("deviceId") String deviceId, @Param("isOnline") Boolean isOnline); + + /** + * 更新设备心跳时间 + * + * @param deviceId 设备编号 + * @param heartbeatTime 心跳时间 + * @return 更新的记录数 + */ + @Modifying + @Transactional + @Query("UPDATE TrafficLight t SET t.lastHeartbeat = :heartbeatTime, t.isOnline = true, t.updatedTime = CURRENT_TIMESTAMP WHERE t.deviceId = :deviceId") + int updateHeartbeat(@Param("deviceId") String deviceId, @Param("heartbeatTime") LocalDateTime heartbeatTime); + + /** + * 查找心跳超时的设备 + * + * @param timeoutTime 超时时间点 + * @return 心跳超时的设备列表 + */ + @Query("SELECT t FROM TrafficLight t WHERE t.isOnline = true AND (t.lastHeartbeat IS NULL OR t.lastHeartbeat < :timeoutTime)") + List findHeartbeatTimeoutDevices(@Param("timeoutTime") LocalDateTime timeoutTime); + + /** + * 批量设置心跳超时设备为离线状态 + * + * @param timeoutTime 超时时间点 + * @return 更新的记录数 + */ + @Modifying + @Transactional + @Query("UPDATE TrafficLight t SET t.isOnline = false, t.updatedTime = CURRENT_TIMESTAMP WHERE t.isOnline = true AND (t.lastHeartbeat IS NULL OR t.lastHeartbeat < :timeoutTime)") + int setTimeoutDevicesOffline(@Param("timeoutTime") LocalDateTime timeoutTime); + + /** + * 统计在线设备数量 + * + * @return 在线设备数量 + */ + long countByIsOnlineTrue(); + + /** + * 统计激活设备数量 + * + * @return 激活设备数量 + */ + long countByIsActiveTrue(); + + /** + * 根据路口编号统计设备数量 + * + * @param intersectionId 路口编号 + * @return 该路口的设备数量 + */ + long countByIntersectionId(String intersectionId); + + /** + * 根据设备名称模糊查询 + * + * @param name 设备名称关键字 + * @return 匹配的设备列表 + */ + @Query("SELECT t FROM TrafficLight t WHERE t.deviceName LIKE %:name%") + List findByDeviceNameContaining(@Param("name") String name); +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/spatial/Intersection.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/spatial/Intersection.java new file mode 100644 index 00000000..87c96b9e --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/spatial/Intersection.java @@ -0,0 +1,116 @@ +package com.qaup.collision.common.model.spatial; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +import java.time.LocalDateTime; + +/** + * 路口信息实体类 + * + * 用于存储机场内各个路口的基本信息,包括位置坐标、区域归属等 + * 路口是红绿灯设备的载体,一个路口可以有多个红绿灯设备 + */ +@Entity +@Table(name = "intersections") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Intersection { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 路口唯一编号 + */ + @Column(name = "intersection_id", unique = true, nullable = false, length = 50) + private String intersectionId; + + /** + * 路口名称 + */ + @Column(name = "intersection_name", nullable = false, length = 100) + private String intersectionName; + + /** + * 纬度坐标 + */ + @Column(name = "latitude", nullable = false) + private Double latitude; + + /** + * 经度坐标 + */ + @Column(name = "longitude", nullable = false) + private Double longitude; + + /** + * 所属区域编码 + */ + @Column(name = "area_code", length = 20) + private String areaCode; + + /** + * 路口描述 + */ + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + /** + * 是否激活 + */ + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + /** + * 创建时间 + */ + @Column(name = "created_time") + private LocalDateTime createdTime; + + /** + * 更新时间 + */ + @Column(name = "updated_time") + private LocalDateTime updatedTime; + + /** + * 预设置创建和更新时间 + */ + @PrePersist + protected void onCreate() { + LocalDateTime now = LocalDateTime.now(); + createdTime = now; + updatedTime = now; + } + + @PreUpdate + protected void onUpdate() { + updatedTime = LocalDateTime.now(); + } + + /** + * 获取路口位置的字符串表示 + * + * @return 格式化的位置字符串 + */ + public String getLocationString() { + return String.format("%.6f,%.6f", latitude, longitude); + } + + /** + * 检查路口是否可用(激活状态) + * + * @return true如果路口激活,false否则 + */ + public boolean isAvailable() { + return Boolean.TRUE.equals(isActive); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/spatial/TrafficLight.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/spatial/TrafficLight.java new file mode 100644 index 00000000..d180001c --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/spatial/TrafficLight.java @@ -0,0 +1,178 @@ +package com.qaup.collision.common.model.spatial; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 红绿灯设备实体类 + * + * 用于存储红绿灯设备的基本信息,包括设备编号、名称、关联路口等 + * 每个设备通过intersection_id与路口建立关联关系 + */ +@Entity +@Table(name = "traffic_lights") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TrafficLight { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 红绿灯设备唯一编号 + */ + @Column(name = "device_id", unique = true, nullable = false, length = 50) + private String deviceId; + + /** + * 设备名称 + */ + @Column(name = "device_name", nullable = false, length = 100) + private String deviceName; + + /** + * 关联的路口编号 + */ + @Column(name = "intersection_id", nullable = false, length = 50) + private String intersectionId; + + /** + * 设备类型 + */ + @Column(name = "device_type", length = 20) + @Builder.Default + private String deviceType = "STANDARD"; + + /** + * 制造商 + */ + @Column(name = "manufacturer", length = 50) + private String manufacturer; + + /** + * 型号 + */ + @Column(name = "model", length = 50) + private String model; + + /** + * 安装日期 + */ + @Column(name = "install_date") + private LocalDate installDate; + + /** + * 是否在线 + */ + @Column(name = "is_online") + @Builder.Default + private Boolean isOnline = false; + + /** + * 最后心跳时间 + */ + @Column(name = "last_heartbeat") + private LocalDateTime lastHeartbeat; + + /** + * 是否激活 + */ + @Column(name = "is_active") + @Builder.Default + private Boolean isActive = true; + + /** + * 创建时间 + */ + @Column(name = "created_time") + private LocalDateTime createdTime; + + /** + * 更新时间 + */ + @Column(name = "updated_time") + private LocalDateTime updatedTime; + + /** + * 预设置创建和更新时间 + */ + @PrePersist + protected void onCreate() { + LocalDateTime now = LocalDateTime.now(); + createdTime = now; + updatedTime = now; + } + + @PreUpdate + protected void onUpdate() { + updatedTime = LocalDateTime.now(); + } + + /** + * 更新设备心跳时间 + */ + public void updateHeartbeat() { + this.lastHeartbeat = LocalDateTime.now(); + } + + /** + * 设置设备在线状态 + * + * @param online 是否在线 + */ + public void setOnlineStatus(boolean online) { + this.isOnline = online; + if (online) { + updateHeartbeat(); + } + } + + /** + * 检查设备是否可用(激活且在线) + * + * @return true如果设备可用,false否则 + */ + public boolean isAvailable() { + return Boolean.TRUE.equals(isActive) && Boolean.TRUE.equals(isOnline); + } + + /** + * 检查设备心跳是否超时 + * + * @param timeoutMinutes 超时时间(分钟) + * @return true如果心跳超时,false否则 + */ + public boolean isHeartbeatTimeout(int timeoutMinutes) { + if (lastHeartbeat == null) { + return true; + } + return lastHeartbeat.isBefore(LocalDateTime.now().minusMinutes(timeoutMinutes)); + } + + /** + * 获取设备状态描述 + * + * @return 设备状态字符串 + */ + public String getStatusDescription() { + if (!Boolean.TRUE.equals(isActive)) { + return "未激活"; + } + if (!Boolean.TRUE.equals(isOnline)) { + return "离线"; + } + if (isHeartbeatTimeout(5)) { + return "心跳超时"; + } + return "正常"; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/config/GracefulShutdownConfig.java b/qaup-collision/src/main/java/com/qaup/collision/config/GracefulShutdownConfig.java new file mode 100644 index 00000000..20deabfa --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/config/GracefulShutdownConfig.java @@ -0,0 +1,45 @@ +package com.qaup.collision.config; + +import com.qaup.collision.datacollector.server.TrafficLightTcpServer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +import jakarta.annotation.PreDestroy; + +/** + * 优雅关闭配置 + * + * 确保系统关闭时所有资源都能正确释放 + */ +@Slf4j +@Configuration +public class GracefulShutdownConfig { + + @Autowired + private TrafficLightTcpServer trafficLightTcpServer; + + /** + * 系统关闭时的清理工作 + */ + @PreDestroy + public void onShutdown() { + log.info("系统正在关闭,开始清理资源..."); + + try { + // 关闭红绿灯TCP服务器 + if (trafficLightTcpServer != null) { + trafficLightTcpServer.stopServer(); + log.info("红绿灯TCP服务器已关闭"); + } + + // 等待一段时间确保所有连接都已关闭 + Thread.sleep(1000); + + log.info("系统资源清理完成"); + + } catch (Exception e) { + log.error("系统关闭时清理资源异常", e); + } + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/controller/HealthController.java b/qaup-collision/src/main/java/com/qaup/collision/controller/HealthController.java new file mode 100644 index 00000000..44909183 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/controller/HealthController.java @@ -0,0 +1,170 @@ +package com.qaup.collision.controller; + +import com.qaup.collision.datacollector.server.TrafficLightTcpServer; +import com.qaup.collision.datacollector.service.TrafficLightDataCollector; +import com.qaup.collision.dataprocessing.service.TrafficLightService; +import com.qaup.collision.dataprocessing.service.IntersectionService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 健康检查控制器 + * + * 提供系统健康状态检查接口,包括红绿灯系统的状态 + */ +@Slf4j +@RestController +@RequestMapping("/health") +public class HealthController { + + @Autowired + private TrafficLightTcpServer trafficLightTcpServer; + + @Autowired + private TrafficLightDataCollector trafficLightDataCollector; + + @Autowired + private TrafficLightService trafficLightService; + + @Autowired + private IntersectionService intersectionService; + + /** + * 系统整体健康检查 + */ + @GetMapping + public ResponseEntity> healthCheck() { + Map health = new HashMap<>(); + + try { + health.put("status", "UP"); + health.put("timestamp", LocalDateTime.now()); + health.put("components", getComponentsHealth()); + + return ResponseEntity.ok(health); + + } catch (Exception e) { + log.error("健康检查异常", e); + + health.put("status", "DOWN"); + health.put("timestamp", LocalDateTime.now()); + health.put("error", e.getMessage()); + + return ResponseEntity.status(503).body(health); + } + } + + /** + * 红绿灯系统健康检查 + */ + @GetMapping("/traffic-light") + public ResponseEntity> trafficLightHealth() { + Map health = new HashMap<>(); + + try { + // TCP服务器状态 + TrafficLightTcpServer.ServerStatus serverStatus = trafficLightTcpServer.getServerStatus(); + health.put("tcpServer", Map.of( + "running", serverStatus.isRunning(), + "port", serverStatus.getPort(), + "activeConnections", serverStatus.getActiveConnections(), + "totalConnections", serverStatus.getTotalConnections(), + "totalMessages", serverStatus.getTotalMessages(), + "errorCount", serverStatus.getErrorCount() + )); + + // 数据采集器状态 + TrafficLightDataCollector.HealthStatus collectorHealth = trafficLightDataCollector.getHealthStatus(); + health.put("dataCollector", Map.of( + "status", collectorHealth.getStatus(), + "healthScore", collectorHealth.getHealthScore(), + "totalSignals", collectorHealth.getTotalSignals(), + "successRate", collectorHealth.getSuccessRate(), + "errorRate", collectorHealth.getErrorRate(), + "activeClients", collectorHealth.getActiveClients() + )); + + // 设备统计 + TrafficLightService.DeviceStatistics deviceStats = trafficLightService.getStatistics(); + health.put("devices", Map.of( + "totalCount", deviceStats.getTotalCount(), + "activeCount", deviceStats.getActiveCount(), + "onlineCount", deviceStats.getOnlineCount(), + "offlineCount", deviceStats.getOfflineCount() + )); + + // 路口统计 + IntersectionService.IntersectionStatistics intersectionStats = intersectionService.getStatistics(); + health.put("intersections", Map.of( + "totalCount", intersectionStats.getTotalCount(), + "activeCount", intersectionStats.getActiveCount(), + "inactiveCount", intersectionStats.getInactiveCount() + )); + + // 整体状态判断 + boolean isHealthy = serverStatus.isRunning() && + "HEALTHY".equals(collectorHealth.getStatus()) && + deviceStats.getActiveCount() > 0; + + health.put("status", isHealthy ? "UP" : "DOWN"); + health.put("timestamp", LocalDateTime.now()); + + return ResponseEntity.ok(health); + + } catch (Exception e) { + log.error("红绿灯系统健康检查异常", e); + + health.put("status", "DOWN"); + health.put("timestamp", LocalDateTime.now()); + health.put("error", e.getMessage()); + + return ResponseEntity.status(503).body(health); + } + } + + /** + * 获取各组件健康状态 + */ + private Map getComponentsHealth() { + Map components = new HashMap<>(); + + // 数据库连接检查 + try { + long intersectionCount = intersectionService.getStatistics().getTotalCount(); + components.put("database", Map.of( + "status", "UP", + "details", "Connected, " + intersectionCount + " intersections" + )); + } catch (Exception e) { + components.put("database", Map.of( + "status", "DOWN", + "error", e.getMessage() + )); + } + + // 红绿灯TCP服务器检查 + try { + TrafficLightTcpServer.ServerStatus serverStatus = trafficLightTcpServer.getServerStatus(); + components.put("trafficLightTcp", Map.of( + "status", serverStatus.isRunning() ? "UP" : "DOWN", + "port", serverStatus.getPort(), + "connections", serverStatus.getActiveConnections() + )); + } catch (Exception e) { + components.put("trafficLightTcp", Map.of( + "status", "DOWN", + "error", e.getMessage() + )); + } + + return components; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/controller/IntersectionController.java b/qaup-collision/src/main/java/com/qaup/collision/controller/IntersectionController.java new file mode 100644 index 00000000..a0febf79 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/controller/IntersectionController.java @@ -0,0 +1,256 @@ +package com.qaup.collision.controller; + +import com.qaup.collision.common.exception.IntersectionNotFoundException; +import com.qaup.collision.common.model.spatial.Intersection; +import com.qaup.collision.dataprocessing.service.IntersectionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 路口管理控制器 + * + * 提供路口信息的增删改查接口 + */ +@Slf4j +@RestController +@RequestMapping("/traffic-light/intersections") +@Tag(name = "路口管理", description = "红绿灯路口信息管理接口") +public class IntersectionController { + + @Autowired + private IntersectionService intersectionService; + + /** + * 获取所有激活的路口 + */ + @GetMapping + @Operation(summary = "获取所有激活的路口", description = "返回系统中所有激活状态的路口列表") + public ResponseEntity> getAllActiveIntersections() { + List intersections = intersectionService.getAllActiveIntersections(); + return ResponseEntity.ok(intersections); + } + + /** + * 根据路口ID获取路口信息 + */ + @GetMapping("/{intersectionId}") + @Operation(summary = "获取路口详情", description = "根据路口ID获取路口的详细信息") + public ResponseEntity getIntersectionById( + @Parameter(description = "路口ID", required = true) + @PathVariable String intersectionId) { + + Optional intersection = intersectionService.getIntersectionById(intersectionId); + + if (intersection.isPresent()) { + return ResponseEntity.ok(intersection.get()); + } else { + throw new IntersectionNotFoundException("路口不存在: " + intersectionId); + } + } + + /** + * 根据区域编码获取路口 + */ + @GetMapping("/area/{areaCode}") + @Operation(summary = "根据区域获取路口", description = "根据区域编码获取该区域内的所有激活路口") + public ResponseEntity> getIntersectionsByArea( + @Parameter(description = "区域编码", required = true) + @PathVariable String areaCode) { + + List intersections = intersectionService.getActiveIntersectionsByArea(areaCode); + return ResponseEntity.ok(intersections); + } + + /** + * 根据坐标范围查找路口 + */ + @GetMapping("/bounds") + @Operation(summary = "根据坐标范围查找路口", description = "在指定的坐标范围内查找路口") + public ResponseEntity> getIntersectionsInBounds( + @Parameter(description = "最小纬度", required = true) + @RequestParam double minLat, + @Parameter(description = "最大纬度", required = true) + @RequestParam double maxLat, + @Parameter(description = "最小经度", required = true) + @RequestParam double minLng, + @Parameter(description = "最大经度", required = true) + @RequestParam double maxLng) { + + List intersections = intersectionService.getIntersectionsInBounds(minLat, maxLat, minLng, maxLng); + return ResponseEntity.ok(intersections); + } + + /** + * 根据名称模糊查询路口 + */ + @GetMapping("/search") + @Operation(summary = "搜索路口", description = "根据路口名称关键字模糊查询路口") + public ResponseEntity> searchIntersections( + @Parameter(description = "搜索关键字", required = true) + @RequestParam String name) { + + List intersections = intersectionService.searchIntersectionsByName(name); + return ResponseEntity.ok(intersections); + } + + /** + * 创建新路口 + */ + @PostMapping + @Operation(summary = "创建路口", description = "创建新的路口信息") + public ResponseEntity createIntersection( + @Parameter(description = "路口信息", required = true) + @Valid @RequestBody IntersectionCreateRequest request) { + + Intersection intersection = Intersection.builder() + .intersectionId(request.getIntersectionId()) + .intersectionName(request.getIntersectionName()) + .latitude(request.getLatitude()) + .longitude(request.getLongitude()) + .areaCode(request.getAreaCode()) + .description(request.getDescription()) + .isActive(request.getIsActive() != null ? request.getIsActive() : true) + .build(); + + Intersection createdIntersection = intersectionService.addIntersection(intersection); + + log.info("创建路口成功: intersectionId={}, name={}", + createdIntersection.getIntersectionId(), createdIntersection.getIntersectionName()); + + return ResponseEntity.ok(createdIntersection); + } + + /** + * 更新路口信息 + */ + @PutMapping("/{intersectionId}") + @Operation(summary = "更新路口", description = "更新指定路口的信息") + public ResponseEntity updateIntersection( + @Parameter(description = "路口ID", required = true) + @PathVariable String intersectionId, + @Parameter(description = "路口信息", required = true) + @Valid @RequestBody IntersectionUpdateRequest request) { + + // 先检查路口是否存在 + Optional existingIntersection = intersectionService.getIntersectionById(intersectionId); + if (existingIntersection.isEmpty()) { + throw new IntersectionNotFoundException("路口不存在: " + intersectionId); + } + + Intersection intersection = existingIntersection.get(); + + // 更新字段 + if (request.getIntersectionName() != null) { + intersection.setIntersectionName(request.getIntersectionName()); + } + if (request.getLatitude() != null) { + intersection.setLatitude(request.getLatitude()); + } + if (request.getLongitude() != null) { + intersection.setLongitude(request.getLongitude()); + } + if (request.getAreaCode() != null) { + intersection.setAreaCode(request.getAreaCode()); + } + if (request.getDescription() != null) { + intersection.setDescription(request.getDescription()); + } + if (request.getIsActive() != null) { + intersection.setIsActive(request.getIsActive()); + } + + Intersection updatedIntersection = intersectionService.updateIntersection(intersection); + + log.info("更新路口成功: intersectionId={}, name={}", + updatedIntersection.getIntersectionId(), updatedIntersection.getIntersectionName()); + + return ResponseEntity.ok(updatedIntersection); + } + + /** + * 激活或停用路口 + */ + @PatchMapping("/{intersectionId}/status") + @Operation(summary = "设置路口状态", description = "激活或停用指定的路口") + public ResponseEntity setIntersectionStatus( + @Parameter(description = "路口ID", required = true) + @PathVariable String intersectionId, + @Parameter(description = "是否激活", required = true) + @RequestParam boolean active) { + + Optional intersection = intersectionService.setIntersectionActive(intersectionId, active); + + if (intersection.isPresent()) { + log.info("设置路口状态成功: intersectionId={}, active={}", intersectionId, active); + return ResponseEntity.ok(intersection.get()); + } else { + throw new IntersectionNotFoundException("路口不存在: " + intersectionId); + } + } + + /** + * 获取路口统计信息 + */ + @GetMapping("/statistics") + @Operation(summary = "获取路口统计", description = "获取路口的统计信息") + public ResponseEntity getStatistics() { + IntersectionService.IntersectionStatistics statistics = intersectionService.getStatistics(); + return ResponseEntity.ok(statistics); + } + + /** + * 路口创建请求DTO + */ + @lombok.Data + public static class IntersectionCreateRequest { + @jakarta.validation.constraints.NotBlank(message = "路口编号不能为空") + private String intersectionId; + + @jakarta.validation.constraints.NotBlank(message = "路口名称不能为空") + private String intersectionName; + + @jakarta.validation.constraints.NotNull(message = "纬度不能为空") + @jakarta.validation.constraints.DecimalMin(value = "-90.0", message = "纬度必须在-90到90之间") + @jakarta.validation.constraints.DecimalMax(value = "90.0", message = "纬度必须在-90到90之间") + private Double latitude; + + @jakarta.validation.constraints.NotNull(message = "经度不能为空") + @jakarta.validation.constraints.DecimalMin(value = "-180.0", message = "经度必须在-180到180之间") + @jakarta.validation.constraints.DecimalMax(value = "180.0", message = "经度必须在-180到180之间") + private Double longitude; + + private String areaCode; + private String description; + private Boolean isActive; + } + + /** + * 路口更新请求DTO + */ + @lombok.Data + public static class IntersectionUpdateRequest { + private String intersectionName; + + @jakarta.validation.constraints.DecimalMin(value = "-90.0", message = "纬度必须在-90到90之间") + @jakarta.validation.constraints.DecimalMax(value = "90.0", message = "纬度必须在-90到90之间") + private Double latitude; + + @jakarta.validation.constraints.DecimalMin(value = "-180.0", message = "经度必须在-180到180之间") + @jakarta.validation.constraints.DecimalMax(value = "180.0", message = "经度必须在-180到180之间") + private Double longitude; + + private String areaCode; + private String description; + private Boolean isActive; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/controller/TrafficLightController.java b/qaup-collision/src/main/java/com/qaup/collision/controller/TrafficLightController.java new file mode 100644 index 00000000..1fce817e --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/controller/TrafficLightController.java @@ -0,0 +1,325 @@ +package com.qaup.collision.controller; + +import com.qaup.collision.common.exception.DeviceNotFoundException; +import com.qaup.collision.common.exception.IntersectionNotFoundException; +import com.qaup.collision.common.model.spatial.TrafficLight; +import com.qaup.collision.dataprocessing.service.TrafficLightService; +import com.qaup.collision.dataprocessing.service.IntersectionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.validation.Valid; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +/** + * 红绿灯设备管理控制器 + * + * 提供红绿灯设备信息的增删改查接口 + */ +@Slf4j +@RestController +@RequestMapping("/traffic-light/devices") +@Tag(name = "红绿灯设备管理", description = "红绿灯设备信息管理接口") +public class TrafficLightController { + + @Autowired + private TrafficLightService trafficLightService; + + @Autowired + private IntersectionService intersectionService; + + /** + * 获取所有在线设备 + */ + @GetMapping("/online") + @Operation(summary = "获取在线设备", description = "返回所有在线的红绿灯设备") + public ResponseEntity> getOnlineDevices() { + List devices = trafficLightService.getOnlineDevices(); + return ResponseEntity.ok(devices); + } + + /** + * 获取所有激活且在线的设备 + */ + @GetMapping("/active-online") + @Operation(summary = "获取激活且在线的设备", description = "返回所有激活且在线的红绿灯设备") + public ResponseEntity> getActiveOnlineDevices() { + List devices = trafficLightService.getActiveOnlineDevices(); + return ResponseEntity.ok(devices); + } + + /** + * 根据设备ID获取设备信息 + */ + @GetMapping("/{deviceId}") + @Operation(summary = "获取设备详情", description = "根据设备ID获取设备的详细信息") + public ResponseEntity getDeviceById( + @Parameter(description = "设备ID", required = true) + @PathVariable String deviceId) { + + Optional device = trafficLightService.getTrafficLightByDeviceId(deviceId); + + if (device.isPresent()) { + return ResponseEntity.ok(device.get()); + } else { + throw new DeviceNotFoundException("设备不存在: " + deviceId); + } + } + + /** + * 根据路口ID获取设备列表 + */ + @GetMapping("/intersection/{intersectionId}") + @Operation(summary = "根据路口获取设备", description = "根据路口ID获取该路口的所有红绿灯设备") + public ResponseEntity> getDevicesByIntersection( + @Parameter(description = "路口ID", required = true) + @PathVariable String intersectionId) { + + List devices = trafficLightService.getTrafficLightsByIntersection(intersectionId); + return ResponseEntity.ok(devices); + } + + /** + * 根据路口ID获取激活的设备列表 + */ + @GetMapping("/intersection/{intersectionId}/active") + @Operation(summary = "根据路口获取激活设备", description = "根据路口ID获取该路口的所有激活红绿灯设备") + public ResponseEntity> getActiveDevicesByIntersection( + @Parameter(description = "路口ID", required = true) + @PathVariable String intersectionId) { + + List devices = trafficLightService.getActiveTrafficLightsByIntersection(intersectionId); + return ResponseEntity.ok(devices); + } + + /** + * 根据制造商查找设备 + */ + @GetMapping("/manufacturer/{manufacturer}") + @Operation(summary = "根据制造商查找设备", description = "根据制造商名称查找设备") + public ResponseEntity> getDevicesByManufacturer( + @Parameter(description = "制造商", required = true) + @PathVariable String manufacturer) { + + List devices = trafficLightService.getDevicesByManufacturer(manufacturer); + return ResponseEntity.ok(devices); + } + + /** + * 根据设备名称模糊查询 + */ + @GetMapping("/search") + @Operation(summary = "搜索设备", description = "根据设备名称关键字模糊查询设备") + public ResponseEntity> searchDevices( + @Parameter(description = "搜索关键字", required = true) + @RequestParam String name) { + + List devices = trafficLightService.searchDevicesByName(name); + return ResponseEntity.ok(devices); + } + + /** + * 创建新设备 + */ + @PostMapping + @Operation(summary = "创建设备", description = "创建新的红绿灯设备") + public ResponseEntity createDevice( + @Parameter(description = "设备信息", required = true) + @Valid @RequestBody DeviceCreateRequest request) { + + // 验证关联的路口是否存在 + if (!intersectionService.getActiveIntersectionById(request.getIntersectionId()).isPresent()) { + throw new IntersectionNotFoundException("关联的路口不存在或未激活: " + request.getIntersectionId()); + } + + TrafficLight device = TrafficLight.builder() + .deviceId(request.getDeviceId()) + .deviceName(request.getDeviceName()) + .intersectionId(request.getIntersectionId()) + .deviceType(request.getDeviceType() != null ? request.getDeviceType() : "STANDARD") + .manufacturer(request.getManufacturer()) + .model(request.getModel()) + .installDate(request.getInstallDate()) + .isActive(request.getIsActive() != null ? request.getIsActive() : true) + .isOnline(false) // 新创建的设备默认离线 + .build(); + + TrafficLight createdDevice = trafficLightService.addTrafficLight(device); + + log.info("创建红绿灯设备成功: deviceId={}, name={}, intersectionId={}", + createdDevice.getDeviceId(), createdDevice.getDeviceName(), createdDevice.getIntersectionId()); + + return ResponseEntity.ok(createdDevice); + } + + /** + * 更新设备信息 + */ + @PutMapping("/{deviceId}") + @Operation(summary = "更新设备", description = "更新指定设备的信息") + public ResponseEntity updateDevice( + @Parameter(description = "设备ID", required = true) + @PathVariable String deviceId, + @Parameter(description = "设备信息", required = true) + @Valid @RequestBody DeviceUpdateRequest request) { + + // 先检查设备是否存在 + Optional existingDevice = trafficLightService.getTrafficLightByDeviceId(deviceId); + if (existingDevice.isEmpty()) { + throw new DeviceNotFoundException("设备不存在: " + deviceId); + } + + TrafficLight device = existingDevice.get(); + + // 更新字段 + if (request.getDeviceName() != null) { + device.setDeviceName(request.getDeviceName()); + } + if (request.getIntersectionId() != null) { + // 验证新的路口是否存在 + if (!intersectionService.getActiveIntersectionById(request.getIntersectionId()).isPresent()) { + throw new IntersectionNotFoundException("关联的路口不存在或未激活: " + request.getIntersectionId()); + } + device.setIntersectionId(request.getIntersectionId()); + } + if (request.getDeviceType() != null) { + device.setDeviceType(request.getDeviceType()); + } + if (request.getManufacturer() != null) { + device.setManufacturer(request.getManufacturer()); + } + if (request.getModel() != null) { + device.setModel(request.getModel()); + } + if (request.getInstallDate() != null) { + device.setInstallDate(request.getInstallDate()); + } + if (request.getIsActive() != null) { + device.setIsActive(request.getIsActive()); + } + + TrafficLight updatedDevice = trafficLightService.updateTrafficLight(device); + + log.info("更新红绿灯设备成功: deviceId={}, name={}", + updatedDevice.getDeviceId(), updatedDevice.getDeviceName()); + + return ResponseEntity.ok(updatedDevice); + } + + /** + * 激活或停用设备 + */ + @PatchMapping("/{deviceId}/status") + @Operation(summary = "设置设备状态", description = "激活或停用指定的设备") + public ResponseEntity setDeviceStatus( + @Parameter(description = "设备ID", required = true) + @PathVariable String deviceId, + @Parameter(description = "是否激活", required = true) + @RequestParam boolean active) { + + Optional device = trafficLightService.setDeviceActive(deviceId, active); + + if (device.isPresent()) { + log.info("设置设备状态成功: deviceId={}, active={}", deviceId, active); + return ResponseEntity.ok(device.get()); + } else { + throw new DeviceNotFoundException("设备不存在: " + deviceId); + } + } + + /** + * 检查设备是否可用 + */ + @GetMapping("/{deviceId}/availability") + @Operation(summary = "检查设备可用性", description = "检查设备是否可用(激活且在线)") + public ResponseEntity checkDeviceAvailability( + @Parameter(description = "设备ID", required = true) + @PathVariable String deviceId) { + + boolean available = trafficLightService.isDeviceAvailable(deviceId); + Optional device = trafficLightService.getTrafficLightByDeviceId(deviceId); + + DeviceAvailabilityResponse response = new DeviceAvailabilityResponse(); + response.setDeviceId(deviceId); + response.setAvailable(available); + + if (device.isPresent()) { + TrafficLight d = device.get(); + response.setActive(d.getIsActive()); + response.setOnline(d.getIsOnline()); + response.setStatusDescription(d.getStatusDescription()); + response.setLastHeartbeat(d.getLastHeartbeat()); + } else { + response.setActive(false); + response.setOnline(false); + response.setStatusDescription("设备不存在"); + } + + return ResponseEntity.ok(response); + } + + /** + * 获取设备统计信息 + */ + @GetMapping("/statistics") + @Operation(summary = "获取设备统计", description = "获取红绿灯设备的统计信息") + public ResponseEntity getStatistics() { + TrafficLightService.DeviceStatistics statistics = trafficLightService.getStatistics(); + return ResponseEntity.ok(statistics); + } + + /** + * 设备创建请求DTO + */ + @lombok.Data + public static class DeviceCreateRequest { + @jakarta.validation.constraints.NotBlank(message = "设备编号不能为空") + private String deviceId; + + @jakarta.validation.constraints.NotBlank(message = "设备名称不能为空") + private String deviceName; + + @jakarta.validation.constraints.NotBlank(message = "关联路口编号不能为空") + private String intersectionId; + + private String deviceType; + private String manufacturer; + private String model; + private LocalDate installDate; + private Boolean isActive; + } + + /** + * 设备更新请求DTO + */ + @lombok.Data + public static class DeviceUpdateRequest { + private String deviceName; + private String intersectionId; + private String deviceType; + private String manufacturer; + private String model; + private LocalDate installDate; + private Boolean isActive; + } + + /** + * 设备可用性响应DTO + */ + @lombok.Data + public static class DeviceAvailabilityResponse { + private String deviceId; + private boolean available; + private boolean active; + private boolean online; + private String statusDescription; + private java.time.LocalDateTime lastHeartbeat; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/config/TrafficLightProperties.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/config/TrafficLightProperties.java new file mode 100644 index 00000000..8f72447a --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/config/TrafficLightProperties.java @@ -0,0 +1,100 @@ +package com.qaup.collision.datacollector.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 红绿灯配置属性 + * + * 绑定application.yml中的红绿灯相关配置 + */ +@Data +@Component +@ConfigurationProperties(prefix = "traffic.light") +public class TrafficLightProperties { + + /** + * TCP服务器配置 + */ + private Tcp tcp = new Tcp(); + + /** + * 路口配置 + */ + private Intersection intersection = new Intersection(); + + /** + * 处理配置 + */ + private Processing processing = new Processing(); + + @Data + public static class Tcp { + /** + * 是否启用TCP服务器 + */ + private boolean enabled = true; + + /** + * TCP监听端口 + */ + private int port = 8082; + + /** + * 最大连接数 + */ + private int maxConnections = 50; + + /** + * 连接超时时间(毫秒) + */ + private int connectionTimeout = 30000; + + /** + * 心跳超时时间(分钟) + */ + private int heartbeatTimeoutMinutes = 5; + } + + @Data + public static class Intersection { + /** + * 默认路口ID + */ + private String defaultId = "DEFAULT_INTERSECTION"; + + /** + * 默认纬度 + */ + private double defaultLatitude = 0.0; + + /** + * 默认经度 + */ + private double defaultLongitude = 0.0; + } + + @Data + public static class Processing { + /** + * 是否启用统计功能 + */ + private boolean enableStatistics = true; + + /** + * 统计信息输出间隔(毫秒) + */ + private long statisticsInterval = 60000; + + /** + * 是否启用调试日志 + */ + private boolean enableDebugLog = false; + + /** + * 信号处理超时时间(毫秒) + */ + private int signalProcessTimeout = 5000; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/server/TrafficLightSignalHandler.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/server/TrafficLightSignalHandler.java new file mode 100644 index 00000000..b0ecfa70 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/server/TrafficLightSignalHandler.java @@ -0,0 +1,17 @@ +package com.qaup.collision.datacollector.server; + +/** + * 红绿灯信号处理器接口 + * + * 定义处理从TCP服务器接收到的红绿灯信号的方法 + */ +public interface TrafficLightSignalHandler { + + /** + * 处理红绿灯信号 + * + * @param clientId 客户端ID + * @param signal 信号内容 + */ + void handleSignal(String clientId, String signal); +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/server/TrafficLightTcpServer.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/server/TrafficLightTcpServer.java new file mode 100644 index 00000000..debc477e --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/server/TrafficLightTcpServer.java @@ -0,0 +1,398 @@ +package com.qaup.collision.datacollector.server; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 红绿灯TCP服务器 + * + * 监听指定端口,接收红绿灯硬件发送的状态信号 + * 支持多客户端并发连接,提供连接管理和异常处理功能 + */ +@Slf4j +@Component +public class TrafficLightTcpServer { + + @Value("${traffic.light.tcp.port:8082}") + private int serverPort; + + @Value("${traffic.light.tcp.enabled:true}") + private boolean serverEnabled; + + @Value("${traffic.light.tcp.max-connections:50}") + private int maxConnections; + + @Value("${traffic.light.tcp.connection-timeout:30000}") + private int connectionTimeout; + + @Autowired + private TrafficLightSignalHandler signalHandler; + + // 服务器状态 + private final AtomicBoolean serverRunning = new AtomicBoolean(false); + private ServerSocket serverSocket; + private ExecutorService connectionExecutor; + + // 连接管理 + private final ConcurrentHashMap activeConnections = new ConcurrentHashMap<>(); + private final AtomicInteger connectionCounter = new AtomicInteger(0); + + // 统计信息 + private final AtomicLong totalConnections = new AtomicLong(0); + private final AtomicLong totalMessages = new AtomicLong(0); + private final AtomicLong errorCount = new AtomicLong(0); + + /** + * 服务器初始化 + */ + @PostConstruct + public void init() { + if (serverEnabled) { + startServer(); + } else { + log.info("红绿灯TCP服务器已禁用"); + } + } + + /** + * 启动TCP服务器 + */ + public synchronized void startServer() { + if (serverRunning.get()) { + log.warn("TCP服务器已经在运行中"); + return; + } + + try { + // 创建服务器套接字 + serverSocket = new ServerSocket(serverPort); + serverSocket.setSoTimeout(1000); // 设置accept超时,用于优雅关闭 + + // 创建连接处理线程池 + connectionExecutor = Executors.newFixedThreadPool(maxConnections, + r -> new Thread(r, "TrafficLight-TCP-" + connectionCounter.incrementAndGet())); + + serverRunning.set(true); + + log.info("红绿灯TCP服务器启动成功 - 端口:{}, 最大连接数:{}", serverPort, maxConnections); + + // 启动服务器监听线程 + Thread serverThread = new Thread(this::serverLoop, "TrafficLight-TCP-Server"); + serverThread.setDaemon(true); + serverThread.start(); + + } catch (IOException e) { + log.error("启动TCP服务器失败 - 端口:{}", serverPort, e); + serverRunning.set(false); + + // 尝试使用备用端口 + tryAlternativePorts(); + } + } + + /** + * 服务器主循环 + */ + private void serverLoop() { + log.info("TCP服务器开始监听连接 - 端口:{}", serverPort); + + while (serverRunning.get()) { + try { + // 接受客户端连接 + Socket clientSocket = serverSocket.accept(); + + // 检查连接数限制 + if (activeConnections.size() >= maxConnections) { + log.warn("连接数已达上限 {}, 拒绝新连接: {}", maxConnections, clientSocket.getRemoteSocketAddress()); + clientSocket.close(); + continue; + } + + // 处理客户端连接 + handleClientConnection(clientSocket); + + } catch (SocketTimeoutException e) { + // 超时是正常的,用于检查服务器状态 + continue; + } catch (IOException e) { + if (serverRunning.get()) { + log.error("接受客户端连接异常", e); + errorCount.incrementAndGet(); + } + } + } + + log.info("TCP服务器监听循环结束"); + } + + /** + * 处理客户端连接 + * + * @param clientSocket 客户端套接字 + */ + private void handleClientConnection(Socket clientSocket) { + String clientId = generateClientId(clientSocket); + totalConnections.incrementAndGet(); + + log.info("新的红绿灯设备连接: {} (总连接数: {})", clientId, totalConnections.get()); + + // 创建客户端连接对象 + ClientConnection connection = new ClientConnection(clientId, clientSocket); + activeConnections.put(clientId, connection); + + // 提交到线程池处理 + connectionExecutor.submit(() -> processClientConnection(connection)); + } + + /** + * 处理客户端连接的消息 + * + * @param connection 客户端连接 + */ + private void processClientConnection(ClientConnection connection) { + String clientId = connection.getClientId(); + Socket clientSocket = connection.getSocket(); + + try { + // 设置连接超时 + clientSocket.setSoTimeout(connectionTimeout); + + // 创建输入流读取器 + BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), "UTF-8")); + + String message; + while (serverRunning.get() && !clientSocket.isClosed() && (message = reader.readLine()) != null) { + if (!message.trim().isEmpty()) { + // 更新连接活动时间 + connection.updateLastActivity(); + + // 处理接收到的消息 + processReceivedMessage(clientId, message.trim()); + + totalMessages.incrementAndGet(); + log.debug("接收到红绿灯信号: {} - {}", clientId, message); + } + } + + } catch (SocketTimeoutException e) { + log.warn("客户端连接超时: {}", clientId); + } catch (IOException e) { + if (serverRunning.get()) { + log.warn("客户端连接异常: {}", clientId, e); + errorCount.incrementAndGet(); + } + } finally { + // 清理连接 + closeClientConnection(connection); + } + } + + /** + * 处理接收到的消息 + * + * @param clientId 客户端ID + * @param message 消息内容 + */ + private void processReceivedMessage(String clientId, String message) { + try { + // 委托给信号处理器处理 + signalHandler.handleSignal(clientId, message); + } catch (Exception e) { + log.error("处理红绿灯信号异常: clientId={}, message={}", clientId, message, e); + errorCount.incrementAndGet(); + } + } + + /** + * 关闭客户端连接 + * + * @param connection 客户端连接 + */ + private void closeClientConnection(ClientConnection connection) { + String clientId = connection.getClientId(); + + try { + connection.getSocket().close(); + } catch (IOException e) { + log.debug("关闭客户端连接异常: {}", clientId, e); + } + + activeConnections.remove(clientId); + log.info("客户端连接已关闭: {} (当前连接数: {})", clientId, activeConnections.size()); + } + + /** + * 生成客户端ID + * + * @param clientSocket 客户端套接字 + * @return 客户端ID + */ + private String generateClientId(Socket clientSocket) { + String remoteAddress = clientSocket.getRemoteSocketAddress().toString(); + long timestamp = System.currentTimeMillis(); + return String.format("TL-Client-%s-%d", remoteAddress.replace("/", "").replace(":", "-"), timestamp); + } + + /** + * 尝试使用备用端口 + */ + private void tryAlternativePorts() { + int[] alternativePorts = {8083, 8084, 8085}; + + for (int port : alternativePorts) { + try { + serverSocket = new ServerSocket(port); + serverSocket.setSoTimeout(1000); + serverPort = port; + + connectionExecutor = Executors.newFixedThreadPool(maxConnections, + r -> new Thread(r, "TrafficLight-TCP-" + connectionCounter.incrementAndGet())); + + serverRunning.set(true); + + log.info("使用备用端口启动TCP服务器成功 - 端口:{}", port); + + Thread serverThread = new Thread(this::serverLoop, "TrafficLight-TCP-Server"); + serverThread.setDaemon(true); + serverThread.start(); + + return; + } catch (IOException e) { + log.debug("备用端口 {} 也被占用", port); + } + } + + log.error("所有端口都被占用,TCP服务器启动失败"); + } + + /** + * 停止TCP服务器 + */ + @PreDestroy + public synchronized void stopServer() { + if (!serverRunning.get()) { + return; + } + + log.info("正在关闭红绿灯TCP服务器..."); + + serverRunning.set(false); + + // 关闭所有客户端连接 + activeConnections.values().forEach(this::closeClientConnection); + activeConnections.clear(); + + // 关闭服务器套接字 + if (serverSocket != null && !serverSocket.isClosed()) { + try { + serverSocket.close(); + } catch (IOException e) { + log.debug("关闭服务器套接字异常", e); + } + } + + // 关闭线程池 + if (connectionExecutor != null && !connectionExecutor.isShutdown()) { + connectionExecutor.shutdown(); + try { + if (!connectionExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) { + connectionExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + connectionExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + log.info("红绿灯TCP服务器已关闭"); + } + + /** + * 获取服务器状态 + * + * @return 服务器状态 + */ + public ServerStatus getServerStatus() { + return ServerStatus.builder() + .running(serverRunning.get()) + .port(serverPort) + .activeConnections(activeConnections.size()) + .maxConnections(maxConnections) + .totalConnections(totalConnections.get()) + .totalMessages(totalMessages.get()) + .errorCount(errorCount.get()) + .build(); + } + + /** + * 客户端连接信息 + */ + private static class ClientConnection { + private final String clientId; + private final Socket socket; + private volatile long lastActivity; + + public ClientConnection(String clientId, Socket socket) { + this.clientId = clientId; + this.socket = socket; + this.lastActivity = System.currentTimeMillis(); + } + + public String getClientId() { + return clientId; + } + + public Socket getSocket() { + return socket; + } + + public void updateLastActivity() { + this.lastActivity = System.currentTimeMillis(); + } + + public long getLastActivity() { + return lastActivity; + } + } + + /** + * 服务器状态信息 + */ + @lombok.Data + @lombok.Builder + @lombok.NoArgsConstructor + @lombok.AllArgsConstructor + public static class ServerStatus { + private boolean running; + private int port; + private int activeConnections; + private int maxConnections; + private long totalConnections; + private long totalMessages; + private long errorCount; + + @Override + public String toString() { + return String.format("TCP服务器状态 - 运行:%s, 端口:%d, 活跃连接:%d/%d, 总连接:%d, 总消息:%d, 错误:%d", + running ? "是" : "否", port, activeConnections, maxConnections, + totalConnections, totalMessages, errorCount); + } + } +} \ No newline at end of file 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 4f392d86..906db52f 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 @@ -83,6 +83,9 @@ public class DataCollectorService { @Autowired private VehicleLocationFilter vehicleLocationFilter; // 注入车辆位置过滤器 + + @Autowired + private TrafficLightDataCollector trafficLightDataCollector; // 注入红绿灯数据采集器 private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); // SRID 4326 for WGS84 @@ -656,11 +659,16 @@ public class DataCollectorService { long totalVehicles = 0; // 同上 // 实际上可以统计已处理的事件数量等 + // 获取红绿灯数据采集统计 + TrafficLightDataCollector.CollectionStatistics trafficLightStats = trafficLightDataCollector.getStatistics(); + return String.format("DataCollectorService Stats: " + "Total Aircraft Processed: %d, " + "Total Airport Vehicles Processed: %d, " + - "Total Unmanned Vehicles Processed: %d", - totalAircraft, totalVehicles, vehicleLocationService.countAllUnmannedVehicles()); + "Total Unmanned Vehicles Processed: %d, " + + "Traffic Light Signals: %d (Success Rate: %.2f%%)", + totalAircraft, totalVehicles, vehicleLocationService.countAllUnmannedVehicles(), + trafficLightStats.getTotalSignalsReceived(), trafficLightStats.getSuccessRate()); } @PreDestroy diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/TrafficLightDataCollector.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/TrafficLightDataCollector.java new file mode 100644 index 00000000..fedbb2cb --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/TrafficLightDataCollector.java @@ -0,0 +1,309 @@ +package com.qaup.collision.datacollector.service; + +import com.qaup.collision.datacollector.config.TrafficLightProperties; +import com.qaup.collision.datacollector.server.TrafficLightSignalHandler; +import com.qaup.collision.dataprocessing.service.DataProcessingService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.ConcurrentHashMap; +import java.time.LocalDateTime; + +/** + * 红绿灯数据采集器 + * + * 集成到现有数据采集框架,处理TCP服务器接收到的红绿灯信号 + * 实现TrafficLightSignalHandler接口,处理来自TCP服务器的信号 + */ +@Slf4j +@Service +public class TrafficLightDataCollector implements TrafficLightSignalHandler { + + @Autowired + private DataProcessingService dataProcessingService; + + @Autowired + private TrafficLightProperties trafficLightProperties; + + // 采集统计信息 + private final AtomicLong totalSignalsReceived = new AtomicLong(0); + private final AtomicLong validSignalsProcessed = new AtomicLong(0); + private final AtomicLong invalidSignalsSkipped = new AtomicLong(0); + private final AtomicLong processingErrors = new AtomicLong(0); + + // 客户端连接信息缓存 + private final ConcurrentHashMap clientInfoCache = new ConcurrentHashMap<>(); + + /** + * 处理TCP服务器接收到的红绿灯信号 + * + * @param clientId 客户端ID + * @param signal 信号内容 + */ + @Override + public void handleSignal(String clientId, String signal) { + totalSignalsReceived.incrementAndGet(); + + if (signal == null || signal.trim().isEmpty()) { + log.warn("接收到空的红绿灯信号: clientId={}", clientId); + invalidSignalsSkipped.incrementAndGet(); + return; + } + + try { + // 更新客户端信息 + updateClientInfo(clientId); + + // 记录调试日志 + if (trafficLightProperties.getProcessing().isEnableDebugLog()) { + log.debug("处理红绿灯信号: clientId={}, signal={}", clientId, signal); + } + + // 委托给数据处理服务处理 + dataProcessingService.processTrafficLightSignal(signal.trim()); + + validSignalsProcessed.incrementAndGet(); + + log.debug("成功处理红绿灯信号: clientId={}", clientId); + + } catch (Exception e) { + log.error("处理红绿灯信号异常: clientId={}, signal={}", clientId, signal, e); + processingErrors.incrementAndGet(); + } + } + + /** + * 更新客户端信息 + * + * @param clientId 客户端ID + */ + private void updateClientInfo(String clientId) { + ClientInfo clientInfo = clientInfoCache.computeIfAbsent(clientId, + id -> new ClientInfo(id, LocalDateTime.now())); + + clientInfo.updateLastActivity(); + clientInfo.incrementMessageCount(); + } + + /** + * 定期输出采集统计信息 + */ + @Scheduled(fixedRateString = "${traffic.light.processing.statistics-interval:60000}") + public void logStatistics() { + if (!trafficLightProperties.getProcessing().isEnableStatistics()) { + return; + } + + CollectionStatistics stats = getStatistics(); + + if (stats.getTotalSignalsReceived() > 0) { + log.info("红绿灯数据采集统计: {}", stats); + + // 输出客户端连接统计 + if (!clientInfoCache.isEmpty()) { + log.info("活跃客户端数量: {}", clientInfoCache.size()); + + if (trafficLightProperties.getProcessing().isEnableDebugLog()) { + clientInfoCache.forEach((clientId, info) -> + log.debug("客户端 {}: 消息数={}, 最后活动={}", + clientId, info.getMessageCount(), info.getLastActivity())); + } + } + } + } + + /** + * 定期清理过期的客户端信息 + */ + @Scheduled(fixedRate = 300000) // 每5分钟执行一次 + public void cleanupExpiredClients() { + LocalDateTime expireTime = LocalDateTime.now().minusMinutes(10); // 10分钟无活动视为过期 + + clientInfoCache.entrySet().removeIf(entry -> { + ClientInfo info = entry.getValue(); + if (info.getLastActivity().isBefore(expireTime)) { + log.debug("清理过期客户端信息: {}", entry.getKey()); + return true; + } + return false; + }); + } + + /** + * 获取采集统计信息 + * + * @return 统计信息 + */ + public CollectionStatistics getStatistics() { + return CollectionStatistics.builder() + .totalSignalsReceived(totalSignalsReceived.get()) + .validSignalsProcessed(validSignalsProcessed.get()) + .invalidSignalsSkipped(invalidSignalsSkipped.get()) + .processingErrors(processingErrors.get()) + .activeClients(clientInfoCache.size()) + .successRate(calculateSuccessRate()) + .build(); + } + + /** + * 计算成功率 + * + * @return 成功率百分比 + */ + private double calculateSuccessRate() { + long total = totalSignalsReceived.get(); + if (total == 0) { + return 0.0; + } + return (double) validSignalsProcessed.get() / total * 100.0; + } + + /** + * 重置统计信息 + */ + public void resetStatistics() { + totalSignalsReceived.set(0); + validSignalsProcessed.set(0); + invalidSignalsSkipped.set(0); + processingErrors.set(0); + clientInfoCache.clear(); + + log.info("红绿灯数据采集统计信息已重置"); + } + + /** + * 获取客户端信息列表 + * + * @return 客户端信息列表 + */ + public java.util.List getActiveClients() { + return new java.util.ArrayList<>(clientInfoCache.values()); + } + + /** + * 检查数据采集器健康状态 + * + * @return 健康状态信息 + */ + public HealthStatus getHealthStatus() { + CollectionStatistics stats = getStatistics(); + + // 计算健康分数 + double healthScore = 100.0; + + // 如果错误率过高,降低健康分数 + if (stats.getTotalSignalsReceived() > 0) { + double errorRate = (double) stats.getProcessingErrors() / stats.getTotalSignalsReceived() * 100.0; + if (errorRate > 10) { // 错误率超过10% + healthScore -= Math.min(50, errorRate * 2); // 最多扣50分 + } + } + + // 如果无效信号过多,降低健康分数 + if (stats.getTotalSignalsReceived() > 0) { + double invalidRate = (double) stats.getInvalidSignalsSkipped() / stats.getTotalSignalsReceived() * 100.0; + if (invalidRate > 20) { // 无效率超过20% + healthScore -= Math.min(30, invalidRate); // 最多扣30分 + } + } + + String status; + if (healthScore >= 90) { + status = "HEALTHY"; + } else if (healthScore >= 70) { + status = "WARNING"; + } else { + status = "UNHEALTHY"; + } + + return HealthStatus.builder() + .status(status) + .healthScore(healthScore) + .totalSignals(stats.getTotalSignalsReceived()) + .successRate(stats.getSuccessRate()) + .errorRate(stats.getTotalSignalsReceived() > 0 ? + (double) stats.getProcessingErrors() / stats.getTotalSignalsReceived() * 100.0 : 0.0) + .activeClients(stats.getActiveClients()) + .lastUpdateTime(LocalDateTime.now()) + .build(); + } + + /** + * 客户端信息类 + */ + @lombok.Data + @lombok.AllArgsConstructor + public static class ClientInfo { + private String clientId; + private LocalDateTime firstConnected; + private volatile LocalDateTime lastActivity; + private final AtomicLong messageCount = new AtomicLong(0); + + public ClientInfo(String clientId, LocalDateTime firstConnected) { + this.clientId = clientId; + this.firstConnected = firstConnected; + this.lastActivity = firstConnected; + } + + public void updateLastActivity() { + this.lastActivity = LocalDateTime.now(); + } + + public void incrementMessageCount() { + this.messageCount.incrementAndGet(); + } + + public long getMessageCount() { + return messageCount.get(); + } + } + + /** + * 采集统计信息数据类 + */ + @lombok.Data + @lombok.Builder + @lombok.NoArgsConstructor + @lombok.AllArgsConstructor + public static class CollectionStatistics { + private long totalSignalsReceived; + private long validSignalsProcessed; + private long invalidSignalsSkipped; + private long processingErrors; + private int activeClients; + private double successRate; + + @Override + public String toString() { + return String.format("总接收:%d, 有效处理:%d, 无效跳过:%d, 处理错误:%d, 活跃客户端:%d, 成功率:%.2f%%", + totalSignalsReceived, validSignalsProcessed, invalidSignalsSkipped, + processingErrors, activeClients, successRate); + } + } + + /** + * 健康状态数据类 + */ + @lombok.Data + @lombok.Builder + @lombok.NoArgsConstructor + @lombok.AllArgsConstructor + public static class HealthStatus { + private String status; + private double healthScore; + private long totalSignals; + private double successRate; + private double errorRate; + private int activeClients; + private LocalDateTime lastUpdateTime; + + @Override + public String toString() { + return String.format("状态:%s, 健康分数:%.1f, 总信号:%d, 成功率:%.2f%%, 错误率:%.2f%%, 活跃客户端:%d", + status, healthScore, totalSignals, successRate, errorRate, activeClients); + } + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/model/TrafficLightStatus.java b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/model/TrafficLightStatus.java new file mode 100644 index 00000000..cfcce9f6 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/model/TrafficLightStatus.java @@ -0,0 +1,120 @@ +package com.qaup.collision.dataprocessing.model; + +import com.qaup.collision.common.model.enums.SignalState; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; + +/** + * 红绿灯状态数据模型 + * + * 用于表示解析后的红绿灯状态信息,包含设备ID、路口ID、各方向信号状态等 + * 这是内部处理使用的数据模型,不直接对应数据库表 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TrafficLightStatus { + + /** + * 红绿灯设备编号 + */ + private String deviceId; + + /** + * 关联的路口编号(通过设备查询获得) + */ + private String intersectionId; + + /** + * 南北方向信号状态 + */ + private SignalState nsStatus; + + /** + * 东西方向信号状态 + */ + private SignalState ewStatus; + + /** + * 信号时间戳(微秒级) + */ + private long timestamp; + + /** + * 原始信号数据(用于调试和日志记录) + */ + private String rawSignal; + + /** + * 检查状态是否有效 + * + * @return true如果状态有效,false否则 + */ + public boolean isValid() { + return deviceId != null && !deviceId.trim().isEmpty() + && nsStatus != null && ewStatus != null + && timestamp > 0; + } + + /** + * 检查是否为安全状态(至少一个方向为红灯或黄灯) + * + * @return true如果是安全状态,false否则 + */ + public boolean isSafeState() { + return nsStatus.isSafeState() || ewStatus.isSafeState(); + } + + /** + * 检查是否存在冲突状态(两个方向都是绿灯) + * + * @return true如果存在冲突,false否则 + */ + public boolean hasConflict() { + return nsStatus == SignalState.GREEN && ewStatus == SignalState.GREEN; + } + + /** + * 获取状态描述字符串 + * + * @return 格式化的状态描述 + */ + public String getStatusDescription() { + return String.format("南北:%s, 东西:%s", nsStatus.getDescription(), ewStatus.getDescription()); + } + + /** + * 创建默认安全状态(两个方向都是红灯) + * + * @param deviceId 设备编号 + * @return 安全状态的TrafficLightStatus + */ + public static TrafficLightStatus createSafeDefault(String deviceId) { + return TrafficLightStatus.builder() + .deviceId(deviceId) + .nsStatus(SignalState.RED) + .ewStatus(SignalState.RED) + .timestamp(System.currentTimeMillis() * 1000) // 微秒级时间戳 + .rawSignal("{\"error\":\"default_safe_state\"}") + .build(); + } + + /** + * 创建未知状态 + * + * @param deviceId 设备编号 + * @return 未知状态的TrafficLightStatus + */ + public static TrafficLightStatus createUnknown(String deviceId) { + return TrafficLightStatus.builder() + .deviceId(deviceId) + .nsStatus(SignalState.UNKNOWN) + .ewStatus(SignalState.UNKNOWN) + .timestamp(System.currentTimeMillis() * 1000) // 微秒级时间戳 + .rawSignal("{\"error\":\"unknown_state\"}") + .build(); + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/parser/TrafficLightSignalParser.java b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/parser/TrafficLightSignalParser.java new file mode 100644 index 00000000..865704c9 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/parser/TrafficLightSignalParser.java @@ -0,0 +1,254 @@ +package com.qaup.collision.dataprocessing.parser; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.qaup.collision.common.model.enums.SignalState; +import com.qaup.collision.dataprocessing.model.TrafficLightStatus; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * 红绿灯信号解析器 + * + * 负责解析DI格式的红绿灯信号JSON数据,转换为内部的TrafficLightStatus对象 + * 支持多种异常情况的处理,确保系统稳定性 + */ +@Slf4j +@Component +public class TrafficLightSignalParser { + + private final ObjectMapper objectMapper; + + // 解析统计信息 + private final AtomicLong totalParsed = new AtomicLong(0); + private final AtomicLong successfulParsed = new AtomicLong(0); + private final AtomicLong failedParsed = new AtomicLong(0); + private final AtomicLong invalidSignals = new AtomicLong(0); + + public TrafficLightSignalParser() { + this.objectMapper = new ObjectMapper(); + } + + /** + * 解析原始JSON信号为TrafficLightStatus对象 + * + * @param rawJsonSignal 原始JSON信号字符串 + * @return 解析后的红绿灯状态,解析失败时返回安全默认状态 + */ + public TrafficLightStatus parseSignal(String rawJsonSignal) { + totalParsed.incrementAndGet(); + + if (!isValidSignal(rawJsonSignal)) { + log.warn("无效的红绿灯信号格式: {}", rawJsonSignal); + failedParsed.incrementAndGet(); + return TrafficLightStatus.createSafeDefault("UNKNOWN_DEVICE"); + } + + try { + JsonNode rootNode = objectMapper.readTree(rawJsonSignal); + + // 提取设备ID + String deviceId = extractDeviceId(rootNode); + if (deviceId == null) { + log.warn("红绿灯信号中缺少设备ID: {}", rawJsonSignal); + failedParsed.incrementAndGet(); + return TrafficLightStatus.createSafeDefault("UNKNOWN_DEVICE"); + } + + // 解析南北方向信号状态 (DI-11: 北红, DI-12: 北黄, DI-13: 北绿) + SignalState nsStatus = parseDirectionSignal(rootNode, "DI-11", "DI-12", "DI-13"); + + // 解析东西方向信号状态 (DI-14: 东红, DI-15: 东黄, DI-16: 东绿) + SignalState ewStatus = parseDirectionSignal(rootNode, "DI-14", "DI-15", "DI-16"); + + // 创建状态对象 + TrafficLightStatus status = TrafficLightStatus.builder() + .deviceId(deviceId) + .nsStatus(nsStatus) + .ewStatus(ewStatus) + .timestamp(System.currentTimeMillis() * 1000) // 微秒级时间戳 + .rawSignal(rawJsonSignal) + .build(); + + // 验证解析结果 + if (!status.isValid()) { + log.warn("解析的红绿灯状态无效: deviceId={}, nsStatus={}, ewStatus={}", + deviceId, nsStatus, ewStatus); + invalidSignals.incrementAndGet(); + return TrafficLightStatus.createSafeDefault(deviceId); + } + + // 检查冲突状态 + if (status.hasConflict()) { + log.error("检测到红绿灯冲突状态(两个方向都是绿灯): deviceId={}, 状态={}", + deviceId, status.getStatusDescription()); + // 冲突时返回安全状态 + return TrafficLightStatus.createSafeDefault(deviceId); + } + + successfulParsed.incrementAndGet(); + log.debug("成功解析红绿灯信号: deviceId={}, 状态={}", deviceId, status.getStatusDescription()); + + return status; + + } catch (Exception e) { + log.error("解析红绿灯信号异常: {}", rawJsonSignal, e); + failedParsed.incrementAndGet(); + return TrafficLightStatus.createSafeDefault("UNKNOWN_DEVICE"); + } + } + + /** + * 验证信号格式是否有效 + * + * @param rawJsonSignal 原始JSON信号 + * @return true如果格式有效,false否则 + */ + public boolean isValidSignal(String rawJsonSignal) { + if (rawJsonSignal == null || rawJsonSignal.trim().isEmpty()) { + return false; + } + + try { + JsonNode rootNode = objectMapper.readTree(rawJsonSignal); + + // 检查是否为JSON对象 + if (!rootNode.isObject()) { + return false; + } + + // 检查是否包含必要的DI字段 + boolean hasRequiredFields = rootNode.has("DI-11") || rootNode.has("DI-12") || rootNode.has("DI-13") || + rootNode.has("DI-14") || rootNode.has("DI-15") || rootNode.has("DI-16"); + + return hasRequiredFields; + + } catch (Exception e) { + log.debug("信号格式验证失败: {}", rawJsonSignal, e); + return false; + } + } + + /** + * 提取设备ID + * 优先从device_id字段获取,如果没有则尝试其他可能的字段 + * + * @param rootNode JSON根节点 + * @return 设备ID,如果找不到则返回null + */ + private String extractDeviceId(JsonNode rootNode) { + // 优先使用device_id字段 + if (rootNode.has("device_id")) { + return rootNode.get("device_id").asText(); + } + + // 尝试其他可能的字段名 + String[] possibleFields = {"deviceId", "id", "device", "equipment_id"}; + for (String field : possibleFields) { + if (rootNode.has(field)) { + return rootNode.get(field).asText(); + } + } + + // 如果都没有,返回null,调用方会使用默认值 + return null; + } + + /** + * 解析单个方向的信号状态 + * + * @param rootNode JSON根节点 + * @param redField 红灯字段名 + * @param yellowField 黄灯字段名 + * @param greenField 绿灯字段名 + * @return 该方向的信号状态 + */ + private SignalState parseDirectionSignal(JsonNode rootNode, String redField, String yellowField, String greenField) { + try { + // 获取各个信号的值(1表示激活,0表示未激活) + int redValue = rootNode.has(redField) ? rootNode.get(redField).asInt(0) : 0; + int yellowValue = rootNode.has(yellowField) ? rootNode.get(yellowField).asInt(0) : 0; + int greenValue = rootNode.has(greenField) ? rootNode.get(greenField).asInt(0) : 0; + + // 根据优先级确定状态:红灯 > 黄灯 > 绿灯 + if (redValue == 1) { + return SignalState.RED; + } else if (yellowValue == 1) { + return SignalState.YELLOW; + } else if (greenValue == 1) { + return SignalState.GREEN; + } else { + // 如果没有任何信号激活,默认为红灯(安全状态) + log.debug("方向信号无激活状态,默认为红灯: red={}, yellow={}, green={}", + redValue, yellowValue, greenValue); + return SignalState.RED; + } + + } catch (Exception e) { + log.warn("解析方向信号异常,默认为红灯: redField={}, yellowField={}, greenField={}", + redField, yellowField, greenField, e); + return SignalState.RED; + } + } + + /** + * 获取解析统计信息 + * + * @return 统计信息对象 + */ + public ParseStatistics getStatistics() { + return ParseStatistics.builder() + .totalParsed(totalParsed.get()) + .successfulParsed(successfulParsed.get()) + .failedParsed(failedParsed.get()) + .invalidSignals(invalidSignals.get()) + .successRate(calculateSuccessRate()) + .build(); + } + + /** + * 计算成功率 + * + * @return 成功率百分比 + */ + private double calculateSuccessRate() { + long total = totalParsed.get(); + if (total == 0) { + return 0.0; + } + return (double) successfulParsed.get() / total * 100.0; + } + + /** + * 重置统计信息 + */ + public void resetStatistics() { + totalParsed.set(0); + successfulParsed.set(0); + failedParsed.set(0); + invalidSignals.set(0); + } + + /** + * 解析统计信息数据类 + */ + @lombok.Data + @lombok.Builder + @lombok.NoArgsConstructor + @lombok.AllArgsConstructor + public static class ParseStatistics { + private long totalParsed; + private long successfulParsed; + private long failedParsed; + private long invalidSignals; + private double successRate; + + @Override + public String toString() { + return String.format("解析统计 - 总计:%d, 成功:%d, 失败:%d, 无效:%d, 成功率:%.2f%%", + totalParsed, successfulParsed, failedParsed, invalidSignals, successRate); + } + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java index 86c837ae..b30807d0 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java @@ -6,6 +6,7 @@ import com.qaup.collision.common.model.UnmannedVehicle; import com.qaup.collision.common.model.spatial.VehicleLocation; import com.qaup.collision.common.adapter.QuapDataAdapter; import com.qaup.collision.datacollector.service.VehicleDataPersistenceService; +import com.qaup.collision.dataprocessing.parser.TrafficLightSignalParser; import com.qaup.collision.websocket.event.PositionUpdateEvent; import com.qaup.collision.websocket.message.PositionUpdatePayload; import com.qaup.collision.pathconflict.service.PathConflictDetectionService; @@ -468,4 +469,164 @@ public class DataProcessingService { return null; } } + + // ============================================ + // 红绿灯信号处理方法 + // ============================================ + + @Autowired + private TrafficLightSignalParser trafficLightSignalParser; + + @Autowired + private TrafficLightService trafficLightService; + + @Autowired + private IntersectionService intersectionService; + + /** + * 处理红绿灯信号数据 + * + * @param rawJsonSignal 原始JSON信号字符串 + */ + public void processTrafficLightSignal(String rawJsonSignal) { + if (rawJsonSignal == null || rawJsonSignal.trim().isEmpty()) { + log.warn("接收到空的红绿灯信号"); + return; + } + + try { + // 解析红绿灯信号 + com.qaup.collision.dataprocessing.model.TrafficLightStatus status = + trafficLightSignalParser.parseSignal(rawJsonSignal); + + if (!status.isValid()) { + log.warn("解析的红绿灯状态无效: {}", status); + return; + } + + // 验证设备是否存在和激活 + if (!isValidTrafficLightDevice(status.getDeviceId())) { + log.warn("红绿灯设备不存在或未激活: deviceId={}", status.getDeviceId()); + return; + } + + // 更新设备心跳和在线状态 + updateDeviceStatus(status.getDeviceId()); + + // 获取设备信息和关联的路口信息 + Optional deviceOpt = + trafficLightService.getActiveTrafficLightByDeviceId(status.getDeviceId()); + + if (deviceOpt.isEmpty()) { + log.warn("无法获取红绿灯设备信息: deviceId={}", status.getDeviceId()); + return; + } + + com.qaup.collision.common.model.spatial.TrafficLight device = deviceOpt.get(); + status.setIntersectionId(device.getIntersectionId()); + + Optional intersectionOpt = + intersectionService.getActiveIntersectionById(device.getIntersectionId()); + + if (intersectionOpt.isEmpty()) { + log.warn("无法获取路口信息: intersectionId={}", device.getIntersectionId()); + return; + } + + com.qaup.collision.common.model.spatial.Intersection intersection = intersectionOpt.get(); + + // 创建WebSocket消息载荷 + com.qaup.collision.websocket.message.TrafficLightStatusPayload payload = + createTrafficLightPayload(status, device, intersection); + + // 发布红绿灯状态变更事件 + publishTrafficLightStatusEvent(payload); + + log.debug("成功处理红绿灯信号: deviceId={}, intersectionId={}, 状态={}", + status.getDeviceId(), status.getIntersectionId(), status.getStatusDescription()); + + } catch (Exception e) { + log.error("处理红绿灯信号异常: signal={}", rawJsonSignal, e); + } + } + + /** + * 验证设备是否存在和激活 + * + * @param deviceId 设备编号 + * @return true如果设备有效,false否则 + */ + private boolean isValidTrafficLightDevice(String deviceId) { + if (deviceId == null || deviceId.trim().isEmpty()) { + return false; + } + + return trafficLightService.isDeviceAvailable(deviceId); + } + + /** + * 更新设备心跳和在线状态 + * + * @param deviceId 设备编号 + */ + private void updateDeviceStatus(String deviceId) { + try { + trafficLightService.updateDeviceHeartbeat(deviceId); + log.debug("更新设备心跳成功: deviceId={}", deviceId); + } catch (Exception e) { + log.warn("更新设备心跳失败: deviceId={}", deviceId, e); + } + } + + /** + * 创建WebSocket消息载荷 + * + * @param status 红绿灯状态 + * @param device 设备信息 + * @param intersection 路口信息 + * @return WebSocket消息载荷 + */ + private com.qaup.collision.websocket.message.TrafficLightStatusPayload createTrafficLightPayload( + com.qaup.collision.dataprocessing.model.TrafficLightStatus status, + com.qaup.collision.common.model.spatial.TrafficLight device, + com.qaup.collision.common.model.spatial.Intersection intersection) { + + // 创建位置信息 + com.qaup.collision.websocket.message.TrafficLightStatusPayload.Position position = + com.qaup.collision.websocket.message.TrafficLightStatusPayload.Position.builder() + .latitude(intersection.getLatitude()) + .longitude(intersection.getLongitude()) + .build(); + + return com.qaup.collision.websocket.message.TrafficLightStatusPayload.builder() + .intersectionId(intersection.getIntersectionId()) + .intersectionName(intersection.getIntersectionName()) + .deviceId(device.getDeviceId()) + .deviceName(device.getDeviceName()) + .position(position) + .nsStatus(status.getNsStatus().getCode()) + .ewStatus(status.getEwStatus().getCode()) + .timestamp(status.getTimestamp()) + .areaCode(intersection.getAreaCode()) + .build(); + } + + /** + * 发布红绿灯状态变更事件 + * + * @param payload WebSocket消息载荷 + */ + private void publishTrafficLightStatusEvent(com.qaup.collision.websocket.message.TrafficLightStatusPayload payload) { + try { + com.qaup.collision.websocket.event.TrafficLightStatusEvent event = + new com.qaup.collision.websocket.event.TrafficLightStatusEvent(payload); + + eventPublisher.publishEvent(event); + + log.debug("发布红绿灯状态事件成功: intersectionId={}", payload.getIntersectionId()); + + } catch (Exception e) { + log.error("发布红绿灯状态事件失败: intersectionId={}", payload.getIntersectionId(), e); + } + } } \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/IntersectionService.java b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/IntersectionService.java new file mode 100644 index 00000000..741df0aa --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/IntersectionService.java @@ -0,0 +1,338 @@ +package com.qaup.collision.dataprocessing.service; + +import com.qaup.collision.common.model.repository.IntersectionRepository; +import com.qaup.collision.common.model.spatial.Intersection; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * 路口管理服务 + * + * 提供路口信息的CRUD操作和业务逻辑处理 + * 支持路口的创建、查询、更新和删除等功能 + */ +@Slf4j +@Service +public class IntersectionService { + + @Autowired + private IntersectionRepository intersectionRepository; + + /** + * 根据路口编号获取路口信息 + * + * @param intersectionId 路口编号 + * @return 路口信息(可选) + */ + public Optional getIntersectionById(String intersectionId) { + if (intersectionId == null || intersectionId.trim().isEmpty()) { + log.warn("路口编号不能为空"); + return Optional.empty(); + } + + try { + return intersectionRepository.findByIntersectionId(intersectionId.trim()); + } catch (Exception e) { + log.error("查询路口信息异常: intersectionId={}", intersectionId, e); + return Optional.empty(); + } + } + + /** + * 根据路口编号获取激活的路口信息 + * + * @param intersectionId 路口编号 + * @return 激活的路口信息(可选) + */ + public Optional getActiveIntersectionById(String intersectionId) { + if (intersectionId == null || intersectionId.trim().isEmpty()) { + log.warn("路口编号不能为空"); + return Optional.empty(); + } + + try { + return intersectionRepository.findByIntersectionIdAndIsActive(intersectionId.trim(), true); + } catch (Exception e) { + log.error("查询激活路口信息异常: intersectionId={}", intersectionId, e); + return Optional.empty(); + } + } + + /** + * 获取所有激活的路口 + * + * @return 激活的路口列表 + */ + public List getAllActiveIntersections() { + try { + List intersections = intersectionRepository.findByIsActiveTrue(); + log.debug("查询到 {} 个激活路口", intersections.size()); + return intersections; + } catch (Exception e) { + log.error("查询所有激活路口异常", e); + return List.of(); + } + } + + /** + * 根据区域编码获取激活的路口 + * + * @param areaCode 区域编码 + * @return 该区域激活的路口列表 + */ + public List getActiveIntersectionsByArea(String areaCode) { + if (areaCode == null || areaCode.trim().isEmpty()) { + log.warn("区域编码不能为空"); + return List.of(); + } + + try { + List intersections = intersectionRepository.findByAreaCodeAndIsActiveTrue(areaCode.trim()); + log.debug("区域 {} 查询到 {} 个激活路口", areaCode, intersections.size()); + return intersections; + } catch (Exception e) { + log.error("根据区域查询激活路口异常: areaCode={}", areaCode, e); + return List.of(); + } + } + + /** + * 添加新路口 + * + * @param intersection 路口信息 + * @return 保存后的路口信息 + * @throws IllegalArgumentException 如果路口信息无效 + * @throws IllegalStateException 如果路口编号已存在 + */ + @Transactional + public Intersection addIntersection(Intersection intersection) { + if (intersection == null) { + throw new IllegalArgumentException("路口信息不能为空"); + } + + // 验证必要字段 + validateIntersectionData(intersection); + + // 检查路口编号是否已存在 + if (intersectionRepository.existsByIntersectionId(intersection.getIntersectionId())) { + throw new IllegalStateException("路口编号已存在: " + intersection.getIntersectionId()); + } + + try { + // 设置默认值 + if (intersection.getIsActive() == null) { + intersection.setIsActive(true); + } + + Intersection savedIntersection = intersectionRepository.save(intersection); + log.info("成功添加路口: intersectionId={}, name={}", + savedIntersection.getIntersectionId(), savedIntersection.getIntersectionName()); + + return savedIntersection; + } catch (Exception e) { + log.error("添加路口异常: intersectionId={}", intersection.getIntersectionId(), e); + throw new RuntimeException("添加路口失败", e); + } + } + + /** + * 更新路口信息 + * + * @param intersection 路口信息 + * @return 更新后的路口信息 + * @throws IllegalArgumentException 如果路口信息无效 + * @throws IllegalStateException 如果路口不存在 + */ + @Transactional + public Intersection updateIntersection(Intersection intersection) { + if (intersection == null) { + throw new IllegalArgumentException("路口信息不能为空"); + } + + // 验证必要字段 + validateIntersectionData(intersection); + + // 检查路口是否存在 + Optional existingIntersection = intersectionRepository.findByIntersectionId(intersection.getIntersectionId()); + if (existingIntersection.isEmpty()) { + throw new IllegalStateException("路口不存在: " + intersection.getIntersectionId()); + } + + try { + // 保留原有的ID和创建时间 + Intersection existing = existingIntersection.get(); + intersection.setId(existing.getId()); + intersection.setCreatedTime(existing.getCreatedTime()); + + Intersection updatedIntersection = intersectionRepository.save(intersection); + log.info("成功更新路口: intersectionId={}, name={}", + updatedIntersection.getIntersectionId(), updatedIntersection.getIntersectionName()); + + return updatedIntersection; + } catch (Exception e) { + log.error("更新路口异常: intersectionId={}", intersection.getIntersectionId(), e); + throw new RuntimeException("更新路口失败", e); + } + } + + /** + * 激活或停用路口 + * + * @param intersectionId 路口编号 + * @param active 是否激活 + * @return 更新后的路口信息(可选) + */ + @Transactional + public Optional setIntersectionActive(String intersectionId, boolean active) { + if (intersectionId == null || intersectionId.trim().isEmpty()) { + log.warn("路口编号不能为空"); + return Optional.empty(); + } + + try { + Optional intersectionOpt = intersectionRepository.findByIntersectionId(intersectionId.trim()); + if (intersectionOpt.isEmpty()) { + log.warn("路口不存在: intersectionId={}", intersectionId); + return Optional.empty(); + } + + Intersection intersection = intersectionOpt.get(); + intersection.setIsActive(active); + + Intersection updatedIntersection = intersectionRepository.save(intersection); + log.info("成功{}路口: intersectionId={}", active ? "激活" : "停用", intersectionId); + + return Optional.of(updatedIntersection); + } catch (Exception e) { + log.error("设置路口激活状态异常: intersectionId={}, active={}", intersectionId, active, e); + return Optional.empty(); + } + } + + /** + * 根据坐标范围查找路口 + * + * @param minLat 最小纬度 + * @param maxLat 最大纬度 + * @param minLng 最小经度 + * @param maxLng 最大经度 + * @return 范围内的路口列表 + */ + public List getIntersectionsInBounds(double minLat, double maxLat, double minLng, double maxLng) { + // 验证坐标范围 + if (minLat >= maxLat || minLng >= maxLng) { + log.warn("无效的坐标范围: minLat={}, maxLat={}, minLng={}, maxLng={}", + minLat, maxLat, minLng, maxLng); + return List.of(); + } + + try { + List intersections = intersectionRepository.findIntersectionsInBounds(minLat, maxLat, minLng, maxLng); + log.debug("坐标范围内查询到 {} 个路口", intersections.size()); + return intersections; + } catch (Exception e) { + log.error("根据坐标范围查询路口异常: bounds=[{},{},{},{}]", minLat, maxLat, minLng, maxLng, e); + return List.of(); + } + } + + /** + * 根据名称模糊查询路口 + * + * @param name 路口名称关键字 + * @return 匹配的路口列表 + */ + public List searchIntersectionsByName(String name) { + if (name == null || name.trim().isEmpty()) { + log.warn("查询名称不能为空"); + return List.of(); + } + + try { + List intersections = intersectionRepository.findByIntersectionNameContaining(name.trim()); + log.debug("名称模糊查询 '{}' 找到 {} 个路口", name, intersections.size()); + return intersections; + } catch (Exception e) { + log.error("根据名称模糊查询路口异常: name={}", name, e); + return List.of(); + } + } + + /** + * 获取路口统计信息 + * + * @return 路口统计信息 + */ + public IntersectionStatistics getStatistics() { + try { + long totalCount = intersectionRepository.count(); + long activeCount = intersectionRepository.countByIsActiveTrue(); + + return IntersectionStatistics.builder() + .totalCount(totalCount) + .activeCount(activeCount) + .inactiveCount(totalCount - activeCount) + .build(); + } catch (Exception e) { + log.error("获取路口统计信息异常", e); + return IntersectionStatistics.builder().build(); + } + } + + /** + * 验证路口数据 + * + * @param intersection 路口信息 + * @throws IllegalArgumentException 如果数据无效 + */ + private void validateIntersectionData(Intersection intersection) { + if (intersection.getIntersectionId() == null || intersection.getIntersectionId().trim().isEmpty()) { + throw new IllegalArgumentException("路口编号不能为空"); + } + + if (intersection.getIntersectionName() == null || intersection.getIntersectionName().trim().isEmpty()) { + throw new IllegalArgumentException("路口名称不能为空"); + } + + if (intersection.getLatitude() == null) { + throw new IllegalArgumentException("纬度不能为空"); + } + + if (intersection.getLongitude() == null) { + throw new IllegalArgumentException("经度不能为空"); + } + + // 验证坐标范围 + if (intersection.getLatitude() < -90 || intersection.getLatitude() > 90) { + throw new IllegalArgumentException("纬度必须在-90到90之间"); + } + + if (intersection.getLongitude() < -180 || intersection.getLongitude() > 180) { + throw new IllegalArgumentException("经度必须在-180到180之间"); + } + } + + /** + * 路口统计信息数据类 + */ + @lombok.Data + @lombok.Builder + @lombok.NoArgsConstructor + @lombok.AllArgsConstructor + public static class IntersectionStatistics { + private long totalCount; + private long activeCount; + private long inactiveCount; + + @Override + public String toString() { + return String.format("路口统计 - 总计:%d, 激活:%d, 停用:%d", + totalCount, activeCount, inactiveCount); + } + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/TrafficLightService.java b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/TrafficLightService.java new file mode 100644 index 00000000..9184e924 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/TrafficLightService.java @@ -0,0 +1,469 @@ +package com.qaup.collision.dataprocessing.service; + +import com.qaup.collision.common.model.repository.TrafficLightRepository; +import com.qaup.collision.common.model.spatial.TrafficLight; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * 红绿灯设备管理服务 + * + * 提供红绿灯设备的CRUD操作和业务逻辑处理 + * 支持设备的创建、查询、更新、在线状态管理等功能 + */ +@Slf4j +@Service +public class TrafficLightService { + + @Autowired + private TrafficLightRepository trafficLightRepository; + + @Autowired + private IntersectionService intersectionService; + + // 心跳超时时间(分钟) + private static final int HEARTBEAT_TIMEOUT_MINUTES = 5; + + /** + * 根据设备编号获取红绿灯设备信息 + * + * @param deviceId 设备编号 + * @return 设备信息(可选) + */ + public Optional getTrafficLightByDeviceId(String deviceId) { + if (deviceId == null || deviceId.trim().isEmpty()) { + log.warn("设备编号不能为空"); + return Optional.empty(); + } + + try { + return trafficLightRepository.findByDeviceId(deviceId.trim()); + } catch (Exception e) { + log.error("查询红绿灯设备信息异常: deviceId={}", deviceId, e); + return Optional.empty(); + } + } + + /** + * 根据设备编号获取激活的红绿灯设备信息 + * + * @param deviceId 设备编号 + * @return 激活的设备信息(可选) + */ + public Optional getActiveTrafficLightByDeviceId(String deviceId) { + if (deviceId == null || deviceId.trim().isEmpty()) { + log.warn("设备编号不能为空"); + return Optional.empty(); + } + + try { + return trafficLightRepository.findByDeviceIdAndIsActive(deviceId.trim(), true); + } catch (Exception e) { + log.error("查询激活红绿灯设备信息异常: deviceId={}", deviceId, e); + return Optional.empty(); + } + } + + /** + * 根据路口编号获取红绿灯设备列表 + * + * @param intersectionId 路口编号 + * @return 该路口的设备列表 + */ + public List getTrafficLightsByIntersection(String intersectionId) { + if (intersectionId == null || intersectionId.trim().isEmpty()) { + log.warn("路口编号不能为空"); + return List.of(); + } + + try { + List devices = trafficLightRepository.findByIntersectionId(intersectionId.trim()); + log.debug("路口 {} 查询到 {} 个红绿灯设备", intersectionId, devices.size()); + return devices; + } catch (Exception e) { + log.error("根据路口查询红绿灯设备异常: intersectionId={}", intersectionId, e); + return List.of(); + } + } + + /** + * 根据路口编号获取激活的红绿灯设备列表 + * + * @param intersectionId 路口编号 + * @return 该路口激活的设备列表 + */ + public List getActiveTrafficLightsByIntersection(String intersectionId) { + if (intersectionId == null || intersectionId.trim().isEmpty()) { + log.warn("路口编号不能为空"); + return List.of(); + } + + try { + List devices = trafficLightRepository.findByIntersectionIdAndIsActiveTrue(intersectionId.trim()); + log.debug("路口 {} 查询到 {} 个激活的红绿灯设备", intersectionId, devices.size()); + return devices; + } catch (Exception e) { + log.error("根据路口查询激活红绿灯设备异常: intersectionId={}", intersectionId, e); + return List.of(); + } + } + + /** + * 获取所有在线的设备 + * + * @return 在线设备列表 + */ + public List getOnlineDevices() { + try { + List devices = trafficLightRepository.findByIsOnlineTrue(); + log.debug("查询到 {} 个在线设备", devices.size()); + return devices; + } catch (Exception e) { + log.error("查询在线设备异常", e); + return List.of(); + } + } + + /** + * 获取所有激活且在线的设备 + * + * @return 激活且在线的设备列表 + */ + public List getActiveOnlineDevices() { + try { + List devices = trafficLightRepository.findByIsActiveTrueAndIsOnlineTrue(); + log.debug("查询到 {} 个激活且在线的设备", devices.size()); + return devices; + } catch (Exception e) { + log.error("查询激活且在线设备异常", e); + return List.of(); + } + } + + /** + * 添加新的红绿灯设备 + * + * @param trafficLight 设备信息 + * @return 保存后的设备信息 + * @throws IllegalArgumentException 如果设备信息无效 + * @throws IllegalStateException 如果设备编号已存在或路口不存在 + */ + @Transactional + public TrafficLight addTrafficLight(TrafficLight trafficLight) { + if (trafficLight == null) { + throw new IllegalArgumentException("设备信息不能为空"); + } + + // 验证必要字段 + validateTrafficLightData(trafficLight); + + // 检查设备编号是否已存在 + if (trafficLightRepository.existsByDeviceId(trafficLight.getDeviceId())) { + throw new IllegalStateException("设备编号已存在: " + trafficLight.getDeviceId()); + } + + // 检查关联的路口是否存在 + if (!intersectionService.getActiveIntersectionById(trafficLight.getIntersectionId()).isPresent()) { + throw new IllegalStateException("关联的路口不存在或未激活: " + trafficLight.getIntersectionId()); + } + + try { + // 设置默认值 + if (trafficLight.getIsActive() == null) { + trafficLight.setIsActive(true); + } + if (trafficLight.getIsOnline() == null) { + trafficLight.setIsOnline(false); + } + if (trafficLight.getDeviceType() == null) { + trafficLight.setDeviceType("STANDARD"); + } + + TrafficLight savedDevice = trafficLightRepository.save(trafficLight); + log.info("成功添加红绿灯设备: deviceId={}, name={}, intersectionId={}", + savedDevice.getDeviceId(), savedDevice.getDeviceName(), savedDevice.getIntersectionId()); + + return savedDevice; + } catch (Exception e) { + log.error("添加红绿灯设备异常: deviceId={}", trafficLight.getDeviceId(), e); + throw new RuntimeException("添加设备失败", e); + } + } + + /** + * 更新红绿灯设备信息 + * + * @param trafficLight 设备信息 + * @return 更新后的设备信息 + * @throws IllegalArgumentException 如果设备信息无效 + * @throws IllegalStateException 如果设备不存在 + */ + @Transactional + public TrafficLight updateTrafficLight(TrafficLight trafficLight) { + if (trafficLight == null) { + throw new IllegalArgumentException("设备信息不能为空"); + } + + // 验证必要字段 + validateTrafficLightData(trafficLight); + + // 检查设备是否存在 + Optional existingDevice = trafficLightRepository.findByDeviceId(trafficLight.getDeviceId()); + if (existingDevice.isEmpty()) { + throw new IllegalStateException("设备不存在: " + trafficLight.getDeviceId()); + } + + try { + // 保留原有的ID和创建时间 + TrafficLight existing = existingDevice.get(); + trafficLight.setId(existing.getId()); + trafficLight.setCreatedTime(existing.getCreatedTime()); + + TrafficLight updatedDevice = trafficLightRepository.save(trafficLight); + log.info("成功更新红绿灯设备: deviceId={}, name={}", + updatedDevice.getDeviceId(), updatedDevice.getDeviceName()); + + return updatedDevice; + } catch (Exception e) { + log.error("更新红绿灯设备异常: deviceId={}", trafficLight.getDeviceId(), e); + throw new RuntimeException("更新设备失败", e); + } + } + + /** + * 更新设备在线状态 + * + * @param deviceId 设备编号 + * @param isOnline 是否在线 + * @return 更新的记录数 + */ + @Transactional + public boolean updateDeviceOnlineStatus(String deviceId, boolean isOnline) { + if (deviceId == null || deviceId.trim().isEmpty()) { + log.warn("设备编号不能为空"); + return false; + } + + try { + int updatedCount = trafficLightRepository.updateOnlineStatus(deviceId.trim(), isOnline); + if (updatedCount > 0) { + log.debug("成功更新设备在线状态: deviceId={}, isOnline={}", deviceId, isOnline); + return true; + } else { + log.warn("设备不存在,无法更新在线状态: deviceId={}", deviceId); + return false; + } + } catch (Exception e) { + log.error("更新设备在线状态异常: deviceId={}, isOnline={}", deviceId, isOnline, e); + return false; + } + } + + /** + * 更新设备心跳时间 + * + * @param deviceId 设备编号 + * @return 更新的记录数 + */ + @Transactional + public boolean updateDeviceHeartbeat(String deviceId) { + if (deviceId == null || deviceId.trim().isEmpty()) { + log.warn("设备编号不能为空"); + return false; + } + + try { + LocalDateTime now = LocalDateTime.now(); + int updatedCount = trafficLightRepository.updateHeartbeat(deviceId.trim(), now); + if (updatedCount > 0) { + log.debug("成功更新设备心跳时间: deviceId={}, heartbeat={}", deviceId, now); + return true; + } else { + log.warn("设备不存在,无法更新心跳时间: deviceId={}", deviceId); + return false; + } + } catch (Exception e) { + log.error("更新设备心跳时间异常: deviceId={}", deviceId, e); + return false; + } + } + + /** + * 激活或停用设备 + * + * @param deviceId 设备编号 + * @param active 是否激活 + * @return 更新后的设备信息(可选) + */ + @Transactional + public Optional setDeviceActive(String deviceId, boolean active) { + if (deviceId == null || deviceId.trim().isEmpty()) { + log.warn("设备编号不能为空"); + return Optional.empty(); + } + + try { + Optional deviceOpt = trafficLightRepository.findByDeviceId(deviceId.trim()); + if (deviceOpt.isEmpty()) { + log.warn("设备不存在: deviceId={}", deviceId); + return Optional.empty(); + } + + TrafficLight device = deviceOpt.get(); + device.setIsActive(active); + + TrafficLight updatedDevice = trafficLightRepository.save(device); + log.info("成功{}设备: deviceId={}", active ? "激活" : "停用", deviceId); + + return Optional.of(updatedDevice); + } catch (Exception e) { + log.error("设置设备激活状态异常: deviceId={}, active={}", deviceId, active, e); + return Optional.empty(); + } + } + + /** + * 检查设备是否可用(激活且在线) + * + * @param deviceId 设备编号 + * @return true如果设备可用,false否则 + */ + public boolean isDeviceAvailable(String deviceId) { + Optional deviceOpt = getActiveTrafficLightByDeviceId(deviceId); + return deviceOpt.map(TrafficLight::isAvailable).orElse(false); + } + + /** + * 定时检查心跳超时的设备并设置为离线状态 + * 每分钟执行一次 + */ + @Scheduled(fixedRate = 60000) // 每分钟执行一次 + public void checkHeartbeatTimeout() { + try { + LocalDateTime timeoutTime = LocalDateTime.now().minusMinutes(HEARTBEAT_TIMEOUT_MINUTES); + int offlineCount = trafficLightRepository.setTimeoutDevicesOffline(timeoutTime); + + if (offlineCount > 0) { + log.info("检测到 {} 个设备心跳超时,已设置为离线状态", offlineCount); + } + } catch (Exception e) { + log.error("检查设备心跳超时异常", e); + } + } + + /** + * 根据制造商查找设备 + * + * @param manufacturer 制造商 + * @return 该制造商的设备列表 + */ + public List getDevicesByManufacturer(String manufacturer) { + if (manufacturer == null || manufacturer.trim().isEmpty()) { + log.warn("制造商名称不能为空"); + return List.of(); + } + + try { + List devices = trafficLightRepository.findByManufacturer(manufacturer.trim()); + log.debug("制造商 {} 查询到 {} 个设备", manufacturer, devices.size()); + return devices; + } catch (Exception e) { + log.error("根据制造商查询设备异常: manufacturer={}", manufacturer, e); + return List.of(); + } + } + + /** + * 根据设备名称模糊查询 + * + * @param name 设备名称关键字 + * @return 匹配的设备列表 + */ + public List searchDevicesByName(String name) { + if (name == null || name.trim().isEmpty()) { + log.warn("查询名称不能为空"); + return List.of(); + } + + try { + List devices = trafficLightRepository.findByDeviceNameContaining(name.trim()); + log.debug("名称模糊查询 '{}' 找到 {} 个设备", name, devices.size()); + return devices; + } catch (Exception e) { + log.error("根据名称模糊查询设备异常: name={}", name, e); + return List.of(); + } + } + + /** + * 获取设备统计信息 + * + * @return 设备统计信息 + */ + public DeviceStatistics getStatistics() { + try { + long totalCount = trafficLightRepository.count(); + long activeCount = trafficLightRepository.countByIsActiveTrue(); + long onlineCount = trafficLightRepository.countByIsOnlineTrue(); + + return DeviceStatistics.builder() + .totalCount(totalCount) + .activeCount(activeCount) + .inactiveCount(totalCount - activeCount) + .onlineCount(onlineCount) + .offlineCount(totalCount - onlineCount) + .build(); + } catch (Exception e) { + log.error("获取设备统计信息异常", e); + return DeviceStatistics.builder().build(); + } + } + + /** + * 验证设备数据 + * + * @param trafficLight 设备信息 + * @throws IllegalArgumentException 如果数据无效 + */ + private void validateTrafficLightData(TrafficLight trafficLight) { + if (trafficLight.getDeviceId() == null || trafficLight.getDeviceId().trim().isEmpty()) { + throw new IllegalArgumentException("设备编号不能为空"); + } + + if (trafficLight.getDeviceName() == null || trafficLight.getDeviceName().trim().isEmpty()) { + throw new IllegalArgumentException("设备名称不能为空"); + } + + if (trafficLight.getIntersectionId() == null || trafficLight.getIntersectionId().trim().isEmpty()) { + throw new IllegalArgumentException("关联路口编号不能为空"); + } + } + + /** + * 设备统计信息数据类 + */ + @lombok.Data + @lombok.Builder + @lombok.NoArgsConstructor + @lombok.AllArgsConstructor + public static class DeviceStatistics { + private long totalCount; + private long activeCount; + private long inactiveCount; + private long onlineCount; + private long offlineCount; + + @Override + public String toString() { + return String.format("设备统计 - 总计:%d, 激活:%d, 停用:%d, 在线:%d, 离线:%d", + totalCount, activeCount, inactiveCount, onlineCount, offlineCount); + } + } +} \ No newline at end of file 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 24f9bad3..9fd2c735 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 @@ -62,7 +62,7 @@ public class WebSocketMessageBroadcaster { .payload((PositionUpdatePayload) event.getPayload()) .build(); - broadcastMessage(message); + broadcastMessageInternal(message); } /** @@ -78,7 +78,7 @@ public class WebSocketMessageBroadcaster { .payload((VehicleCommandPayload) event.getPayload()) .build(); - broadcastMessage(message); + broadcastMessageInternal(message); } /** @@ -94,7 +94,7 @@ public class WebSocketMessageBroadcaster { .payload((TrafficLightStatusPayload) event.getPayload()) .build(); - broadcastMessage(message); + broadcastMessageInternal(message); } /** @@ -110,7 +110,7 @@ public class WebSocketMessageBroadcaster { .payload((CollisionWarningPayload) event.getPayload()) .build(); - broadcastMessage(message); + broadcastMessageInternal(message); } // === 规则事件处理方法 === @@ -128,7 +128,7 @@ public class WebSocketMessageBroadcaster { .payload((RuleViolationPayload) event.getPayload()) .build(); - broadcastMessage(message); + broadcastMessageInternal(message); } /** @@ -144,7 +144,7 @@ public class WebSocketMessageBroadcaster { .payload((RuleExecutionStatusPayload) event.getPayload()) .build(); - broadcastMessage(message); + broadcastMessageInternal(message); } /** @@ -160,7 +160,7 @@ public class WebSocketMessageBroadcaster { .payload((RuleStateChangePayload) event.getPayload()) .build(); - broadcastMessage(message); + broadcastMessageInternal(message); } /** @@ -176,7 +176,7 @@ public class WebSocketMessageBroadcaster { .payload((PathConflictAlertMessage) event.getPayload()) .build(); - broadcastMessage(message); + broadcastMessageInternal(message); } /** @@ -192,14 +192,22 @@ public class WebSocketMessageBroadcaster { .payload((GeofenceAlertPayload) event.getPayload()) .build(); - broadcastMessage(message); + broadcastMessageInternal(message); } /** - * 统一消息广播方法 - 使用原生WebSocket发送JSON消息 + * 统一消息广播方法 - 使用原生WebSocket发送JSON消息(公共方法) * @param message 要广播的消息 */ - private void broadcastMessage(UniversalMessage message) { + public void broadcastMessage(UniversalMessage message) { + broadcastMessageInternal(message); + } + + /** + * 统一消息广播方法 - 使用原生WebSocket发送JSON消息(内部方法) + * @param message 要广播的消息 + */ + private void broadcastMessageInternal(UniversalMessage message) { try { // 使用Jackson ObjectMapper将UniversalMessage序列化为JSON字符串 // 这样前端可以获得消息类型、时间戳、消息ID等完整信息 @@ -236,7 +244,7 @@ public class WebSocketMessageBroadcaster { : messageCacheService.getRecentMessages(messageType, count); // 逐个发送最近消息 - recentMessages.forEach(this::broadcastMessage); + recentMessages.forEach(this::broadcastMessageInternal); } catch (Exception e) { System.err.println("Failed to send recent messages: " + e.getMessage()); diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/listener/TrafficLightStatusEventListener.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/listener/TrafficLightStatusEventListener.java new file mode 100644 index 00000000..62fccc23 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/listener/TrafficLightStatusEventListener.java @@ -0,0 +1,75 @@ +package com.qaup.collision.websocket.listener; + +import com.qaup.collision.websocket.event.TrafficLightStatusEvent; +import com.qaup.collision.websocket.message.UniversalMessage; +import com.qaup.collision.websocket.message.TrafficLightStatusPayload; +import com.qaup.collision.websocket.broadcaster.WebSocketMessageBroadcaster; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * 红绿灯状态事件监听器 + * + * 监听红绿灯状态变更事件,并通过WebSocket广播给前端客户端 + * 使用UniversalMessage格式确保与前端的一致性 + */ +@Slf4j +@Component +public class TrafficLightStatusEventListener { + + @Autowired + private WebSocketMessageBroadcaster webSocketMessageBroadcaster; + + /** + * 处理红绿灯状态事件 + * + * @param event 红绿灯状态事件 + */ + @EventListener + public void handleTrafficLightStatusEvent(TrafficLightStatusEvent event) { + try { + TrafficLightStatusPayload payload = event.getPayload(); + + if (payload == null) { + log.warn("红绿灯状态事件载荷为空,跳过广播"); + return; + } + + // 创建UniversalMessage包装 + UniversalMessage universalMessage = + UniversalMessage.trafficLightStatus(payload); + + // 广播消息给所有连接的WebSocket客户端 + broadcastStatusUpdate(universalMessage); + + log.debug("红绿灯状态事件已通过WebSocket广播: intersectionId={}, deviceId={}, 状态={}:{}", + payload.getIntersectionId(), + payload.getDeviceId(), + payload.getNsStatus(), + payload.getEwStatus()); + + } catch (Exception e) { + log.error("处理红绿灯状态事件失败: eventId={}", event.getEventId(), e); + } + } + + /** + * 广播WebSocket消息 + * + * @param message 要广播的消息 + */ + private void broadcastStatusUpdate(UniversalMessage message) { + try { + webSocketMessageBroadcaster.broadcastMessage(message); + + log.debug("红绿灯状态更新广播成功: intersectionId={}", + message.getPayload().getIntersectionId()); + + } catch (Exception e) { + log.error("广播红绿灯状态更新失败: intersectionId={}", + message.getPayload().getIntersectionId(), e); + } + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/message/TrafficLightStatusPayload.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/TrafficLightStatusPayload.java index 85278042..1a3bcdac 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/websocket/message/TrafficLightStatusPayload.java +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/message/TrafficLightStatusPayload.java @@ -22,6 +22,24 @@ public class TrafficLightStatusPayload { @JsonProperty("intersection_id") private String intersectionId; + /** + * 路口名称 + */ + @JsonProperty("intersection_name") + private String intersectionName; + + /** + * 红绿灯设备ID + */ + @JsonProperty("device_id") + private String deviceId; + + /** + * 红绿灯设备名称 + */ + @JsonProperty("device_name") + private String deviceName; + /** * 路口位置 */ @@ -46,6 +64,12 @@ public class TrafficLightStatusPayload { @JsonProperty("timestamp") private Long timestamp; + /** + * 区域编码 + */ + @JsonProperty("area_code") + private String areaCode; + /** * 位置坐标类 */ diff --git a/qaup-collision/src/test/java/com/qaup/collision/datacollector/server/TrafficLightTcpServerTest.java b/qaup-collision/src/test/java/com/qaup/collision/datacollector/server/TrafficLightTcpServerTest.java new file mode 100644 index 00000000..c3339965 --- /dev/null +++ b/qaup-collision/src/test/java/com/qaup/collision/datacollector/server/TrafficLightTcpServerTest.java @@ -0,0 +1,274 @@ +package com.qaup.collision.datacollector.server; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.PrintWriter; +import java.net.Socket; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * 红绿灯TCP服务器集成测试 + */ +@ExtendWith(MockitoExtension.class) +class TrafficLightTcpServerTest { + + @Mock + private TrafficLightSignalHandler signalHandler; + + private TrafficLightTcpServer tcpServer; + + @BeforeEach + void setUp() { + tcpServer = new TrafficLightTcpServer(); + + // 设置测试配置 + ReflectionTestUtils.setField(tcpServer, "serverPort", 0); // 使用随机端口 + ReflectionTestUtils.setField(tcpServer, "serverEnabled", true); + ReflectionTestUtils.setField(tcpServer, "maxConnections", 10); + ReflectionTestUtils.setField(tcpServer, "connectionTimeout", 5000); + ReflectionTestUtils.setField(tcpServer, "signalHandler", signalHandler); + } + + @Test + @DisplayName("测试TCP服务器启动和关闭") + void testServerStartAndStop() throws InterruptedException { + // 启动服务器 + tcpServer.startServer(); + + // 等待服务器启动 + Thread.sleep(100); + + // 验证服务器状态 + TrafficLightTcpServer.ServerStatus status = tcpServer.getServerStatus(); + assertTrue(status.isRunning()); + assertTrue(status.getPort() > 0); + assertEquals(0, status.getActiveConnections()); + + // 关闭服务器 + tcpServer.stopServer(); + + // 验证服务器已关闭 + status = tcpServer.getServerStatus(); + assertFalse(status.isRunning()); + } + + @Test + @DisplayName("测试客户端连接和消息处理") + void testClientConnectionAndMessageProcessing() throws Exception { + // 启动服务器 + tcpServer.startServer(); + Thread.sleep(100); + + TrafficLightTcpServer.ServerStatus status = tcpServer.getServerStatus(); + int serverPort = status.getPort(); + + // 创建测试用的信号处理计数器 + CountDownLatch messageReceived = new CountDownLatch(1); + doAnswer(invocation -> { + messageReceived.countDown(); + return null; + }).when(signalHandler).handleSignal(anyString(), anyString()); + + // 创建客户端连接 + try (Socket clientSocket = new Socket("localhost", serverPort); + PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true)) { + + // 等待连接建立 + Thread.sleep(100); + + // 验证连接已建立 + status = tcpServer.getServerStatus(); + assertEquals(1, status.getActiveConnections()); + + // 发送测试消息 + String testMessage = """ + { + "device_id": "TL_TEST_001", + "DI-11": 1, + "DI-12": 0, + "DI-13": 0, + "DI-14": 0, + "DI-15": 0, + "DI-16": 1 + } + """; + + writer.println(testMessage); + writer.flush(); + + // 等待消息处理 + assertTrue(messageReceived.await(5, TimeUnit.SECONDS), "消息应该被处理"); + + // 验证信号处理器被调用 + verify(signalHandler, timeout(1000)).handleSignal(anyString(), eq(testMessage)); + + // 验证统计信息 + status = tcpServer.getServerStatus(); + assertEquals(1, status.getTotalMessages()); + + } finally { + tcpServer.stopServer(); + } + } + + @Test + @DisplayName("测试多客户端并发连接") + void testMultipleClientConnections() throws Exception { + // 启动服务器 + tcpServer.startServer(); + Thread.sleep(100); + + TrafficLightTcpServer.ServerStatus status = tcpServer.getServerStatus(); + int serverPort = status.getPort(); + + int clientCount = 3; + CountDownLatch allMessagesReceived = new CountDownLatch(clientCount); + + doAnswer(invocation -> { + allMessagesReceived.countDown(); + return null; + }).when(signalHandler).handleSignal(anyString(), anyString()); + + // 创建多个客户端连接 + Socket[] clients = new Socket[clientCount]; + PrintWriter[] writers = new PrintWriter[clientCount]; + + try { + // 建立连接 + for (int i = 0; i < clientCount; i++) { + clients[i] = new Socket("localhost", serverPort); + writers[i] = new PrintWriter(clients[i].getOutputStream(), true); + } + + // 等待连接建立 + Thread.sleep(200); + + // 验证所有连接都已建立 + status = tcpServer.getServerStatus(); + assertEquals(clientCount, status.getActiveConnections()); + + // 每个客户端发送消息 + for (int i = 0; i < clientCount; i++) { + String message = String.format(""" + { + "device_id": "TL_TEST_%03d", + "DI-11": %d, + "DI-16": %d + } + """, i + 1, i % 2, (i + 1) % 2); + + writers[i].println(message); + writers[i].flush(); + } + + // 等待所有消息处理完成 + assertTrue(allMessagesReceived.await(10, TimeUnit.SECONDS), "所有消息应该被处理"); + + // 验证信号处理器被调用了正确的次数 + verify(signalHandler, times(clientCount)).handleSignal(anyString(), anyString()); + + } finally { + // 关闭所有客户端连接 + for (int i = 0; i < clientCount; i++) { + if (writers[i] != null) { + writers[i].close(); + } + if (clients[i] != null && !clients[i].isClosed()) { + clients[i].close(); + } + } + + tcpServer.stopServer(); + } + } + + @Test + @DisplayName("测试信号处理异常处理") + void testSignalProcessingException() throws Exception { + // 配置信号处理器抛出异常 + doThrow(new RuntimeException("测试异常")).when(signalHandler).handleSignal(anyString(), anyString()); + + // 启动服务器 + tcpServer.startServer(); + Thread.sleep(100); + + TrafficLightTcpServer.ServerStatus status = tcpServer.getServerStatus(); + int serverPort = status.getPort(); + + try (Socket clientSocket = new Socket("localhost", serverPort); + PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true)) { + + // 发送测试消息 + writer.println("{\"device_id\": \"TL_ERROR_TEST\", \"DI-11\": 1}"); + writer.flush(); + + // 等待处理 + Thread.sleep(500); + + // 验证异常被处理,服务器仍然运行 + status = tcpServer.getServerStatus(); + assertTrue(status.isRunning()); + assertTrue(status.getErrorCount() > 0); + + } finally { + tcpServer.stopServer(); + } + } + + @Test + @DisplayName("测试服务器禁用状态") + void testServerDisabled() { + // 设置服务器为禁用状态 + ReflectionTestUtils.setField(tcpServer, "serverEnabled", false); + + // 初始化(应该不启动服务器) + tcpServer.init(); + + // 验证服务器未启动 + TrafficLightTcpServer.ServerStatus status = tcpServer.getServerStatus(); + assertFalse(status.isRunning()); + } + + @Test + @DisplayName("测试获取服务器状态信息") + void testGetServerStatus() throws InterruptedException { + // 启动服务器 + tcpServer.startServer(); + + try { + Thread.sleep(100); + + TrafficLightTcpServer.ServerStatus status = tcpServer.getServerStatus(); + + // 验证状态信息 + assertNotNull(status); + assertTrue(status.isRunning()); + assertTrue(status.getPort() > 0); + assertEquals(0, status.getActiveConnections()); + assertTrue(status.getMaxConnections() > 0); + assertEquals(0, status.getTotalConnections()); + assertEquals(0, status.getTotalMessages()); + assertEquals(0, status.getErrorCount()); + + // 验证toString方法 + String statusString = status.toString(); + assertNotNull(statusString); + assertTrue(statusString.contains("TCP服务器状态")); + + } finally { + tcpServer.stopServer(); + } + } +} \ No newline at end of file diff --git a/qaup-collision/src/test/java/com/qaup/collision/dataprocessing/parser/TrafficLightSignalParserTest.java b/qaup-collision/src/test/java/com/qaup/collision/dataprocessing/parser/TrafficLightSignalParserTest.java new file mode 100644 index 00000000..0829d847 --- /dev/null +++ b/qaup-collision/src/test/java/com/qaup/collision/dataprocessing/parser/TrafficLightSignalParserTest.java @@ -0,0 +1,313 @@ +package com.qaup.collision.dataprocessing.parser; + +import com.qaup.collision.common.model.enums.SignalState; +import com.qaup.collision.dataprocessing.model.TrafficLightStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 红绿灯信号解析器单元测试 + */ +class TrafficLightSignalParserTest { + + private TrafficLightSignalParser parser; + + @BeforeEach + void setUp() { + parser = new TrafficLightSignalParser(); + } + + @Test + @DisplayName("测试正常的红绿灯信号解析") + void testParseNormalSignal() { + // 准备测试数据:南北红灯,东西绿灯 + String jsonSignal = """ + { + "device_id": "TL_001", + "DI-01": 0, + "DI-02": 0, + "DI-11": 1, + "DI-12": 0, + "DI-13": 0, + "DI-14": 0, + "DI-15": 0, + "DI-16": 1, + "DI-17": 0, + "DI-18": 0 + } + """; + + // 执行解析 + TrafficLightStatus result = parser.parseSignal(jsonSignal); + + // 验证结果 + assertNotNull(result); + assertEquals("TL_001", result.getDeviceId()); + assertEquals(SignalState.RED, result.getNsStatus()); + assertEquals(SignalState.GREEN, result.getEwStatus()); + assertTrue(result.isValid()); + assertFalse(result.hasConflict()); + assertTrue(result.getTimestamp() > 0); + } + + @Test + @DisplayName("测试黄灯信号解析") + void testParseYellowSignal() { + // 准备测试数据:南北黄灯,东西红灯 + String jsonSignal = """ + { + "device_id": "TL_002", + "DI-11": 0, + "DI-12": 1, + "DI-13": 0, + "DI-14": 1, + "DI-15": 0, + "DI-16": 0 + } + """; + + // 执行解析 + TrafficLightStatus result = parser.parseSignal(jsonSignal); + + // 验证结果 + assertNotNull(result); + assertEquals("TL_002", result.getDeviceId()); + assertEquals(SignalState.YELLOW, result.getNsStatus()); + assertEquals(SignalState.RED, result.getEwStatus()); + assertTrue(result.isValid()); + assertFalse(result.hasConflict()); + } + + @Test + @DisplayName("测试冲突状态处理(两个方向都是绿灯)") + void testConflictStateHandling() { + // 准备测试数据:两个方向都是绿灯(冲突状态) + String jsonSignal = """ + { + "device_id": "TL_003", + "DI-11": 0, + "DI-12": 0, + "DI-13": 1, + "DI-14": 0, + "DI-15": 0, + "DI-16": 1 + } + """; + + // 执行解析 + TrafficLightStatus result = parser.parseSignal(jsonSignal); + + // 验证结果:应该返回安全默认状态 + assertNotNull(result); + assertEquals("TL_003", result.getDeviceId()); + assertEquals(SignalState.RED, result.getNsStatus()); + assertEquals(SignalState.RED, result.getEwStatus()); + assertTrue(result.isValid()); + assertFalse(result.hasConflict()); + } + + @Test + @DisplayName("测试多信号同时激活的优先级处理") + void testSignalPriority() { + // 准备测试数据:红灯和绿灯同时激活(红灯优先级更高) + String jsonSignal = """ + { + "device_id": "TL_004", + "DI-11": 1, + "DI-12": 0, + "DI-13": 1, + "DI-14": 0, + "DI-15": 1, + "DI-16": 1 + } + """; + + // 执行解析 + TrafficLightStatus result = parser.parseSignal(jsonSignal); + + // 验证结果:红灯应该优先于绿灯,黄灯应该优先于绿灯 + assertNotNull(result); + assertEquals("TL_004", result.getDeviceId()); + assertEquals(SignalState.RED, result.getNsStatus()); // 红灯优先 + assertEquals(SignalState.YELLOW, result.getEwStatus()); // 黄灯优先 + assertTrue(result.isValid()); + assertFalse(result.hasConflict()); + } + + @Test + @DisplayName("测试无信号激活时的默认状态") + void testNoSignalActivated() { + // 准备测试数据:所有信号都是0 + String jsonSignal = """ + { + "device_id": "TL_005", + "DI-11": 0, + "DI-12": 0, + "DI-13": 0, + "DI-14": 0, + "DI-15": 0, + "DI-16": 0 + } + """; + + // 执行解析 + TrafficLightStatus result = parser.parseSignal(jsonSignal); + + // 验证结果:应该默认为红灯(安全状态) + assertNotNull(result); + assertEquals("TL_005", result.getDeviceId()); + assertEquals(SignalState.RED, result.getNsStatus()); + assertEquals(SignalState.RED, result.getEwStatus()); + assertTrue(result.isValid()); + assertFalse(result.hasConflict()); + } + + @Test + @DisplayName("测试缺少设备ID的信号处理") + void testMissingDeviceId() { + // 准备测试数据:缺少device_id字段 + String jsonSignal = """ + { + "DI-11": 1, + "DI-12": 0, + "DI-13": 0, + "DI-14": 0, + "DI-15": 0, + "DI-16": 1 + } + """; + + // 执行解析 + TrafficLightStatus result = parser.parseSignal(jsonSignal); + + // 验证结果:应该使用默认设备ID + assertNotNull(result); + assertEquals("UNKNOWN_DEVICE", result.getDeviceId()); + assertEquals(SignalState.RED, result.getNsStatus()); + assertEquals(SignalState.RED, result.getEwStatus()); + } + + @Test + @DisplayName("测试格式错误的JSON处理") + void testInvalidJsonFormat() { + // 准备测试数据:格式错误的JSON + String invalidJson = "{ invalid json format }"; + + // 执行解析 + TrafficLightStatus result = parser.parseSignal(invalidJson); + + // 验证结果:应该返回安全默认状态 + assertNotNull(result); + assertEquals("UNKNOWN_DEVICE", result.getDeviceId()); + assertEquals(SignalState.RED, result.getNsStatus()); + assertEquals(SignalState.RED, result.getEwStatus()); + } + + @Test + @DisplayName("测试空字符串和null输入处理") + void testEmptyAndNullInput() { + // 测试空字符串 + TrafficLightStatus result1 = parser.parseSignal(""); + assertNotNull(result1); + assertEquals("UNKNOWN_DEVICE", result1.getDeviceId()); + assertEquals(SignalState.RED, result1.getNsStatus()); + assertEquals(SignalState.RED, result1.getEwStatus()); + + // 测试null输入 + TrafficLightStatus result2 = parser.parseSignal(null); + assertNotNull(result2); + assertEquals("UNKNOWN_DEVICE", result2.getDeviceId()); + assertEquals(SignalState.RED, result2.getNsStatus()); + assertEquals(SignalState.RED, result2.getEwStatus()); + } + + @Test + @DisplayName("测试信号格式验证") + void testSignalValidation() { + // 有效信号 + String validSignal = """ + { + "device_id": "TL_001", + "DI-11": 1, + "DI-16": 0 + } + """; + assertTrue(parser.isValidSignal(validSignal)); + + // 无效信号:不是JSON对象 + String invalidSignal1 = "not a json"; + assertFalse(parser.isValidSignal(invalidSignal1)); + + // 无效信号:缺少DI字段 + String invalidSignal2 = """ + { + "device_id": "TL_001", + "other_field": "value" + } + """; + assertFalse(parser.isValidSignal(invalidSignal2)); + + // null和空字符串 + assertFalse(parser.isValidSignal(null)); + assertFalse(parser.isValidSignal("")); + assertFalse(parser.isValidSignal(" ")); + } + + @Test + @DisplayName("测试解析统计信息") + void testParseStatistics() { + // 重置统计信息 + parser.resetStatistics(); + + // 执行一些解析操作 + parser.parseSignal(""" + { + "device_id": "TL_001", + "DI-11": 1, + "DI-16": 1 + } + """); + + parser.parseSignal("invalid json"); + parser.parseSignal(null); + + // 获取统计信息 + TrafficLightSignalParser.ParseStatistics stats = parser.getStatistics(); + + // 验证统计信息 + assertEquals(3, stats.getTotalParsed()); + assertEquals(1, stats.getSuccessfulParsed()); + assertEquals(2, stats.getFailedParsed()); + assertTrue(stats.getSuccessRate() > 0); + assertTrue(stats.getSuccessRate() < 100); + } + + @Test + @DisplayName("测试不同的设备ID字段名") + void testDifferentDeviceIdFields() { + // 测试deviceId字段 + String signal1 = """ + { + "deviceId": "TL_001", + "DI-11": 1, + "DI-16": 0 + } + """; + TrafficLightStatus result1 = parser.parseSignal(signal1); + assertEquals("TL_001", result1.getDeviceId()); + + // 测试id字段 + String signal2 = """ + { + "id": "TL_002", + "DI-11": 0, + "DI-16": 1 + } + """; + TrafficLightStatus result2 = parser.parseSignal(signal2); + assertEquals("TL_002", result2.getDeviceId()); + } +} \ No newline at end of file diff --git a/qaup-collision/src/test/java/com/qaup/collision/dataprocessing/service/IntersectionServiceTest.java b/qaup-collision/src/test/java/com/qaup/collision/dataprocessing/service/IntersectionServiceTest.java new file mode 100644 index 00000000..13b1f902 --- /dev/null +++ b/qaup-collision/src/test/java/com/qaup/collision/dataprocessing/service/IntersectionServiceTest.java @@ -0,0 +1,340 @@ +package com.qaup.collision.dataprocessing.service; + +import com.qaup.collision.common.model.repository.IntersectionRepository; +import com.qaup.collision.common.model.spatial.Intersection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * 路口管理服务单元测试 + */ +@ExtendWith(MockitoExtension.class) +class IntersectionServiceTest { + + @Mock + private IntersectionRepository intersectionRepository; + + @InjectMocks + private IntersectionService intersectionService; + + private Intersection testIntersection; + + @BeforeEach + void setUp() { + testIntersection = Intersection.builder() + .intersectionId("TEST_001") + .intersectionName("测试路口1号") + .latitude(39.9042) + .longitude(116.4074) + .areaCode("AREA_A") + .description("测试路口描述") + .isActive(true) + .build(); + } + + @Test + @DisplayName("根据路口编号获取路口信息 - 成功") + void testGetIntersectionById_Success() { + // 准备测试数据 + when(intersectionRepository.findByIntersectionId("TEST_001")) + .thenReturn(Optional.of(testIntersection)); + + // 执行测试 + Optional result = intersectionService.getIntersectionById("TEST_001"); + + // 验证结果 + assertTrue(result.isPresent()); + assertEquals("TEST_001", result.get().getIntersectionId()); + assertEquals("测试路口1号", result.get().getIntersectionName()); + + // 验证方法调用 + verify(intersectionRepository).findByIntersectionId("TEST_001"); + } + + @Test + @DisplayName("根据路口编号获取路口信息 - 路口不存在") + void testGetIntersectionById_NotFound() { + // 准备测试数据 + when(intersectionRepository.findByIntersectionId("NONEXISTENT")) + .thenReturn(Optional.empty()); + + // 执行测试 + Optional result = intersectionService.getIntersectionById("NONEXISTENT"); + + // 验证结果 + assertFalse(result.isPresent()); + + // 验证方法调用 + verify(intersectionRepository).findByIntersectionId("NONEXISTENT"); + } + + @Test + @DisplayName("根据路口编号获取路口信息 - 空参数") + void testGetIntersectionById_EmptyParameter() { + // 执行测试 + Optional result1 = intersectionService.getIntersectionById(null); + Optional result2 = intersectionService.getIntersectionById(""); + Optional result3 = intersectionService.getIntersectionById(" "); + + // 验证结果 + assertFalse(result1.isPresent()); + assertFalse(result2.isPresent()); + assertFalse(result3.isPresent()); + + // 验证没有调用repository方法 + verify(intersectionRepository, never()).findByIntersectionId(any()); + } + + @Test + @DisplayName("获取激活的路口信息 - 成功") + void testGetActiveIntersectionById_Success() { + // 准备测试数据 + when(intersectionRepository.findByIntersectionIdAndIsActive("TEST_001", true)) + .thenReturn(Optional.of(testIntersection)); + + // 执行测试 + Optional result = intersectionService.getActiveIntersectionById("TEST_001"); + + // 验证结果 + assertTrue(result.isPresent()); + assertEquals("TEST_001", result.get().getIntersectionId()); + assertTrue(result.get().getIsActive()); + + // 验证方法调用 + verify(intersectionRepository).findByIntersectionIdAndIsActive("TEST_001", true); + } + + @Test + @DisplayName("获取所有激活的路口") + void testGetAllActiveIntersections() { + // 准备测试数据 + List activeIntersections = List.of(testIntersection); + when(intersectionRepository.findByIsActiveTrue()).thenReturn(activeIntersections); + + // 执行测试 + List result = intersectionService.getAllActiveIntersections(); + + // 验证结果 + assertEquals(1, result.size()); + assertEquals("TEST_001", result.get(0).getIntersectionId()); + + // 验证方法调用 + verify(intersectionRepository).findByIsActiveTrue(); + } + + @Test + @DisplayName("根据区域编码获取激活的路口") + void testGetActiveIntersectionsByArea() { + // 准备测试数据 + List areaIntersections = List.of(testIntersection); + when(intersectionRepository.findByAreaCodeAndIsActiveTrue("AREA_A")) + .thenReturn(areaIntersections); + + // 执行测试 + List result = intersectionService.getActiveIntersectionsByArea("AREA_A"); + + // 验证结果 + assertEquals(1, result.size()); + assertEquals("AREA_A", result.get(0).getAreaCode()); + + // 验证方法调用 + verify(intersectionRepository).findByAreaCodeAndIsActiveTrue("AREA_A"); + } + + @Test + @DisplayName("添加新路口 - 成功") + void testAddIntersection_Success() { + // 准备测试数据 + when(intersectionRepository.existsByIntersectionId("TEST_001")).thenReturn(false); + when(intersectionRepository.save(any(Intersection.class))).thenReturn(testIntersection); + + // 执行测试 + Intersection result = intersectionService.addIntersection(testIntersection); + + // 验证结果 + assertNotNull(result); + assertEquals("TEST_001", result.getIntersectionId()); + + // 验证方法调用 + verify(intersectionRepository).existsByIntersectionId("TEST_001"); + verify(intersectionRepository).save(testIntersection); + } + + @Test + @DisplayName("添加新路口 - 路口编号已存在") + void testAddIntersection_DuplicateId() { + // 准备测试数据 + when(intersectionRepository.existsByIntersectionId("TEST_001")).thenReturn(true); + + // 执行测试并验证异常 + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> intersectionService.addIntersection(testIntersection)); + + assertTrue(exception.getMessage().contains("路口编号已存在")); + + // 验证方法调用 + verify(intersectionRepository).existsByIntersectionId("TEST_001"); + verify(intersectionRepository, never()).save(any()); + } + + @Test + @DisplayName("添加新路口 - 无效数据") + void testAddIntersection_InvalidData() { + // 测试空路口编号 + Intersection invalidIntersection = Intersection.builder() + .intersectionId("") + .intersectionName("测试路口") + .latitude(39.9042) + .longitude(116.4074) + .build(); + + // 执行测试并验证异常 + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> intersectionService.addIntersection(invalidIntersection)); + + assertTrue(exception.getMessage().contains("路口编号不能为空")); + + // 验证没有调用repository方法 + verify(intersectionRepository, never()).save(any()); + } + + @Test + @DisplayName("更新路口信息 - 成功") + void testUpdateIntersection_Success() { + // 准备测试数据 + when(intersectionRepository.findByIntersectionId("TEST_001")) + .thenReturn(Optional.of(testIntersection)); + when(intersectionRepository.save(any(Intersection.class))).thenReturn(testIntersection); + + // 执行测试 + Intersection result = intersectionService.updateIntersection(testIntersection); + + // 验证结果 + assertNotNull(result); + assertEquals("TEST_001", result.getIntersectionId()); + + // 验证方法调用 + verify(intersectionRepository).findByIntersectionId("TEST_001"); + verify(intersectionRepository).save(any(Intersection.class)); + } + + @Test + @DisplayName("更新路口信息 - 路口不存在") + void testUpdateIntersection_NotFound() { + // 准备测试数据 + when(intersectionRepository.findByIntersectionId("TEST_001")) + .thenReturn(Optional.empty()); + + // 执行测试并验证异常 + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> intersectionService.updateIntersection(testIntersection)); + + assertTrue(exception.getMessage().contains("路口不存在")); + + // 验证方法调用 + verify(intersectionRepository).findByIntersectionId("TEST_001"); + verify(intersectionRepository, never()).save(any()); + } + + @Test + @DisplayName("设置路口激活状态 - 成功") + void testSetIntersectionActive_Success() { + // 准备测试数据 + when(intersectionRepository.findByIntersectionId("TEST_001")) + .thenReturn(Optional.of(testIntersection)); + when(intersectionRepository.save(any(Intersection.class))).thenReturn(testIntersection); + + // 执行测试 + Optional result = intersectionService.setIntersectionActive("TEST_001", false); + + // 验证结果 + assertTrue(result.isPresent()); + + // 验证方法调用 + verify(intersectionRepository).findByIntersectionId("TEST_001"); + verify(intersectionRepository).save(any(Intersection.class)); + } + + @Test + @DisplayName("根据坐标范围查找路口") + void testGetIntersectionsInBounds() { + // 准备测试数据 + List boundedIntersections = List.of(testIntersection); + when(intersectionRepository.findIntersectionsInBounds(39.0, 40.0, 116.0, 117.0)) + .thenReturn(boundedIntersections); + + // 执行测试 + List result = intersectionService.getIntersectionsInBounds(39.0, 40.0, 116.0, 117.0); + + // 验证结果 + assertEquals(1, result.size()); + assertEquals("TEST_001", result.get(0).getIntersectionId()); + + // 验证方法调用 + verify(intersectionRepository).findIntersectionsInBounds(39.0, 40.0, 116.0, 117.0); + } + + @Test + @DisplayName("根据坐标范围查找路口 - 无效范围") + void testGetIntersectionsInBounds_InvalidBounds() { + // 执行测试(最小纬度大于最大纬度) + List result = intersectionService.getIntersectionsInBounds(40.0, 39.0, 116.0, 117.0); + + // 验证结果 + assertTrue(result.isEmpty()); + + // 验证没有调用repository方法 + verify(intersectionRepository, never()).findIntersectionsInBounds(anyDouble(), anyDouble(), anyDouble(), anyDouble()); + } + + @Test + @DisplayName("根据名称模糊查询路口") + void testSearchIntersectionsByName() { + // 准备测试数据 + List searchResults = List.of(testIntersection); + when(intersectionRepository.findByIntersectionNameContaining("测试")) + .thenReturn(searchResults); + + // 执行测试 + List result = intersectionService.searchIntersectionsByName("测试"); + + // 验证结果 + assertEquals(1, result.size()); + assertTrue(result.get(0).getIntersectionName().contains("测试")); + + // 验证方法调用 + verify(intersectionRepository).findByIntersectionNameContaining("测试"); + } + + @Test + @DisplayName("获取路口统计信息") + void testGetStatistics() { + // 准备测试数据 + when(intersectionRepository.count()).thenReturn(10L); + when(intersectionRepository.countByIsActiveTrue()).thenReturn(8L); + + // 执行测试 + IntersectionService.IntersectionStatistics stats = intersectionService.getStatistics(); + + // 验证结果 + assertEquals(10L, stats.getTotalCount()); + assertEquals(8L, stats.getActiveCount()); + assertEquals(2L, stats.getInactiveCount()); + + // 验证方法调用 + verify(intersectionRepository).count(); + verify(intersectionRepository).countByIsActiveTrue(); + } +} \ No newline at end of file diff --git a/qaup-framework/src/main/java/com/qaup/framework/web/exception/GlobalExceptionHandler.java b/qaup-framework/src/main/java/com/qaup/framework/web/exception/GlobalExceptionHandler.java index 34e8f768..13ff04b3 100644 --- a/qaup-framework/src/main/java/com/qaup/framework/web/exception/GlobalExceptionHandler.java +++ b/qaup-framework/src/main/java/com/qaup/framework/web/exception/GlobalExceptionHandler.java @@ -144,4 +144,4 @@ public class GlobalExceptionHandler { return AjaxResult.error("演示模式,不允许操作"); } -} +} \ No newline at end of file diff --git a/sql/create_traffic_light_tables.sql b/sql/create_traffic_light_tables.sql new file mode 100644 index 00000000..953ba809 --- /dev/null +++ b/sql/create_traffic_light_tables.sql @@ -0,0 +1,107 @@ +-- 红绿灯系统数据库表创建脚本 +-- 创建时间: 2025-01-05 +-- 描述: 创建路口信息表和红绿灯设备表,支持多路口多设备管理 + +-- 1. 路口信息表 +CREATE TABLE IF NOT EXISTS intersections ( + id BIGSERIAL PRIMARY KEY, + intersection_id VARCHAR(50) UNIQUE NOT NULL, -- 路口编号 + intersection_name VARCHAR(100) NOT NULL, -- 路口名称 + latitude DECIMAL(10, 8) NOT NULL, -- 纬度 + longitude DECIMAL(11, 8) NOT NULL, -- 经度 + area_code VARCHAR(20), -- 区域编码 + description TEXT, -- 路口描述 + is_active BOOLEAN DEFAULT true, -- 是否激活 + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 2. 红绿灯设备表 +CREATE TABLE IF NOT EXISTS traffic_lights ( + id BIGSERIAL PRIMARY KEY, + device_id VARCHAR(50) UNIQUE NOT NULL, -- 红绿灯设备编号 + device_name VARCHAR(100) NOT NULL, -- 设备名称 + intersection_id VARCHAR(50) NOT NULL, -- 关联的路口编号 + device_type VARCHAR(20) DEFAULT 'STANDARD', -- 设备类型 + manufacturer VARCHAR(50), -- 制造商 + model VARCHAR(50), -- 型号 + install_date DATE, -- 安装日期 + is_online BOOLEAN DEFAULT false, -- 是否在线 + last_heartbeat TIMESTAMP, -- 最后心跳时间 + is_active BOOLEAN DEFAULT true, -- 是否激活 + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 外键约束 + CONSTRAINT fk_traffic_light_intersection + FOREIGN KEY (intersection_id) + REFERENCES intersections(intersection_id) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_intersection_id ON intersections(intersection_id); +CREATE INDEX IF NOT EXISTS idx_intersection_area_code ON intersections(area_code); +CREATE INDEX IF NOT EXISTS idx_intersection_active ON intersections(is_active); + +CREATE INDEX IF NOT EXISTS idx_traffic_light_device_id ON traffic_lights(device_id); +CREATE INDEX IF NOT EXISTS idx_traffic_light_intersection ON traffic_lights(intersection_id); +CREATE INDEX IF NOT EXISTS idx_traffic_light_online ON traffic_lights(is_online); +CREATE INDEX IF NOT EXISTS idx_traffic_light_active ON traffic_lights(is_active); + +-- 创建更新时间触发器函数 +CREATE OR REPLACE FUNCTION update_updated_time_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- 为路口表创建更新时间触发器 +CREATE TRIGGER update_intersections_updated_time + BEFORE UPDATE ON intersections + FOR EACH ROW + EXECUTE FUNCTION update_updated_time_column(); + +-- 为红绿灯设备表创建更新时间触发器 +CREATE TRIGGER update_traffic_lights_updated_time + BEFORE UPDATE ON traffic_lights + FOR EACH ROW + EXECUTE FUNCTION update_updated_time_column(); + +-- 插入示例数据 +INSERT INTO intersections (intersection_id, intersection_name, latitude, longitude, area_code, description) +VALUES + ('INTERSECTION_001', '主要路口1号', 39.9042, 116.4074, 'AREA_A', '机场主要路口,连接航站楼和跑道区域'), + ('INTERSECTION_002', '次要路口2号', 39.9050, 116.4080, 'AREA_B', '机场次要路口,连接货运区域'), + ('INTERSECTION_003', '货运区路口', 39.9035, 116.4065, 'AREA_C', '货运区域主要路口'), + ('INTERSECTION_004', '维修区路口', 39.9055, 116.4085, 'AREA_D', '维修区域路口'), + ('INTERSECTION_005', '停机坪路口', 39.9045, 116.4070, 'AREA_A', '停机坪入口路口') +ON CONFLICT (intersection_id) DO NOTHING; + +INSERT INTO traffic_lights (device_id, device_name, intersection_id, device_type, manufacturer, model, install_date) +VALUES + ('TL_001', '主路口红绿灯1号', 'INTERSECTION_001', 'STANDARD', '海康威视', 'DS-TL100', '2024-01-01'), + ('TL_002', '次路口红绿灯2号', 'INTERSECTION_002', 'STANDARD', '大华技术', 'DH-TL200', '2024-01-15'), + ('TL_003', '货运区红绿灯', 'INTERSECTION_003', 'STANDARD', '海康威视', 'DS-TL100', '2024-02-01'), + ('TL_004', '维修区红绿灯', 'INTERSECTION_004', 'HEAVY_DUTY', '大华技术', 'DH-TL300', '2024-02-15'), + ('TL_005', '停机坪红绿灯', 'INTERSECTION_005', 'STANDARD', '海康威视', 'DS-TL100', '2024-03-01') +ON CONFLICT (device_id) DO NOTHING; + +-- 添加注释 +COMMENT ON TABLE intersections IS '路口信息表,存储机场内各个路口的基本信息'; +COMMENT ON TABLE traffic_lights IS '红绿灯设备表,存储红绿灯设备信息及其与路口的绑定关系'; + +COMMENT ON COLUMN intersections.intersection_id IS '路口唯一编号'; +COMMENT ON COLUMN intersections.intersection_name IS '路口名称'; +COMMENT ON COLUMN intersections.latitude IS '路口纬度坐标'; +COMMENT ON COLUMN intersections.longitude IS '路口经度坐标'; +COMMENT ON COLUMN intersections.area_code IS '所属区域编码'; + +COMMENT ON COLUMN traffic_lights.device_id IS '红绿灯设备唯一编号'; +COMMENT ON COLUMN traffic_lights.device_name IS '设备名称'; +COMMENT ON COLUMN traffic_lights.intersection_id IS '关联的路口编号'; +COMMENT ON COLUMN traffic_lights.is_online IS '设备是否在线'; +COMMENT ON COLUMN traffic_lights.last_heartbeat IS '最后一次心跳时间'; \ No newline at end of file diff --git a/sql/validate_traffic_light_data.sql b/sql/validate_traffic_light_data.sql new file mode 100644 index 00000000..4a2124a7 --- /dev/null +++ b/sql/validate_traffic_light_data.sql @@ -0,0 +1,154 @@ +-- 红绿灯系统数据验证脚本 +-- 用于验证数据完整性和约束 + +-- 1. 验证路口数据 +SELECT 'Intersection Validation' as validation_type; + +-- 检查路口数据完整性 +SELECT + COUNT(*) as total_intersections, + COUNT(CASE WHEN is_active = true THEN 1 END) as active_intersections, + COUNT(CASE WHEN latitude IS NULL OR longitude IS NULL THEN 1 END) as missing_coordinates +FROM intersections; + +-- 检查坐标范围是否合理 +SELECT + intersection_id, + intersection_name, + latitude, + longitude, + CASE + WHEN latitude < -90 OR latitude > 90 THEN 'Invalid latitude' + WHEN longitude < -180 OR longitude > 180 THEN 'Invalid longitude' + ELSE 'Valid coordinates' + END as coordinate_status +FROM intersections +WHERE latitude < -90 OR latitude > 90 OR longitude < -180 OR longitude > 180; + +-- 2. 验证红绿灯设备数据 +SELECT 'Traffic Light Device Validation' as validation_type; + +-- 检查设备数据完整性 +SELECT + COUNT(*) as total_devices, + COUNT(CASE WHEN is_active = true THEN 1 END) as active_devices, + COUNT(CASE WHEN is_online = true THEN 1 END) as online_devices, + COUNT(CASE WHEN intersection_id IS NULL THEN 1 END) as orphaned_devices +FROM traffic_lights; + +-- 检查设备与路口的关联关系 +SELECT + tl.device_id, + tl.device_name, + tl.intersection_id, + CASE + WHEN i.intersection_id IS NULL THEN 'Orphaned device - intersection not found' + WHEN i.is_active = false THEN 'Device linked to inactive intersection' + ELSE 'Valid association' + END as association_status +FROM traffic_lights tl +LEFT JOIN intersections i ON tl.intersection_id = i.intersection_id +WHERE i.intersection_id IS NULL OR i.is_active = false; + +-- 3. 验证外键约束 +SELECT 'Foreign Key Validation' as validation_type; + +-- 检查是否有违反外键约束的数据 +SELECT + tl.device_id, + tl.intersection_id, + 'Missing intersection reference' as issue +FROM traffic_lights tl +WHERE NOT EXISTS ( + SELECT 1 FROM intersections i + WHERE i.intersection_id = tl.intersection_id +); + +-- 4. 统计信息 +SELECT 'Statistics' as validation_type; + +-- 按区域统计路口数量 +SELECT + area_code, + COUNT(*) as intersection_count, + COUNT(CASE WHEN is_active = true THEN 1 END) as active_count +FROM intersections +GROUP BY area_code +ORDER BY area_code; + +-- 按路口统计设备数量 +SELECT + i.intersection_id, + i.intersection_name, + COUNT(tl.device_id) as device_count, + COUNT(CASE WHEN tl.is_active = true THEN 1 END) as active_device_count, + COUNT(CASE WHEN tl.is_online = true THEN 1 END) as online_device_count +FROM intersections i +LEFT JOIN traffic_lights tl ON i.intersection_id = tl.intersection_id +GROUP BY i.intersection_id, i.intersection_name +ORDER BY i.intersection_id; + +-- 按制造商统计设备数量 +SELECT + manufacturer, + COUNT(*) as device_count, + COUNT(CASE WHEN is_active = true THEN 1 END) as active_count, + COUNT(CASE WHEN is_online = true THEN 1 END) as online_count +FROM traffic_lights +WHERE manufacturer IS NOT NULL +GROUP BY manufacturer +ORDER BY device_count DESC; + +-- 5. 数据质量检查 +SELECT 'Data Quality Check' as validation_type; + +-- 检查重复的路口编号 +SELECT + intersection_id, + COUNT(*) as duplicate_count +FROM intersections +GROUP BY intersection_id +HAVING COUNT(*) > 1; + +-- 检查重复的设备编号 +SELECT + device_id, + COUNT(*) as duplicate_count +FROM traffic_lights +GROUP BY device_id +HAVING COUNT(*) > 1; + +-- 检查空值或无效数据 +SELECT + 'intersections' as table_name, + COUNT(CASE WHEN intersection_id IS NULL OR intersection_id = '' THEN 1 END) as null_ids, + COUNT(CASE WHEN intersection_name IS NULL OR intersection_name = '' THEN 1 END) as null_names, + COUNT(CASE WHEN latitude IS NULL THEN 1 END) as null_latitudes, + COUNT(CASE WHEN longitude IS NULL THEN 1 END) as null_longitudes +FROM intersections + +UNION ALL + +SELECT + 'traffic_lights' as table_name, + COUNT(CASE WHEN device_id IS NULL OR device_id = '' THEN 1 END) as null_ids, + COUNT(CASE WHEN device_name IS NULL OR device_name = '' THEN 1 END) as null_names, + COUNT(CASE WHEN intersection_id IS NULL OR intersection_id = '' THEN 1 END) as null_intersection_ids, + 0 as null_longitudes +FROM traffic_lights; + +-- 6. 索引使用情况检查 +SELECT 'Index Usage Check' as validation_type; + +-- 检查索引是否存在 +SELECT + schemaname, + tablename, + indexname, + indexdef +FROM pg_indexes +WHERE tablename IN ('intersections', 'traffic_lights') +ORDER BY tablename, indexname; + +-- 完成验证 +SELECT 'Validation Complete' as status, NOW() as completion_time; \ No newline at end of file diff --git a/tools/test_traffic_light_client.py b/tools/test_traffic_light_client.py new file mode 100644 index 00000000..0cb69641 --- /dev/null +++ b/tools/test_traffic_light_client.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +红绿灯信号测试客户端 +用于测试红绿灯TCP服务器的功能 +""" + +import socket +import json +import time +import threading +import random + +class TrafficLightTestClient: + def __init__(self, host='localhost', port=8082): + self.host = host + self.port = port + self.socket = None + self.running = False + + def connect(self): + """连接到红绿灯TCP服务器""" + try: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect((self.host, self.port)) + print(f"✅ 成功连接到红绿灯服务器 {self.host}:{self.port}") + return True + except Exception as e: + print(f"❌ 连接失败: {e}") + return False + + def disconnect(self): + """断开连接""" + self.running = False + if self.socket: + try: + self.socket.close() + print("🔌 连接已断开") + except: + pass + + def send_signal(self, device_id, ns_red=0, ns_yellow=0, ns_green=0, + ew_red=0, ew_yellow=0, ew_green=0): + """发送红绿灯信号""" + signal = { + "device_id": device_id, + "DI-01": 0, + "DI-02": 0, + "DI-11": ns_red, # 北红 + "DI-12": ns_yellow, # 北黄 + "DI-13": ns_green, # 北绿 + "DI-14": ew_red, # 东红 + "DI-15": ew_yellow, # 东黄 + "DI-16": ew_green, # 东绿 + "DI-17": 0, + "DI-18": 0 + } + + try: + message = json.dumps(signal) + '\n' + self.socket.send(message.encode('utf-8')) + print(f"📡 发送信号: {device_id} - NS:{self._get_state(ns_red, ns_yellow, ns_green)} EW:{self._get_state(ew_red, ew_yellow, ew_green)}") + return True + except Exception as e: + print(f"❌ 发送信号失败: {e}") + return False + + def _get_state(self, red, yellow, green): + """获取信号状态描述""" + if red == 1: + return "红" + elif yellow == 1: + return "黄" + elif green == 1: + return "绿" + else: + return "未知" + + def simulate_traffic_light_cycle(self, device_id, cycles=5): + """模拟红绿灯周期""" + print(f"🚦 开始模拟设备 {device_id} 的红绿灯周期...") + + # 红绿灯状态序列:南北绿-东西红 -> 南北黄-东西红 -> 南北红-东西绿 -> 南北红-东西黄 + states = [ + (0, 0, 1, 1, 0, 0), # NS绿, EW红 + (0, 1, 0, 1, 0, 0), # NS黄, EW红 + (1, 0, 0, 0, 0, 1), # NS红, EW绿 + (1, 0, 0, 0, 1, 0), # NS红, EW黄 + ] + + for cycle in range(cycles): + print(f"\n--- 第 {cycle + 1} 个周期 ---") + for i, (ns_r, ns_y, ns_g, ew_r, ew_y, ew_g) in enumerate(states): + if not self.running: + break + + success = self.send_signal(device_id, ns_r, ns_y, ns_g, ew_r, ew_y, ew_g) + if not success: + break + + # 每个状态持续3秒 + time.sleep(3) + + if not self.running: + break + + def send_random_signals(self, device_id, duration=30): + """发送随机信号""" + print(f"🎲 开始发送随机信号,持续 {duration} 秒...") + + start_time = time.time() + while time.time() - start_time < duration and self.running: + # 随机生成信号状态 + ns_state = random.choice([(1,0,0), (0,1,0), (0,0,1)]) # 红、黄、绿 + ew_state = random.choice([(1,0,0), (0,1,0), (0,0,1)]) + + success = self.send_signal(device_id, + ns_state[0], ns_state[1], ns_state[2], + ew_state[0], ew_state[1], ew_state[2]) + if not success: + break + + # 随机间隔 1-3 秒 + time.sleep(random.uniform(1, 3)) + +def test_single_device(): + """测试单个设备""" + client = TrafficLightTestClient() + + if not client.connect(): + return + + client.running = True + + try: + # 模拟正常的红绿灯周期 + client.simulate_traffic_light_cycle("TL_001", cycles=3) + + print("\n" + "="*50) + print("测试完成!") + + except KeyboardInterrupt: + print("\n⏹️ 测试被用户中断") + finally: + client.disconnect() + +def test_multiple_devices(): + """测试多个设备并发""" + devices = ["TL_001", "TL_002", "TL_003"] + clients = [] + threads = [] + + print(f"🚀 启动多设备测试,设备数量: {len(devices)}") + + def device_worker(device_id): + client = TrafficLightTestClient() + clients.append(client) + + if client.connect(): + client.running = True + client.send_random_signals(device_id, duration=20) + + client.disconnect() + + try: + # 启动多个线程模拟多个设备 + for device_id in devices: + thread = threading.Thread(target=device_worker, args=(device_id,)) + thread.start() + threads.append(thread) + time.sleep(1) # 错开连接时间 + + # 等待所有线程完成 + for thread in threads: + thread.join() + + print("\n" + "="*50) + print("多设备测试完成!") + + except KeyboardInterrupt: + print("\n⏹️ 测试被用户中断") + # 停止所有客户端 + for client in clients: + client.running = False + +def main(): + print("🚦 红绿灯信号测试客户端") + print("="*50) + + while True: + print("\n请选择测试模式:") + print("1. 单设备测试(模拟正常红绿灯周期)") + print("2. 多设备测试(并发随机信号)") + print("3. 退出") + + choice = input("\n请输入选择 (1-3): ").strip() + + if choice == '1': + test_single_device() + elif choice == '2': + test_multiple_devices() + elif choice == '3': + print("👋 再见!") + break + else: + print("❌ 无效选择,请重新输入") + +if __name__ == "__main__": + main() \ No newline at end of file