完善日志管理

This commit is contained in:
Tian jianyong 2025-10-16 18:33:44 +08:00
parent 1086de063f
commit fbd7e5e7ad
8 changed files with 1281 additions and 1028 deletions

142
IFLOW.md Normal file
View File

@ -0,0 +1,142 @@
# QAUP-Management 项目概览
## 项目简介
QAUP-Management 是一个机场无人车冲突管理平台集成了机场冲突避免系统。该项目基于若依框架RuoYi开发采用 Maven 多模块架构,提供了完整的车辆管理、空间分析、实时监控等功能。
核心模块 `qaup-collision` 实现了无人车与飞机、其他车辆之间的冲突检测算法,利用 PostGIS 进行空间数据分析,并通过 WebSocket 实现实时数据推送。
## 技术栈
- **后端框架**: Spring Boot 3.5.3, Spring MVC, Spring Data JPA, MyBatis
- **数据库**: PostgreSQL 与 PostGIS 扩展(空间数据处理)
- **缓存**: Redis
- **实时通信**: WebSocket + STOMP
- **空间计算**: JTS (Java Topology Suite), GeoTools
- **构建工具**: Maven 3.6+
- **Java版本**: JDK 21
- **API文档**: SpringDoc OpenAPI (Swagger UI)
- **数据库迁移**: Flyway
## 项目结构
```
QAUP-Management/
├── qaup-admin/ # Web服务入口集成所有模块
├── qaup-framework/ # 核心框架,提供通用功能
├── qaup-system/ # 系统管理模块(用户、角色、权限等)
├── qaup-collision/ # 冲突避免系统模块(核心业务逻辑)
├── qaup-common/ # 公共工具类和基础组件
├── qaup-quartz/ # 定时任务调度
├── qaup-generator/ # 代码生成器
├── qaup-ui/ # 前端Vue项目
├── sql/ # 数据库初始化脚本
├── deploy/ # 部署相关脚本和配置
├── tools/ # 开发和测试工具
└── doc/ # 项目文档
```
## 核心模块详解
### qaup-collision (冲突避免系统)
这是项目的核心模块,负责实现无人车冲突检测与管理功能。
**主要特性**:
- 基于 PostGIS 的空间数据分析与几何计算
- WebSocket 实时车辆位置监控与数据推送
- 机场区域(跑道、滑行道、停机坪)配置与监控
- 车辆与飞机、车辆与车辆间的实时冲突检测算法
- 统一数据访问适配器 `QuapDataAdapter`,避免重复 DAO 开发
- Redis 缓存支持高性能数据存取
**关键组件**:
- `QuapDataAdapter`: 统一数据访问接口,连接若依系统数据
- `WebSocketConfig`: WebSocket 配置,支持实时数据推送
- `VehicleLocationService`: 车辆位置管理服务
- 空间实体类:继承空间基类并配置 PostGIS 映射
## 构建与运行
### 环境准备
- JDK 21
- Maven 3.6+
- PostgreSQL (启用 PostGIS 扩展)
- Redis
### 快速启动
1. **数据库配置**:
- 创建 PostgreSQL 数据库并启用 PostGIS 扩展
- 执行 SQL 初始化脚本 (位于 `sql/` 目录)
- 配置 `qaup-admin/src/main/resources/application.yml` 中的数据库连接信息
2. **启动依赖服务**:
```bash
# 启动 Redis
redis-server
# 启动 PostgreSQL (确保已创建数据库并启用 PostGIS)
```
3. **编译和启动**:
```bash
# 清理并编译整个项目
mvn clean install
# 启动应用
cd qaup-admin
mvn spring-boot:run
# 或者运行打包后的jar
java -jar target/qaup-admin.jar
```
4. **使用启动脚本**:
```bash
# 复制环境配置模板
cp .env.example .env
# 修改 .env 中的配置值
# 启动应用
./qaup.sh start
# 停止应用
./qaup.sh stop
# 重启应用
./qaup.sh restart
# 查看状态
./qaup.sh status
```
### 访问系统
- 管理后台: `http://localhost:8080`
- WebSocket 端点: `ws://localhost:8080/collision`
- API 文档: `http://localhost:8080/swagger-ui.html`
## 开发指南
### 项目合并说明
本项目已将独立的 CollisionAvoidanceSystem 项目合并到若依框架中,通过以下方式实现:
1. **模块化集成**: 创建 `qaup-collision` 模块,保持功能独立性
2. **数据适配器**: `QuapDataAdapter` 提供统一数据访问,连接若依 Service 层
3. **配置整合**: 将 PostGIS、JPA、WebSocket 等配置整合到若依配置体系
4. **版本兼容**: 升级 Spring Boot 版本至 3.5.3,使用 JDK 21
### collision 模块开发要点
- 新增空间实体时,需继承空间基类并正确配置 PostGIS 映射
- WebSocket 消息通过 `/topic` 前缀向客户端广播
- 使用 `QuapDataAdapter` 获取车辆和司机数据,避免直接访问 DAO
- 所有空间计算应使用 JTS 和 GeoTools 库
### 数据采集与处理
系统支持从多个数据源采集数据:
- 机场数据 API
- ADXP 适配器服务
- 无人车厂商 API
数据采集配置可在 `application.yml` 中调整,包括采集间隔、超时时间等参数。

