adxp-adapter自主管理登录状态

This commit is contained in:
tian 2025-11-21 13:23:00 +08:00
parent 7641184388
commit f08a4dcf0a
14 changed files with 1624 additions and 326 deletions

203
README.md
View File

@ -17,6 +17,7 @@
## 核心特性
### 🚗 冲突避免系统 (collision模块)
- **空间数据分析**: 基于PostGIS的空间计算和几何分析
- **实时车辆监控**: WebSocket实时位置数据推送和展示
- **机场区域管理**: 跑道、滑行道、停机坪等区域配置和监控
@ -26,15 +27,15 @@
### 🛠️ 系统管理功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
@ -55,65 +56,194 @@
- **构建工具**: Maven 3.6+
- **Java版本**: JDK 8
## 快速启动
## 部署指南
### 方式一:自动部署(推荐)
使用提供的自动化部署脚本:
### 1. 环境准备
```bash
# Java 8
# 1. 进入部署目录
cd deploy/
# 2. 赋予执行权限
chmod +x deploy-all.sh
# 3. 执行自动部署
./deploy-all.sh
```
自动化部署脚本将自动完成:
- ✅ 环境检查和依赖验证
- ✅ Docker及Docker Compose安装检查
- ✅ 磁盘空间检查至少3GB
- ✅ 端口占用检测
- ✅ Docker镜像处理本地载入或在线拉取
- ✅ 冲突容器清理
- ✅ 数据目录创建和权限设置
- ✅ 配置文件验证
- ✅ 基础设施服务启动PostgreSQL + PostGIS、Redis
- ✅ 应用服务启动和数据库迁移
- ✅ 健康状态检查和验证
### 方式二:手动部署
#### 1. 环境准备
**必需组件:**
```bash
# Java 8+
java -version
# Maven 3.6+
mvn -version
# PostgreSQL + PostGIS
# Docker & Docker Compose
docker --version
docker-compose --version
```
**可选组件(用于本地开发):**
```bash
# PostgreSQL + PostGIS用于本地开发
psql --version
# Redis
# Redis(用于本地开发)
redis-server --version
```
### 2. 数据库配置
1. 创建PostgreSQL数据库并启用PostGIS扩展
2. 执行SQL初始化脚本
- `sql/create_qaup_database.sql` - 创建数据库
- `sql/create_sys_vehicle_info_table.sql` - 车辆信息表
- `sql/create_sys_driver_info_table.sql` - 司机信息表
#### 2. 数据库配置
**Docker启动数据库**
### 3. 启动Redis服务
```bash
redis-server
# 启动PostgreSQL + PostGIS
docker run -d \
--name qaup-postgres \
-e POSTGRES_DB=qaup \
-e POSTGRES_USER=qaup \
-e POSTGRES_PASSWORD=qaup123 \
-e POSTGRES_INITDB_ARGS="--encoding=UTF-8 --lc-collate=C --lc-ctype=C" \
-p 5432:5432 \
m.daocloud.io/docker.io/postgis/postgis:17-3.5-alpine
# 启动Redis
docker run -d \
--name qaup-redis \
-e REDIS_PASSWORD=qaup123 \
-p 6379:6379 \
m.daocloud.io/docker.io/library/redis:8.0-alpine redis-server --requirepass qaup123
```
### 4. 配置应用
**本地数据库初始化:**
1. 执行SQL初始化脚本
```bash
# 创建数据库和基础表
psql -U qaup -d qaup -f sql/create_qaup_database.sql
psql -U qaup -d qaup -f sql/create_sys_vehicle_info_table.sql
psql -U qaup -d qaup -f sql/create_sys_driver_info_table.sql
```
#### 3. 环境配置
**更新应用配置:**
修改 `qaup-admin/src/main/resources/application.yml`:
```yaml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/your_database
username: your_username
password: your_password
url: jdbc:postgresql://localhost:5432/qaup
username: qaup
password: qaup123
driver-class-name: org.postgresql.Driver
redis:
host: localhost
port: 6379
password: qaup123
database: 0
```
### 5. 编译和启动
**环境变量配置:**
确保各模块 `.env` 文件配置正确:
- `adxp-adapter/.env.example` - ADXP数据中台适配器配置
- `qaup-admin/.env.example` - 主应用配置
#### 4. 编译和启动
```bash
# 清理并编译整个项目
# 1. 清理并编译整个项目
mvn clean install
# 启动应用
# 2. 进入管理模块
cd qaup-admin
# 3. 启动应用(开发模式)
mvn spring-boot:run
# 或者运行打包后的jar
# 或打包启动(生产模式)
mvn clean package -DskipTests
java -jar target/qaup-admin.jar
```
### 6. 访问系统
- 管理后台: http://localhost:8080
- WebSocket端点: ws://localhost:8080/ws
- API文档: http://localhost:8080/swagger-ui/
#### 5. 验证部署
**服务状态检查:**
```bash
# 检查应用健康状态
curl http://localhost:8080/actuator/health
# 检查数据库连接
docker exec qaup-postgres pg_isready -U qaup
# 检查Redis连接
docker exec qaup-redis redis-cli -a qaup123 ping
```
### 6. 系统访问
- **管理后台**: <http://localhost:8080>
- **初始登录**:
- 用户名: `admin`
- 密码: `admin123`
- **WebSocket端点**: ws://localhost:8080/ws
- **API文档**: <http://localhost:8080/swagger-ui/>
- **健康检查**: <http://localhost:8080/actuator/health>
### 7. 管理命令
```bash
# 查看服务状态
docker compose ps
# 查看应用日志
docker compose logs -f qaup-app
# 查看数据库日志
docker compose logs -f qaup-postgres
# 查看Redis日志
docker compose logs -f qaup-redis
# 重启应用
docker compose restart qaup-app
# 停止所有服务
docker compose down
# 查看数据库迁移状态
docker exec qaup-postgres psql -U qaup -d qaup -c \
"SELECT version,description FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;"
# 清理Docker资源
docker system prune -a
```
## 项目合并说明
@ -127,16 +257,19 @@ java -jar target/qaup-admin.jar
## 开发指南
### collision模块核心组件
- `QuapDataAdapter`: 数据访问适配器,连接若依系统数据
- `WebSocketConfig`: WebSocket配置支持实时数据推送
- `VehicleLocationService`: 车辆位置管理服务
- `GeopositionController`: WebSocket消息控制器
### 扩展开发
1. 新增spatial实体时继承spatial基类并配置PostGIS映射
2. WebSocket消息通过`/topic`前缀向客户端广播
3. 使用`QuapDataAdapter`获取车辆和司机数据避免直接访问DAO
## 版本信息
- 当前版本: 1.0.1
- 更新日志: 详见 `change_log.md`
- 更新日志: 详见 `change_log.md`

View File

@ -4,13 +4,68 @@
# 1. 复制此文件为 .env: cp .env.example .env
# 2. 修改 .env 中的配置值
# 3. 启动应用时会自动加载 .env 文件中的环境变量
# 4. 适用于ADXP主动连接架构
# ============================================================
# ========== ADXP 数据中台配置 ==========
ADXP_HOST=localhost
ADXP_PORT=7001
# ========== ADXP 数据中台连接配置 ==========
# ADXP数据中台的主机地址和端口
ADXP_HOST=10.32.38.2
ADXP_PORT=8081
# 连接ADXP数据中台的用户名和密码
ADXP_USERNAME=dianxin
ADXP_PASSWORD=dianxin@123
ADXP_PASSWORD=Dianxin#2025
# ========== 适配器服务配置 ==========
# 适配器服务端口
SERVER_PORT=8086
# 应用环境标识
SPRING_PROFILES_ACTIVE=prod
# ========== 日志配置 ==========
# QAUP应用日志级别
LOG_LEVEL_QAUP=info
# Spring框架日志级别
LOG_LEVEL_SPRING=warn
# ADXP SDK日志级别
LOG_LEVEL_ADXP=info
# ========== 健康检查和监控配置 ==========
# 健康检查端点路径(默认:/actuator/health
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=health,info,metrics
# 健康检查详细程度simple/detailed
MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS=simple
# ========== 数据采集配置 ==========
# 数据采集间隔(毫秒)
DATA_COLLECTOR_INTERVAL=250
# 重连延迟(毫秒)
RECONNECT_DELAY_MILLIS=3000
# ========== WebSocket配置 ==========
# WebSocket端点路径
WEBSOCKET_ENDPOINT=/ws/flight-notifications
# ========== Redis配置用于缓存和会话管理 ==========
# Redis主机地址
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DATABASE=0
REDIS_PASSWORD=
# Redis最大内存限制
REDIS_MAX_MEMORY=256mb
# Redis内存淘汰策略
REDIS_MAX_MEMORY_POLICY=volatile-lru
# ========== 生产环境示例配置 ==========
# 生产环境时请根据实际情况修改以下配置:
# ADXP_HOST=10.32.38.2 # 真实的ADXP数据中台IP地址
# ADXP_PORT=8081 # 真实的ADXP数据中台端口
# ADXP_USERNAME=dianxin # 真实的用户名
# ADXP_PASSWORD=Dianxin#2025 # 真实的密码
# LOG_LEVEL_QAUP=info # 生产环境建议使用info级别
# REDIS_MAX_MEMORY=1gb # 生产环境建议增加Redis内存
# ============================================================
# 注意事项:
@ -18,4 +73,6 @@ ADXP_PASSWORD=dianxin@123
# 2. 字符串值不需要引号
# 3. #开头的行为注释
# 4. 请勿将 .env 文件提交到Git仓库
# 5. 生产环境请务必修改默认密码
# 6. ADXP适配器采用主动连接架构启动时自动连接数据中台
# ============================================================

View File

@ -2,7 +2,17 @@
ADXP 数据中台 SDK 适配器服务 - 基于 JDK 8 运行的独立微服务
## 功能说明
## 新架构特性ADXP主动连接
### 🔄 **适配器主动连接架构**
- **启动即连接**: 适配器服务启动时自动连接到ADXP数据中台
- **无需登录接口**: 移除 `/login` 端点,由适配器自动管理认证
- **连接状态管理**: 提供 `/status``/reconnect` 端点进行连接管理
- **WebSocket实时推送**: 支持实时向客户端推送航班消息
- **健康监控**: 内置连接状态监控和自动重连机制
## 功能特性
将青岛机场数据中台 SDK (adxp-client-2.6.9.jar) 封装为 REST API 服务,解决 SDK 与现代 Java 版本的兼容性问题。
@ -28,7 +38,55 @@ docker compose logs -f
docker compose down
```
## API 文档
### API接口ADXP主动连接架构
#### 获取连接状态
```
GET /api/adxp/status
```
返回当前ADXP数据中台的连接状态和会话信息。
#### 强制重连
```
POST /api/adxp/reconnect
```
强制断开当前连接并重新连接ADXP数据中台。
#### 获取消息接口
```
GET /api/adxp/messages
```
使用当前活动连接获取ADXP数据中台的航班消息。
#### 断开连接接口
```
POST /api/adxp/disconnect
```
主动断开与ADXP数据中台的连接。
#### 健康检查
```
GET /api/adxp/health
```
检查适配器服务健康状态和ADXP连接状态。
#### WebSocket实时推送
```
ws://localhost:8086/ws/flight-notifications
```
实时接收来自ADXP数据中台的航班消息推送。
### 1. 登录
@ -43,6 +101,7 @@ Content-Type: application/json
```
响应:
```json
{
"success": true,
@ -58,6 +117,7 @@ GET /api/adxp/messages?sessionId=550e8400-e29b-41d4-a716-446655440000
```
响应:
```json
{
"success": true,
@ -90,6 +150,7 @@ GET /api/adxp/health
```
响应:
```json
{
"status": "UP",
@ -99,18 +160,36 @@ GET /api/adxp/health
## 快速测试
### API调用示例
#### 1. 检查连接状态
```bash
curl http://localhost:8086/api/adxp/status
```
#### 2. 强制重连
```bash
curl -X POST http://localhost:8086/api/adxp/reconnect
```
#### 3. 获取消息
```bash
curl http://localhost:8086/api/adxp/messages
```
#### 4. 断开连接
```bash
curl -X POST http://localhost:8086/api/adxp/disconnect
```
#### 5. 健康检查
```bash
# 健康检查
curl http://localhost:8086/api/adxp/health
# 预期输出: {"status":"UP","activeSessions":0}
# 登录
curl -X POST http://localhost:8086/api/adxp/login \
-H "Content-Type: application/json" \
-d '{"username":"dianxin","password":"dianxin@123"}'
# 接收消息(替换为实际的 sessionId
curl "http://localhost:8086/api/adxp/messages?sessionId=YOUR_SESSION_ID"
```
## 配置说明

View File

@ -7,8 +7,10 @@ services:
- "8086:8086"
environment:
# ADXP 数据中台连接配置(必须设置)
ADXP_HOST: ${ADXP_HOST:-10.10.10.100}
ADXP_PORT: ${ADXP_PORT:-7001}
ADXP_HOST: ${ADXP_HOST:-10.32.38.2}
ADXP_PORT: ${ADXP_PORT:-8081}
ADXP_USERNAME: ${ADXP_USERNAME:-dianxin}
ADXP_PASSWORD: ${ADXP_PASSWORD:-Dianxin#2025}
# Spring Profile
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod}
volumes:

View File

@ -26,51 +26,82 @@ public class AdxpController {
private AdxpWebSocketHandler adxpWebSocketHandler;
/**
* 登录接口
* 获取连接状态
*/
@PostMapping(value = "/login", produces = "application/json")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> getStatus() {
try {
String sessionId = adxpSdkService.login(request.getUsername(), request.getPassword());
return ResponseEntity.ok(LoginResponse.success(sessionId));
Map<String, Object> status = new HashMap<>();
status.put("connected", adxpSdkService.isConnected());
status.put("connectionInfo", adxpSdkService.getConnectionInfo());
status.put("timestamp", System.currentTimeMillis());
return ResponseEntity.ok(status);
} catch (Exception e) {
log.error("登录失败", e);
return ResponseEntity.ok(LoginResponse.failure(e.getMessage()));
log.error("获取连接状态失败", e);
Map<String, Object> error = new HashMap<>();
error.put("connected", false);
error.put("error", e.getMessage());
return ResponseEntity.ok(error);
}
}
/**
* 强制重连
*/
@PostMapping("/reconnect")
public ResponseEntity<Map<String, Object>> reconnect() {
try {
adxpSdkService.forceReconnect();
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "重连成功");
result.put("connected", adxpSdkService.isConnected());
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("重连失败", e);
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "重连失败: " + e.getMessage());
error.put("connected", false);
return ResponseEntity.ok(error);
}
}
/**
* 接收消息接口
* 获取消息接口 - 使用当前连接
*/
@GetMapping("/messages")
public ResponseEntity<MessageResponse> getMessages(@RequestParam String sessionId) {
public ResponseEntity<MessageResponse> getMessages() {
try {
List<FlightMessage> messages = adxpSdkService.receiveMessages(sessionId);
if (!adxpSdkService.isConnected()) {
return ResponseEntity.ok(MessageResponse.failure("未连接到ADXP数据中台"));
}
List<FlightMessage> messages = adxpSdkService.getMessages();
return ResponseEntity.ok(MessageResponse.success(messages));
} catch (Exception e) {
log.error("接收消息失败: sessionId={}", sessionId, e);
log.error("接收消息失败", e);
return ResponseEntity.ok(MessageResponse.failure(e.getMessage()));
}
}
/**
* 登出接口
* 断开连接接口
*/
@PostMapping("/logout")
public ResponseEntity<Map<String, Object>> logout(@RequestBody Map<String, String> request) {
@PostMapping("/disconnect")
public ResponseEntity<Map<String, Object>> disconnect() {
try {
String sessionId = request.get("sessionId");
adxpSdkService.logout(sessionId);
Map<String, Object> response = new HashMap<String, Object>();
adxpSdkService.disconnectFromADXPServer();
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "登出成功");
response.put("message", "已断开ADXP数据中台连接");
response.put("connected", false);
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("登出失败", e);
Map<String, Object> response = new HashMap<String, Object>();
log.error("断开连接失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
response.put("message", "断开连接失败: " + e.getMessage());
response.put("connected", adxpSdkService.isConnected());
return ResponseEntity.ok(response);
}
}
@ -80,12 +111,20 @@ public class AdxpController {
*/
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
Map<String, Object> health = new HashMap<String, Object>();
health.put("status", "UP");
health.put("activeSessions", adxpSdkService.getActiveSessionCount());
if (adxpWebSocketHandler != null) {
health.put("websocketConnections", adxpWebSocketHandler.getSessionCount());
try {
Map<String, Object> health = adxpSdkService.healthCheck();
if (adxpWebSocketHandler != null) {
health.put("websocketConnections", adxpWebSocketHandler.getSessionCount());
}
return ResponseEntity.ok(health);
} catch (Exception e) {
log.error("健康检查失败", e);
Map<String, Object> health = new HashMap<>();
health.put("status", "DOWN");
health.put("message", "健康检查失败: " + e.getMessage());
health.put("connected", false);
health.put("activeConnections", 0);
return ResponseEntity.ok(health);
}
return ResponseEntity.ok(health);
}
}

View File

@ -12,9 +12,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Service
public class AdxpSdkService {
@ -27,98 +31,200 @@ public class AdxpSdkService {
@Value("${adxp.port}")
private int port;
@Value("${adxp.username:}")
@Value("${adxp.username:dianxin}")
private String defaultUsername;
@Value("${adxp.password:}")
@Value("${adxp.password:Dianxin#2025}")
private String defaultPassword;
// Session 管理: sessionId -> SessionInfo
private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
public Map<String, SessionInfo> getSessions() {
return sessions;
}
// 连接状态管理
private volatile boolean connected = false;
private volatile String currentSessionId = null;
private ADXPClient currentClient = null;
public static class SessionInfo {
ADXPClient client;
String username;
String password;
SessionInfo(ADXPClient client, String username, String password) {
this.client = client;
this.username = username;
this.password = password;
}
public ADXPClient getClient() {
return client;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
// 定时重连任务
private final ScheduledExecutorService reconnectScheduler = Executors.newSingleThreadScheduledExecutor();
private final Object lock = new Object();
/**
* 登录数据中台
*/
public String login(String username, String password) {
// 如果未提供用户名或密码则使用配置文件中的默认值
String actualUsername = (username == null || username.isEmpty()) ? defaultUsername : username;
String actualPassword = (password == null || password.isEmpty()) ? defaultPassword : password;
// 应用启动时主动连接
@PostConstruct
public void initConnection() {
log.info("ADXP适配器启动主动连接数据中台...");
try {
log.info("正在登录 ADXP 数据中台: host={}, port={}, username={}", host, port, actualUsername);
// 创建 SDK 客户端
ADXPClient client = ADXPClientFactory.createWSClient(host, port);
// 调用登录
LoginResult result = client.login(actualUsername, actualPassword);
// 生成 session ID即使登录响应解析失败也创建 session
String sessionId = UUID.randomUUID().toString();
sessions.put(sessionId, new SessionInfo(client, actualUsername, actualPassword));
if (result == null || !Boolean.TRUE.equals(result.isSuccess())) {
String errorMsg = result != null ?
String.format("code=%s, message=%s", result.getCode(), result.getMessage()) :
"LoginResult is null (但 SDK 后台线程可能已启动)";
log.warn("登录响应解析失败: {} - 但仍创建 session尝试接收消息", errorMsg);
} else {
log.info("登录成功: sessionId={}", sessionId);
}
return sessionId;
connectToADXP();
// 启动重连监控
startReconnectMonitor();
} catch (Exception e) {
log.error("登录异常", e);
throw new RuntimeException("登录异常: " + e.getMessage(), e);
log.error("ADXP数据中台初始连接失败: {}", e.getMessage());
// 启动定时重连
scheduleReconnect();
}
}
// 应用关闭时清理资源
@PreDestroy
public void cleanup() {
log.info("ADXP适配器关闭清理资源...");
disconnectFromADXP();
reconnectScheduler.shutdown();
}
// 主动连接到ADXP数据中台
private void connectToADXP() {
synchronized (lock) {
try {
log.info("正在主动连接 ADXP 数据中台: host={}, port={}, username={}",
host, port, defaultUsername);
// 创建客户端
currentClient = ADXPClientFactory.createWSClient(host, port);
// 登录
LoginResult result = currentClient.login(defaultUsername, defaultPassword);
currentSessionId = UUID.randomUUID().toString();
connected = true;
if (result == null || !Boolean.TRUE.equals(result.isSuccess())) {
String errorMsg = result != null ?
String.format("code=%s, message=%s", result.getCode(), result.getMessage()) :
"登录响应为空";
log.warn("登录响应解析失败: {} - 但连接可能已建立", errorMsg);
} else {
log.info("ADXP数据中台连接成功! sessionId={}", currentSessionId);
}
// 启动消息监听
startMessageListener();
} catch (Exception e) {
connected = false;
currentClient = null;
currentSessionId = null;
log.error("ADXP数据中台连接失败: {}", e.getMessage());
throw new RuntimeException("ADXP连接失败", e);
}
}
}
// 断开连接
private void disconnectFromADXP() {
synchronized (lock) {
try {
if (currentClient != null) {
currentClient.logout(defaultUsername, defaultPassword);
}
connected = false;
currentSessionId = null;
currentClient = null;
log.info("已断开ADXP数据中台连接");
} catch (Exception e) {
log.error("断开连接时发生错误", e);
}
}
}
// 启动重连监控
private void startReconnectMonitor() {
// 这里可以添加连接健康检查
log.info("连接监控已启动");
}
// 定时重连任务
private void scheduleReconnect() {
reconnectScheduler.scheduleAtFixedRate(() -> {
if (!connected) {
try {
log.info("尝试重连ADXP数据中台...");
connectToADXP();
log.info("重连成功!");
} catch (Exception e) {
log.debug("重连失败: {}", e.getMessage());
}
}
}, 30, 30, TimeUnit.SECONDS); // 每30秒重连一次
}
/**
* 检查连接状态 - 供外部使用
*/
public boolean isConnected() {
return connected && currentClient != null;
}
/**
* 获取当前连接信息
*/
public String getConnectionInfo() {
if (connected) {
return String.format("已连接到ADXP数据中台 (host=%s, port=%s, sessionId=%s)",
host, port, currentSessionId);
} else {
return "未连接到ADXP数据中台";
}
}
/**
* 强制重连 - 供外部调用
*/
public void forceReconnect() {
log.info("外部请求强制重连ADXP数据中台");
disconnectFromADXP();
try {
connectToADXP();
log.info("强制重连成功");
} catch (Exception e) {
log.error("强制重连失败", e);
throw new RuntimeException("重连失败", e);
}
}
/**
* 接收消息
* 启动消息监听线程
*/
public List<FlightMessage> receiveMessages(String sessionId) {
SessionInfo sessionInfo = sessions.get(sessionId);
if (sessionInfo == null) {
throw new IllegalStateException("Session 不存在或已过期: " + sessionId);
private void startMessageListener() {
Thread messageListener = new Thread(() -> {
log.info("ADXP消息监听线程已启动");
while (connected && currentClient != null) {
try {
List<FlightMessage> messages = receiveMessagesInternal();
if (!messages.isEmpty()) {
log.info("接收到 {} 条消息", messages.size());
// 这里可以将消息发送到WebSocket或消息队列
processMessages(messages);
}
Thread.sleep(1000); // 每秒检查一次
} catch (InterruptedException e) {
log.info("消息监听线程被中断");
break;
} catch (Exception e) {
log.error("消息监听异常", e);
try {
Thread.sleep(5000); // 异常时等待5秒再重试
} catch (InterruptedException ie) {
break;
}
}
}
log.info("ADXP消息监听线程已结束");
}, "ADXP-Message-Listener");
messageListener.setDaemon(true);
messageListener.start();
}
/**
* 内部消息接收方法
*/
private List<FlightMessage> receiveMessagesInternal() {
if (!connected || currentClient == null) {
return Collections.emptyList();
}
try {
MessageResult result = sessionInfo.client.receiveMessage();
MessageResult result = currentClient.receiveMessage();
if (result == null || !Boolean.TRUE.equals(result.isSuccess())) {
String errorMsg = result != null ?
String.format("code=%s", result.getCode()) :
"MessageResult is null";
log.warn("接收消息失败: {}", errorMsg);
return Collections.emptyList();
}
@ -141,34 +247,77 @@ public class AdxpSdkService {
}
}
log.debug("接收到 {} 条消息", messages.size());
return messages;
} catch (Exception e) {
log.error("接收消息异常: sessionId={}", sessionId, e);
throw new RuntimeException("接收消息异常: " + e.getMessage(), e);
log.error("接收消息异常", e);
return Collections.emptyList();
}
}
/**
* 登出
* 处理接收到的消息
*/
public void logout(String sessionId) {
SessionInfo sessionInfo = sessions.remove(sessionId);
if (sessionInfo != null) {
try {
sessionInfo.client.logout(sessionInfo.username, sessionInfo.password);
log.info("登出成功: sessionId={}", sessionId);
} catch (Exception e) {
log.error("登出异常: sessionId={}", sessionId, e);
}
private void processMessages(List<FlightMessage> messages) {
// TODO: 实现消息处理逻辑
// 可以发送到消息队列WebSocket推送等
for (FlightMessage message : messages) {
log.debug("处理消息: serviceCode={}", message.getServiceCode());
}
}
/**
* 获取当前会话数
* 获取消息 - 供外部调用
*/
public List<FlightMessage> getMessages() {
return receiveMessagesInternal();
}
/**
* 断开连接 - 主动断开与ADXP数据中台的连接
* 注意在ADXP主动连接架构下适配器启动时自动连接
* 该方法主要用于管理连接状态或强制断开连接
*/
public void disconnectFromADXPServer() {
log.info("主动断开ADXP数据中台连接");
disconnectFromADXP();
}
/**
* 向后兼容的logout方法 - 调用disconnectFromADXPServer
* @deprecated 请使用 {@link #disconnectFromADXPServer()} 替代
*/
@Deprecated
public void logout() {
log.warn("logout()方法已废弃,请使用 disconnectFromADXPServer() 替代");
disconnectFromADXPServer();
}
/**
* 获取连接状态统计
*/
public int getActiveSessionCount() {
return sessions.size();
return connected ? 1 : 0;
}
/**
* 健康检查
*/
public Map<String, Object> healthCheck() {
Map<String, Object> health = new HashMap<>();
health.put("status", connected ? "UP" : "DOWN");
health.put("connected", connected);
health.put("host", host);
health.put("port", port);
health.put("sessionId", currentSessionId);
health.put("activeConnections", getActiveSessionCount());
if (connected) {
health.put("message", "ADXP数据中台连接正常");
} else {
health.put("message", "ADXP数据中台连接异常");
}
return health;
}
}

View File

@ -3,11 +3,15 @@
## 一、打包环境操作
### 1. 生成程序更新文件
修改代码后生成仅包含应用jar的更新包
```bash
./deploy/package-update.sh
```
**输出**
- `qaup-admin.jar` - 应用程序
- `UPDATE-INSTRUCTIONS.md` - 更新说明
- `VERSION-INFO.txt` - 版本信息
@ -15,7 +19,9 @@
**注意**如需单独jar文件可直接从更新包中提取。
### 2. 生成完整部署包
生成包含所有组件的完整部署包(首次部署或重大更新):
```bash
./deploy/package-all.sh
```
@ -27,19 +33,25 @@
### 1. 首次部署
#### 步骤
1. **解压部署包**
```bash
mkdir qaup-deploy
tar -xzf qaup-deploy-xxx.tar.gz -C qaup-deploy
cd qaup-deploy
tar -xzf qaup-deploy-xxx.tar.gz -C qaup-deploy
cd qaup-deploy
```
2. **执行部署**
```bash
./deploy-all.sh
```
3. **验证部署**
```bash
docker compose ps # 检查服务状态
curl http://localhost:8080/actuator/health # 验证应用健康
@ -48,6 +60,7 @@ cd qaup-deploy
### 2. 程序更新仅更新jar文件
#### 前置准备
```bash
cd qaup-deploy
@ -60,6 +73,7 @@ cp config.yml config.yml.backup
```
#### 更新步骤
```bash
# 1. 准备新版本文件(二选一)
# 方案A使用完整更新包
@ -78,7 +92,9 @@ cp /path/to/qaup-admin-xxx.jar new-app.jar
```
### 3. 配置文件更新
如果需要更新配置文件:
```bash
cd qaup-deploy
@ -95,9 +111,11 @@ vi config.yml
### 4. 更新失败处理
#### 自动回滚
更新失败时脚本会自动尝试回滚。
#### 手动回滚
```bash
cd qaup-deploy
@ -112,6 +130,7 @@ cp app.jar.backup.* app.jar
```
#### 问题排查
```bash
# 查看应用日志
docker compose logs qaup-app
@ -121,9 +140,11 @@ docker compose ps -a
```
### 5. 完全重新部署
如果需要重新部署整个系统(包括更新镜像、配置):
#### 方案A不保留数据
```bash
cd qaup-deploy
@ -136,6 +157,7 @@ rm -rf data/ # 注意:会删除所有数据
```
#### 方案B保留数据
```bash
# 1. 备份旧数据
cd qaup-deploy
@ -157,21 +179,34 @@ cd qaup-deploy-new
## 三、常见问题
### Q: 更新后无法访问系统?
A: 检查端口和应用日志:
```bash
docker compose logs qaup-app
netstat -tlnp | grep 8080
```
### Q: 端口冲突?
A: 检查端口占用:
```bash
netstat -tlnp | grep 8080
```
### Q: 数据库连接失败?
A: 检查数据库服务状态:
```bash
docker compose logs qaup-postgres
docker exec qaup-postgres pg_isready -U qaup
```
## 四、更新记录
建议记录每次更新信息:
```
时间 | 版本号 | 人员 | 类型 | 结果 | 问题
2025-01-05 | v1.0.1→v1.0.2 | 张三 | 程序更新 | 成功 | -
@ -181,14 +216,17 @@ docker exec qaup-postgres pg_isready -U qaup
## 五、数据库部署和更新策略
### 1. 数据库版本管理
系统使用Flyway进行数据库版本管理所有迁移脚本位于`qaup-admin/src/main/resources/db/migration/`目录下。
**迁移脚本列表**
- `V1.0.0__Initial_baseline.sql` - 基线结构86KB
- `V1.0.1__Initial_data.sql` - 初始数据50KB
- `README.md` - 迁移脚本说明
#### Flyway配置在docker-compose.yml中
```yaml
SPRING_FLYWAY_ENABLED: true # 启用Flyway
SPRING_FLYWAY_BASELINE_ON_MIGRATE: true # 基线迁移(支持已有数据库)
@ -200,6 +238,7 @@ SPRING_FLYWAY_LOCATIONS: classpath:db/migration # 迁移脚本路径
### 2. 数据库部署流程
#### 首次部署(全新环境)
```bash
# 1. 启动基础服务PostgreSQL + Redis
docker compose up -d qaup-postgres qaup-redis
@ -217,6 +256,7 @@ curl http://localhost:8080/actuator/health
```
#### 已有环境更新
```bash
# Flyway自动处理数据库迁移
# 应用启动时会自动检测并执行需要的迁移
@ -229,12 +269,14 @@ docker exec qaup-postgres psql -U qaup -d qaup -c "SELECT * FROM flyway_schema_h
### 3. 数据库迁移脚本管理
#### 脚本命名规范
- `V{版本号}__{描述}.sql` - 版本化迁移脚本
- `V1.0.0__Initial_baseline.sql` - 基线结构
- `V1.0.1__Initial_data.sql` - 初始数据
- `V1.0.2__Add_new_feature.sql` - 新功能
#### 迁移类型
1. **结构迁移** - 修改表结构、索引等
2. **数据迁移** - 数据清洗、转换等
3. **函数/存储过程迁移** - 业务逻辑更新
@ -242,6 +284,7 @@ docker exec qaup-postgres psql -U qaup -d qaup -c "SELECT * FROM flyway_schema_h
### 4. 数据库维护操作
#### 数据备份和恢复(仅限紧急情况)
```bash
# 备份数据
docker exec qaup-postgres pg_dump -U qaup qaup > backup-$(date +%Y%m%d_%H%M%S).sql
@ -251,6 +294,7 @@ cat backup-20250115_143000.sql | docker exec -i qaup-postgres psql -U qaup -d qa
```
#### 数据库状态监控
```bash
# 查看Flyway迁移状态应用启动日志
docker compose logs qaup-app | grep -i flyway
@ -268,15 +312,18 @@ curl http://localhost:8080/actuator/health
### 5. 数据库更新最佳实践
#### 开发环境
- 可以使用`flyway.clean()`清理数据库
- 可以使用`flyway.migrate().clean()`重建
#### 生产环境
- 禁止使用`flyway.clean()`
- 优先使用增量迁移脚本
- 重大变更需要测试环境验证
#### 回滚策略
1. **自动化回滚** - 备份恢复
2. **脚本化回滚** - 创建回滚脚本
3. **版本控制** - 通过Flyway版本管理
@ -284,15 +331,18 @@ curl http://localhost:8080/actuator/health
## 六、文件说明
### 打包环境脚本
- `deploy/package-update.sh` - 生成程序更新包
- `deploy/package-all.sh` - 生成完整部署包
### 生产环境脚本
- `deploy-all.sh` - 首次部署/完全重新部署
- `deploy-update.sh` - 程序更新脚本(自动处理数据库迁移)
- `DeployGuide.md` - 本说明文档
### 数据库脚本目录
- `src/main/resources/db/migration/` - Flyway迁移脚本
- `sql/qaup_database_complete_init.sql` - 完整初始化脚本(参考用途)
- `sql/unified_database_migration.sql` - 合并迁移脚本(历史用途)
- `sql/unified_database_migration.sql` - 合并迁移脚本(历史用途)