View File

@ -15,12 +15,16 @@
<!-- 系统日志输出 --> <!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file> <file>${log.path}/sys-info.log</file>
<!-- 循环政策:基于时间创建日志文件 --> <!-- 循环政策:基于时间和文件大小创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件名格式 --> <!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志最大的历史 60天 --> <!-- 日志最大的历史 30天 -->
<maxHistory>60</maxHistory> <maxHistory>30</maxHistory>
<!-- 每个日志文件最大100MB -->
<maxFileSize>100MB</maxFileSize>
<!-- 所有日志文件总大小最多10GB -->
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy> </rollingPolicy>
<encoder> <encoder>
<pattern>${log.pattern}</pattern> <pattern>${log.pattern}</pattern>
@ -37,12 +41,16 @@
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file> <file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 --> <!-- 循环政策:基于时间和文件大小创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件名格式 --> <!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志最大的历史 60天 --> <!-- 日志最大的历史 30天 -->
<maxHistory>60</maxHistory> <maxHistory>30</maxHistory>
<!-- 每个日志文件最大100MB -->
<maxFileSize>100MB</maxFileSize>
<!-- 所有日志文件总大小最多10GB -->
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy> </rollingPolicy>
<encoder> <encoder>
<pattern>${log.pattern}</pattern> <pattern>${log.pattern}</pattern>
@ -60,21 +68,51 @@
<!-- 用户访问日志输出 --> <!-- 用户访问日志输出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-user.log</file> <file>${log.path}/sys-user.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 按天回滚 daily --> <!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 日志最大的历史 60天 --> <!-- 日志最大的历史 30天 -->
<maxHistory>60</maxHistory> <maxHistory>30</maxHistory>
<!-- 每个日志文件最大100MB -->
<maxFileSize>100MB</maxFileSize>
<!-- 所有日志文件总大小最多10GB -->
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy> </rollingPolicy>
<encoder> <encoder>
<pattern>${log.pattern}</pattern> <pattern>${log.pattern}</pattern>
</encoder> </encoder>
</appender> </appender>
<!-- 异步日志记录 -->
<appender name="async_file_info" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<includeCallerData>false</includeCallerData>
<appender-ref ref="file_info" />
</appender>
<appender name="async_file_error" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<includeCallerData>false</includeCallerData>
<appender-ref ref="file_error" />
</appender>
<appender name="async_sys_user" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<includeCallerData>false</includeCallerData>
<appender-ref ref="sys-user" />
</appender>
<!-- 系统模块日志级别控制 --> <!-- 系统模块日志级别控制 -->
<logger name="com.qaup" level="info" /> <logger name="com.qaup" level="info" />
<logger name="com.qaup.collision.service.impl.VehicleLocationServiceImpl" level="warn" />
<logger name="com.qaup.collision.websocket.handler.CollisionWebSocketHandler" level="warn" />
<!-- Spring日志级别控制 --> <!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" /> <logger name="org.springframework" level="warn" />
<logger name="org.springframework.web.socket" level="warn" />
<root level="info"> <root level="info">
<appender-ref ref="console" /> <appender-ref ref="console" />
@ -82,12 +120,12 @@
<!--系统操作日志--> <!--系统操作日志-->
<root level="info"> <root level="info">
<appender-ref ref="file_info" /> <appender-ref ref="async_file_info" />
<appender-ref ref="file_error" /> <appender-ref ref="async_file_error" />
</root> </root>
<!--系统用户操作日志--> <!--系统用户操作日志-->
<logger name="sys-user" level="info"> <logger name="sys-user" level="info">
<appender-ref ref="sys-user"/> <appender-ref ref="async_sys_user"/>
</logger> </logger>
</configuration> </configuration>

Binary file not shown.