View File

@ -1,4 +1,5 @@
# QAUP 极简离线部署方案
Spring Boot + Redis + PostgreSQL with PostGIS一键部署、一键升级
## 1. 设计原则
@ -12,6 +13,7 @@ Spring Boot + Redis + PostgreSQL with PostGIS一键部署、一键升级
## 2. 打包准备(开发环境执行一次)
### 2.1 打包脚本package-all.sh
```bash
#!/bin/bash
echo "=== QAUP 打包脚本 ==="
@ -42,6 +44,7 @@ echo "打包完成: qaup-deploy-$(date +%Y%m%d-%H%M%S).tar.gz"
```
### 2.2 部署包结构
```
qaup-deploy.tar.gz
├── images.tar.gz # Docker镜像包PostGIS + Redis + Java21
@ -55,6 +58,7 @@ qaup-deploy.tar.gz
```
## 3. docker-compose.yml当前实际版本
```yaml
services:
# PostgreSQL + PostGIS 数据库服务
@ -143,6 +147,7 @@ networks:
```
## 4. 外部配置文件config.yml
```yaml
# 服务器配置
server:
@ -172,6 +177,7 @@ logging:
## 5. 自动化数据库迁移Flyway
### 5.1 迁移脚本位置
```
qaup-admin/src/main/resources/db/migration/
├── V1.0.0__Initial_baseline.sql # 数据库基线结构86KB
@ -180,12 +186,14 @@ qaup-admin/src/main/resources/db/migration/
```
### 5.2 自动迁移机制
- **应用启动时自动执行**无需手动运行SQL脚本
- **版本控制**:所有迁移按版本顺序执行
- **幂等性**:安全重复执行,不会重复创建对象
- **健康检查**:依赖数据库健康状态启动应用
### 5.3 迁移状态查询
```bash
# 查看应用日志中的迁移信息
docker compose logs qaup-app | grep -i flyway
@ -195,6 +203,7 @@ docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT * FROM flyway_sche
```
## 6. 一键部署deploy-all.sh
```bash
#!/bin/bash
echo "=== QAUP 一键部署 ==="
@ -241,11 +250,13 @@ fi
```
## 7. 客户部署1条命令
```bash
tar -xzf qaup-deploy.tar.gz && cd qaup-deploy && ./deploy-all.sh
```
## 8. 一键升级deploy-update.sh
```bash
#!/bin/bash
echo "=== QAUP 一键升级 ==="
@ -311,11 +322,14 @@ fi
## 9. Java版本兼容性注意事项
### 9.1 编译与运行环境匹配
- **编译环境**: 确保项目在Java 21环境下编译
- **运行环境**: Docker镜像使用 `eclipse-temurin:21-jre`
### 9.2 常见版本错误
如果遇到 `UnsupportedClassVersionError`,确认以下配置:
```yaml
# docker-compose.yml 中必须使用Java 21镜像
services:
@ -325,6 +339,7 @@ services:
```
### 9.3 版本验证命令
```bash
# 检查容器中Java版本
docker exec qaup-app java -version
@ -339,6 +354,7 @@ curl http://localhost:8080/actuator/health
## 10. 系统维护操作
### 10.1 日常运维命令
```bash
# 查看服务状态
docker compose ps
@ -363,6 +379,7 @@ docker exec -it qaup-redis redis-cli ping
```
### 10.2 监控数据库迁移状态
```bash
# 查看Flyway迁移历史
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT * FROM flyway_schema_history ORDER BY installed_rank;"
@ -375,6 +392,7 @@ docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT version, descripti
```
### 10.3 数据备份和恢复
```bash
# 手动备份数据库
docker exec qaup-postgres pg_dump -U qaup qaup > backup/manual-backup-$(date +%Y%m%d_%H%M%S).sql
@ -386,6 +404,7 @@ docker compose start qaup-app
```
### 10.4 完全重置(开发测试用)
```bash
# 停止所有服务
docker compose down
@ -400,6 +419,7 @@ rm -rf data/
## 11. 操作总结
### 开发环境(一次性)
```bash
# 在macOS上构建
mvn clean package -DskipTests
@ -410,6 +430,7 @@ mvn clean package -DskipTests
```
### 客户环境
```bash
# 首次部署
tar -xzf qaup-deploy.tar.gz && cd qaup-deploy && ./deploy-all.sh

View File

@ -0,0 +1,696 @@
# QAUP系统部署指南
本文档提供了QAUP系统的完整部署指南包括环境配置、服务启动、监控和故障排除。
## 目录结构
- [1. 系统架构概述](#1-系统架构概述)
- [2. 环境准备](#2-环境准备)
- [3. 配置管理](#3-配置管理)
- [4. 服务部署](#4-服务部署)
- [5. 监控与维护](#6-监控与维护)
- [6. 故障排除](#7-故障排除)
- [7. 性能优化](#8-性能优化)
## 1. 系统架构概述
QAUP系统采用微服务架构主要组件包括
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 前端界面 │ │ 后端服务 │ │ ADXP适配器 │
│ (qaup-ui) │◄──►│ (qaup-collision) │◄──►│ (adxp-adapter) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 外部接口 │ │
│ │ (地图、车辆) │ │
│ └─────────────────┘ │
│ │ │
└───────────────────────┼───────────────────────┘
┌─────────────────┐ ┌─────────────────┐
│ 数据存储 │ │ 缓存服务 │
│ (PostgreSQL) │ │ (Redis) │
└─────────────────┘ └─────────────────┘
```
### 核心服务
- **qaup-collision**: 核心碰撞检测和数据处理服务
- **adxp-adapter**: ADXP数据适配器负责与机场系统通信
- **qaup-admin**: 管理后台服务
- **qaup-ui**: 前端用户界面
## 2. 环境准备
### 2.1 系统要求
#### 最低配置
- **操作系统**: Linux (Ubuntu 20.04+) / Windows Server 2019+
- **内存**: 8GB RAM
- **存储**: 50GB SSD
- **网络**: 千兆以太网
#### 推荐配置
- **操作系统**: Linux (Ubuntu 22.04 LTS)
- **内存**: 16GB RAM 或更高
- **存储**: 100GB SSD
- **网络**: 千兆以太网
### 2.2 软件依赖
```bash
# 必需软件版本
- Docker: 24.0+
- Docker Compose: 2.20+
- JDK: 21+ (OpenJDK 推荐)
- Maven: 3.8+
- Node.js: 18+ (用于前端构建)
```
### 2.3 端口分配
| 服务 | 端口 | 描述 |
|-----|------|------|
| qaup-collision | 8080 | 核心服务API |
| qaup-admin | 8081 | 管理后台API |
| qaup-ui | 80/443 | 前端界面 |
| adxp-adapter | 8086 | ADXP适配器 |
| PostgreSQL | 5432 | 数据库服务 |
| Redis | 6379 | 缓存服务 |
## 3. 配置管理
### 3.1 环境变量配置
在项目根目录创建 `.env` 文件:
```bash
# ============================================
# QAUP系统环境变量配置
# ============================================
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_NAME=qaup
DB_USERNAME=qaup
DB_PASSWORD=qaup123
# Redis配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# 应用服务配置
SERVER_PORT=8080
ADMIN_PORT=8081
ADAPTER_PORT=8086
# ADXP适配器配置
ADXP_SERVER_URL=http://localhost:8090
ADXP_USERNAME=admin
ADXP_PASSWORD=admin123
ADXP_ENABLED=true
# 日志配置
LOG_LEVEL=INFO
LOG_PATH=/app/logs
# 数据采集配置
DATA_COLLECTOR_INTERVAL=250
DATA_COLLECTOR_ENABLED=true
# 外部服务配置
AIRPORT_VEHICLE_API_URL=http://10.32.38.3:8090
MAP_SERVICE_URL=http://221.215.103.144:8090/iserver/services/map-QDJC_DT-GX3/rest/maps
# 安全配置
JWT_SECRET=your-jwt-secret-key-here
ENCRYPT_KEY=your-encryption-key-here
# 监控配置
ACTUATOR_ENABLED=true
HEALTH_CHECK_INTERVAL=30
```
### 3.2 配置文件结构
```
config/
├── application.yml # 主配置文件
├── application-dev.yml # 开发环境配置
├── application-test.yml # 测试环境配置
├── application-prod.yml # 生产环境配置
├── application-druid.yml # 数据库连接池配置
└── logback-spring.xml # 日志配置
```
### 3.3 敏感信息管理
```bash
# 使用密钥管理敏感配置
docker secret create db_password db_password.txt
docker secret create jwt_secret jwt_secret.txt
# 在docker-compose.yml中引用
secrets:
- db_password
- jwt_secret
```
## 4. 服务部署
### 4.1 快速启动
使用提供的服务管理脚本:
```bash
# 进入部署目录
cd deploy/
# 启动所有服务
./qaup-service.sh start
# 查看服务状态
./qaup-service.sh status
# 查看服务日志
./qaup-service.sh logs qaup-app
```
### 4.2 手动部署步骤
#### 4.2.1 数据库初始化
```bash
# 启动数据库服务
docker compose up -d postgres
# 等待数据库启动
docker exec qaup-postgres pg_isready -U qaup
# 执行数据库迁移
docker exec qaup-app java -jar qaup-admin.jar --spring.profiles.active=prod,druid --spring.jpa.hibernate.ddl-auto=update
```
#### 4.2.2 应用服务部署
```bash
# 构建应用镜像
docker compose build qaup-app
# 启动应用服务
docker compose up -d qaup-app
# 等待应用启动
curl http://localhost:8080/actuator/health
```
#### 4.2.3 ADXP适配器部署
```bash
# 构建ADXP适配器镜像
docker compose -f docker-compose.yml -f docker-compose.adxp.yml build adxp-adapter
# 启动ADXP适配器
docker compose -f docker-compose.yml -f docker-compose.adxp.yml up -d adxp-adapter
# 检查适配器状态
curl http://localhost:8086/health
```
### 4.3 Docker Compose配置
创建 `docker-compose.yml`
```yaml
version: '3.8'
services:
postgres:
image: postgis/postgis:15-3.3
container_name: qaup-postgres
environment:
POSTGRES_DB: ${DB_NAME:-qaup}
POSTGRES_USER: ${DB_USERNAME:-qaup}
POSTGRES_PASSWORD: ${DB_PASSWORD:-qaup123}
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./sql/init:/docker-entrypoint-initdb.d
networks:
- qaup-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-qaup}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: qaup-redis
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
networks:
- qaup-network
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
qaup-app:
build:
context: ../qaup-collision
dockerfile: Dockerfile
container_name: qaup-app
environment:
SPRING_PROFILES_ACTIVE: prod,druid
DB_HOST: postgres
REDIS_HOST: redis
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
ports:
- "${SERVER_PORT:-8080}:8080"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- qaup-network
volumes:
- ./logs:/app/logs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
postgres_data:
redis_data:
networks:
qaup-network:
driver: bridge
```
## 5. 前端部署
### 5.1 前端构建
```bash
# 进入前端目录
cd qaup-ui/
# 安装依赖
npm install
# 构建生产版本
npm run build:prod
# 使用Nginx部署
docker run -d -p 80:80 -v $(pwd)/dist:/usr/share/nginx/html nginx:alpine
```
### 5.2 Nginx配置
创建 `nginx.conf`
```nginx
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 前端路由支持
location / {
try_files $uri $uri/ /index.html;
}
# API代理
location /api/ {
proxy_pass http://qaup-app:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket支持
location /ws/ {
proxy_pass http://qaup-app:8080/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
```
## 6. 监控与维护
### 6.1 健康检查
```bash
# 脚本化健康检查
#!/bin/bash
echo "QAUP系统健康检查 - $(date)"
services=("qaup-app:8080" "qaup-admin:8081" "adxp-adapter:8086")
for service in "${services[@]}"; do
name=$(echo $service | cut -d: -f1)
port=$(echo $service | cut -d: -f2)
if curl -f -s http://localhost:$port/actuator/health > /dev/null; then
echo "✓ $name 服务正常"
else
echo "✗ $name 服务异常"
fi
done
# 检查数据库连接
if docker exec qaup-postgres pg_isready -U qaup > /dev/null; then
echo "✓ 数据库连接正常"
else
echo "✗ 数据库连接异常"
fi
# 检查Redis连接
if docker exec qaup-redis redis-cli ping > /dev/null; then
echo "✓ Redis连接正常"
else
echo "✗ Redis连接异常"
fi
```
### 6.2 日志管理
```bash
# 日志轮转配置
# /etc/logrotate.d/qaup
/app/logs/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 644 qaup qaup
postrotate
docker kill -s USR1 qaup-app
endscript
}
```
### 6.3 性能监控
使用Actuator端点监控
```bash
# 应用指标
curl http://localhost:8080/actuator/metrics
# 数据库连接池监控
curl http://localhost:8080/actuator/druid
# 系统信息
curl http://localhost:8080/actuator/info
```
## 7. 故障排除
### 7.1 常见问题
#### 服务启动失败
```bash
# 检查端口占用
netstat -tlnp | grep :8080
# 检查Docker容器日志
docker logs qaup-app --tail 100
# 检查资源使用
docker stats
```
#### 数据库连接问题
```bash
# 检查数据库状态
docker exec qaup-postgres pg_isready -U qaup
# 检查网络连接
docker exec qaup-app ping postgres
# 检查数据库日志
docker logs qaup-postgres --tail 50
```
#### ADXP适配器问题
```bash
# 检查适配器配置
docker exec adxp-adapter cat /app/config/adxp.properties
# 测试ADXP连接
curl -X GET "http://localhost:8090/api/health" \
-H "Authorization: Bearer $ADXP_TOKEN"
# 检查消息队列
docker exec qaup-app java -jar qaup-admin.jar \
--spring.profiles.active=test \
--adxp.test.connection=true
```
### 7.2 紧急恢复
```bash
# 快速重启所有服务
./qaup-service.sh restart
# 重建数据库(慎用)
docker-compose down -v
docker volume prune -f
./qaup-service.sh start
# 从备份恢复
docker exec -i qaup-postgres psql -U qaup -d qaup < backup.sql
```
### 7.3 调试模式
```bash
# 启用调试日志
export LOG_LEVEL=DEBUG
./qaup-service.sh restart
# 进入容器调试
docker exec -it qaup-app /bin/bash
# 查看实时日志
tail -f /app/logs/qaup-app.log | grep -E "(ERROR|WARN|Exception)"
```
## 8. 性能优化
### 8.1 JVM调优
```bash
# 生产环境JVM参数
JAVA_OPTS="
-Xms2g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+UnlockExperimentalVMOptions
-XX:+UseJVMCICompiler
-Djava.security.egd=file:/dev/./urandom
-Dspring.jmx.enabled=false
"
```
### 8.2 数据库优化
```sql
-- PostgreSQL配置优化
ALTER SYSTEM SET shared_buffers = '512MB';
ALTER SYSTEM SET effective_cache_size = '2GB';
ALTER SYSTEM SET maintenance_work_mem = '64MB';
ALTER SYSTEM SET checkpoint_completion_target = 0.9;
ALTER SYSTEM SET wal_buffers = '16MB';
ALTER SYSTEM SET default_statistics_target = 100;
ALTER SYSTEM SET random_page_cost = 1.1;
ALTER SYSTEM SET effective_io_concurrency = 200;
SELECT pg_reload_conf();
```
### 8.3 Redis优化
```bash
# Redis配置优化
maxmemory 512mb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000
```
## 9. 安全配置
### 9.1 网络安全
```yaml
# docker-compose.yml中的网络安全配置
networks:
qaup-network:
driver: bridge
internal: true # 内部网络
```
### 9.2 SSL/TLS配置
```nginx
# Nginx SSL配置
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /etc/ssl/certs/qaup.crt;
ssl_certificate_key /etc/ssl/private/qaup.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
}
```
### 9.3 访问控制
```java
// Spring Security配置示例
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
}
```
## 10. 备份与恢复
### 10.1 数据备份
```bash
#!/bin/bash
# backup.sh - 数据备份脚本
BACKUP_DIR="/backup/qaup/$(date +%Y%m%d_%H%M%S)"
mkdir -p $BACKUP_DIR
# 数据库备份
docker exec qaup-postgres pg_dump -U qaup qaup > $BACKUP_DIR/database.sql
# 配置文件备份
cp .env $BACKUP_DIR/
cp -r config/ $BACKUP_DIR/
# 日志备份
tar -czf $BACKUP_DIR/logs.tar.gz /app/logs/
echo "备份完成: $BACKUP_DIR"
```
### 10.2 数据恢复
```bash
#!/bin/bash
# restore.sh - 数据恢复脚本
BACKUP_FILE=$1
if [ -z "$BACKUP_FILE" ]; then
echo "使用方法: ./restore.sh <backup_file>"
exit 1
fi
# 停止应用服务
docker compose stop qaup-app
# 恢复数据库
docker exec -i qaup-postgres psql -U qaup qaup < $BACKUP_FILE/database.sql
# 重启服务
docker compose start qaup-app
echo "恢复完成"
```
## 附录
### A. 部署检查清单
- [ ] 系统环境准备完成
- [ ] Docker和依赖软件安装
- [ ] 配置文件准备和验证
- [ ] 数据库初始化完成
- [ ] 所有服务启动成功
- [ ] 健康检查通过
- [ ] 前端访问正常
- [ ] 监控和日志配置完成
- [ ] 备份策略实施
### B. 紧急联系信息
- **技术支持**: <tech-support@qaup.com>
- **运维团队**: <ops@qaup.com>
- **值班电话**: +86-xxx-xxxx-xxxx
### C. 相关文档
- [API文档](./api_documentation.md)
- [配置指南](./configuration_guide.md)
- [JDK21升级指南](./JDK21-升级指南.md)
- [环境配置](./environment.md)
---
**文档版本**: v2.0
**最后更新**: 2025-01-17
**维护者**: QAUP Development Team

131
qaup-admin/.env.example Normal file
View File

@ -0,0 +1,131 @@
# ============================================================
# QAUP 主应用环境变量配置模板
# 使用说明:
# 1. 复制此文件为 .env: cp .env.example .env
# 2. 修改 .env 中的配置值
# 3. 启动应用时会自动加载 .env 文件中的环境变量
# ============================================================
# ========== 服务器配置 ==========
# 服务器端口
SERVER_PORT=8080
# 应用环境标识
SPRING_PROFILES_ACTIVE=prod,druid
# ========== 数据库配置 ==========
# 数据库连接信息
DATABASE_URL=jdbc:postgresql://localhost:5432/qaup
DATABASE_USERNAME=qaup
DATABASE_PASSWORD=your_password_here
DATABASE_DRIVER=org.postgresql.Driver
# 数据库连接池配置
DATABASE_POOL_MAX_ACTIVE=50
DATABASE_POOL_MAX_IDLE=20
DATABASE_POOL_MIN_IDLE=5
DATABASE_POOL_MAX_WAIT=60000
# ========== Redis配置 ==========
# Redis连接配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DATABASE=0
REDIS_PASSWORD=
REDIS_MAX_MEMORY=1gb
REDIS_MAX_MEMORY_POLICY=volatile-lru
# ========== 日志配置 ==========
# 应用日志级别
LOG_LEVEL_QAUP=info
LOG_LEVEL_SPRING=warn
LOG_LEVEL_HIBERNATE=warn
LOG_LEVEL_DRUID=warn
# ========== ADXP适配器配置 ==========
# ADXP适配器服务地址
ADXP_HOST=localhost
ADXP_PORT=8086
# 连接重试配置
RECONNECT_DELAY_MILLIS=3000
# ========== 机场API配置 ==========
# 机场数据源API
AIRPORT_API_BASE_URL=http://localhost:8090
AIRPORT_API_USERNAME=dianxin
AIRPORT_API_PASSWORD=dianxin@123
# 滑行路由API
AIRPORT_API_GLIDE_URL=http://localhost:8099
# ========== 无人车API配置 ==========
# 无人车厂商数据源
VEHICLE_API_BASE_URL=http://localhost:8091
VEHICLE_API_TIMEOUT=1000
VEHICLE_API_RETRY=3
# ========== 数据采集配置 ==========
# 数据采集间隔(毫秒)
DATA_COLLECTOR_INTERVAL=250
# 检测间隔(毫秒)
DATA_DETECTION_INTERVAL=1000
# ========== 文件上传配置 ==========
# 文件上传路径
UPLOAD_PATH=/app/uploadPath
# 文件上传大小限制
UPLOAD_MAX_FILE_SIZE=10MB
UPLOAD_MAX_REQUEST_SIZE=20MB
# ========== Token配置 ==========
# JWT令牌密钥
TOKEN_SECRET=abcdefghijklmnopqrstuvwxyz
# 令牌过期时间(分钟)
TOKEN_EXPIRE_TIME=30
# ========== 安全配置 ==========
# 密码最大错误次数
PASSWORD_MAX_RETRY_COUNT=5
# 密码锁定时间(分钟)
PASSWORD_LOCK_TIME=10
# ========== 性能调优配置 ==========
# Tomcat线程配置
TOMCAT_MAX_THREADS=800
TOMCAT_MIN_SPARE_THREADS=100
TOMCAT_ACCEPT_COUNT=1000
# MyBatis批处理配置
MYBATIS_BATCH_SIZE=50
MYBATIS_FETCH_SIZE=50
# ========== 监控和健康检查 ==========
# Actuator健康检查配置
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=health,info,metrics,loggers
MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS=simple
# ========== 生产环境优化配置 ==========
# 生产环境建议配置:
# SPRING_PROFILES_ACTIVE=prod,druid
# DATABASE_URL=jdbc:postgresql://your-db-host:5432/qaup
# DATABASE_PASSWORD=your_secure_password
# REDIS_HOST=your-redis-host
# REDIS_MAX_MEMORY=2gb
# LOG_LEVEL_QAUP=info
# UPLOAD_PATH=/home/qaup/uploadPath
# TOKEN_SECRET=your_very_long_and_secure_secret_key
# AIRPORT_API_BASE_URL=http://your-airport-api-host:8090
# VEHICLE_API_BASE_URL=http://your-vehicle-api-host:8091
# ============================================================
# 注意事项:
# 1. 等号两边不要有空格
# 2. 密码等敏感信息请妥善保管
# 3. #开头的行为注释
# 4. 请勿将 .env 文件提交到Git仓库
# 5. 生产环境请务必使用强密码和HTTPS
# 6. 根据实际部署环境调整各项配置值
# ============================================================