55
tools/logging_config.py Normal file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import os
from logging.handlers import RotatingFileHandler
def setup_logger(name, log_file, level=logging.INFO, max_bytes=10*1024*1024, backup_count=5):
"""
配置日志记录器
Args:
name: 日志记录器名称
log_file: 日志文件路径
level: 日志级别
max_bytes: 单个日志文件最大字节数默认10MB
backup_count: 保留的备份日志文件数量
Returns:
logging.Logger: 配置好的日志记录器
"""
# 创建logs目录如果不存在
log_dir = os.path.dirname(log_file)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir)
# 创建日志记录器
logger = logging.getLogger(name)
logger.setLevel(level)
# 避免重复添加处理器
if not logger.handlers:
# 创建文件处理器(带滚动)
file_handler = RotatingFileHandler(
log_file,
maxBytes=max_bytes,
backupCount=backup_count,
encoding='utf-8'
)
# 创建控制台处理器
console_handler = logging.StreamHandler()
# 设置格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加处理器到记录器
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger

View File

@ -28,13 +28,19 @@ from datetime import datetime
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
# 日志配置 - 只显示关键信息 # 导入统一的日志配置
logging.basicConfig( import sys
level=logging.WARNING, # 只显示 WARNING 和 ERROR减少海量日志 import os
format="%(asctime)s [%(levelname)s] %(message)s" sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from logging_config import setup_logger
# 设置当前模块的日志记录器
logger = setup_logger(
name='mock_adxp',
log_file='logs/mock_adxp.log',
max_bytes=10*1024*1024, # 10MB
backup_count=5
) )
logger = logging.getLogger("mock-adxp")
logger.setLevel(logging.INFO) # 自己的日志保持 INFO
# 服务代码 # 服务代码
SERVICE_CODES = { SERVICE_CODES = {

View File

@ -7,18 +7,17 @@ import threading
import atexit import atexit
from typing import Any, Literal, final, TypedDict from typing import Any, Literal, final, TypedDict
# 创建 logs 目录(如果不存在) # 导入统一的日志配置
if not os.path.exists('logs'): import sys
os.makedirs('logs') sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from logging_config import setup_logger
# 配置日志 # 设置当前模块的日志记录器
logging.basicConfig( logger = setup_logger(
level=logging.DEBUG, name='mock_airport',
format='%(asctime)s - %(levelname)s - %(message)s', log_file='logs/mock_airport.log',
handlers=[ max_bytes=10*1024*1024, # 10MB
logging.FileHandler('logs/mock_server.log'), backup_count=5
logging.StreamHandler()
]
) )
app = Flask(__name__) app = Flask(__name__)
@ -1584,14 +1583,14 @@ class AircraftRouteManager:
""" """
self.flight_configs[flight_no] = config self.flight_configs[flight_no] = config
self.followers[flight_no] = AircraftRouteFollower(flight_no) self.followers[flight_no] = AircraftRouteFollower(flight_no)
logging.info(f"已添加飞机 {flight_no} 到路径跟随系统,行为模式: {config.get('primary_behavior', 'cycle')}") logger.info(f"已添加飞机 {flight_no} 到路径跟随系统,行为模式: {config.get('primary_behavior', 'cycle')}")
def update_aircraft_position(self, aircraft: dict[str, Any], elapsed_time: float): def update_aircraft_position(self, aircraft: dict[str, Any], elapsed_time: float):
"""统一的航空器位置更新入口""" """统一的航空器位置更新入口"""
flight_no = aircraft.get("flightNo") flight_no = aircraft.get("flightNo")
if flight_no not in self.followers: if flight_no not in self.followers:
logging.warning(f"飞机 {flight_no} 未注册到路径跟随系统") logger.warning(f"飞机 {flight_no} 未注册到路径跟随系统")
return return
follower = self.followers[flight_no] follower = self.followers[flight_no]
@ -1955,7 +1954,7 @@ class BackgroundUpdateManager:
self.running = False self.running = False
if self.update_thread: if self.update_thread:
self.update_thread.join(timeout=2.0) self.update_thread.join(timeout=2.0)
logging.info("后台更新管理器已停止") logger.info("后台更新管理器已停止")
def _update_loop(self): def _update_loop(self):
"""主更新循环""" """主更新循环"""
@ -1978,7 +1977,7 @@ class BackgroundUpdateManager:
time.sleep(UPDATE_INTERVAL) time.sleep(UPDATE_INTERVAL)
except Exception as e: except Exception as e:
logging.error(f"后台更新出错: {str(e)}") logger.error(f"后台更新出错: {str(e)}")
time.sleep(1.0) # 出错时短暂休眠后继续 time.sleep(1.0) # 出错时短暂休眠后继续
# 全局后台更新管理器实例 # 全局后台更新管理器实例
@ -1988,15 +1987,15 @@ def check_auth() -> bool:
auth_header = request.headers.get('Authorization') auth_header = request.headers.get('Authorization')
if not auth_header: if not auth_header:
print("认证失败: 缺少Authorization头") logger.warning("认证失败: 缺少Authorization头")
return False return False
# 直接比较完整的Authorization头 # 直接比较完整的Authorization头
result = auth_header == AUTH_TOKEN result = auth_header == AUTH_TOKEN
if not result: if not result:
print(f"认证失败: token不匹配") logger.warning(f"认证失败: token不匹配")
print(f"收到的token: {auth_header}") logger.debug(f"收到的token: {auth_header}")
print(f"期望的token: {AUTH_TOKEN}") logger.debug(f"期望的token: {AUTH_TOKEN}")
return result return result
def to_float_safe(x: object) -> float | None: def to_float_safe(x: object) -> float | None:
@ -2137,7 +2136,7 @@ def get_vehicle_positions():
"data": response_data "data": response_data
}) })
except Exception as e: except Exception as e:
print(f"Error in get_vehicle_positions: {str(e)}") logger.error(f"Error in get_vehicle_positions: {str(e)}")
return jsonify({ return jsonify({
"status": 500, "status": 500,
"msg": str(e), "msg": str(e),
@ -2203,12 +2202,12 @@ def login():
except: except:
pass pass
print(f"收到登录请求: username={username}, password={password}") logger.info(f"收到登录请求: username={username}, password={password}")
print(f"请求 URL: {request.url}") logger.debug(f"请求 URL: {request.url}")
print(f"请求方法: {request.method}") logger.debug(f"请求方法: {request.method}")
print(f"请求参数: {request.args}") logger.debug(f"请求参数: {request.args}")
print(f"请求表单: {request.form}") logger.debug(f"请求表单: {request.form}")
print(f"请求JSON: {request.get_json(silent=True)}") logger.debug(f"请求JSON: {request.get_json(silent=True)}")
if not username or not password: if not username or not password:
return jsonify({ return jsonify({
@ -2281,7 +2280,7 @@ def get_arrival_taxiway_route():
contact_cross = request.args.get('contactCross', 'F1') contact_cross = request.args.get('contactCross', 'F1')
seat = request.args.get('seat', '138') seat = request.args.get('seat', '138')
logging.info(f"进港路线查询: inRunway={in_runway}, outRunway={out_runway}, contactCross={contact_cross}, seat={seat}") logger.info(f"进港路线查询: inRunway={in_runway}, outRunway={out_runway}, contactCross={contact_cross}, seat={seat}")
# 根据参数匹配对应的航班路由 # 根据参数匹配对应的航班路由
matching_flight = None matching_flight = None
@ -2296,10 +2295,10 @@ def get_arrival_taxiway_route():
# 如果找到匹配的航班返回对应路由否则使用MU5123作为默认 # 如果找到匹配的航班返回对应路由否则使用MU5123作为默认
if matching_flight and matching_flight in aircraft_routes: if matching_flight and matching_flight in aircraft_routes:
route_data = aircraft_routes[matching_flight]["arrival"] route_data = aircraft_routes[matching_flight]["arrival"]
logging.info(f"匹配航班 {matching_flight} 的进港路由") logger.info(f"匹配航班 {matching_flight} 的进港路由")
else: else:
route_data = aircraft_routes["MU5123"]["arrival"] route_data = aircraft_routes["MU5123"]["arrival"]
logging.info(f"未找到匹配航班使用MU5123的进港路由作为默认") logger.info(f"未找到匹配航班使用MU5123的进港路由作为默认")
# 将路由数据转换为CGCS2000坐标系格式 # 将路由数据转换为CGCS2000坐标系格式
converted_route_data = convert_route_to_cgcs2000(route_data) converted_route_data = convert_route_to_cgcs2000(route_data)
@ -2328,7 +2327,7 @@ def get_departure_taxiway_route():
out_runway = request.args.get('outRunway', '34') out_runway = request.args.get('outRunway', '34')
start_seat = request.args.get('startSeat', '138') start_seat = request.args.get('startSeat', '138')
logging.info(f"出港路线查询: inRunway={in_runway}, outRunway={out_runway}, startSeat={start_seat}") logger.info(f"出港路线查询: inRunway={in_runway}, outRunway={out_runway}, startSeat={start_seat}")
# 根据参数匹配对应的航班路由 # 根据参数匹配对应的航班路由
matching_flight = None matching_flight = None
@ -2342,10 +2341,10 @@ def get_departure_taxiway_route():
# 如果找到匹配的航班返回对应路由否则使用CA1234作为默认 # 如果找到匹配的航班返回对应路由否则使用CA1234作为默认
if matching_flight and matching_flight in aircraft_routes: if matching_flight and matching_flight in aircraft_routes:
route_data = aircraft_routes[matching_flight]["departure"] route_data = aircraft_routes[matching_flight]["departure"]
logging.info(f"匹配航班 {matching_flight} 的出港路由") logger.info(f"匹配航班 {matching_flight} 的出港路由")
else: else:
route_data = aircraft_routes["CA1234"]["departure"] route_data = aircraft_routes["CA1234"]["departure"]
logging.info(f"未找到匹配航班使用CA1234的出港路由作为默认") logger.info(f"未找到匹配航班使用CA1234的出港路由作为默认")
# 将路由数据转换为CGCS2000坐标系格式 # 将路由数据转换为CGCS2000坐标系格式
converted_route_data = convert_route_to_cgcs2000(route_data) converted_route_data = convert_route_to_cgcs2000(route_data)
@ -2384,9 +2383,9 @@ def get_active_flights() -> list[dict[str, Any]]:
} }
active_flights.append(flight_notification) active_flights.append(flight_notification)
logging.info(f"航班 {flight_no} {event_type}通知中 (路径状态: {follower.flight_status}, 进度: {follower.route_progress:.2f})") logger.info(f"航班 {flight_no} {event_type}通知中 (路径状态: {follower.flight_status}, 进度: {follower.route_progress:.2f})")
else: else:
logging.warning(f"航班 {flight_no} 在路径跟随器中但不在flight_data中") logger.warning(f"航班 {flight_no} 在路径跟随器中但不在flight_data中")
return active_flights return active_flights
@ -2407,7 +2406,7 @@ def get_flights():
# 获取当前活跃航班 # 获取当前活跃航班
active_flights = get_active_flights() active_flights = get_active_flights()
logging.info(f"进出港航班查询: 返回 {len(active_flights)} 个活跃航班") logger.info(f"进出港航班查询: 返回 {len(active_flights)} 个活跃航班")
return jsonify({ return jsonify({
"status": 200, "status": 200,
@ -2416,7 +2415,7 @@ def get_flights():
}) })
except Exception as e: except Exception as e:
logging.error(f"进出港航班查询失败: {str(e)}") logger.error(f"进出港航班查询失败: {str(e)}")
return jsonify({ return jsonify({
"status": 500, "status": 500,
"msg": f"查询失败: {str(e)}", "msg": f"查询失败: {str(e)}",

View File

@ -17,6 +17,20 @@ from datetime import datetime
import signal import signal
from typing import TypedDict from typing import TypedDict
# 导入统一的日志配置
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from logging_config import setup_logger
# 设置当前模块的日志记录器
logger = setup_logger(
name='mock_traffic_light',
log_file='logs/mock_traffic_light.log',
max_bytes=10*1024*1024, # 10MB
backup_count=5
)
class TLState(TypedDict): class TLState(TypedDict):
name: str name: str
duration: int duration: int
@ -82,10 +96,10 @@ class TrafficLightDevice:
self.socket.connect((self.host, self.port)) self.socket.connect((self.host, self.port))
# 连接成功后取消超时设置 # 连接成功后取消超时设置
self.socket.settimeout(None) self.socket.settimeout(None)
print(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ 已连接到服务器 {self.host}:{self.port}") logger.info(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ 已连接到服务器 {self.host}:{self.port}")
return True return True
except Exception as e: except Exception as e:
print(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ 连接失败: {e}") logger.error(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ 连接失败: {e}")
if self.socket: if self.socket:
try: try:
self.socket.close() self.socket.close()
@ -99,7 +113,7 @@ class TrafficLightDevice:
if self.socket: if self.socket:
try: try:
self.socket.close() self.socket.close()
print(f"[{datetime.now().strftime('%H:%M:%S')}] 已断开连接") logger.info(f"[{datetime.now().strftime('%H:%M:%S')}] 已断开连接")
except: except:
pass pass
self.socket = None self.socket = None
@ -107,15 +121,15 @@ class TrafficLightDevice:
def send_signal(self, di_signal): def send_signal(self, di_signal):
"""发送DI信号到服务器""" """发送DI信号到服务器"""
if not self.socket: if not self.socket:
print("未连接到服务器") logger.warning("未连接到服务器")
return False return False
try: try:
self.socket.sendall(di_signal.encode('utf-8')) self.socket.sendall(di_signal.encode('utf-8'))
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📡 发送信号: {di_signal}") logger.info(f"[{datetime.now().strftime('%H:%M:%S')}] 📡 发送信号: {di_signal}")
return True return True
except Exception as e: except Exception as e:
print(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ 发送失败: {e}") logger.error(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ 发送失败: {e}")
# 发送失败时关闭socket触发重连 # 发送失败时关闭socket触发重连
try: try:
self.socket.close() self.socket.close()
@ -160,9 +174,9 @@ class TrafficLightDevice:
def run_continuous_simulation(self): def run_continuous_simulation(self):
"""运行连续的红绿灯信号模拟""" """运行连续的红绿灯信号模拟"""
print(f"开始连续发送红绿灯信号,间隔: {self.interval}") logger.info(f"开始连续发送红绿灯信号,间隔: {self.interval}")
print("按 Ctrl+C 停止模拟") logger.info("按 Ctrl+C 停止模拟")
print("服务端断开时会自动重连...") logger.info("服务端断开时会自动重连...")
self.running = True self.running = True
start_time = time.time() start_time = time.time()
@ -174,7 +188,7 @@ class TrafficLightDevice:
if not connected: if not connected:
connected = self.connect() connected = self.connect()
if not connected: if not connected:
print("连接失败5秒后重试...") logger.warning("连接失败5秒后重试...")
time.sleep(5) time.sleep(5)
continue continue
else: else:
@ -196,22 +210,22 @@ class TrafficLightDevice:
# 发送信号 # 发送信号
success = self.send_signal(di_signal) success = self.send_signal(di_signal)
if not success: if not success:
print("发送失败,连接已断开") logger.warning("发送失败,连接已断开")
self.disconnect() self.disconnect()
connected = False connected = False
continue continue
print(f"当前状态: {state_name}") logger.info(f"当前状态: {state_name}")
# 等待下一次发送 # 等待下一次发送
time.sleep(self.interval) time.sleep(self.interval)
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n收到停止信号...") logger.info("\n收到停止信号...")
finally: finally:
self.running = False self.running = False
self.disconnect() self.disconnect()
print("红绿灯模拟已停止") logger.info("红绿灯模拟已停止")
def test_signal_states(self): def test_signal_states(self):
"""测试各种信号状态""" """测试各种信号状态"""

View File

@ -15,18 +15,17 @@ import logging
import os import os
from typing import Any, Literal, final, TypedDict from typing import Any, Literal, final, TypedDict
# 创建 logs 目录(如果不存在) # 导入统一的日志配置
if not os.path.exists('logs'): import sys
os.makedirs('logs') sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from logging_config import setup_logger
# 配置日志 # 设置当前模块的日志记录器
logging.basicConfig( logger = setup_logger(
level=logging.DEBUG, name='mock_unmanned_vehicle',
format='%(asctime)s - %(levelname)s - %(message)s', log_file='logs/mock_unmanned_vehicle.log',
handlers=[ max_bytes=10*1024*1024, # 10MB
logging.FileHandler('logs/unmanned_vehicle_mock_server.log'), backup_count=5
logging.StreamHandler()
]
) )
app = Flask(__name__) app = Flask(__name__)
@ -706,12 +705,12 @@ def handle_vehicle_command():
target_lat = data.get("latitude", None) target_lat = data.get("latitude", None)
target_lon = data.get("longitude", None) target_lon = data.get("longitude", None)
logging.info(f"🎛️ 收到无人车控制指令: vehicle_id={vehicle_id}, type={command_type}, reason={reason}, signal_state={signal_state}") logger.info(f"🎛️ 收到无人车控制指令: vehicle_id={vehicle_id}, type={command_type}, reason={reason}, signal_state={signal_state}")
logging.info(f"完整请求数据: {data}") logger.info(f"完整请求数据: {data}")
# 检查 SIGNAL 类型命令必须包含 signalState # 检查 SIGNAL 类型命令必须包含 signalState
if command_type == "SIGNAL" and not signal_state: if command_type == "SIGNAL" and not signal_state:
logging.warning(f"SIGNAL 类型命令缺少 signalState") logger.warning(f"SIGNAL 类型命令缺少 signalState")
return jsonify({ return jsonify({
"code": 400, "code": 400,
"msg": "SIGNAL command must include signalState", "msg": "SIGNAL command must include signalState",
@ -722,7 +721,7 @@ def handle_vehicle_command():
# 检查车辆是否存在 # 检查车辆是否存在
vehicle_state = vehicle_states.get(vehicle_id) vehicle_state = vehicle_states.get(vehicle_id)
if not vehicle_state: if not vehicle_state:
logging.warning(f"未找到无人车: {vehicle_id}") logger.warning(f"未找到无人车: {vehicle_id}")
return jsonify({ return jsonify({
"code": 404, "code": 404,
"msg": f"Vehicle {vehicle_id} not found", "msg": f"Vehicle {vehicle_id} not found",
@ -731,16 +730,16 @@ def handle_vehicle_command():
}), 404 }), 404
# 打印当前车辆状态 # 打印当前车辆状态
logging.info(f"当前无人车状态: vehicle={vehicle_id}") logger.info(f"当前无人车状态: vehicle={vehicle_id}")
vehicle_state.log_state() vehicle_state.log_state()
# 检查指令优先级 # 检查指令优先级
check_command = signal_state if command_type == "SIGNAL" else command_type check_command = signal_state if command_type == "SIGNAL" else command_type
can_override = vehicle_state.can_be_overridden_by(check_command) can_override = vehicle_state.can_be_overridden_by(check_command)
logging.info(f"指令优先级检查: vehicle={vehicle_id}, current_command={str(vehicle_state.current_command)}, new_command={check_command}, can_override={can_override}") logger.info(f"指令优先级检查: vehicle={vehicle_id}, current_command={str(vehicle_state.current_command)}, new_command={check_command}, can_override={can_override}")
if not can_override: if not can_override:
logging.warning(f"指令优先级过低: vehicle={vehicle_id}, current_priority={vehicle_state.command_priority}, command={check_command}") logger.warning(f"指令优先级过低: vehicle={vehicle_id}, current_priority={vehicle_state.command_priority}, command={check_command}")
return jsonify({ return jsonify({
"code": 400, "code": 400,
"msg": "Command priority too low", "msg": "Command priority too low",
@ -752,7 +751,7 @@ def handle_vehicle_command():
if command_type == "SIGNAL": if command_type == "SIGNAL":
# 更新红绿灯状态和指令状态 # 更新红绿灯状态和指令状态
vehicle_state.update_command(signal_state, target_lat, target_lon) vehicle_state.update_command(signal_state, target_lat, target_lon)
logging.info(f"无人车 {vehicle_id} 更新状态: command={command_type}, traffic_light={vehicle_state.traffic_light_state}") logger.info(f"无人车 {vehicle_id} 更新状态: command={command_type}, traffic_light={vehicle_state.traffic_light_state}")
return jsonify({ return jsonify({
"code": 200, "code": 200,
"msg": "Traffic light state updated", "msg": "Traffic light state updated",
@ -768,7 +767,7 @@ def handle_vehicle_command():
break break
# 执行告警指令直接停车 # 执行告警指令直接停车
logging.info(f"执行{'紧急' if command_type == 'ALERT' else '正常'}制动: vehicle={vehicle_id}") logger.info(f"执行{'紧急' if command_type == 'ALERT' else '正常'}制动: vehicle={vehicle_id}")
vehicle_state.current_command = command_type vehicle_state.current_command = command_type
vehicle_state.command_priority = COMMAND_PRIORITIES.get(command_type, 0) vehicle_state.command_priority = COMMAND_PRIORITIES.get(command_type, 0)
vehicle_state.is_running = False vehicle_state.is_running = False
@ -785,7 +784,7 @@ def handle_vehicle_command():
}) })
elif command_type == "RESUME": elif command_type == "RESUME":
logging.info(f"执行恢复指令: vehicle_id={vehicle_id}") logger.info(f"执行恢复指令: vehicle_id={vehicle_id}")
# 检查当前车辆 # 检查当前车辆
current_vehicle = None current_vehicle = None
for v in unmanned_vehicle_data: for v in unmanned_vehicle_data:
@ -795,7 +794,7 @@ def handle_vehicle_command():
# 清除限制性指令 # 清除限制性指令
if vehicle_state.current_command in ["ALERT", "WARNING"]: if vehicle_state.current_command in ["ALERT", "WARNING"]:
logging.info(f"清除限制性指令: vehicle={vehicle_id}") logger.info(f"清除限制性指令: vehicle={vehicle_id}")
vehicle_state.current_command = None vehicle_state.current_command = None
vehicle_state.command_priority = 0 vehicle_state.command_priority = 0
vehicle_state.is_running = True vehicle_state.is_running = True
@ -807,13 +806,13 @@ def handle_vehicle_command():
# 更新车辆运行状态 # 更新车辆运行状态
vehicle_state.is_running = vehicle_state.can_move() vehicle_state.is_running = vehicle_state.can_move()
if vehicle_state.is_running and current_vehicle: if vehicle_state.is_running and current_vehicle:
logging.info(f"无人车 {vehicle_id} 恢复运行") logger.info(f"无人车 {vehicle_id} 恢复运行")
# 记录状态变化但不更新指令 # 记录状态变化但不更新指令
vehicle_state.command_reason = reason vehicle_state.command_reason = reason
vehicle_state.last_command_time = time.time() vehicle_state.last_command_time = time.time()
logging.info(f"Vehicle {vehicle_id} state updated: running={vehicle_state.is_running}, " logger.info(f"Vehicle {vehicle_id} state updated: running={vehicle_state.is_running}, "
f"command={vehicle_state.current_command}, traffic_light={vehicle_state.traffic_light_state}, " f"command={vehicle_state.current_command}, traffic_light={vehicle_state.traffic_light_state}, "
f"reason={reason}, priority={vehicle_state.command_priority}, " f"reason={reason}, priority={vehicle_state.command_priority}, "
f"target_speed={vehicle_state.target_speed}, brake_mode={vehicle_state.brake_mode}") f"target_speed={vehicle_state.target_speed}, brake_mode={vehicle_state.brake_mode}")
@ -833,7 +832,7 @@ def handle_vehicle_command():
}) })
except Exception as e: except Exception as e:
logging.error(f"Error handling vehicle command: {str(e)}") logger.error(f"Error handling vehicle command: {str(e)}")
return jsonify({ return jsonify({
"code": 500, "code": 500,
"msg": str(e), "msg": str(e),
@ -849,7 +848,7 @@ def get_vehicle_status_universal(vehicle_id):
if request.method == 'OPTIONS': if request.method == 'OPTIONS':
return '', 204 return '', 204
logging.info(f"🔍 收到无人车状态查询请求: vehicleId={vehicle_id}") logger.info(f"🔍 收到无人车状态查询请求: vehicleId={vehicle_id}")
# 检查vehicleId格式字母数字 3~20 位) # 检查vehicleId格式字母数字 3~20 位)
if not vehicle_id or len(vehicle_id) < 3 or len(vehicle_id) > 20: if not vehicle_id or len(vehicle_id) < 3 or len(vehicle_id) > 20:
@ -915,7 +914,7 @@ def get_vehicle_status_universal(vehicle_id):
# 返回成功响应 # 返回成功响应
battery_level = status_data.get('batteryStatus', {}).get('mainBattery', {}).get('chargeLevel', '未知') battery_level = status_data.get('batteryStatus', {}).get('mainBattery', {}).get('chargeLevel', '未知')
logging.info(f"🚗 返回无人车状态数据: vehicleId={vehicle_id}, 电量={battery_level}%") logger.info(f"🚗 返回无人车状态数据: vehicleId={vehicle_id}, 电量={battery_level}%")
return jsonify({ return jsonify({
"code": 200, "code": 200,
"message": "success", "message": "success",
@ -924,7 +923,7 @@ def get_vehicle_status_universal(vehicle_id):
}) })
except Exception as e: except Exception as e:
logging.error(f"Error in get_vehicle_status_universal: {str(e)}") logger.error(f"Error in get_vehicle_status_universal: {str(e)}")
return jsonify({ return jsonify({
"code": 500, "code": 500,
"message": "Internal Server Error", "message": "Internal Server Error",
@ -947,12 +946,12 @@ def add_cors_headers(response):
return response return response
if __name__ == '__main__': if __name__ == '__main__':
print("🚗 无人车厂商平台模拟服务启动中...") logger.info("🚗 无人车厂商平台模拟服务启动中...")
print("📍 服务端点:") logger.info("📍 服务端点:")
print(" - 车辆控制指令: POST /api/VehicleCommandInfo") logger.info(" - 车辆控制指令: POST /api/VehicleCommandInfo")
print(" - 通用状态接口: GET /api/v1/vehicles/<vehicle_id>/status") logger.info(" - 通用状态接口: GET /api/v1/vehicles/<vehicle_id>/status")
print(" - 车辆状态获取: GET /openApi/getVehicleStatus") logger.info(" - 车辆状态获取: GET /openApi/getVehicleStatus")
print(f"🌐 服务地址: http://localhost:8091") logger.info(f"🌐 服务地址: http://localhost:8091")
print("🔐 认证Token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...") logger.info("🔐 认证Token: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...")
app.run(host='localhost', port=8091, debug=True) app.run(host='localhost', port=8091, debug=True)