View File

@ -43,6 +43,7 @@ public class QuapApplication
* 配置Spring MVC使用虚拟线程处理请求
* JDK21虚拟线程可以显著提高并发性能
*/
@SuppressWarnings("null")
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());

View File

@ -83,9 +83,9 @@ data:
# 适配器端口
port: ${ADXP_PORT:8086}
# 登录用户名
username: ${ADXP_USERNAME}
username: ${ADXP_USERNAME:dianxin}
# 登录密码
password: ${ADXP_PASSWORD}
password: ${ADXP_PASSWORD:Dianxin#2025}
# 重连延迟(毫秒)
reconnect-delay-millis: ${RECONNECT_DELAY_MILLIS:3000}

View File

@ -42,7 +42,7 @@ public class AdxpFlightServiceHttpClient implements org.springframework.beans.fa
private final RestTemplate restTemplate;
private final ReentrantLock sessionLock = new ReentrantLock();
private String sessionId;
private String baseUrl;
public AdxpFlightServiceHttpClient(FlightSdkProperties properties) {
@ -62,91 +62,39 @@ public class AdxpFlightServiceHttpClient implements org.springframework.beans.fa
}
this.baseUrl = String.format("http://%s:%d/api/adxp", properties.getHost(), properties.getPort());
tryLogin();
log.info("ADXP HTTP客户端已初始化等待连接状态确认");
}
@Override
public void destroy() {
sessionLock.lock();
try {
if (sessionId != null) {
logout();
}
} finally {
sessionId = null;
sessionLock.unlock();
}
log.info("ADXP HTTP客户端已销毁");
}
private void tryLogin() {
sessionLock.lock();
private void disconnect() {
try {
log.info("正在登录 ADXP 适配器服务: url={}", baseUrl);
// 登录适配器服务适配器会使用这些认证信息连接真实数据中台
Map<String, Object> loginRequest = new HashMap<>();
loginRequest.put("username", properties.getUsername());
loginRequest.put("password", properties.getPassword());
log.info("正在断开 ADXP 适配器连接");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(loginRequest, headers);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(new HashMap<>(), headers);
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
baseUrl + "/login",
restTemplate.exchange(
baseUrl + "/disconnect",
HttpMethod.POST,
entity,
new ParameterizedTypeReference<Map<String, Object>>() {}
);
Map<String, Object> body = response.getBody();
if (body != null && Boolean.TRUE.equals(body.get("success"))) {
this.sessionId = (String) body.get("sessionId");
log.info("已登录 ADXP 适配器服务: sessionId={}", sessionId);
} else {
String message = body != null ? (String) body.get("message") : "Unknown error";
log.warn("登录 ADXP 适配器服务失败: {}", message);
}
log.info("已断开 ADXP 适配器连接");
} catch (Exception e) {
log.warn("登录 ADXP 适配器服务失败,适配器服务将暂时不可用", e);
} finally {
sessionLock.unlock();
}
}
private void logout() {
try {
log.info("正在登出 ADXP 适配器服务: sessionId={}", sessionId);
Map<String, String> logoutRequest = new HashMap<>();
logoutRequest.put("sessionId", sessionId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> entity = new HttpEntity<>(logoutRequest, headers);
restTemplate.exchange(
baseUrl + "/logout",
HttpMethod.POST,
entity,
new ParameterizedTypeReference<Map<String, String>>() {}
);
log.info("已登出 ADXP 适配器服务");
} catch (Exception e) {
log.warn("登出 ADXP 适配器服务失败", e);
log.warn("断开 ADXP 适配器连接失败", e);
}
}
public List<FlightNotificationDTO> fetchFlightNotifications() {
if (sessionId == null) {
return Collections.emptyList();
}
sessionLock.lock();
try {
String url = baseUrl + "/messages?sessionId=" + sessionId;
String url = baseUrl + "/messages";
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
url,
HttpMethod.GET,
@ -158,11 +106,6 @@ public class AdxpFlightServiceHttpClient implements org.springframework.beans.fa
if (body == null || !Boolean.TRUE.equals(body.get("success"))) {
String message = body != null ? (String) body.get("message") : "Unknown error";
log.warn("接收消息失败: {}", message);
// 如果是 session 过期尝试重新登录
if (message != null && message.contains("Session")) {
tryLogin();
}
return Collections.emptyList();
}
@ -313,6 +256,60 @@ public class AdxpFlightServiceHttpClient implements org.springframework.beans.fa
}
public boolean isEnabled() {
return sessionId != null;
// 检查适配器是否连接到了ADXP数据中台
return isConnected();
}
/**
* 检查ADXP适配器连接状态
*/
public boolean isConnected() {
try {
String url = baseUrl + "/status";
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<Map<String, Object>>() {}
);
Map<String, Object> body = response.getBody();
return body != null && Boolean.TRUE.equals(body.get("connected"));
} catch (Exception e) {
log.warn("检查ADXP适配器连接状态失败", e);
return false;
}
}
/**
* 强制重连ADXP适配器
*/
public boolean reconnect() {
try {
String url = baseUrl + "/reconnect";
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
url,
HttpMethod.POST,
null,
new ParameterizedTypeReference<Map<String, Object>>() {}
);
Map<String, Object> body = response.getBody();
boolean success = body != null && Boolean.TRUE.equals(body.get("success"));
if (success) {
log.info("✅ 已强制重连ADXP适配器");
} else {
String message = body != null ? (String) body.get("message") : "Unknown error";
log.warn("❌ 重连ADXP适配器失败: {}", message);
}
return success;
} catch (Exception e) {
log.error("❌ 重连ADXP适配器时发生异常", e);
return false;
}
}
}

View File

@ -35,7 +35,7 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
private final AtomicLong errorCount = new AtomicLong(0);
private WebSocketSession session;
private String sessionId;
private Thread reconnectThread;
public AdxpFlightServiceWebSocketClient(WebSocketClient webSocketClient,
@ -64,7 +64,7 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
public void stop() {
if (isRunning.compareAndSet(true, false)) {
log.info("停止ADXP航班通知WebSocket客户端");
disconnect();
disconnectWebSocket();
}
}
@ -79,15 +79,7 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
}
try {
// 首先通过HTTP登录获取sessionId
sessionId = loginAndGetSessionId();
if (sessionId == null) {
log.error("无法获取sessionIdWebSocket连接失败");
scheduleReconnect();
return;
}
// 构建WebSocket URL
// 构建WebSocket URL - 新架构下不需要sessionId
String wsUrl = String.format("ws://%s:%d/ws/flight-notifications",
properties.getHost(), properties.getPort());
@ -111,68 +103,20 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
}
}
/**
* 通过HTTP登录获取sessionId
*/
private String loginAndGetSessionId() {
try {
String baseUrl = String.format("http://%s:%d/api/adxp",
properties.getHost(), properties.getPort());
// 创建登录请求
java.util.Map<String, Object> loginRequest = new java.util.HashMap<>();
loginRequest.put("username", properties.getUsername());
loginRequest.put("password", properties.getPassword());
// 发送HTTP POST请求
java.net.http.HttpClient httpClient = java.net.http.HttpClient.newHttpClient();
String loginUrl = baseUrl + "/login";
String requestBody = objectMapper.writeValueAsString(loginRequest);
java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder()
.uri(URI.create(loginUrl))
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(requestBody))
.build();
java.net.http.HttpResponse<String> response = httpClient.send(request,
java.net.http.HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
java.util.Map<String, Object> responseBody = objectMapper.readValue(
response.body(), new TypeReference<java.util.Map<String, Object>>() {});
if (Boolean.TRUE.equals(responseBody.get("success"))) {
String sessionId = (String) responseBody.get("sessionId");
log.info("✅ 登录ADXP适配器成功: sessionId={}", sessionId);
return sessionId;
} else {
String message = (String) responseBody.get("message");
log.warn("❌ 登录ADXP适配器失败: {}", message);
}
} else {
log.warn("❌ 登录ADXP适配器HTTP请求失败: status={}", response.statusCode());
}
} catch (Exception e) {
log.error("❌ 登录ADXP适配器时发生异常", e);
errorCount.incrementAndGet();
}
return null;
}
/**
* 断开WebSocket连接
*/
public void disconnect() {
public void disconnectWebSocket() {
try {
if (session != null && session.isOpen()) {
session.close();
log.info("WebSocket连接已关闭");
}
// 尝试登出
logout();
// 断开ADXP适配器连接
disconnectADXP();
} catch (Exception e) {
log.error("断开WebSocket连接时发生异常", e);
errorCount.incrementAndGet();
@ -183,36 +127,27 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
}
/**
* 登出
* 断开ADXP适配器连接
*/
private void logout() {
if (sessionId == null) {
return;
}
private void disconnectADXP() {
try {
String baseUrl = String.format("http://%s:%d/api/adxp",
properties.getHost(), properties.getPort());
// 创建登出请求
java.util.Map<String, String> logoutRequest = new java.util.HashMap<>();
logoutRequest.put("sessionId", sessionId);
// 发送HTTP POST请求
java.net.http.HttpClient httpClient = java.net.http.HttpClient.newHttpClient();
String logoutUrl = baseUrl + "/logout";
String disconnectUrl = baseUrl + "/disconnect";
String requestBody = objectMapper.writeValueAsString(logoutRequest);
java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder()
.uri(URI.create(logoutUrl))
.uri(URI.create(disconnectUrl))
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(requestBody))
.POST(java.net.http.HttpRequest.BodyPublishers.ofString("{}"))
.build();
httpClient.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
log.info("✅ 已登出ADXP适配器服务");
log.info("✅ 已断开ADXP适配器连接");
} catch (Exception e) {
log.warn("登出ADXP适配器服务失败", e);
log.warn("断开ADXP适配器连接失败", e);
errorCount.incrementAndGet();
}
}
@ -451,6 +386,14 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
public boolean isConnected() {
return isConnected.get();
}
/**
* 检查是否启用
* 主动连接架构下返回连接状态
*/
public boolean isEnabled() {
return isConnected();
}
/**
* 安排重新连接