Compare commits

..

1 Commits

Author SHA1 Message Date
85c3e7ec98 testfenzhishan 2025-11-12 14:36:24 +08:00
62 changed files with 2557 additions and 4650 deletions

43
.gitignore vendored
View File

@ -54,45 +54,4 @@ logs/
Users/
qaup-deploy/
deploy/offline_packages/
######################################################################
# Python
*.py[cod]
*$py.class
__pycache__/
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
pip-log.txt
pip-delete-this-directory.txt
.venv/
venv/
ENV/
env/
*.log
######################################################################
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm
.yarn-integrity
dist/
.cache/
deploy/offline_packages/

203
README.md
View File

@ -17,7 +17,6 @@
## 核心特性
### 🚗 冲突避免系统 (collision模块)
- **空间数据分析**: 基于PostGIS的空间计算和几何分析
- **实时车辆监控**: WebSocket实时位置数据推送和展示
- **机场区域管理**: 跑道、滑行道、停机坪等区域配置和监控
@ -27,15 +26,15 @@
### 🛠️ 系统管理功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
@ -56,194 +55,65 @@
- **构建工具**: Maven 3.6+
- **Java版本**: JDK 8
## 部署指南
### 方式一:自动部署(推荐)
使用提供的自动化部署脚本:
## 快速启动
### 1. 环境准备
```bash
# 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 8
java -version
# Maven 3.6+
mvn -version
# Docker & Docker Compose
docker --version
docker-compose --version
```
**可选组件(用于本地开发):**
```bash
# PostgreSQL + PostGIS用于本地开发
# PostgreSQL + PostGIS
psql --version
# Redis(用于本地开发)
# Redis
redis-server --version
```
#### 2. 数据库配置
**Docker启动数据库**
### 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` - 司机信息表
### 3. 启动Redis服务
```bash
# 启动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
redis-server
```
**本地数据库初始化:**
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. 环境配置
**更新应用配置:**
### 4. 配置应用
修改 `qaup-admin/src/main/resources/application.yml`:
```yaml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/qaup
username: qaup
password: qaup123
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/your_database
username: your_username
password: your_password
redis:
host: localhost
port: 6379
password: qaup123
database: 0
```
**环境变量配置:**
确保各模块 `.env` 文件配置正确:
- `adxp-adapter/.env.example` - ADXP数据中台适配器配置
- `qaup-admin/.env.example` - 主应用配置
#### 4. 编译和启动
### 5. 编译和启动
```bash
# 1. 清理并编译整个项目
# 清理并编译整个项目
mvn clean install
# 2. 进入管理模块
# 启动应用
cd qaup-admin
# 3. 启动应用(开发模式)
mvn spring-boot:run
# 或打包启动(生产模式)
mvn clean package -DskipTests
# 或者运行打包后的jar
java -jar target/qaup-admin.jar
```
#### 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
```
### 6. 访问系统
- 管理后台: http://localhost:8080
- WebSocket端点: ws://localhost:8080/ws
- API文档: http://localhost:8080/swagger-ui/
## 项目合并说明
@ -257,19 +127,16 @@ docker system prune -a
## 开发指南
### 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,68 +4,13 @@
# 1. 复制此文件为 .env: cp .env.example .env
# 2. 修改 .env 中的配置值
# 3. 启动应用时会自动加载 .env 文件中的环境变量
# 4. 适用于ADXP主动连接架构
# ============================================================
# ========== ADXP 数据中台连接配置 ==========
# ADXP数据中台的主机地址和端口
ADXP_HOST=10.32.38.2
ADXP_PORT=8081
# 连接ADXP数据中台的用户名和密码
# ========== ADXP 数据中台配置 ==========
ADXP_HOST=localhost
ADXP_PORT=7001
ADXP_USERNAME=dianxin
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内存
ADXP_PASSWORD=dianxin@123
# ============================================================
# 注意事项:
@ -73,6 +18,4 @@ REDIS_MAX_MEMORY_POLICY=volatile-lru
# 2. 字符串值不需要引号
# 3. #开头的行为注释
# 4. 请勿将 .env 文件提交到Git仓库
# 5. 生产环境请务必修改默认密码
# 6. ADXP适配器采用主动连接架构启动时自动连接数据中台
# ============================================================

438
adxp-adapter/QUICKSTART.md Normal file
View File

@ -0,0 +1,438 @@
# ADXP SDK Adapter 快速开始
## 方案概述
ADXP SDK (adxp-client-2.6.9.jar) 基于 JDK 8 开发,无法在 Java 11+ 环境运行。本方案通过独立的适配器服务解决兼容性问题。
### 架构
```
主应用 (Java 17) → HTTP → ADXP Adapter (Java 8, Docker) → SOAP → 数据中台
└─ port 8080 └─ port 8086 └─ port 7001
```
### Apple Silicon (M1/M2/M3) 用户
**推荐使用 Docker**,无需安装 Java 8Homebrew 的 openjdk@8 只有 x86_64 版本)
---
## 🚀 快速开始Docker 方式,推荐)
### 步骤 1: 启动 Mock 服务器(模拟数据中台)
```bash
# 在项目根目录
cd tools
python3 mock_adxp.py --host 0.0.0.0 --port 7001 --auto --interval 10
```
**说明**: Mock 服务器监听 `0.0.0.0:7001`,每 10 秒自动生成航班消息
### 步骤 2: 编译适配器服务
```bash
cd ../adxp-adapter
mvn clean package -DskipTests
```
**预期输出**: `BUILD SUCCESS`,生成 `target/adxp-adapter.jar`
### 步骤 3: 构建 Docker 镜像
```bash
docker build -t adxp-adapter:1.0.0 .
```
**说明**: 使用 OpenJDK 8非 Alpine包含 `net-tools`SDK 需要 `ifconfig` 命令)
### 步骤 4: 启动 Docker 容器
```bash
docker run -d \
-p 8086:8086 \
-e ADXP_HOST=host.docker.internal \
-e ADXP_PORT=7001 \
--name adxp-adapter \
adxp-adapter:1.0.0
```
**说明**:
- `host.docker.internal` 指向宿主机(访问本地 mock 服务器)
- 环境变量可覆盖配置文件
### 步骤 5: 验证适配器服务
```bash
# 健康检查
curl http://localhost:8086/api/adxp/health
# 预期输出: {"activeSessions":0,"status":"UP"}
```
### 步骤 6: 测试完整流程
```bash
# 1. 登录
LOGIN_RESPONSE=$(curl -s -X POST http://localhost:8086/api/adxp/login \
-H "Content-Type: application/json" \
-d '{"username":"dianxin","password":"dianxin@123"}')
echo "登录响应: $LOGIN_RESPONSE"
# 预期: {"success":true,"sessionId":"xxx-xxx-xxx","message":"登录成功"}
# 2. 提取 SessionId
SESSION_ID=$(echo "$LOGIN_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['sessionId'])")
echo "SessionId: $SESSION_ID"
# 3. 接收消息
curl -s "http://localhost:8086/api/adxp/messages?sessionId=$SESSION_ID" | python3 -m json.tool
# 4. 退出登录
curl -s -X POST http://localhost:8086/api/adxp/logout \
-H "Content-Type: application/json" \
-d "{\"sessionId\":\"$SESSION_ID\"}" | python3 -m json.tool
# 预期: {"success":true,"message":"登出成功"}
```
### 步骤 7: 启动主应用
```bash
cd ../qaup-admin
mvn spring-boot:run -Dspring-boot.run.profiles=dev,druid
```
**主应用会自动通过 HTTP 调用适配器服务!**
---
## 📋 Docker 常用命令
```bash
# 查看日志
docker logs -f adxp-adapter
# 重启容器
docker restart adxp-adapter
# 停止并删除容器
docker stop adxp-adapter && docker rm adxp-adapter
# 重新构建镜像(代码修改后)
cd adxp-adapter
mvn clean package -DskipTests
docker stop adxp-adapter && docker rm adxp-adapter
docker build -t adxp-adapter:1.0.0 .
docker run -d -p 8086:8086 -e ADXP_HOST=host.docker.internal -e ADXP_PORT=7001 --name adxp-adapter adxp-adapter:1.0.0
```
---
## 🛠️ 本地运行(需要 Java 8
**仅适用于 x86_64 机器或已安装 Java 8 的环境**
### 步骤 1: 启动 Mock 服务器
```bash
cd tools
python3 mock_adxp.py --host 0.0.0.0 --port 7001 --auto --interval 10
```
### 步骤 2: 编译
```bash
cd ../adxp-adapter
mvn clean package -DskipTests
```
### 步骤 3: 启动适配器
```bash
# 如果系统默认是 Java 8
java -jar target/adxp-adapter.jar
# 或指定 Java 8 路径
/path/to/jdk8/bin/java -jar target/adxp-adapter.jar
# 或使用启动脚本(自动查找 Java 8
./start.sh
```
### 步骤 4: 测试
同 Docker 方式的步骤 6
## 配置说明
### 主应用配置 (application-dev.yml)
```yaml
data:
collector:
adxp-adapter:
host: localhost # 适配器服务地址
port: 8086 # 适配器服务端口
username: dianxin
password: dianxin@123
reconnect-delay-millis: 3000
```
### 适配器服务配置 (application.yml)
```yaml
adxp:
host: localhost # 真实数据中台地址(或 mock 服务器)
port: 7001 # 数据中台端口
```
---
## 🐳 Docker Compose 部署(生产环境)
### 1. 准备环境变量
```bash
# 创建 .env 文件
cat > .env << EOF
ADXP_HOST=10.10.10.100 # 真实数据中台 IP
ADXP_PORT=7001 # 真实数据中台端口
EOF
```
### 2. 编译和构建
```bash
cd adxp-adapter
mvn clean package -DskipTests
docker build -t adxp-adapter:1.0.0 .
```
### 3. 启动服务
```bash
# 使用 Docker Compose
docker-compose up -d
# 查看日志
docker-compose logs -f adxp-adapter
# 查看状态
docker-compose ps
```
### 4. 验证部署
```bash
# 健康检查
curl http://localhost:8086/api/adxp/health
# 测试登录
curl -X POST http://localhost:8086/api/adxp/login \
-H "Content-Type: application/json" \
-d '{"username":"dianxin","password":"dianxin@123"}'
```
### 5. 停止服务
```bash
docker-compose down
```
---
## 🔧 环境切换
### 开发环境(使用 Mock
**主应用配置** (`qaup-admin/src/main/resources/application-dev.yml`):
```yaml
data:
collector:
adxp-adapter:
host: localhost
port: 8086
username: dianxin
password: dianxin@123
reconnect-delay-millis: 3000
```
**适配器配置** (`.env` 或环境变量):
```bash
ADXP_HOST=host.docker.internal # 指向宿主机 mock 服务器
ADXP_PORT=7001
```
**Mock 服务器**:
```bash
python3 tools/mock_adxp.py --host 0.0.0.0 --port 7001 --auto --interval 10
```
### 生产环境(连接真实数据中台)
**主应用配置** (`qaup-admin/src/main/resources/application-prod.yml`):
```yaml
data:
collector:
adxp-adapter:
host: 192.168.1.100 # 适配器服务的真实 IP
port: 8086
username: ${ADXP_USERNAME} # 从环境变量读取
password: ${ADXP_PASSWORD}
reconnect-delay-millis: 3000
```
**适配器配置** (`.env` 文件):
```bash
ADXP_HOST=10.10.10.100 # 真实数据中台 IP
ADXP_PORT=7001
```
---
## 🐛 故障排查
### 问题 1: Docker 容器启动失败
**检查日志**:
```bash
docker logs adxp-adapter
```
**常见错误**:
- `Cannot find ifconfig` → Dockerfile 使用了 Alpine 镜像,应使用 `openjdk:8-jdk`
- `Connection refused` → Mock 服务器未启动或端口错误
### 问题 2: 登录失败 (code=802/803)
**原因**: SDK 无法连接到数据中台
**排查步骤**:
```bash
# 1. 检查 mock 服务器是否运行
lsof -ti:7001
# 2. 测试网络连接
curl http://localhost:7001/LoginService?wsdl
# 3. 检查适配器环境变量
docker exec adxp-adapter env | grep ADXP
# 4. 查看适配器日志
docker logs adxp-adapter 2>&1 | grep -E "ERROR|Exception"
```
### 问题 3: HTTP 406 Not Acceptable
**原因**: DTO 类缺少 getter/setter 方法
**解决方案**:
- 确保 `LoginResponse`、`MessageResponse` 有显式的 getter/setter不要只依赖 Lombok
- 确保 `jackson-databind` 依赖存在
**验证**:
```bash
# 查看 pom.xml 中的 Jackson 依赖
grep -A 5 "jackson-databind" adxp-adapter/pom.xml
```
### 问题 4: Apple Silicon 兼容性
**错误**: `openjdk@8: The x86_64 architecture is required`
**解决方案**: 使用 Docker推荐或使用 Rosetta 2 运行 x86_64 版本
### 问题 5: Mock 响应格式错误
**症状**: SDK 返回 code=803 "发送请求错误"
**排查**:
```bash
# 查看 mock 服务器日志
tail -f /tmp/mock_adxp.log
# 检查 SOAP 响应格式
curl -X POST http://localhost:7001/LoginService \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body><log:login xmlns:log="http://LoginService">
<username>dianxin</username><password>dianxin@123</password>
</log:login></soapenv:Body>
</soapenv:Envelope>'
```
---
## 📊 监控和日志
### 健康检查端点
```bash
# 适配器健康状态
curl http://localhost:8086/api/adxp/health
# 返回: {"activeSessions":0,"status":"UP"}
# Spring Boot Actuator
curl http://localhost:8086/actuator/health
```
### 日志查看
```bash
# Docker 容器日志
docker logs -f adxp-adapter
# 实时过滤错误
docker logs -f adxp-adapter 2>&1 | grep -E "ERROR|WARN"
# Mock 服务器日志
tail -f /tmp/mock_adxp.log
# 主应用日志
tail -f qaup-admin/app.log | grep -E "adxp|flight"
```
### 性能监控
```bash
# 容器资源使用
docker stats adxp-adapter
# 网络延迟测试
time curl -s http://localhost:8086/api/adxp/health
```
---
## 📈 性能指标
- **HTTP 延迟**: < 5ms局域网
- **适配器内存**: ~256MB基础+ SDK 使用
- **并发支持**: 多个主应用实例可共享一个适配器
- **会话管理**: 自动重连,无需手动维护
---
## ✅ 验收检查清单
部署前请确认:
- [ ] Mock 服务器正常运行(开发环境)
- [ ] 适配器 Docker 镜像构建成功
- [ ] 适配器容器启动成功
- [ ] 健康检查返回 `{"status":"UP"}`
- [ ] 登录测试成功,返回 sessionId
- [ ] 主应用能通过 HTTP 调用适配器
- [ ] 日志中无 ERROR 级别错误
---
## 🚀 下一步
1. ✅ **开发环境测试完成** - Mock 服务器 + 适配器正常工作
2. 🔧 **生产部署** - 修改 ADXP_HOST 连接真实数据中台
3. 📊 **监控集成** - 添加 Prometheus metrics可选
4. 🔒 **安全加固** - 添加 API 认证/授权(可选)
5. 📦 **K8s 部署** - 创建 Deployment 和 Service 配置(可选)

View File

@ -2,17 +2,7 @@
ADXP 数据中台 SDK 适配器服务 - 基于 JDK 8 运行的独立微服务
## 新架构特性ADXP主动连接
### 🔄 **适配器主动连接架构**
- **启动即连接**: 适配器服务启动时自动连接到ADXP数据中台
- **无需登录接口**: 移除 `/login` 端点,由适配器自动管理认证
- **连接状态管理**: 提供 `/status``/reconnect` 端点进行连接管理
- **WebSocket实时推送**: 支持实时向客户端推送航班消息
- **健康监控**: 内置连接状态监控和自动重连机制
## 功能特性
## 功能说明
将青岛机场数据中台 SDK (adxp-client-2.6.9.jar) 封装为 REST API 服务,解决 SDK 与现代 Java 版本的兼容性问题。
@ -25,68 +15,51 @@ ADXP 数据中台 SDK 适配器服务 - 基于 JDK 8 运行的独立微服务
## 快速开始
### 方式1Docker Compose推荐
### 方式1本地运行(需要 JDK 8
```bash
# 1. 构建并启动服务
docker compose up -d
# 1. 编译
mvn clean package
# 2. 查看日志
docker compose logs -f
# 3. 停止服务
docker compose down
# 2. 运行
java -jar target/adxp-adapter.jar \
--adxp.host=10.10.10.100 \
--adxp.port=7001
```
### API接口ADXP主动连接架构
### 方式2Docker 运行(推荐
#### 获取连接状态
```bash
# 1. 构建镜像
docker build -t adxp-adapter:1.0.0 .
```
GET /api/adxp/status
# 2. 运行容器
docker run -d \
-p 8086:8086 \
-e ADXP_HOST=10.10.10.100 \
-e ADXP_PORT=7001 \
--name adxp-adapter \
adxp-adapter:1.0.0
```
返回当前ADXP数据中台的连接状态和会话信息。
### 方式3Docker Compose最简单
#### 强制重连
```bash
# 1. 设置环境变量
export ADXP_HOST=10.10.10.100
export ADXP_PORT=7001
```
POST /api/adxp/reconnect
# 2. 启动服务
docker-compose up -d
# 3. 查看日志
docker-compose logs -f
# 4. 停止服务
docker-compose down
```
强制断开当前连接并重新连接ADXP数据中台。
#### 获取消息接口
```
GET /api/adxp/messages
```
使用当前活动连接获取ADXP数据中台的航班消息。
#### 断开连接接口
```
POST /api/adxp/disconnect
```
主动断开与ADXP数据中台的连接。
#### 健康检查
```
GET /api/adxp/health
```
检查适配器服务健康状态和ADXP连接状态。
#### WebSocket实时推送
```
ws://localhost:8086/ws/flight-notifications
```
实时接收来自ADXP数据中台的航班消息推送。
## API 文档
### 1. 登录
@ -101,7 +74,6 @@ Content-Type: application/json
```
响应:
```json
{
"success": true,
@ -117,7 +89,6 @@ GET /api/adxp/messages?sessionId=550e8400-e29b-41d4-a716-446655440000
```
响应:
```json
{
"success": true,
@ -150,7 +121,6 @@ GET /api/adxp/health
```
响应:
```json
{
"status": "UP",
@ -158,37 +128,18 @@ GET /api/adxp/health
}
```
## 快速测试
### API调用示例
#### 1. 检查连接状态
## 测试
```bash
curl http://localhost:8086/api/adxp/status
```
# 1. 登录
curl -X POST http://localhost:8086/api/adxp/login \
-H "Content-Type: application/json" \
-d '{"username":"dianxin","password":"dianxin@123"}'
#### 2. 强制重连
# 2. 接收消息(替换为实际的 sessionId
curl "http://localhost:8086/api/adxp/messages?sessionId=YOUR_SESSION_ID"
```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
# 3. 健康检查
curl http://localhost:8086/api/adxp/health
```
@ -200,8 +151,7 @@ curl http://localhost:8086/api/adxp/health
|--------|------|--------|
| `ADXP_HOST` | ADXP 数据中台主机地址 | localhost |
| `ADXP_PORT` | ADXP 数据中台端口 | 7001 |
| `ADXP_USERNAME` | ADXP 用户名 | dianxin |
| `ADXP_PASSWORD` | ADXP 密码 | dianxin@123 |
| `SPRING_PROFILES_ACTIVE` | Spring Profile | prod |
### application.yml
@ -209,32 +159,62 @@ curl http://localhost:8086/api/adxp/health
adxp:
host: ${ADXP_HOST:localhost}
port: ${ADXP_PORT:7001}
username: ${ADXP_USERNAME:dianxin}
password: ${ADXP_PASSWORD:dianxin@123}
```
## 部署说明
### 开发环境
使用 mock 服务器进行开发测试:
```bash
# 1. 启动适配器(默认指向 localhost
docker compose up -d
# 启动 mock ADXP 服务器
cd /path/to/QAUP-Management/tools
python3 mock_adxp.py --auto --interval 10
# 启动适配器(指向 localhost
docker-compose up -d
```
### 生产环境
```bash
# 1. 创建环境变量文件
cat > .env << EOF
ADXP_HOST=10.10.10.100 # 真实数据中台 IP
ADXP_PORT=7001 # 真实数据中台端口
ADXP_USERNAME=dianxin # 真实数据中台用户名
ADXP_PASSWORD=dianxin@123 # 真实数据中台密码
EOF
连接真实的 ADXP 数据中台:
# 2. 启动服务
docker compose up -d
```bash
# 设置真实环境变量
export ADXP_HOST=10.10.10.100 # 真实数据中台 IP
export ADXP_PORT=7001
# 启动服务
docker-compose -f docker-compose.yml up -d
```
## 监控
- **健康检查**: `http://localhost:8086/api/adxp/health`
- **Actuator**: `http://localhost:8086/actuator/health`
- **日志文件**: `logs/adxp-adapter.log`
## 故障排查
### 登录失败
1. 检查 ADXP 服务器是否可达
2. 验证用户名密码是否正确
3. 查看日志: `docker-compose logs adxp-adapter`
### Session 过期
客户端需要实现重新登录逻辑,定期刷新 session。
### 网络问题
确保容器能够访问 ADXP 服务器:
```bash
# 进入容器测试网络
docker exec -it adxp-adapter sh
ping ${ADXP_HOST}
```
## 开发说明
@ -243,21 +223,31 @@ docker compose up -d
```
adxp-adapter/
├── pom.xml # Maven 配置
├── Dockerfile # Docker 镜像定义
├── docker-compose.yml # Docker Compose 配置
├── libs/ # 数据中台 SDK
├── pom.xml # Maven 配置
├── Dockerfile # Docker 镜像定义
├── docker-compose.yml # Docker Compose 配置
├── libs/ # SDK JAR 文件
│ ├── adxp-client-2.6.9.jar
│ └── mq.allclient-9.0.jar
└── src/ # 源代码
└── src/main/java/com/qaup/adxp/adapter/
├── AdxpAdapterApplication.java # 主应用类
├── controller/
│ └── AdxpController.java # REST API 控制器
├── service/
│ └── AdxpSdkService.java # SDK 封装服务
└── dto/
├── LoginRequest.java
├── LoginResponse.java
├── FlightMessage.java
└── MessageResponse.java
```
### 核心依赖
### 依赖说明
- **adxp-client-2.6.9.jar**: 数据中台 SDK
- **Apache CXF 3.2.4**: SOAP 协议支持
- **Jackson 1.9.13**: JSON 序列化
- **Spring Boot 2.7.18**: 应用框架
- `adxp-client-2.6.9.jar`: 数据中台 SDK
- `mq.allclient-9.0.jar`: IBM MQ 客户端
- Apache CXF 3.2.4: SOAP 支持
- Jackson 1.9.13: JSON 序列化
## 许可证

View File

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

View File

@ -1,6 +1,8 @@
package com.qaup.adxp.adapter.config;
import com.qaup.adxp.adapter.service.AdxpSdkService;
import com.qaup.adxp.adapter.websocket.AdxpWebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
@ -11,9 +13,12 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private AdxpSdkService adxpSdkService;
@Bean
public AdxpWebSocketHandler adxpWebSocketHandler() {
return new AdxpWebSocketHandler();
return new AdxpWebSocketHandler(adxpSdkService);
}
@Override

View File

@ -26,82 +26,51 @@ public class AdxpController {
private AdxpWebSocketHandler adxpWebSocketHandler;
/**
* 获取连接状态
* 登录接口
*/
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> getStatus() {
@PostMapping(value = "/login", produces = "application/json")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
try {
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);
String sessionId = adxpSdkService.login(request.getUsername(), request.getPassword());
return ResponseEntity.ok(LoginResponse.success(sessionId));
} catch (Exception e) {
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);
log.error("登录失败", e);
return ResponseEntity.ok(LoginResponse.failure(e.getMessage()));
}
}
/**
* 获取消息接口 - 使用当前连接
* 接收消息接口
*/
@GetMapping("/messages")
public ResponseEntity<MessageResponse> getMessages() {
public ResponseEntity<MessageResponse> getMessages(@RequestParam String sessionId) {
try {
if (!adxpSdkService.isConnected()) {
return ResponseEntity.ok(MessageResponse.failure("未连接到ADXP数据中台"));
}
List<FlightMessage> messages = adxpSdkService.getMessages();
List<FlightMessage> messages = adxpSdkService.receiveMessages(sessionId);
return ResponseEntity.ok(MessageResponse.success(messages));
} catch (Exception e) {
log.error("接收消息失败", e);
log.error("接收消息失败: sessionId={}", sessionId, e);
return ResponseEntity.ok(MessageResponse.failure(e.getMessage()));
}
}
/**
* 断开连接接口
* 登出接口
*/
@PostMapping("/disconnect")
public ResponseEntity<Map<String, Object>> disconnect() {
@PostMapping("/logout")
public ResponseEntity<Map<String, Object>> logout(@RequestBody Map<String, String> request) {
try {
adxpSdkService.disconnectFromADXPServer();
Map<String, Object> response = new HashMap<>();
String sessionId = request.get("sessionId");
adxpSdkService.logout(sessionId);
Map<String, Object> response = new HashMap<String, Object>();
response.put("success", true);
response.put("message", "已断开ADXP数据中台连接");
response.put("connected", false);
response.put("message", "登出成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("断开连接失败", e);
Map<String, Object> response = new HashMap<>();
log.error("登出失败", e);
Map<String, Object> response = new HashMap<String, Object>();
response.put("success", false);
response.put("message", "断开连接失败: " + e.getMessage());
response.put("connected", adxpSdkService.isConnected());
response.put("message", e.getMessage());
return ResponseEntity.ok(response);
}
}
@ -111,20 +80,12 @@ public class AdxpController {
*/
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
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);
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());
}
return ResponseEntity.ok(health);
}
}

View File

@ -1,7 +1,6 @@
package com.qaup.adxp.adapter.service;
import com.qaup.adxp.adapter.dto.FlightMessage;
import com.qaup.adxp.adapter.websocket.AdxpWebSocketHandler;
import com.taocares.adxp.client.ADXPClient;
import com.taocares.adxp.client.ADXPClientFactory;
import com.taocares.adxp.model.LoginResult;
@ -9,18 +8,13 @@ import com.taocares.adxp.model.MessageResult;
import com.taocares.adxp.model.MessageList;
import com.taocares.adxp.model.MsgType;
import com.taocares.adxp.model.HeadType;
import com.taocares.adxp.AdxpConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class AdxpSdkService {
@ -33,199 +27,98 @@ public class AdxpSdkService {
@Value("${adxp.port}")
private int port;
@Value("${adxp.username:dianxin}")
@Value("${adxp.username:}")
private String defaultUsername;
@Value("${adxp.password:Dianxin#2025}")
@Value("${adxp.password:}")
private String defaultPassword;
@Autowired
private AdxpWebSocketHandler adxpWebSocketHandler;
// 单一连接状态管理
private volatile boolean connected = false;
private ADXPClient adxpClient = null;
// 定时重连任务
private final ScheduledExecutorService reconnectScheduler = Executors.newSingleThreadScheduledExecutor();
private final Object lock = new Object();
// 应用启动时主动连接
@PostConstruct
public void initConnection() {
log.info("ADXP适配器启动主动连接数据中台...");
try {
connectToADXP();
} catch (Exception e) {
log.error("ADXP数据中台初始连接失败: {}", e.getMessage());
// 启动定时重连
scheduleReconnect();
}
// Session 管理: sessionId -> SessionInfo
private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
public Map<String, SessionInfo> getSessions() {
return sessions;
}
// 应用关闭时清理资源
@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);
// 创建客户端
adxpClient = ADXPClientFactory.createWSClient(host, port);
// 登录
LoginResult result = adxpClient.login(defaultUsername, defaultPassword);
boolean loginSuccess = result != null && Boolean.TRUE.equals(result.isSuccess());
if (loginSuccess) {
connected = true;
log.info("用户: {} 登录平台成功", defaultUsername);
// 启动消息监听
startMessageListener();
} else {
connected = false;
String errorMsg = result != null ?
String.format("code=%s, message=%s", result.getCode(), result.getMessage()) :
"登录响应为空";
log.error("用户: {} 登录平台失败: {}", defaultUsername, errorMsg);
throw new RuntimeException("ADXP登录失败: " + errorMsg);
}
} catch (Exception e) {
connected = false;
adxpClient = null;
log.error("ADXP数据中台连接失败: {}", e.getMessage());
throw new RuntimeException("ADXP连接失败", e);
}
}
}
// 断开连接
private void disconnectFromADXP() {
synchronized (lock) {
try {
if (adxpClient != null) {
adxpClient.logout(defaultUsername, defaultPassword);
}
connected = false;
adxpClient = null;
log.info("已断开ADXP数据中台连接");
} catch (Exception e) {
log.error("断开连接时发生错误", e);
}
}
}
// 定时重连任务
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 && adxpClient != null;
}
// 旧的healthCheck方法已移除使用返回Map的新版本
/**
* 获取当前连接信息
*/
public String getConnectionInfo() {
if (connected) {
return String.format("已连接到ADXP数据中台 (host=%s, port=%s)", host, port);
} else {
return "未连接到ADXP数据中台";
}
}
/**
* 强制重连 - 供外部调用
*/
public void forceReconnect() {
log.info("外部请求强制重连ADXP数据中台");
disconnectFromADXP();
try {
connectToADXP();
log.info("强制重连成功");
} catch (Exception e) {
log.error("强制重连失败", e);
throw new RuntimeException("重连失败", e);
}
}
/**
* 启动消息监听线程
*/
private void startMessageListener() {
Thread messageListener = new Thread(() -> {
log.info("ADXP消息监听线程已启动");
while (connected && adxpClient != null) {
try {
List<FlightMessage> messages = receiveMessages();
if (!messages.isEmpty()) {
log.info("接收到 {} 条消息", messages.size());
// 处理接收到的消息
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");
public static class SessionInfo {
ADXPClient client;
String username;
String password;
messageListener.setDaemon(true);
messageListener.start();
}
/**
* 消息接收方法
*/
private List<FlightMessage> receiveMessages() {
if (!connected || adxpClient == null) {
return Collections.emptyList();
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;
}
}
/**
* 登录数据中台
*/
public String login(String username, String password) {
// 如果未提供用户名或密码则使用配置文件中的默认值
String actualUsername = (username == null || username.isEmpty()) ? defaultUsername : username;
String actualPassword = (password == null || password.isEmpty()) ? defaultPassword : password;
try {
MessageResult result = adxpClient.receiveMessage();
log.info("正在登录 ADXP 数据中台: host={}, port={}, username={}", host, port, actualUsername);
// 使用统一的错误码处理方法
if (handleMessageResultError(result)) {
return Collections.emptyList();
}
// 创建 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;
} catch (Exception e) {
log.error("登录异常", e);
throw new RuntimeException("登录异常: " + e.getMessage(), e);
}
}
/**
* 接收消息
*/
public List<FlightMessage> receiveMessages(String sessionId) {
SessionInfo sessionInfo = sessions.get(sessionId);
if (sessionInfo == null) {
throw new IllegalStateException("Session 不存在或已过期: " + sessionId);
}
try {
MessageResult result = sessionInfo.client.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();
}
@ -248,123 +141,34 @@ public class AdxpSdkService {
}
}
log.debug("接收到 {} 条消息", messages.size());
return messages;
} catch (Exception e) {
handleConnectionError(e);
return Collections.emptyList();
log.error("接收消息异常: sessionId={}", sessionId, e);
throw new RuntimeException("接收消息异常: " + e.getMessage(), e);
}
}
private void handleConnectionError(Exception e) {
log.error("ADXP连接错误: {}", e.getMessage());
// 对于所有连接错误直接尝试重新连接
log.info("尝试重新连接ADXP服务器");
reconnect();
}
/**
* 处理接收到的消息并广播到WebSocket客户端
*/
private void processMessages(List<FlightMessage> messages) {
if (messages != null && !messages.isEmpty()) {
try {
log.info("接收到 {} 条消息,准备广播", messages.size());
// 将消息广播到WebSocket客户端
if (adxpWebSocketHandler != null) {
adxpWebSocketHandler.broadcastMessages(messages);
}
} catch (Exception e) {
log.error("广播消息失败: {}", e.getMessage());
}
}
}
/**
* 处理消息结果错误码
*/
private boolean handleMessageResultError(MessageResult result) {
if (result == null) {
return false;
}
int code = result.getCode();
// 使用正确的常量类引用
if (code == AdxpConstants.RC_CLIENT_NOT_LOGGED_IN ||
code == AdxpConstants.RC_TOKEN_EXPIRED ||
code == AdxpConstants.RC_CLIENT_EXCEPTION ||
code == AdxpConstants.RC_REMOTE_EXCEPTION) {
log.info("遇到错误码 {},需要重新连接", code);
reconnect();
return true;
}
return false;
}
/**
* 获取消息 - 供外部调用
* 登出
*/
public List<FlightMessage> getMessages() {
return receiveMessages();
}
/**
* 断开连接 - 主动断开与ADXP数据中台的连接
*/
public void disconnectFromADXPServer() {
log.info("主动断开ADXP数据中台连接");
disconnectFromADXP();
}
/**
* 重新连接方法
*/
private void reconnect() {
synchronized (lock) {
public void logout(String sessionId) {
SessionInfo sessionInfo = sessions.remove(sessionId);
if (sessionInfo != null) {
try {
log.info("尝试重新连接ADXP数据中台");
// 先断开现有连接
if (adxpClient != null) {
try {
adxpClient.logout(defaultUsername, defaultPassword);
} catch (Exception ignored) {}
}
// 重新创建客户端并登录
adxpClient = ADXPClientFactory.createWSClient(host, port);
LoginResult loginResult = adxpClient.login(defaultUsername, defaultPassword);
if (loginResult != null && Boolean.TRUE.equals(loginResult.isSuccess())) {
connected = true;
log.info("重新连接成功");
} else {
connected = false;
log.error("重新连接失败");
}
sessionInfo.client.logout(sessionInfo.username, sessionInfo.password);
log.info("登出成功: sessionId={}", sessionId);
} catch (Exception e) {
connected = false;
adxpClient = null;
log.error("重新连接异常: {}", e.getMessage());
log.error("登出异常: sessionId={}", sessionId, e);
}
}
}
/**
* 健康检查
* 获取当前会话数
*/
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);
if (connected) {
health.put("message", "ADXP数据中台连接正常");
} else {
health.put("message", "ADXP数据中台连接异常");
}
return health;
public int getActiveSessionCount() {
return sessions.size();
}
}

View File

@ -8,6 +8,8 @@ import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
@Service
@ -21,36 +23,97 @@ public class MessageListenerService {
@Autowired
private AdxpWebSocketHandler adxpWebSocketHandler;
private ExecutorService executorService;
private final AtomicBoolean isRunning = new AtomicBoolean(false);
/**
* 服务启动时的初始化方法
* 注意消息监听逻辑已移至AdxpSdkService中本服务仅负责管理和监控
*/
@PostConstruct
public void start() {
if (isRunning.compareAndSet(false, true)) {
log.info("消息监听服务已启动 - 消息监听逻辑已移至AdxpSdkService");
executorService = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "ADXP-Message-Listener");
t.setDaemon(true);
return t;
});
executorService.submit(this::listenForMessages);
log.info("消息监听服务已启动");
}
}
@PreDestroy
public void stop() {
if (isRunning.compareAndSet(true, false)) {
if (executorService != null) {
executorService.shutdownNow();
}
log.info("消息监听服务已停止");
}
}
private void listenForMessages() {
log.info("开始监听数据中台消息");
// 获取所有活跃的会话ID
while (isRunning.get()) {
try {
// 获取当前会话数量
int sessionCount = adxpSdkService.getSessions().size();
if (sessionCount == 0) {
log.debug("当前没有活跃的会话,等待连接...");
Thread.sleep(1000);
continue;
}
// 遍历所有会话并接收消息
adxpSdkService.getSessions().forEach((sessionId, sessionInfo) -> {
try {
// 调用SDK接收消息这可能会阻塞直到有消息到达
java.util.List<com.qaup.adxp.adapter.dto.FlightMessage> messages =
adxpSdkService.receiveMessages(sessionId);
// 如果有消息广播给所有WebSocket客户端
if (messages != null && !messages.isEmpty()) {
adxpWebSocketHandler.broadcastMessages(messages);
log.info("接收到 {} 条航班消息并广播给 {} 个WebSocket客户端",
messages.size(), adxpWebSocketHandler.getSessionCount());
}
} catch (Exception e) {
log.error("处理会话消息失败: sessionId={}", sessionId, e);
}
});
// 短暂休眠避免过于频繁的轮询
Thread.sleep(100);
} catch (InterruptedException e) {
log.info("消息监听线程被中断");
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("消息监听过程中发生错误", e);
// 出错后短暂休眠再重试
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
log.info("消息监听服务已停止监听");
}
/**
* 获取服务统计信息
*/
public String getStats() {
return String.format("MessageListenerService Stats: " +
"isRunning=%s, " +
"adxpConnected=%s, " +
"sessions=%d, " +
"webSocketClients=%d",
isRunning.get(),
adxpSdkService.isConnected(),
adxpSdkService.getSessions().size(),
adxpWebSocketHandler.getSessionCount());
}
}

View File

@ -1,30 +1,32 @@
package com.qaup.adxp.adapter.websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.qaup.adxp.adapter.dto.FlightMessage;
import com.qaup.adxp.adapter.service.AdxpSdkService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.concurrent.atomic.AtomicBoolean;
public class AdxpWebSocketHandler extends TextWebSocketHandler {
private static final Logger log = LoggerFactory.getLogger(AdxpWebSocketHandler.class);
private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
private final AdxpSdkService adxpSdkService;
private final ObjectMapper objectMapper;
private final AtomicBoolean isRunning = new AtomicBoolean(false);
public AdxpWebSocketHandler() {
public AdxpWebSocketHandler(AdxpSdkService adxpSdkService) {
this.adxpSdkService = adxpSdkService;
this.objectMapper = new ObjectMapper();
// 配置ObjectMapper以更好地处理日期和其他序列化问题
this.objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
}
@Override
@ -62,47 +64,41 @@ public class AdxpWebSocketHandler extends TextWebSocketHandler {
* 向所有连接的客户端广播消息
*/
public void broadcastMessages(List<FlightMessage> messages) {
if (messages == null || messages.isEmpty() || sessions.isEmpty()) {
if (messages == null || messages.isEmpty()) {
return;
}
try {
// 序列化消息列表为JSON
String jsonMessage = objectMapper.writeValueAsString(messages);
TextMessage textMessage = new TextMessage(jsonMessage);
// 清理已关闭的会话
cleanupClosedSessions();
int successCount = 0;
int failedCount = 0;
int clientCount = sessions.size();
// 向所有活跃客户端广播消息
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
try {
session.sendMessage(textMessage);
successCount++;
} catch (Exception e) {
log.error("发送消息失败: sessionId={}", session.getId(), e);
// 如果发送失败移除会话
sessions.remove(session);
failedCount++;
}
} else {
sessions.remove(session);
failedCount++;
}
}
if (log.isDebugEnabled()) {
log.debug("已向 {} 个客户端广播 {} 条消息", clientCount, messages.size());
}
log.debug("广播消息完成: 消息数量={}, 连接数={}, 成功发送={}, 失败={}",
messages.size(), sessions.size(), successCount, failedCount);
} catch (Exception e) {
log.error("序列化或广播消息失败", e);
}
}
/**
* 清理已关闭的WebSocket会话
*/
private void cleanupClosedSessions() {
sessions.removeIf(session -> !session.isOpen());
}
/**
* 获取当前连接数
*/
@ -133,10 +129,10 @@ public class AdxpWebSocketHandler extends TextWebSocketHandler {
*/
public List<String> getConnectionDetails() {
return sessions.stream()
.filter(WebSocketSession::isOpen)
.map(session -> String.format("ID: %s, Remote Address: %s",
.map(session -> String.format("ID: %s, Remote Address: %s, Open: %s",
session.getId(),
session.getRemoteAddress()))
.collect(Collectors.toList());
session.getRemoteAddress(),
session.isOpen()))
.collect(java.util.stream.Collectors.toList());
}
}

51
adxp-adapter/start.sh Executable file
View File

@ -0,0 +1,51 @@
#!/bin/bash
# ADXP Adapter 启动脚本
# 项目路径
PROJECT_DIR="/Users/tianjianyong/apps/Company/QAUP-Management/adxp-adapter"
JAR_FILE="$PROJECT_DIR/target/adxp-adapter.jar"
# 检查项目目录是否存在
if [ ! -d "$PROJECT_DIR" ]; then
echo "❌ 项目目录不存在: $PROJECT_DIR"
exit 1
fi
# 进入项目目录
cd "$PROJECT_DIR"
# 检查JAR文件是否存在如果不存在则编译
if [ ! -f "$JAR_FILE" ]; then
echo "🔨 正在编译adxp-adapter项目..."
mvn clean package
if [ $? -ne 0 ]; then
echo "❌ 编译失败"
exit 1
fi
fi
# 检查JAR文件是否存在
if [ ! -f "$JAR_FILE" ]; then
echo "❌ JAR文件不存在: $JAR_FILE"
exit 1
fi
echo "🚀 正在启动ADXP Adapter服务..."
echo "📄 JAR文件: $JAR_FILE"
# 启动服务
java -jar "$JAR_FILE" &
# 等待几秒钟让服务启动
sleep 5
# 检查服务是否启动成功
if pgrep -f "adxp-adapter" > /dev/null; then
echo "✅ ADXP Adapter服务启动成功"
echo "🌐 WebSocket端点: ws://localhost:8086/ws/flight-notifications"
echo "📊 健康检查: http://localhost:8086/actuator/health"
else
echo "❌ ADXP Adapter服务启动失败"
exit 1
fi

118
deploy/BUILD-GUIDE.md Normal file
View File

@ -0,0 +1,118 @@
# QAUP 构建打包指南
## 方案macOS构建 + Ubuntu打包
### 开发环境操作macOS
#### 1. 构建jar文件
```bash
# 在项目根目录
mvn clean package -DskipTests
```
#### 2. 上传jar文件到打包服务器
```bash
# 方式一使用scp
scp qaup-admin/target/qaup-admin.jar code@your-server:/home/code/apps/QAUP_Management/qaup-admin/target/
# 方式二:使用自动化脚本(需要先配置服务器信息)
./deploy/simple/build-and-upload.sh
```
### 打包服务器操作Ubuntu
#### 1. 确认jar文件已上传
```bash
ls -la qaup-admin/target/qaup-admin.jar
```
#### 2. 执行打包
```bash
./deploy/simple/package-server.sh
```
#### 3. 下载部署包
```bash
# 生成的文件类似qaup-deploy-20250105-143022.tar.gz
ls -la qaup-deploy-*.tar.gz
```
## 日常更新流程
### 程序更新(推荐)
```bash
# macOS开发环境
mvn clean package -DskipTests
scp qaup-admin/target/qaup-admin.jar production-server:/path/to/qaup-deploy/new-app.jar
# 生产服务器
cd qaup-deploy
./update.sh
```
**注意:** 从v1.0.1版本开始系统集成了Flyway数据库迁移功能
- 应用启动时会自动执行数据库迁移
- 新版本的数据库结构变更会自动应用
- 无需手动执行SQL脚本
- 迁移失败时应用会启动失败,确保数据一致性
### 完整重新部署
```bash
# macOS → 打包服务器 → 生产服务器
# 按照上述完整流程操作
```
## 服务器配置要求
### 打包服务器Ubuntu
- ✅ Docker已安装
- ✅ 网络连接正常(拉取镜像)
- ✅ 足够磁盘空间约2GB
### 生产服务器Ubuntu
- ✅ Docker已安装
- ✅ 解压工具unzip
## 故障排除
### Q: Docker拉取镜像很慢
A: 使用国内镜像加速器脚本已配置daocloud加速器
### Q: jar文件上传失败
A: 检查网络连接和服务器路径是否正确
### Q: 打包过程中断
A: 重新运行package-server.sh脚本会自动清理并重新开始
### Q: 数据库迁移失败
A: 检查应用日志中的Flyway错误信息
```bash
docker compose logs qaup-app | grep -i flyway
```
常见问题:
- 数据库连接失败:检查数据库服务是否正常
- 迁移脚本错误检查SQL语法和数据库权限
- 版本冲突:可能需要手动修复迁移历史表
### Q: 应用启动失败Flyway相关
A: 查看详细错误信息:
```bash
# 查看应用日志
docker compose logs qaup-app
# 检查数据库迁移状态
docker exec -it qaup-postgres psql -U qaup qaup -c "SELECT * FROM flyway_schema_history ORDER BY installed_rank;"
```
## 脚本说明
### 开发环境脚本:
- `build-and-upload.sh` - 自动构建和上传(需配置服务器信息)
- 手动操作:`mvn package` + `scp`
### 打包服务器脚本:
- `package-server.sh` - 使用已有jar文件进行打包
### 生产服务器脚本:
- `deploy.sh` - 首次部署
- `update.sh` - 程序更新

View File

@ -1,136 +1,131 @@
# QAUP 系统部署和更新说明
## 一、打包环境操作
## 开发环境操作
### 1. 生成程序更新文件
### 生成程序更新文件
修改代码后生成仅包含应用jar的更新包
当代码修改后需要生成新的jar文件供客户更新
```bash
./deploy/package-update.sh
./deploy/simple/build-update.sh
```
**输出**
生成包含详细说明的更新包:
- `qaup-admin.jar` - 应用程序
- `UPDATE-INSTRUCTIONS.md` - 更新说明
- `UPDATE-INSTRUCTIONS.md` - 详细更新说明
- `VERSION-INFO.txt` - 版本信息
**注意**如需单独jar文件可直接从更新包中提取。
### 2. 生成完整部署包
生成包含所有组件的完整部署包(首次部署或重大更新):
**注意**如果只需要jar文件可以从生成的更新包中提取 `qaup-admin.jar` 文件单独发送给客户。
### 生成完整部署包
```bash
./deploy/package-all.sh
./deploy/simple/package.sh
```
生成包含所有组件的完整部署包,用于首次部署或重大版本更新。
---
## 二、生产环境操作
## 客户环境操作
### 1. 首次部署
#### 步骤
1. **解压部署包**
```bash
mkdir 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 # 验证应用健康
```
### 2. 程序更新仅更新jar文件
#### 前置准备
### 首次部署
### 1. 解压部署包
```bash
mkdir qaup-deploy
tar -xzf qaup-deploy-20250105-143022.tar.gz -C qaup-deploy
cd qaup-deploy
```
# 1. 确认当前系统状态
### 2. 执行部署
```bash
./deploy.sh
```
### 3. 验证部署
```bash
# 检查服务状态
docker compose ps
# 2. 备份重要数据(推荐)
docker exec qaup-postgres pg_dump -U qaup qaup > backup-$(date +%Y%m%d).sql
cp config.yml config.yml.backup
# 访问系统
curl http://localhost:8080/actuator/health
```
#### 更新步骤
## 程序更新仅更新jar文件
### 更新前准备
1. **确认当前系统状态**
```bash
cd qaup-deploy
docker compose ps
```
2. **备份重要数据**(可选但推荐):
```bash
# 备份数据库
docker exec qaup-postgres pg_dump -U qaup qaup > backup-$(date +%Y%m%d).sql
# 备份配置文件
cp config.yml config.yml.backup
```
### 更新步骤
### 1. 准备新版本文件
#### 如果收到的是完整更新包(.tar.gz文件
```bash
# 1. 准备新版本文件(二选一)
# 方案A使用完整更新包
cp /path/to/qaup-update-xxx/qaup-admin.jar new-app.jar
# 解压更新包
tar -xzf qaup-update-20250105-143022.tar.gz
cd qaup-update-20250105-143022
# 方案B直接使用jar文件
cp /path/to/qaup-admin-xxx.jar new-app.jar
# 查看更新说明
cat UPDATE-INSTRUCTIONS.md
# 2. 执行更新
./deploy-update.sh
# 3. 验证更新
docker compose ps # 检查服务状态
docker compose logs -f qaup-app # 查看应用日志
curl http://localhost:8080/actuator/health # 验证应用健康
# 复制jar文件到部署目录
cd ../qaup-deploy
cp ../qaup-update-20250105-143022/qaup-admin.jar new-app.jar
```
### 3. 配置文件更新
如果需要更新配置文件:
#### 如果收到的是jar文件
```bash
cd qaup-deploy
# 1. 备份当前配置
cp config.yml config.yml.backup
# 2. 编辑配置文件
vi config.yml
# 3. 重启应用
docker compose restart qaup-app
cp /path/to/qaup-admin-20250105-143022.jar new-app.jar
```
### 4. 更新失败处理
#### 自动回滚
更新失败时脚本会自动尝试回滚。
#### 手动回滚
### 2. 执行程序更新
```bash
cd qaup-deploy
./update.sh
```
# 1. 停止应用
docker compose stop qaup-app
### 3. 验证更新结果
```bash
# 检查服务状态
docker compose ps
# 2. 恢复备份文件
# 检查应用日志
docker compose logs -f qaup-app
# 访问系统确认功能正常
curl http://localhost:8080/actuator/health
```
## 更新失败处理
如果更新失败,脚本会自动尝试回滚。如果自动回滚也失败:
### 手动回滚:
```bash
# 停止应用
docker compose stop qaup-app
# 恢复备份文件
cp app.jar.backup.* app.jar
# 3. 启动应用
docker compose start qaup-app
# 启动应用
docker compose start qaup-app
```
#### 问题排查
### 检查问题:
```bash
# 查看应用日志
docker compose logs qaup-app
@ -139,210 +134,137 @@ docker compose logs qaup-app
docker compose ps -a
```
### 5. 完全重新部署
## 配置文件更新
如果需要重新部署整个系统(包括更新镜像、配置)
如果新版本需要更新配置文件
#### 方案A不保留数据
1. **备份当前配置**
```bash
cp config.yml config.yml.backup
```
2. **更新配置**
```bash
# 编辑配置文件
vi config.yml
```
3. **重启应用**
```bash
docker compose restart qaup-app
```
## 常见问题
### Q: 更新后无法访问系统
A: 检查端口是否被占用,查看应用日志:
```bash
cd qaup-deploy
# 1. 清理旧环境
docker compose down
rm -rf data/ # 注意:会删除所有数据
# 2. 重新部署
./deploy-all.sh
docker compose logs qaup-app
netstat -tlnp | grep 8080
```
#### 方案B保留数据
```bash
# 1. 备份旧数据
cd qaup-deploy
docker exec qaup-postgres pg_dump -U qaup qaup > ../data-backup.sql
# 2. 清理旧环境
docker compose down
# 3. 部署新系统
mkdir qaup-deploy-new
tar -xzf qaup-deploy-new-xxx.tar.gz -C qaup-deploy-new
cd qaup-deploy-new
./deploy-all.sh
# 4. 恢复数据
docker exec -i qaup-postgres psql -U qaup qaup < ../data-backup.sql
```
## 三、常见问题
### Q: 更新后无法访问系统?
A: 检查端口和应用日志:
```bash
docker compose logs qaup-app
```
### Q: 端口冲突?
A: 检查端口占用:
```bash
netstat -tlnp | grep 8080
```
### Q: 数据库连接失败?
### Q: 数据库连接失败
A: 检查数据库服务状态:
```bash
docker compose logs qaup-postgres
docker compose logs qaup-postgres
docker exec qaup-postgres pg_isready -U qaup
```
## 四、更新记录
建议记录每次更新信息:
```
时间 | 版本号 | 人员 | 类型 | 结果 | 问题
2025-01-05 | v1.0.1→v1.0.2 | 张三 | 程序更新 | 成功 | -
2025-01-10 | v1.0.2→v1.0.3 | 李四 | 程序更新 | 失败 | 配置错误,已回滚
```
## 五、数据库部署和更新策略
### 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 # 基线迁移(支持已有数据库)
SPRING_FLYWAY_VALIDATE_ON_MIGRATE: true # 验证迁移
SPRING_FLYWAY_CLEAN_DISABLED: true # 禁止清理生产数据库
SPRING_FLYWAY_LOCATIONS: classpath:db/migration # 迁移脚本路径
```
### 2. 数据库部署流程
#### 首次部署(全新环境)
### Q: 如何完全重新部署
A: 如果更新出现严重问题,可以重新部署:
```bash
# 1. 启动基础服务PostgreSQL + Redis
docker compose up -d qaup-postgres qaup-redis
# 停止所有服务
docker compose down
# 2. 等待数据库就绪
sleep 30
# 清理数据(注意:这会删除所有数据)
rm -rf data/
# 3. 初始化数据库执行Flyway迁移
# 应用启动时会自动执行Flyway迁移
docker compose up -d qaup-app
# 4. 验证部署
docker compose ps
curl http://localhost:8080/actuator/health
# 重新部署
./deploy.sh
```
#### 已有环境更新
## 完整系统重新部署
如果需要重新部署整个系统包括更新Docker镜像、配置等
### 1. 获取新的部署包
```bash
# Flyway自动处理数据库迁移
# 应用启动时会自动检测并执行需要的迁移
docker compose restart qaup-app
# 检查迁移状态
docker exec qaup-postgres psql -U qaup -d qaup -c "SELECT * FROM flyway_schema_history ORDER BY installed_rank;"
# 解压新的部署包到新目录
mkdir qaup-deploy-new
tar -xzf qaup-deploy-new-20250110-100000.tar.gz -C qaup-deploy-new
cd qaup-deploy-new
```
### 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. **函数/存储过程迁移** - 业务逻辑更新
### 4. 数据库维护操作
#### 数据备份和恢复(仅限紧急情况)
### 2. 迁移数据(如果需要保留数据)
```bash
# 备份数据
docker exec qaup-postgres pg_dump -U qaup qaup > backup-$(date +%Y%m%d_%H%M%S).sql
# 从旧系统备份数据
cd ../qaup-deploy
docker exec qaup-postgres pg_dump -U qaup qaup > ../data-backup.sql
# 恢复数据(生产环境需要谨慎)
cat backup-20250115_143000.sql | docker exec -i qaup-postgres psql -U qaup -d qaup
# 停止旧系统
docker compose down
# 在新系统中恢复数据
cd ../qaup-deploy-new
./deploy.sh
docker exec -i qaup-postgres psql -U qaup qaup < ../data-backup.sql
```
#### 数据库状态监控
## 操作流程总结
```bash
# 查看Flyway迁移状态应用启动日志
docker compose logs qaup-app | grep -i flyway
### 开发环境 → 客户环境流程
# 直接查询迁移历史
docker exec qaup-postgres psql -U qaup -d qaup -c "
SELECT version, description, installed_on
FROM flyway_schema_history
ORDER BY installed_rank;"
#### 程序更新流程:
```
开发环境:
1. 代码修改完成
2. ./deploy/simple/build-update.sh # 生成更新包
3. 发送更新包或jar文件给客户
# 检查应用健康状态
curl http://localhost:8080/actuator/health
客户环境:
1. cd qaup-deploy
2. cp new-jar-file.jar new-app.jar # 重命名文件
3. ./update.sh # 执行更新
4. 验证更新结果
```
### 5. 数据库更新最佳实践
#### 完整部署流程:
```
开发环境:
1. ./deploy/simple/package.sh # 生成完整部署包
2. 发送部署包给客户
#### 开发环境
客户环境:
1. mkdir qaup-deploy && tar -xzf qaup-deploy-xxx.tar.gz -C qaup-deploy # 解压部署包
2. cd qaup-deploy
3. ./deploy.sh # 执行部署
4. 验证部署结果
```
- 可以使用`flyway.clean()`清理数据库
- 可以使用`flyway.migrate().clean()`重建
## 更新记录
#### 生产环境
建议记录每次更新的信息:
- 更新时间
- 版本号
- 更新人员
- 更新类型(程序更新/完整部署)
- 是否成功
- 遇到的问题
- 禁止使用`flyway.clean()`
- 优先使用增量迁移脚本
- 重大变更需要测试环境验证
示例:
```
2025-01-05 14:30 - v1.0.1 → v1.0.2 - 张三 - 程序更新 - 成功
2025-01-10 09:15 - v1.0.2 → v1.0.3 - 李四 - 程序更新 - 失败,已回滚
2025-01-15 16:00 - v1.0.3 → v1.1.0 - 王五 - 完整部署 - 成功
```
#### 回滚策略
## 文件说明
1. **自动化回滚** - 备份恢复
2. **脚本化回滚** - 创建回滚脚本
3. **版本控制** - 通过Flyway版本管理
### 开发环境脚本:
- `deploy/simple/build-update.sh` - 生成程序更新包
- `deploy/simple/package.sh` - 生成完整部署包
## 六、文件说明
### 打包环境脚本
- `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` - 合并迁移脚本(历史用途)
### 客户环境脚本:
- `deploy.sh` - 首次部署脚本
- `update.sh` - 程序更新脚本
- `DeployGuide.md` - 本说明文档

65
deploy/build-and-upload.sh Executable file
View File

@ -0,0 +1,65 @@
#!/bin/bash
# QAUP macOS构建和上传脚本
# 在macOS开发环境运行构建jar并上传到打包服务器
set -e
# 配置打包服务器信息(请根据实际情况修改)
PACK_SERVER_HOST="10.0.0.58"
PACK_SERVER_USER="code"
PACK_SERVER_PATH="/home/code/apps/QAUP_Management"
# 颜色输出
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m'
print_message() {
echo -e "${1}${2}${NC}"
}
print_message $BLUE "=== QAUP macOS构建和上传脚本 ==="
# 1. 构建应用
print_message $BLUE "构建应用..."
mvn clean package -DskipTests -q
# 检查构建结果
JAR_FILE="qaup-admin/target/qaup-admin.jar"
if [ ! -f "$JAR_FILE" ]; then
print_message $RED "❌ jar文件构建失败"
exit 1
fi
print_message $GREEN "✓ jar文件构建成功"
# 2. 上传jar文件到打包服务器
print_message $BLUE "上传jar文件到打包服务器..."
# 检查是否配置了服务器信息
if [ "$PACK_SERVER_HOST" = "your-pack-server" ]; then
print_message $RED "❌ 请先配置打包服务器信息"
echo "编辑脚本,修改以下变量:"
echo "PACK_SERVER_HOST=\"your-pack-server-ip\""
echo "PACK_SERVER_USER=\"your-username\""
echo "PACK_SERVER_PATH=\"/path/to/QAUP_Management\""
exit 1
fi
# 上传jar文件
scp "$JAR_FILE" "$PACK_SERVER_USER@$PACK_SERVER_HOST:$PACK_SERVER_PATH/qaup-admin/target/"
print_message $GREEN "✓ jar文件上传成功"
# 3. 在打包服务器上执行打包
print_message $BLUE "在打包服务器上执行打包..."
ssh "$PACK_SERVER_USER@$PACK_SERVER_HOST" "cd $PACK_SERVER_PATH && ./deploy/simple/package-server.sh"
print_message $GREEN "✅ 完整打包流程完成"
echo ""
echo "下一步操作:"
echo "1. 从打包服务器下载生成的 qaup-deploy-*.tar.gz 文件"
echo "2. 将部署包传输到生产服务器进行部署"

110
deploy/build-update.sh Executable file
View File

@ -0,0 +1,110 @@
#!/bin/bash
# QAUP 程序更新打包脚本
# 用于生成仅包含jar文件的更新包
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
print_message() {
echo -e "${1}${2}${NC}"
}
print_message $BLUE "=== QAUP 程序更新打包 ==="
# 1. 构建应用
print_message $BLUE "构建应用..."
cd "$PROJECT_ROOT"
mvn clean package -DskipTests -q
# 检查jar文件是否生成成功
JAR_FILE="$PROJECT_ROOT/qaup-admin/target/qaup-admin.jar"
if [ ! -f "$JAR_FILE" ]; then
print_message $RED "❌ jar文件构建失败"
exit 1
fi
# 2. 创建更新包目录
UPDATE_DIR="qaup-update-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$UPDATE_DIR"
# 3. 复制jar文件
print_message $BLUE "准备更新文件..."
cp "$JAR_FILE" "$UPDATE_DIR/qaup-admin.jar"
# 4. 创建更新说明
cat > "$UPDATE_DIR/UPDATE-INSTRUCTIONS.md" << 'EOF'
# QAUP 程序更新包
## 包含内容
- `qaup-admin.jar` - 新版本应用程序
## 更新步骤
### 1. 上传文件到服务器
`qaup-admin.jar` 上传到服务器的 qaup-deploy 目录
### 2. 重命名文件
```bash
cd qaup-deploy
cp qaup-admin.jar new-app.jar
```
### 3. 执行更新
```bash
./update.sh
```
### 4. 验证更新
```bash
# 检查服务状态
docker compose ps
# 检查应用日志
docker compose logs -f qaup-app
# 访问系统确认功能正常
curl http://localhost:8080/actuator/health
```
## 注意事项
- 更新前建议备份数据库
- 如果更新失败,脚本会自动回滚
- 如有问题,请查看完整的部署更新说明文档
EOF
# 5. 创建版本信息
cat > "$UPDATE_DIR/VERSION-INFO.txt" << EOF
QAUP 程序更新包
构建时间: $(date)
构建主机: $(hostname)
Git提交: $(git rev-parse --short HEAD 2>/dev/null || echo "未知")
Maven版本: $(mvn --version | head -1)
Java版本: $(java -version 2>&1 | head -1)
文件大小: $(du -sh "$JAR_FILE" | cut -f1)
文件MD5: $(md5sum "$JAR_FILE" | cut -d' ' -f1)
EOF
# 6. 创建更新包
PACKAGE_NAME="${UPDATE_DIR}.tar.gz"
tar -czf "$PACKAGE_NAME" "$UPDATE_DIR"
# 7. 清理临时目录
rm -rf "$UPDATE_DIR"
print_message $GREEN "✅ 程序更新包创建完成: $PACKAGE_NAME"
print_message $BLUE "包大小: $(du -sh "$PACKAGE_NAME" | cut -f1)"
echo ""
echo "使用说明:"
echo "1. 将 $PACKAGE_NAME 发送给客户"
echo "2. 客户解压后按照 UPDATE-INSTRUCTIONS.md 操作"
echo "3. 或者直接发送 qaup-admin.jar 文件给客户"

View File

@ -254,8 +254,8 @@ traffic:
coordinate-system:
airport:
# 青岛机场坐标(客户部署时需要修改为实际机场坐标)
center-longitude: 120.08782536
center-latitude: 36.36236547
center-longitude: 120.0834104
center-latitude: 36.35406879
# 管理端点配置
management:

71
deploy/create-init-sql.sh Executable file
View File

@ -0,0 +1,71 @@
#!/bin/bash
# 创建简化的数据库初始化脚本
# 从现有的数据库架构文件生成init.sql
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
echo "=== 创建数据库初始化脚本 ==="
# 输出文件
INIT_SQL="$SCRIPT_DIR/init.sql"
# 创建初始化脚本
cat > "$INIT_SQL" << 'EOF'
-- QAUP 数据库初始化脚本
-- 简化版本:只创建必要的扩展和导入数据
-- 创建PostGIS扩展
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;
-- 显示扩展信息
SELECT 'PostGIS扩展安装完成' as message;
EOF
# 如果存在数据库架构文件,追加到初始化脚本
SCHEMA_FILE="$PROJECT_ROOT/deploy/docker/postgres/qaup_database_schema.sql"
if [ -f "$SCHEMA_FILE" ]; then
echo "-- 导入数据库架构" >> "$INIT_SQL"
cat "$SCHEMA_FILE" >> "$INIT_SQL"
echo "✓ 已添加数据库架构"
else
echo "⚠ 未找到数据库架构文件: $SCHEMA_FILE"
fi
# 添加完整初始数据(优先使用完整数据文件)
COMPLETE_DATA_FILE="$PROJECT_ROOT/deploy/docker/postgres/export/initial_data_complete.sql"
if [ -f "$COMPLETE_DATA_FILE" ]; then
echo "" >> "$INIT_SQL"
echo "-- 导入完整初始数据" >> "$INIT_SQL"
cat "$COMPLETE_DATA_FILE" >> "$INIT_SQL"
echo "✓ 已添加完整初始数据文件"
else
echo "⚠ 未找到完整初始数据文件: $COMPLETE_DATA_FILE"
echo "-- 注意:数据库将只包含基础结构,无初始数据" >> "$INIT_SQL"
fi
# 添加完成信息
cat >> "$INIT_SQL" << 'EOF'
-- 显示数据库信息
SELECT
current_database() as database_name,
current_user as current_user,
version() as postgresql_version;
-- 显示表数量
SELECT
schemaname,
COUNT(*) as table_count
FROM pg_tables
WHERE schemaname = 'public'
GROUP BY schemaname;
SELECT 'QAUP数据库初始化完成' as message;
EOF
echo "✅ 初始化脚本创建完成: $INIT_SQL"
echo "文件大小: $(du -sh "$INIT_SQL" | cut -f1)"

View File

@ -1,262 +0,0 @@
#!/bin/bash
# QAUP 一键部署脚本
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'
print_message() {
echo -e "${1}${2}${NC}"
}
print_message $BLUE "=== QAUP 一键部署 ==="
# 环境检查
print_message $BLUE "1. 检查部署环境..."
# 检查操作系统
if [[ "$OSTYPE" != "linux-gnu"* ]]; then
print_message $YELLOW "⚠️ 建议在Linux环境中运行"
fi
# 检查必要文件
REQUIRED_FILES=("app.jar" "docker-compose.yml" "config.yml" "images.tar.gz")
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$file" ]; then
print_message $RED "❌ 缺失必要文件: $file"
print_message $BLUE "请确保所有文件都存在,或重新解压部署包"
exit 1
fi
done
print_message $GREEN "✓ 所有必要文件检查通过"
# 检查Docker
if ! command -v docker &> /dev/null; then
print_message $RED "❌ Docker 未安装"
print_message $BLUE "请安装 Docker: sudo apt install docker.io"
exit 1
fi
# 检查Docker服务
if ! sudo systemctl is-active --quiet docker; then
print_message $YELLOW "⚠️ Docker 服务未启动,正在启动..."
sudo systemctl start docker
sleep 5
fi
DOCKER_VERSION=$(docker --version)
print_message $GREEN "✓ Docker 版本: $DOCKER_VERSION"
# 检查Docker Compose
if ! docker compose version &> /dev/null && ! docker-compose version &> /dev/null; then
print_message $RED "❌ Docker Compose 未安装"
print_message $BLUE "请安装 Docker Compose: sudo apt install docker-compose"
exit 1
fi
print_message $GREEN "✓ Docker Compose 检查通过"
# 检查磁盘空间至少需要3GB
AVAILABLE_SPACE=$(df . | tail -1 | awk '{print $4}')
REQUIRED_SPACE=$((3 * 1024 * 1024)) # 3GB in KB
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then
print_message $RED "❌ 磁盘空间不足"
print_message $BLUE "可用空间: $(($AVAILABLE_SPACE / 1024 / 1024))GB, 需要: 3GB"
exit 1
fi
print_message $GREEN "✓ 磁盘空间充足: $(($AVAILABLE_SPACE / 1024 / 1024))GB"
# 检查端口占用
PORTS=(8080 5432 6379)
print_message $BLUE "2. 检查端口占用..."
for port in "${PORTS[@]}"; do
if netstat -tuln 2>/dev/null | grep -q ":$port " || ss -tuln 2>/dev/null | grep -q ":$port "; then
print_message $YELLOW "⚠️ 端口 $port 已被占用"
print_message $BLUE "请停止占用该端口的服务或修改端口配置"
fi
done
print_message $GREEN "✓ 端口检查完成"
# 检查Docker镜像
print_message $BLUE "3. 检查Docker镜像..."
if [ -f "images.tar.gz" ]; then
print_message $BLUE "载入预构建镜像..."
if ! docker load -i images.tar.gz; then
print_message $RED "❌ 镜像载入失败"
exit 1
fi
print_message $GREEN "✓ 镜像载入成功"
else
print_message $YELLOW "⚠️ 未找到镜像包,将尝试在线拉取"
# 尝试拉取镜像
IMAGES=("m.daocloud.io/docker.io/postgis/postgis:17-3.5-alpine" \
"m.daocloud.io/docker.io/library/redis:8.0-alpine" \
"m.daocloud.io/docker.io/library/eclipse-temurin:21-jre")
for image in "${IMAGES[@]}"; do
if ! docker pull --platform linux/amd64 "$image"; then
print_message $RED "❌ 镜像拉取失败: $image"
exit 1
fi
done
print_message $GREEN "✓ 所有镜像拉取成功"
fi
# 停止可能存在的冲突容器
print_message $BLUE "4. 清理冲突容器..."
docker compose down 2>/dev/null || true
docker rm -f $(docker ps -aq --filter name=qaup) 2>/dev/null || true
print_message $GREEN "✓ 冲突容器清理完成"
# 创建数据目录
print_message $BLUE "5. 创建数据目录..."
mkdir -p data/postgres data/redis logs backup
# 设置目录权限
chmod 755 data logs backup
chmod 700 data/postgres data/redis
print_message $GREEN "✓ 数据目录创建完成"
# 验证配置文件
print_message $BLUE "6. 验证配置文件..."
# 检查docker-compose.yml语法
if ! docker compose config -q; then
print_message $RED "❌ docker-compose.yml 配置语法错误"
exit 1
fi
print_message $GREEN "✓ docker-compose.yml 语法正确"
# 检查应用配置文件
if ! grep -q "qaup:" config.yml; then
print_message $RED "❌ config.yml 配置不完整"
exit 1
fi
print_message $GREEN "✓ 应用配置文件正常"
# 启动基础设施服务
print_message $BLUE "7. 启动基础设施服务..."
docker compose up -d qaup-postgres qaup-redis
# 等待数据库启动
print_message $BLUE "等待数据库启动30秒..."
sleep 30
# 检查数据库状态
if docker exec qaup-postgres pg_isready -U qaup > /dev/null 2>&1; then
print_message $GREEN "✓ PostgreSQL 数据库启动成功"
else
print_message $RED "❌ PostgreSQL 启动失败"
print_message $BLUE "数据库日志:"
docker compose logs qaup-postgres
exit 1
fi
# 检查Redis状态
if docker exec qaup-redis redis-cli ping > /dev/null 2>&1; then
print_message $GREEN "✓ Redis 缓存服务启动成功"
else
print_message $RED "❌ Redis 启动失败"
print_message $BLUE "Redis日志:"
docker compose logs qaup-redis
exit 1
fi
# 启动应用服务
print_message $BLUE "8. 启动应用服务..."
docker compose up -d qaup-app
# 等待应用启动和数据库迁移
print_message $BLUE "9. 等待应用启动和数据库迁移..."
print_message $BLUE " 这可能需要2-3分钟请耐心等待..."
WAIT_TIME=0
HEALTH_URL="http://localhost:8080/actuator/health"
while [ $WAIT_TIME -lt 180 ]; do
sleep 10
WAIT_TIME=$((WAIT_TIME + 10))
# 检查应用健康状态
if curl -f -s "$HEALTH_URL" > /dev/null 2>&1; then
print_message $GREEN "✓ 应用启动成功!"
break
fi
# 显示等待进度
if [ $((WAIT_TIME % 30)) -eq 0 ]; then
print_message $BLUE " 已等待 ${WAIT_TIME} 秒..."
# 检查是否有迁移相关的日志
MIGRATION_LOGS=$(docker compose logs qaup-app 2>/dev/null | grep -i "flyway\|migration" | tail -3)
if [ -n "$MIGRATION_LOGS" ]; then
print_message $BLUE " 迁移进度:"
echo "$MIGRATION_LOGS" | while read line; do
print_message $BLUE " $line"
done
fi
fi
done
# 最终验证
print_message $BLUE "10. 最终验证..."
if curl -f -s "$HEALTH_URL" > /dev/null 2>&1; then
# 获取服务状态
APP_STATUS=$(docker compose ps qaup-app --format json | jq -r '.[0].State' 2>/dev/null || echo "unknown")
DB_STATUS=$(docker compose ps qaup-postgres --format json | jq -r '.[0].State' 2>/dev/null || echo "unknown")
REDIS_STATUS=$(docker compose ps qaup-redis --format json | jq -r '.[0].State' 2>/dev/null || echo "unknown")
print_message $GREEN "🎉 部署成功完成!"
echo ""
print_message $GREEN "📊 服务状态:"
print_message $BLUE " 应用服务: $APP_STATUS"
print_message $BLUE " 数据库: $DB_STATUS"
print_message $BLUE " 缓存服务: $REDIS_STATUS"
echo ""
print_message $BLUE "🌐 访问信息:"
print_message $BLUE " Web管理: http://localhost:8080"
print_message $BLUE " 健康检查: $HEALTH_URL"
print_message $BLUE " 数据库: localhost:5432 (qaup/qaup123)"
print_message $BLUE " Redis: localhost:6379"
echo ""
print_message $BLUE "👤 初始登录:"
print_message $BLUE " 用户名: admin"
print_message $BLUE " 密码: admin123"
echo ""
print_message $BLUE "📋 管理命令:"
print_message $BLUE " 查看状态: docker compose ps"
print_message $BLUE " 查看日志: docker compose logs -f qaup-app"
print_message $BLUE " 查看迁移: docker exec qaup-postgres psql -U qaup -d qaup -c \"SELECT version,description FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;\""
print_message $BLUE " 停止服务: docker compose down"
print_message $BLUE " 重启应用: docker compose restart qaup-app"
print_message $BLUE " 升级应用: ./deploy-update.sh"
echo ""
print_message $GREEN "🚀 QAUP 系统已就绪!"
else
print_message $RED "❌ 应用启动失败"
print_message $BLUE "请检查以下信息:"
echo ""
print_message $BLUE "📋 应用日志:"
docker compose logs --tail=50 qaup-app
echo ""
print_message $BLUE "📋 数据库日志:"
docker compose logs --tail=20 qaup-postgres
echo ""
print_message $BLUE "📋 容器状态:"
docker compose ps
echo ""
print_message $YELLOW "💡 可能的解决方案:"
print_message $BLUE "1. 检查端口是否被占用: netstat -tuln | grep ':8080'"
print_message $BLUE "2. 检查磁盘空间: df -h"
print_message $BLUE "3. 重启Docker服务: sudo systemctl restart docker"
print_message $BLUE "4. 清理Docker资源: docker system prune -a"
exit 1
fi

View File

@ -1,238 +0,0 @@
#!/bin/bash
# QAUP 一键升级脚本
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'
print_message() {
echo -e "${1}${2}${NC}"
}
print_message $BLUE "=== QAUP 一键升级 ==="
# 检查必要文件
REQUIRED_FILES=("app.jar" "docker-compose.yml" "config.yml")
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$file" ]; then
print_message $RED "❌ 缺失必要文件: $file"
print_message $BLUE "请确保在正确的部署目录中运行此脚本"
exit 1
fi
done
# 1. 环境检查和准备
print_message $BLUE "1. 环境检查..."
# 检查Docker服务状态
if ! docker compose ps > /dev/null 2>&1; then
print_message $RED "❌ 无法连接到Docker服务"
print_message $BLUE "请确保Docker服务正在运行: sudo systemctl status docker"
exit 1
fi
# 检查应用服务状态
APP_STATUS=$(docker compose ps qaup-app --format json 2>/dev/null | jq -r '.[0].State' 2>/dev/null || echo "exited")
if [ "$APP_STATUS" != "running" ]; then
print_message $YELLOW "⚠️ 应用服务当前状态: $APP_STATUS"
print_message $BLUE "正在启动应用服务..."
docker compose up -d qaup-app
sleep 30
fi
# 获取当前应用版本信息
print_message $BLUE "2. 检查应用版本..."
if [ -f "new-app.jar" ]; then
NEW_JAR_SIZE=$(stat -f%z new-app.jar 2>/dev/null || stat -c%s new-app.jar 2>/dev/null || echo "unknown")
CURRENT_JAR_SIZE=$(stat -f%z app.jar 2>/dev/null || stat -c%s app.jar 2>/dev/null || echo "unknown")
print_message $BLUE " 当前版本大小: $CURRENT_JAR_SIZE bytes"
print_message $BLUE " 新版本大小: $NEW_JAR_SIZE bytes"
if [ "$NEW_JAR_SIZE" = "$CURRENT_JAR_SIZE" ]; then
print_message $YELLOW "⚠️ 新旧版本大小相同,请确认版本是否正确"
read -p "是否继续升级?(y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_message $BLUE "升级已取消"
exit 0
fi
fi
else
print_message $RED "❌ 未找到新版本文件: new-app.jar"
print_message $BLUE "请先将新版本文件重命名为 new-app.jar"
print_message $BLUE " 例如: cp qaup-admin-1.0.2.jar new-app.jar"
exit 1
fi
# 3. 数据库备份
print_message $BLUE "3. 备份数据库..."
BACKUP_DIR="backup"
mkdir -p "$BACKUP_DIR"
BACKUP_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/qaup_db_backup_$BACKUP_TIMESTAMP.sql"
if docker exec qaup-postgres pg_dump -U qaup qaup > "$BACKUP_FILE" 2>/dev/null; then
BACKUP_SIZE=$(stat -f%z "$BACKUP_FILE" 2>/dev/null || stat -c%s "$BACKUP_FILE" 2>/dev/null || echo "unknown")
print_message $GREEN "✓ 数据库备份成功: $BACKUP_FILE ($BACKUP_SIZE bytes)"
else
print_message $RED "❌ 数据库备份失败"
print_message $BLUE "升级已取消,请检查数据库连接"
exit 1
fi
# 4. 备份当前应用
print_message $BLUE "4. 备份当前应用..."
BACKUP_JAR="$BACKUP_DIR/app.jar.backup.$BACKUP_TIMESTAMP"
cp app.jar "$BACKUP_JAR"
print_message $GREEN "✓ 应用备份成功: $BACKUP_JAR"
# 5. 获取升级前数据库迁移状态
print_message $BLUE "5. 记录升级前迁移状态..."
BEFORE_MIGRATION=$(docker exec qaup-postgres psql -U qaup -d qaup -t -c "SELECT version,description FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;" 2>/dev/null || echo "无法获取迁移状态")
print_message $BLUE "升级前迁移状态已记录"
# 6. 停止应用服务
print_message $BLUE "6. 停止应用服务..."
docker compose stop qaup-app
sleep 10
print_message $GREEN "✓ 应用服务已停止"
# 7. 替换应用文件
print_message $BLUE "7. 更新应用文件..."
if cp new-app.jar app.jar; then
print_message $GREEN "✓ 应用文件更新成功"
else
print_message $RED "❌ 应用文件更新失败"
print_message $BLUE "正在恢复备份..."
cp "$BACKUP_JAR" app.jar
docker compose start qaup-app
exit 1
fi
# 8. 启动应用服务
print_message $BLUE "8. 启动应用服务..."
docker compose up -d qaup-app
sleep 15
# 9. 监控应用启动和数据库迁移
print_message $BLUE "9. 监控应用启动和数据库迁移..."
print_message $BLUE " 这可能需要2-3分钟请耐心等待..."
WAIT_TIME=0
HEALTH_URL="http://localhost:8080/actuator/health"
while [ $WAIT_TIME -lt 180 ]; do
sleep 10
WAIT_TIME=$((WAIT_TIME + 10))
# 检查应用健康状态
if curl -f -s "$HEALTH_URL" > /dev/null 2>&1; then
print_message $GREEN "✓ 应用启动成功!"
break
fi
# 显示等待进度
if [ $((WAIT_TIME % 30)) -eq 0 ]; then
print_message $BLUE " 已等待 ${WAIT_TIME} 秒..."
# 检查应用日志中的迁移信息
MIGRATION_LOGS=$(docker compose logs qaup-app --tail=10 2>/dev/null | grep -E "(Flyway|migration|Migration)" | tail -2)
if [ -n "$MIGRATION_LOGS" ]; then
print_message $BLUE " 迁移进度:"
echo "$MIGRATION_LOGS" | while read line; do
print_message $BLUE " $line"
done
fi
# 检查是否有错误
ERROR_LOGS=$(docker compose logs qaup-app --tail=5 2>/dev/null | grep -i -E "(error|exception|fail)" || true)
if [ -n "$ERROR_LOGS" ]; then
print_message $YELLOW " 检测到可能的错误:"
echo "$ERROR_LOGS" | while read line; do
print_message $YELLOW " $line"
done
fi
fi
done
# 10. 验证升级结果
print_message $BLUE "10. 验证升级结果..."
if curl -f -s "$HEALTH_URL" > /dev/null 2>&1; then
# 获取升级后数据库迁移状态
print_message $BLUE " 检查数据库迁移状态..."
AFTER_MIGRATION=$(docker exec qaup-postgres psql -U qaup -d qaup -t -c "SELECT version,description FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 3;" 2>/dev/null || echo "无法获取迁移状态")
print_message $GREEN "🎉 升级成功完成!"
echo ""
print_message $GREEN "📊 升级信息:"
print_message $BLUE " 应用状态: 运行中"
print_message $BLUE " 健康检查: 通过"
echo ""
print_message $BLUE "💾 备份信息:"
print_message $BLUE " 应用备份: $BACKUP_JAR"
print_message $BLUE " 数据库备份: $BACKUP_FILE"
echo ""
print_message $BLUE "📋 升级前迁移状态:"
echo "$BEFORE_MIGRATION" | while read line; do
print_message $BLUE " $line"
done
echo ""
print_message $BLUE "📋 升级后迁移状态:"
echo "$AFTER_MIGRATION" | while read line; do
print_message $BLUE " $line"
done
echo ""
print_message $BLUE "🔍 验证命令:"
print_message $BLUE " 查看应用日志: docker compose logs -f qaup-app"
print_message $BLUE " 检查数据库连接: docker exec qaup-postgres psql -U qaup -d qaup -c 'SELECT version();'"
print_message $BLUE " 回滚命令: ./rollback.sh $BACKUP_TIMESTAMP"
# 创建回滚脚本
cat > rollback_$BACKUP_TIMESTAMP.sh << EOF
#!/bin/bash
echo "正在回滚到版本 $BACKUP_TIMESTAMP..."
docker compose stop qaup-app
cp backup/app.jar.backup.$BACKUP_TIMESTAMP app.jar
docker compose start qaup-app
echo "回滚完成,请检查应用状态"
EOF
chmod +x rollback_$BACKUP_TIMESTAMP.sh
print_message $BLUE " 自动回滚脚本: rollback_$BACKUP_TIMESTAMP.sh"
else
print_message $RED "❌ 升级失败"
print_message $BLUE "正在执行自动回滚..."
# 自动回滚
docker compose stop qaup-app
cp "$BACKUP_JAR" app.jar
docker compose start qaup-app
sleep 30
# 验证回滚
if curl -f -s "$HEALTH_URL" > /dev/null 2>&1; then
print_message $GREEN "✓ 自动回滚成功"
print_message $BLUE " 应用已恢复到升级前版本"
print_message $BLUE " 请检查应用日志: docker compose logs qaup-app"
else
print_message $RED "❌ 回滚失败"
print_message $BLUE " 请手动检查并恢复服务"
fi
echo ""
print_message $BLUE "📋 详细信息:"
print_message $BLUE " 应用备份: $BACKUP_JAR"
print_message $BLUE " 数据库备份: $BACKUP_FILE"
print_message $BLUE " 升级前状态: $APP_STATUS"
exit 1
fi

47
deploy/deploy.sh Executable file
View File

@ -0,0 +1,47 @@
#!/bin/bash
# QAUP 一键部署脚本
set -e
echo "=== QAUP 一键部署 ==="
# 检查Docker
if ! command -v docker &> /dev/null; then
echo "❌ Docker 未安装"
exit 1
fi
# 载入镜像
echo "载入Docker镜像..."
docker load -i images.tar.gz
# 创建数据目录
echo "创建数据目录..."
mkdir -p data/postgres data/redis logs
# 启动服务
echo "启动服务..."
docker compose up -d
# 等待服务启动
echo "等待服务启动60秒..."
sleep 60
# 检查服务状态
if curl -f -s http://localhost:8080/actuator/health > /dev/null 2>&1; then
echo "✅ 部署成功!"
echo ""
echo "访问地址: http://localhost:8080"
echo "数据库: localhost:5432 (qaup/qaup123)"
echo "Redis: localhost:6379"
echo ""
echo "管理命令:"
echo " 查看状态: docker compose ps"
echo " 查看日志: docker compose logs -f qaup-app"
echo " 停止服务: docker compose down"
echo " 升级应用: ./update.sh"
else
echo "❌ 服务启动失败,请检查日志:"
docker compose logs
fi

View File

@ -1,381 +0,0 @@
#!/bin/bash
# QAUP 服务器端打包脚本
# 在Ubuntu打包服务器上运行使用已构建的jar文件进行打包
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'
print_message() {
echo -e "${1}${2}${NC}"
}
print_message $BLUE "=== QAUP 服务器端打包脚本 ==="
# 检查基础环境
print_message $BLUE "1. 检查基础环境..."
# 检查操作系统
if [[ "$OSTYPE" != "linux-gnu"* ]]; then
print_message $YELLOW "⚠️ 警告: 当前操作系统为 $OSTYPE建议在Linux上运行"
fi
# 检查Java 21
if ! command -v java &> /dev/null; then
print_message $RED "❌ Java 未安装"
print_message $BLUE "请安装 Java 21: sudo apt install openjdk-21-jdk"
exit 1
fi
JAVA_VERSION=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
REQUIRED_VERSION="21"
if [[ "$(printf '%s\n' "$REQUIRED_VERSION" "$JAVA_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]]; then
print_message $RED "❌ Java 版本不兼容"
print_message $BLUE "当前版本: $JAVA_VERSION, 需要: $REQUIRED_VERSION+"
exit 1
fi
print_message $GREEN "✓ Java 版本检查通过: $JAVA_VERSION"
# 检查Maven
if ! command -v mvn &> /dev/null; then
print_message $RED "❌ Maven 未安装"
print_message $BLUE "请安装 Maven: sudo apt install maven"
exit 1
fi
print_message $GREEN "✓ Maven 版本: $(mvn -version | head -1)"
# 检查Docker和Docker Compose
if ! command -v docker &> /dev/null; then
print_message $RED "❌ Docker 未安装"
exit 1
fi
DOCKER_VERSION=$(docker --version)
print_message $GREEN "✓ Docker 版本: $DOCKER_VERSION"
if ! docker compose version &> /dev/null && ! docker-compose version &> /dev/null; then
print_message $RED "❌ Docker Compose 未安装"
print_message $BLUE "请安装 Docker Compose: sudo apt install docker-compose"
exit 1
fi
print_message $GREEN "✓ Docker Compose 检查通过"
# 检查磁盘空间至少需要5GB
AVAILABLE_SPACE=$(df . | tail -1 | awk '{print $4}')
REQUIRED_SPACE=$((5 * 1024 * 1024)) # 5GB in KB
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then
print_message $RED "❌ 磁盘空间不足"
print_message $BLUE "可用空间: $(($AVAILABLE_SPACE / 1024 / 1024))GB, 需要: 5GB"
exit 1
fi
print_message $GREEN "✓ 磁盘空间充足: $(($AVAILABLE_SPACE / 1024 / 1024))GB"
# 检查jar文件
print_message $BLUE "2. 检查jar文件..."
JAR_FILE="qaup-admin/target/qaup-admin.jar"
if [ ! -f "$JAR_FILE" ]; then
print_message $YELLOW "未找到jar文件尝试构建..."
# 尝试构建项目
if [ -f "pom.xml" ]; then
print_message $BLUE "执行 Maven 构建..."
mvn clean package -DskipTests
if [ ! -f "$JAR_FILE" ]; then
print_message $RED "❌ 构建失败: $JAR_FILE 仍然不存在"
exit 1
fi
else
print_message $RED "❌ 未找到jar文件: $JAR_FILE"
print_message $BLUE "请先在macOS上构建jar文件并上传到服务器"
exit 1
fi
fi
print_message $GREEN "✓ 找到jar文件: $JAR_FILE"
print_message $BLUE " 文件大小: $(du -sh $JAR_FILE | cut -f1)"
# 验证jar文件是否为有效的Java应用
if ! jar -tf "$JAR_FILE" > /dev/null 2>&1; then
print_message $RED "❌ jar文件损坏或不是有效的JAR文件"
exit 1
fi
print_message $GREEN "✓ jar文件验证通过"
# 准备镜像版本
print_message $BLUE "3. 准备Docker镜像..."
POSTGRES_IMAGE="m.daocloud.io/docker.io/postgis/postgis:17-3.5-alpine"
REDIS_IMAGE="m.daocloud.io/docker.io/library/redis:8.0-alpine"
OPENJDK_IMAGE="m.daocloud.io/docker.io/library/eclipse-temurin:21-jre"
# 检查网络连接
if ! ping -c 1 m.daocloud.io &> /dev/null; then
print_message $YELLOW "⚠️ 网络连接测试失败,可能影响镜像拉取"
fi
print_message $BLUE "拉取基础镜像..."
print_message $BLUE " - PostgreSQL + PostGIS"
if ! docker pull --platform linux/amd64 $POSTGRES_IMAGE; then
print_message $RED "❌ PostgreSQL镜像拉取失败"
exit 1
fi
print_message $BLUE " - Redis"
if ! docker pull --platform linux/amd64 $REDIS_IMAGE; then
print_message $RED "❌ Redis镜像拉取失败"
exit 1
fi
print_message $BLUE " - Java 21 Runtime"
if ! docker pull --platform linux/amd64 $OPENJDK_IMAGE; then
print_message $RED "❌ Java镜像拉取失败"
exit 1
fi
# 导出镜像
print_message $BLUE "4. 导出Docker镜像..."
mkdir -p qaup-deploy
# 检查磁盘空间(镜像包需要额外空间)
EXPECTED_IMAGE_SIZE=$((2 * 1024)) # 预计2GB
if [ "$AVAILABLE_SPACE" -lt "$((EXPECTED_IMAGE_SIZE * 1024))" ]; then
print_message $YELLOW "⚠️ 磁盘空间可能不足预计需要额外2GB空间"
fi
if ! docker save $POSTGRES_IMAGE $REDIS_IMAGE $OPENJDK_IMAGE | gzip > qaup-deploy/images.tar.gz; then
print_message $RED "❌ 镜像导出失败"
exit 1
fi
IMAGE_SIZE=$(du -sh qaup-deploy/images.tar.gz | cut -f1)
print_message $GREEN "✓ 镜像包大小: $IMAGE_SIZE"
# 复制必要文件
print_message $BLUE "5. 准备部署文件..."
# 复制核心应用文件
cp "$JAR_FILE" qaup-deploy/app.jar
if [ $? -eq 0 ]; then
print_message $GREEN "✓ 应用JAR文件已复制"
else
print_message $RED "❌ 应用JAR文件复制失败"
exit 1
fi
# 复制Docker配置
cp deploy/docker-compose.yml qaup-deploy/ || {
print_message $RED "❌ docker-compose.yml 复制失败"
exit 1
}
print_message $GREEN "✓ Docker编排配置已复制"
# 复制应用配置
cp deploy/config.yml qaup-deploy/ || {
print_message $RED "❌ config.yml 复制失败"
exit 1
}
print_message $GREEN "✓ 应用配置文件已复制"
# 复制部署脚本
cp deploy/deploy-all.sh qaup-deploy/ || {
print_message $RED "❌ deploy-all.sh 复制失败"
exit 1
}
chmod +x qaup-deploy/deploy-all.sh
cp deploy/deploy-update.sh qaup-deploy/ || {
print_message $RED "❌ deploy-update.sh 复制失败"
exit 1
}
chmod +x qaup-deploy/deploy-update.sh
cp deploy/qaup-service.sh qaup-deploy/ || {
print_message $RED "❌ qaup-service.sh 复制失败"
exit 1
}
chmod +x qaup-deploy/qaup-service.sh
print_message $GREEN "✓ 部署脚本已复制并设置执行权限"
# 复制数据库相关文件(可选)
if [ -f "deploy/qaup_database_export.sql" ]; then
cp deploy/qaup_database_export.sql qaup-deploy/qaup_database_export.sql
print_message $GREEN "✓ 数据库导出文件已复制"
else
print_message $YELLOW "⚠️ 数据库导出文件不存在"
fi
# 复制文档文件
if [ -f "deploy/DeployGuide.md" ]; then
cp deploy/DeployGuide.md qaup-deploy/
print_message $GREEN "✓ 部署指南已复制"
else
print_message $YELLOW "⚠️ 部署指南不存在"
fi
# 创建必需目录
mkdir -p qaup-deploy/{backup,logs,data/postgres,data/redis}
print_message $GREEN "✓ 目录结构已创建"
# 创建README文件
cat > qaup-deploy/README.md << 'EOF'
# QAUP 部署包
## 目录结构
```
qaup-deploy/
├── app.jar # 应用JAR文件
├── docker-compose.yml # Docker编排配置
├── config.yml # 应用配置文件
├── deploy-all.sh # 一键部署脚本
├── deploy-update.sh # 一键升级脚本
├── qaup-service.sh # 统一服务管理脚本
├── images.tar.gz # Docker镜像包
├── README.md # 本文件
├── backup/ # 备份目录
├── logs/ # 日志目录
└── data/ # 数据目录
├── postgres/ # PostgreSQL数据
└── redis/ # Redis数据
```
## 快速部署
```bash
# 解压和部署
tar -xzf qaup-deploy.tar.gz
cd qaup-deploy
./deploy-all.sh
# 检查状态
docker compose ps
curl http://localhost:8080/actuator/health
```
## 统一服务管理
```bash
# 使用统一服务管理脚本
./qaup-service.sh help # 查看帮助
./qaup-service.sh start # 启动所有服务
./qaup-service.sh status # 查看服务状态
./qaup-service.sh logs qaup-app # 查看应用日志
./qaup-service.sh health # 健康检查
./qaup-service.sh backup # 数据备份
```
## 应用升级
```bash
# 方法1: 使用统一管理脚本(推荐)
./qaup-service.sh update # 将新版本文件重命名为new-app.jar后使用
# 方法2: 使用专用升级脚本
cp /path/to/new/qaup-admin.jar ./new-app.jar
./deploy-update.sh
```
## 管理命令
```bash
# 查看状态
./qaup-service.sh status
# 或
docker compose ps
# 查看日志
./qaup-service.sh logs qaup-app
# 或
docker compose logs -f qaup-app
# 停止服务
./qaup-service.sh stop
# 或
docker compose down
# 重启应用
./qaup-service.sh restart
# 或
docker compose restart qaup-app
```
## 默认信息
- **Web访问**: http://localhost:8080
- **数据库**: localhost:5432 (qaup/qaup123)
- **Redis**: localhost:6379
- **初始账号**: admin/admin123
## 支持
如遇问题,请查看日志:
```bash
docker compose logs qaup-app
```
EOF
print_message $GREEN "✓ README文件已创建"
# 验证部署包完整性
print_message $BLUE "6. 验证部署包完整性..."
REQUIRED_FILES=("app.jar" "docker-compose.yml" "config.yml" "deploy-all.sh" "deploy-update.sh" "qaup-service.sh" "images.tar.gz")
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "qaup-deploy/$file" ]; then
print_message $RED "❌ 缺失必要文件: $file"
exit 1
fi
done
print_message $GREEN "✓ 所有必要文件验证通过"
# 创建部署包
print_message $BLUE "7. 创建部署包..."
PACKAGE_NAME="qaup-deploy-$(date +%Y%m%d-%H%M%S).tar.gz"
if tar -czf "$PACKAGE_NAME" -C qaup-deploy .; then
print_message $GREEN "✅ 打包完成: $PACKAGE_NAME"
else
print_message $RED "❌ 打包失败"
exit 1
fi
PACKAGE_SIZE=$(du -sh "$PACKAGE_NAME" | cut -f1)
DEPLOY_DIR_SIZE=$(du -sh qaup-deploy | cut -f1)
print_message $GREEN "✓ 部署包大小: $PACKAGE_SIZE"
print_message $BLUE " 临时目录大小: $DEPLOY_DIR_SIZE"
# 清理临时文件
print_message $BLUE "8. 清理临时文件..."
rm -rf qaup-deploy
print_message $GREEN "✓ 临时目录已清理"
print_message $GREEN "🎉 打包成功完成!"
echo ""
print_message $BLUE "📋 部署说明(生产环境):"
echo ""
echo "⚠️ 由于生产环境安全要求,必须手工文件上传"
echo ""
echo "1. 按安全策略传输部署包到目标服务器:"
echo " scp $PACKAGE_NAME user@生产服务器IP:/opt/qaup/"
echo " 或使用SFTP、文件传输工具等"
echo ""
echo "2. 在生产服务器执行部署:"
echo " ssh user@生产服务器IP"
echo " cd /opt/qaup"
echo " mkdir qaup-deploy && tar -xzf $PACKAGE_NAME -C qaup-deploy"
echo " cd qaup-deploy"
echo " chmod +x *.sh"
echo " ./deploy-all.sh"
echo ""
echo "3. 详细部署说明请查看:"
echo " - 部署指南: DeployGuide.md"
echo ""
print_message $BLUE "📊 打包统计:"
echo " - 部署包: $PACKAGE_NAME ($PACKAGE_SIZE)"
echo " - Java版本: $JAVA_VERSION"
echo " - 打包时间: $(date)"
echo " - 服务器: $(hostname)"
echo ""
print_message $BLUE "📖 文档信息:"
echo " - 部署指南已包含在部署包中: DeployGuide.md"
echo " - 包含完整的生产环境部署和更新说明"
echo ""
print_message $GREEN "🚀 可以开始部署了!"

82
deploy/package-server.sh Executable file
View File

@ -0,0 +1,82 @@
#!/bin/bash
# QAUP 服务器端打包脚本
# 在Ubuntu打包服务器上运行使用已构建的jar文件进行打包
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
print_message() {
echo -e "${1}${2}${NC}"
}
print_message $BLUE "=== QAUP 服务器端打包脚本 ==="
# 检查jar文件是否存在
JAR_FILE="qaup-admin/target/qaup-admin.jar"
if [ ! -f "$JAR_FILE" ]; then
print_message $RED "❌ 未找到jar文件: $JAR_FILE"
echo "请先在macOS上构建jar文件并上传到服务器"
echo "或者将jar文件放到正确位置"
exit 1
fi
print_message $GREEN "✓ 找到jar文件: $JAR_FILE"
# 检查Docker是否可用
if ! command -v docker &> /dev/null; then
print_message $RED "❌ Docker 未安装"
exit 1
fi
# 准备镜像版本
POSTGRES_IMAGE="m.daocloud.io/docker.io/postgis/postgis:17-3.5-alpine"
REDIS_IMAGE="m.daocloud.io/docker.io/library/redis:8.0-alpine"
OPENJDK_IMAGE="m.daocloud.io/docker.io/library/eclipse-temurin:21-jre"
print_message $BLUE "拉取基础镜像..."
docker pull --platform linux/amd64 $POSTGRES_IMAGE
docker pull --platform linux/amd64 $REDIS_IMAGE
docker pull --platform linux/amd64 $OPENJDK_IMAGE
# 导出镜像
print_message $BLUE "导出镜像..."
mkdir -p qaup-deploy
docker save $POSTGRES_IMAGE $REDIS_IMAGE $OPENJDK_IMAGE | gzip > qaup-deploy/images.tar.gz
# 复制必要文件
print_message $BLUE "准备部署文件..."
cp "$JAR_FILE" qaup-deploy/app.jar
cp deploy/simple/docker-compose.yml qaup-deploy/
cp deploy/simple/config.yml qaup-deploy/
# 复制数据库相关文件
mkdir -p qaup-deploy/sql
cp deploy/docker/postgres/qaup_database_schema.sql qaup-deploy/sql/ 2>/dev/null || echo "⚠ 数据库架构文件不存在"
cp deploy/docker/postgres/export/initial_data_complete.sql qaup-deploy/sql/ 2>/dev/null || echo "⚠ 完整初始数据文件不存在"
# 创建数据库初始化脚本
./deploy/simple/create-init-sql.sh
cp deploy/simple/init.sql qaup-deploy/
cp deploy/simple/deploy.sh qaup-deploy/
cp deploy/simple/update.sh qaup-deploy/
cp deploy/simple/DeployGuide.md qaup-deploy/
# 创建部署包
PACKAGE_NAME="qaup-deploy-$(date +%Y%m%d-%H%M%S).tar.gz"
tar -czf "$PACKAGE_NAME" -C qaup-deploy .
print_message $GREEN "✅ 打包完成: $PACKAGE_NAME"
print_message $BLUE "部署包大小: $(du -sh $PACKAGE_NAME | cut -f1)"
echo ""
echo "部署说明:"
echo "1. 将 $PACKAGE_NAME 传输到目标服务器"
echo "2. mkdir qaup-deploy && tar -xzf $PACKAGE_NAME -C qaup-deploy"
echo "3. cd qaup-deploy && ./deploy.sh"

View File

@ -1,157 +0,0 @@
#!/bin/bash
# QAUP 程序更新打包脚本
# 用于生成仅包含jar文件的更新包
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
print_message() {
echo -e "${1}${2}${NC}"
}
print_message $BLUE "=== QAUP 程序更新打包 ==="
# 1. 构建应用
print_message $BLUE "构建应用..."
cd "$PROJECT_ROOT"
mvn clean package -DskipTests -q
# 检查jar文件是否生成成功
JAR_FILE="$PROJECT_ROOT/qaup-admin/target/qaup-admin.jar"
if [ ! -f "$JAR_FILE" ]; then
print_message $RED "❌ jar文件构建失败"
exit 1
fi
# 2. 创建更新包目录
UPDATE_DIR="qaup-update-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$UPDATE_DIR"
# 3. 复制jar文件
print_message $BLUE "准备更新文件..."
cp "$JAR_FILE" "$UPDATE_DIR/qaup-admin.jar"
# 4. 创建更新说明
cat > "$UPDATE_DIR/UPDATE-INSTRUCTIONS.md" << 'EOF'
# QAUP 程序更新包
## 包含内容
- `qaup-admin.jar` - 新版本应用程序
## ⚠️ 生产环境安全要求
由于生产环境安全限制,必须手工文件上传,禁止使用自动化脚本传输。
## 更新步骤
### 1. 文件传输(手工方式)
将更新包 `qaup-admin.jar` 按安全策略传输到生产服务器:
```bash
# 方案A使用SCP
scp qaup-admin.jar user@生产服务器IP:/opt/qaup/qaup-deploy/
# 方案B使用SFTP
# 通过SFTP工具上传文件到 /opt/qaup/qaup-deploy/ 目录
```
### 2. 重命名文件
```bash
ssh user@生产服务器IP
cd /opt/qaup/qaup-deploy
cp qaup-admin.jar new-app.jar
```
### 3. 执行更新
```bash
./deploy-update.sh
```
### 4. 验证更新
```bash
# 检查服务状态
docker compose ps
# 检查应用日志
docker compose logs -f qaup-app
# 访问系统确认功能正常
curl http://localhost:8080/actuator/health
```
## 安全注意事项
- 更新前建议备份数据库:`docker exec qaup-postgres pg_dump -U qaup qaup > backup-$(date +%Y%m%d).sql`
- 如果更新失败,脚本会自动回滚
- 如有更多问题请查看完整的部署指南DeployGuide.md
- 仅授权人员可访问生产服务器
EOF
# 5. 创建版本信息
cat > "$UPDATE_DIR/VERSION-INFO.txt" << EOF
QAUP 程序更新包
构建时间: $(date)
构建主机: $(hostname)
Git提交: $(git rev-parse --short HEAD 2>/dev/null || echo "未知")
Maven版本: $(mvn --version | head -1)
Java版本: $(java -version 2>&1 | head -1)
文件大小: $(du -sh "$JAR_FILE" | cut -f1)
文件MD5: $(md5sum "$JAR_FILE" | cut -d' ' -f1)
更新包内容:
- qaup-admin.jar (主程序)
- UPDATE-INSTRUCTIONS.md (更新说明)
- VERSION-INFO.txt (版本信息)
适用场景:
- 生产环境程序热更新
- 支持自动回滚机制
- 适合已部署环境的增量更新
EOF
# 6. 验证更新包完整性
print_message $BLUE "验证更新包完整性..."
REQUIRED_FILES=("qaup-admin.jar" "UPDATE-INSTRUCTIONS.md" "VERSION-INFO.txt")
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$UPDATE_DIR/$file" ]; then
print_message $RED "❌ 缺失必要文件: $file"
rm -rf "$UPDATE_DIR"
exit 1
fi
done
print_message $GREEN "✓ 所有必要文件验证通过"
# 7. 创建更新包
PACKAGE_NAME="${UPDATE_DIR}.tar.gz"
tar -czf "$PACKAGE_NAME" "$UPDATE_DIR"
# 8. 清理临时目录
rm -rf "$UPDATE_DIR"
print_message $GREEN "✅ 程序更新包创建完成: $PACKAGE_NAME"
print_message $BLUE "包大小: $(du -sh "$PACKAGE_NAME" | cut -f1)"
echo ""
print_message $BLUE "📋 更新说明(生产环境):"
echo ""
echo "⚠️ 由于生产环境安全要求,必须手工文件传输"
echo ""
echo "1. 按安全策略传输更新包到目标服务器:"
echo " scp $PACKAGE_NAME user@生产服务器IP:/opt/qaup/"
echo " 或使用SFTP、文件传输工具等"
echo ""
echo "2. 在生产服务器执行更新:"
echo " ssh user@生产服务器IP"
echo " cd /opt/qaup/qaup-deploy"
echo " tar -xzf ../$PACKAGE_NAME"
echo " cp qaup-admin.jar new-app.jar"
echo " ./deploy-update.sh"
echo ""
echo "3. 详细更新说明请查看:"
echo " - 更新说明: UPDATE-INSTRUCTIONS.md"

View File

@ -1,441 +0,0 @@
#!/bin/bash
# QAUP 统一服务管理脚本
# 支持管理QAUP核心服务、数据库、缓存和ADXP适配器
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 配置变量
COMPOSE_FILE="docker-compose.yml"
ADXP_COMPOSE_FILE="docker-compose.adxp.yml"
APP_SERVICE="qaup-app"
DB_SERVICE="qaup-postgres"
REDIS_SERVICE="qaup-redis"
ADXP_SERVICE="adxp-adapter"
print_message() {
echo -e "${1}${2}${NC}"
}
print_header() {
echo ""
print_message $BLUE "=== QAUP 统一服务管理 ==="
print_message $BLUE "当前目录: $(pwd)"
print_message $BLUE "时间: $(date)"
echo ""
}
# 检查必要文件
check_files() {
if [ ! -f "$COMPOSE_FILE" ]; then
print_message $RED "❌ 找不到 $COMPOSE_FILE 文件"
exit 1
fi
if [ ! -f "config.yml" ]; then
print_message $RED "❌ 找不到 config.yml 文件"
exit 1
fi
}
# 启动所有服务
start_all() {
print_message $BLUE "启动所有QAUP服务..."
# 启动基础设施服务
print_message $BLUE "1. 启动数据库服务..."
docker compose up -d $DB_SERVICE
print_message $BLUE "2. 启动缓存服务..."
docker compose up -d $REDIS_SERVICE
# 等待基础设施就绪
print_message $BLUE "等待基础设施服务就绪..."
sleep 30
# 检查数据库状态
if ! docker exec $DB_SERVICE pg_isready -U qaup > /dev/null 2>&1; then
print_message $RED "❌ 数据库启动失败"
return 1
fi
# 检查Redis状态
if ! docker exec $REDIS_SERVICE redis-cli ping > /dev/null 2>&1; then
print_message $RED "❌ Redis启动失败"
return 1
fi
print_message $GREEN "✓ 基础设施服务就绪"
# 启动应用服务
print_message $BLUE "3. 启动应用服务..."
docker compose up -d $APP_SERVICE
# 启动ADXP适配器如果配置文件存在
if [ -f "$ADXP_COMPOSE_FILE" ]; then
print_message $BLUE "4. 启动ADXP适配器..."
docker compose -f $COMPOSE_FILE -f $ADXP_COMPOSE_FILE up -d $ADXP_SERVICE
fi
print_message $GREEN "🎉 所有服务启动完成!"
show_status
}
# 停止所有服务
stop_all() {
print_message $BLUE "停止所有QAUP服务..."
# 停止ADXP适配器
if [ -f "$ADXP_COMPOSE_FILE" ]; then
docker compose -f $COMPOSE_FILE -f $ADXP_COMPOSE_FILE down $ADXP_SERVICE 2>/dev/null || true
fi
# 停止应用服务
docker compose stop $APP_SERVICE 2>/dev/null || true
# 停止缓存服务
docker compose stop $REDIS_SERVICE 2>/dev/null || true
# 停止数据库服务
docker compose stop $DB_SERVICE 2>/dev/null || true
print_message $GREEN "✓ 所有服务已停止"
}
# 重启所有服务
restart_all() {
print_message $BLUE "重启所有QAUP服务..."
stop_all
sleep 5
start_all
}
# 查看服务状态
show_status() {
print_message $BLUE "服务状态:"
echo ""
# 显示服务状态表格
printf "%-20s %-15s %-15s %-10s\n" "服务名" "状态" "端口" "健康检查"
printf "%-20s %-15s %-15s %-10s\n" "--------------------" "---------------" "---------------" "----------"
# 检查各个服务状态
services=(
"$APP_SERVICE:8080:qaup-app"
"$DB_SERVICE:5432:qaup-postgres"
"$REDIS_SERVICE:6379:qaup-redis"
)
for service_info in "${services[@]}"; do
service=$(echo $service_info | cut -d: -f1)
port=$(echo $service_info | cut -d: -f2)
container=$(echo $service_info | cut -d: -f3)
status=$(docker compose ps $service --format json 2>/dev/null | jq -r '.[0].State' 2>/dev/null || echo "unknown")
# 健康检查
if [ "$status" = "running" ]; then
if [ "$service" = "$APP_SERVICE" ]; then
health=$(curl -f -s http://localhost:8080/actuator/health > /dev/null 2>&1 && echo "正常" || echo "异常")
elif [ "$service" = "$DB_SERVICE" ]; then
health=$(docker exec $DB_SERVICE pg_isready -U qaup > /dev/null 2>&1 && echo "正常" || echo "异常")
elif [ "$service" = "$REDIS_SERVICE" ]; then
health=$(docker exec $REDIS_SERVICE redis-cli ping > /dev/null 2>&1 && echo "正常" || echo "异常")
fi
else
health="未运行"
fi
printf "%-20s %-15s %-15s %-10s\n" "$service" "$status" "$port" "$health"
done
# 检查ADXP适配器状态
if [ -f "$ADXP_COMPOSE_FILE" ]; then
adxp_status=$(docker compose -f $COMPOSE_FILE -f $ADXP_COMPOSE_FILE ps $ADXP_SERVICE --format json 2>/dev/null | jq -r '.[0].State' 2>/dev/null || echo "unknown")
adxp_health=$(curl -f -s http://localhost:8086/health > /dev/null 2>&1 && echo "正常" || echo "异常")
printf "%-20s %-15s %-15s %-10s\n" "$ADXP_SERVICE" "$adxp_status" "8086" "$adxp_health"
fi
echo ""
}
# 查看日志
show_logs() {
local service=${1:-""}
local lines=${2:-50}
if [ -n "$service" ]; then
print_message $BLUE "查看 $service 服务日志 (最后 $lines 行):"
docker compose logs --tail=$lines -f $service
else
print_message $BLUE "查看所有服务日志 (最后 $lines 行):"
docker compose logs --tail=$lines -f
fi
}
# 健康检查
health_check() {
print_message $BLUE "执行健康检查..."
local errors=0
# 检查Docker服务
if ! docker info > /dev/null 2>&1; then
print_message $RED "❌ Docker服务不可用"
((errors++))
else
print_message $GREEN "✓ Docker服务正常"
fi
# 检查应用健康状态
if curl -f -s http://localhost:8080/actuator/health > /dev/null 2>&1; then
print_message $GREEN "✓ 应用服务健康检查通过"
else
print_message $RED "❌ 应用服务健康检查失败"
((errors++))
fi
# 检查数据库连接
if docker exec $DB_SERVICE pg_isready -U qaup > /dev/null 2>&1; then
print_message $GREEN "✓ 数据库连接正常"
else
print_message $RED "❌ 数据库连接失败"
((errors++))
fi
# 检查Redis连接
if docker exec $REDIS_SERVICE redis-cli ping > /dev/null 2>&1; then
print_message $GREEN "✓ Redis连接正常"
else
print_message $RED "❌ Redis连接失败"
((errors++))
fi
# 检查磁盘空间
available_space=$(df . | tail -1 | awk '{print $4}')
if [ "$available_space" -gt $((1024*1024)) ]; then # 1GB
print_message $GREEN "✓ 磁盘空间充足"
else
print_message $RED "❌ 磁盘空间不足"
((errors++))
fi
# 检查端口占用
ports=(8080 5432 6379 8086)
for port in "${ports[@]}"; do
if netstat -tuln 2>/dev/null | grep -q ":$port " || ss -tuln 2>/dev/null | grep -q ":$port "; then
print_message $GREEN "✓ 端口 $port 占用正常"
else
print_message $YELLOW "⚠️ 端口 $port 未占用"
fi
done
if [ $errors -eq 0 ]; then
print_message $GREEN "🎉 所有健康检查通过!"
return 0
else
print_message $RED "❌ 发现 $errors 个问题"
return 1
fi
}
# 数据备份
backup_data() {
local backup_dir="backup"
local timestamp=$(date +%Y%m%d_%H%M%S)
print_message $BLUE "开始数据备份..."
mkdir -p "$backup_dir"
# 备份数据库
local db_backup="$backup_dir/qaup_db_backup_$timestamp.sql"
print_message $BLUE "1. 备份数据库..."
if docker exec $DB_SERVICE pg_dump -U qaup qaup > "$db_backup" 2>/dev/null; then
print_message $GREEN "✓ 数据库备份成功: $db_backup"
else
print_message $RED "❌ 数据库备份失败"
return 1
fi
# 备份Redis数据
local redis_backup="$backup_dir/redis_backup_$timestamp.rdb"
print_message $BLUE "2. 备份Redis数据..."
if docker exec $REDIS_SERVICE redis-cli BGSAVE > /dev/null 2>&1; then
sleep 5
docker cp $REDIS_SERVICE:/data/dump.rdb "$redis_backup" 2>/dev/null
if [ -f "$redis_backup" ]; then
print_message $GREEN "✓ Redis备份成功: $redis_backup"
else
print_message $RED "❌ Redis备份失败"
fi
else
print_message $YELLOW "⚠️ Redis备份跳过"
fi
# 备份应用配置
local config_backup="$backup_dir/config_backup_$timestamp.yml"
print_message $BLUE "3. 备份应用配置..."
if cp config.yml "$config_backup"; then
print_message $GREEN "✓ 配置备份成功: $config_backup"
else
print_message $RED "❌ 配置备份失败"
fi
print_message $GREEN "🎉 数据备份完成!"
ls -la "$backup_dir"/*_$timestamp.*
}
# 清理数据
clean_data() {
print_message $YELLOW "⚠️ 这将删除所有数据目录,确认继续吗?(y/N): "
read -r confirmation
if [[ ! $confirmation =~ ^[Yy]$ ]]; then
print_message $BLUE "操作已取消"
return
fi
print_message $BLUE "清理数据目录..."
# 停止所有服务
stop_all
# 删除数据目录
rm -rf data/ logs/ backup/
print_message $GREEN "✓ 数据目录已清理"
print_message $BLUE "请运行 './qaup-service.sh start' 重新初始化数据"
}
# 更新应用
update_app() {
if [ ! -f "new-app.jar" ]; then
print_message $RED "❌ 未找到新版本文件: new-app.jar"
print_message $BLUE "请先将新版本文件重命名为 new-app.jar"
exit 1
fi
print_message $BLUE "开始应用更新..."
# 备份当前应用
local backup_dir="backup"
local timestamp=$(date +%Y%m%d_%H%M%S)
mkdir -p "$backup_dir"
if [ -f "app.jar" ]; then
cp app.jar "$backup_dir/app.jar.backup.$timestamp"
print_message $BLUE "✓ 当前应用已备份"
fi
# 停止应用服务
docker compose stop $APP_SERVICE
# 替换应用文件
if cp new-app.jar app.jar; then
print_message $GREEN "✓ 应用文件更新成功"
else
print_message $RED "❌ 应用文件更新失败"
exit 1
fi
# 启动应用服务
docker compose up -d $APP_SERVICE
# 等待应用启动
print_message $BLUE "等待应用启动..."
sleep 30
# 验证更新结果
if curl -f -s http://localhost:8080/actuator/health > /dev/null 2>&1; then
print_message $GREEN "🎉 应用更新成功!"
else
print_message $RED "❌ 应用更新失败,正在回滚..."
# 回滚应用
if [ -f "$backup_dir/app.jar.backup.$timestamp" ]; then
docker compose stop $APP_SERVICE
cp "$backup_dir/app.jar.backup.$timestamp" app.jar
docker compose up -d $APP_SERVICE
print_message $BLUE "✓ 应用已回滚"
fi
exit 1
fi
}
# 显示帮助信息
show_help() {
print_message $BLUE "QAUP 统一服务管理脚本"
echo ""
print_message $BLUE "使用方法:"
print_message $BLUE " ./qaup-service.sh <command> [options]"
echo ""
print_message $BLUE "可用命令:"
print_message $BLUE " start 启动所有服务"
print_message $BLUE " stop 停止所有服务"
print_message $BLUE " restart 重启所有服务"
print_message $BLUE " status 查看服务状态"
print_message $BLUE " logs [service] 查看日志 (可指定服务名)"
print_message $BLUE " health 执行健康检查"
print_message $BLUE " backup 备份数据"
print_message $BLUE " clean 清理所有数据"
print_message $BLUE " update 更新应用"
print_message $BLUE " help 显示此帮助信息"
echo ""
print_message $BLUE "示例:"
print_message $BLUE " ./qaup-service.sh start"
print_message $BLUE " ./qaup-service.sh logs qaup-app"
print_message $BLUE " ./qaup-service.sh health"
print_message $BLUE " ./qaup-service.sh backup"
}
# 主函数
main() {
check_files
case "${1:-help}" in
start)
start_all
;;
stop)
stop_all
;;
restart)
restart_all
;;
status)
show_status
;;
logs)
show_logs "$2" "${3:-50}"
;;
health)
health_check
;;
backup)
backup_data
;;
clean)
clean_data
;;
update)
update_app
;;
help|--help|-h)
show_help
;;
*)
print_message $RED "❌ 未知命令: $1"
show_help
exit 1
;;
esac
}
# 执行主函数
main "$@"

46
deploy/update.sh Executable file
View File

@ -0,0 +1,46 @@
#!/bin/bash
# QAUP 一键升级脚本
set -e
echo "=== QAUP 一键升级 ==="
# 检查新版本文件
if [ ! -f "new-app.jar" ]; then
echo "❌ 请先将新版本文件重命名为 new-app.jar"
echo " 例如: cp qaup-admin-1.0.2.jar new-app.jar"
exit 1
fi
# 备份当前版本
echo "备份当前版本..."
cp app.jar app.jar.backup.$(date +%Y%m%d-%H%M%S)
# 停止应用
echo "停止应用服务..."
docker compose stop qaup-app
# 替换应用
echo "更新应用文件..."
cp new-app.jar app.jar
# 启动应用
echo "启动应用服务..."
docker compose start qaup-app
# 等待启动
echo "等待服务启动30秒..."
sleep 30
# 检查服务状态
if curl -f -s http://localhost:8080/actuator/health > /dev/null 2>&1; then
echo "✅ 升级成功!"
echo "备份文件已保存,如有问题可手动回滚"
else
echo "❌ 升级失败,尝试回滚..."
docker compose stop qaup-app
cp app.jar.backup.* app.jar 2>/dev/null || echo "未找到备份文件"
docker compose start qaup-app
echo "请检查日志: docker compose logs qaup-app"
fi

View File

@ -1,26 +1,24 @@
# Ubuntu部署指南
> **重要说明**: 本文档描述了现代化的极简部署方案数据库迁移通过Flyway自动化处理无需手动执行SQL脚本。
## 1. 系统环境准备
### Ubuntu系统要求
- **操作系统**: Ubuntu 20.04 LTS 或更高版本
- **内存**: 至少 8GB RAM
- **存储**: 至少 20GB 可用空间
- **存储**: 至少 10GB 可用空间
- **CPU**: 4核或以上
- **网络**: 稳定的互联网连接用于下载Docker镜像
- **网络**: 稳定的互联网连接
### 安装必要软件
```bash
# 系统更新
sudo apt update && sudo apt upgrade -y
# 安装Java 21(用于本地编译)
# 安装Java 21
sudo apt install -y openjdk-21-jdk
java -version # 验证安装
# 安装Maven(用于构建)
# 安装Maven
sudo apt install -y maven
mvn -version # 验证安装
@ -29,361 +27,269 @@ sudo apt install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker
# 安装Docker Compose
sudo apt install -y docker-compose-plugin
# 将当前用户添加到docker组
sudo usermod -aG docker $USER
newgrp docker # 重新加载组权限
# 验证安装
# 验证Docker
docker --version
docker compose version
```
## 2. 部署方式选择
### 方式一:使用预构建部署包(推荐)
## 2. 获取并编译项目
```bash
# 1. 解压部署包
tar -xzf qaup-deploy.tar.gz
cd qaup-deploy
# 2. 一键部署
./deploy-all.sh
# 3. 检查服务状态
docker compose ps
```
### 方式二:源码编译部署
```bash
# 1. 获取项目源码
# 获取项目
git clone <your-repo-url>
cd QAUP-Management
# 2. 编译项目
# 编译项目
mvn clean package -DskipTests
# 3. 创建部署目录
# 验证编译结果
ls -la qaup-admin/target/qaup-admin.jar
```
## 3. 部署方式
### 3.1 Docker Compose部署推荐
```bash
# 创建部署目录
mkdir -p ~/qaup-deploy
cd ~/qaup-deploy
# 4. 复制部署文件
# 复制部署文件
cp /path/to/QAUP-Management/deploy/docker-compose.yml .
cp /path/to/QAUP-Management/deploy/config.yml .
cp /path/to/QAUP-Management/qaup-admin/target/qaup-admin.jar ./app.jar
# 5. 创建数据目录
# 创建数据目录
mkdir -p data/postgres data/redis logs backup
# 6. 启动服务
# 启动服务
docker compose up -d
# 7. 检查服务状态
# 检查服务状态
docker compose ps
```
### 3.1 部署包内容说明
### 3.2 快速测试部署
```
qaup-deploy/
├── deploy-all.sh # 一键部署脚本
├── deploy-update.sh # 一键更新脚本
├── docker-compose.yml # Docker编排配置
├── config.yml # 应用配置文件
├── app.jar # 应用JAR文件
└── images.tar.gz # 预拉取的Docker镜像包
```
> **注意**: 仅用于快速测试
### 3.2 现代化特性说明
**自动化数据库迁移**: Flyway在应用启动时自动执行数据库迁移
**PostGIS支持**: 内置地理空间数据处理能力
**健康检查**: 自动监控服务状态和依赖关系
**零停机升级**: 只重启应用服务,数据库保持运行
**离线部署**: 预打包镜像,无需互联网连接
## 3. 服务管理
### 3.1 基本服务管理
```bash
# 进入部署目录
cd ~/qaup-deploy
# 创建运行目录
mkdir -p ~/qaup-test
cd ~/qaup-test
# 启动所有服务
# 复制jar文件
cp /path/to/QAUP-Management/qaup-admin/target/qaup-admin.jar ./
# 启动数据库和缓存服务
docker run --name qaup-postgres -e POSTGRES_DB=qaup -e POSTGRES_USER=qaup -e POSTGRES_PASSWORD=qaup123 -p 5432:5432 -d postgis/postgis:17-3.5-alpine
docker run --name qaup-redis -p 6379:6379 -d redis:8.0-alpine
# 等待服务启动
sleep 30
# 运行应用
java -jar qaup-admin.jar --spring.profiles.active=dev
```
## 4. 服务管理
### 启动/停止/重启
```bash
# 启动服务
cd ~/qaup-deploy
docker compose up -d
# 停止所有服务
# 停止服务
docker compose down
# 重启应用服务(不影响数据库)
# 重启应用服务
docker compose restart qaup-app
# 查看服务状态
docker compose ps
# 查看应用日志
# 查看日志
docker compose logs -f qaup-app
# 查看数据库日志
docker compose logs qaup-postgres
# 查看Redis日志
docker compose logs qaup-redis
# 查看系统资源使用
docker stats
```
### 3.2 监控数据库迁移状态
```bash
# 查看Flyway迁移历史
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT * FROM flyway_schema_history ORDER BY installed_rank;"
## 5. 访问系统
# 查看应用日志中的迁移信息
docker compose logs qaup-app | grep -i flyway
# 查看最近的迁移记录
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT version, description, installed_on FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 3;"
```
## 4. 访问系统
### 4.1 系统地址
### 系统地址
- **Web管理**: http://<your-server-ip>:8080
- **WebSocket**: ws://<your-server-ip>:8080/collision
- **健康检查**: http://<your-server-ip>:8080/actuator/health
- **API文档**: http://<your-server-ip>:8080/doc.html
### 4.2 初始登录
### 初始登录
- **用户名**: admin
- **密码**: admin123
### 4.3 快速验证命令
```bash
# 健康检查(验证服务是否正常)
curl http://localhost:8080/actuator/health
## 6. 应用更新
# 检查容器中Java版本
docker exec qaup-app java -version
# 数据库连接测试
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT version();"
# Redis连接测试
docker exec -it qaup-redis redis-cli ping
```
## 5. 应用更新
### 5.1 一键升级(推荐)
使用现代化的升级脚本,支持自动备份、版本检查和回滚:
### 6.1 基本更新流程
```bash
# 1. 复制新版本文件并重命名为 new-app.jar
cp /path/to/new/qaup-admin.jar ~/qaup-deploy/new-app.jar
# 2. 进入部署目录
# 备份当前版本
cd ~/qaup-deploy
cp app.jar app.jar.backup.$(date +%Y%m%d_%H%M%S)
# 3. 执行自动升级
./deploy-update.sh
# 替换新版本
cp /path/to/new/qaup-admin.jar ./app.jar
# 升级过程包括:
# ✅ Java版本兼容性检查
# ✅ 数据库自动备份
# ✅ 应用服务重启
# ✅ Flyway自动迁移
# ✅ 健康检查验证
# ✅ 自动回滚(如需要)
# 重启应用
docker compose restart qaup-app
# 验证更新
curl http://localhost:8080/actuator/health
```
### 5.2 手动升级流程
### 6.2 Java版本冲突解决
如果遇到 `UnsupportedClassVersionError` 错误表示编译的jar文件与容器中Java版本不匹配
**错误示例**
```
Exception in thread "main" java.lang.UnsupportedClassVersionError:
com/qaup/QuapApplication has been compiled by a more recent version of the Java Runtime
(class file version 65.0), this version of the Java Runtime only recognizes
class file versions up to 61.0
```
**解决方案**
1. **检查Docker镜像配置**
确保 `docker-compose.yml` 中使用Java 21镜像
```yaml
# QAUP 应用服务
qaup-app:
image: m.daocloud.io/docker.io/library/eclipse-temurin:21-jre
```
2. **修改并重新部署**
```bash
# 停止服务
docker compose down
# 备份原文件
cp docker-compose.yml docker-compose.yml.bak
# 修改Java版本如果需要
sed -i 's/eclipse-temurin:17-jre/eclipse-temurin:21-jre/' docker-compose.yml
# 重新启动服务
docker compose up -d
```
3. **验证Java版本**
```bash
# 检查容器中Java版本
docker exec qaup-app java -version
```
### 6.3 完整更新流程
当遇到容器名称冲突或其他问题时,使用完整的更新流程:
```bash
# 进入部署目录
cd ~/qaup-deploy
# 1. 数据库备份(重要)
docker exec qaup-postgres pg_dump -U qaup qaup > backup/qaup-backup-$(date +%Y%m%d_%H%M%S).sql
# 1. 停止现有服务
docker compose down
# 2. 备份当前版本
# 2. 确认所有容器已停止
docker ps -a | grep qaup
# 3. 如果还有残留容器,强制删除
docker rm -f $(docker ps -aq --filter name=qaup)
# 4. 备份当前jar文件
cp app.jar app.jar.backup.$(date +%Y%m%d_%H%M%S)
# 3. 替换新版本
# 5. 替换新版本
cp /path/to/new/qaup-admin.jar ./app.jar
# 4. 重启应用服务(数据库保持运行)
docker compose restart qaup-app
# 6. 启动新的服务
docker compose up -d
# 5. 等待启动和迁移完成约2分钟
echo "等待应用启动和数据库迁移..."
sleep 120
# 7. 检查服务状态
docker compose ps
# 6. 验证升级结果
if curl -f -s http://localhost:8080/actuator/health > /dev/null; then
echo "✅ 升级成功!"
else
echo "❌ 升级失败,开始回滚..."
# 恢复版本
LATEST_BACKUP=$(ls -t app.jar.backup.* 2>/dev/null | head -1)
cp "$LATEST_BACKUP" app.jar
docker compose restart qaup-app
fi
# 8. 验证更新
curl -s http://localhost:8080/actuator/health | jq
```
### 5.3 监控升级过程
### 6.4 故障排除
#### 6.4.1 容器名称冲突
如果遇到容器名称冲突错误:
```
Error response from daemon: Conflict. The container name "/qaup-app" is already in use
```
解决方法:
```bash
# 实时查看应用日志包含Flyway迁移信息
docker compose logs -f qaup-app
# 方式1完全重建
docker compose down
docker rm -f $(docker ps -aq --filter name=qaup)
docker compose up -d
# 检查Flyway迁移状态
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT version, description, installed_on FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;"
# 方式2强制重新创建
docker compose up -d --force-recreate
# 查看最近的迁移记录
docker compose logs qaup-app | grep -i "flyway.*migrated"
# 方式3只重新创建特定服务
docker compose up -d --force-recreate qaup-app
```
### 5.4 故障排除
#### 6.4.2 Java版本验证
#### 5.4.1 Java版本兼容性错误
如果遇到 `UnsupportedClassVersionError`
```
Error: A JNI error has occurred, please check your installation and try again
Error: Could not find or load main class
```
**解决方案**
确认容器中Java版本与编译版本匹配
```bash
# 1. 检查容器Java版本
# 查看容器Java版本
docker exec qaup-app java -version
# 2. 验证jar文件编译版本
javap -cp app.jar -version
# 3. 如果版本不匹配,重新编译项目
mvn clean package -DskipTests
# 4. 确认Docker镜像使用Java 21
grep "eclipse-temurin.*21" docker-compose.yml
# 如果需要,检查镜像信息
docker inspect qaup-app | grep -i java
```
#### 5.4.2 数据库连接问题
## 7. 数据备份
如果迁移失败:
```bash
# 数据库备份
docker exec qaup-postgres pg_dump -U qaup qaup > backup-$(date +%Y%m%d).sql
# 日志轮转保留最近7天
find ~/qaup-deploy/logs -name "*.log" -mtime +7 -delete
```
## 8. 常见问题
### 应用启动失败
```bash
# 查看详细日志
docker compose logs qaup-app
# 检查数据库连接
docker compose logs qaup-postgres
docker exec qaup-postgres pg_isready -U qaup
```
### 端口被占用
```bash
# 检查端口使用情况
netstat -tlnp | grep :8080
# 停止占用端口的进程
sudo kill -9 $(lsof -t -i:8080)
```
### 数据库连接失败
```bash
# 检查数据库服务
docker compose ps
docker exec qaup-postgres pg_isready -U qaup
```
## 9. 快速操作指南
### 9.1 开发环境快速部署
```bash
# 克隆项目并编译
git clone <your-repo-url>
cd QAUP-Management
mvn clean package -DskipTests
# 使用部署脚本打包
./deploy/package-all.sh
# 解压并部署
tar -xzf qaup-deploy.tar.gz
cd qaup-deploy
./deploy-all.sh
```
### 9.2 生产环境升级
```bash
cd qaup-deploy
# 复制新版本
cp /path/to/new/qaup-admin.jar ./new-app.jar
# 执行自动升级
./deploy-update.sh
# 验证升级结果
curl http://localhost:8080/actuator/health
```
### 9.3 日常运维检查
```bash
cd qaup-deploy
# 检查服务状态
docker compose ps
# 查看最近日志
docker compose logs --tail=50 qaup-app
# 检查数据库迁移状态
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT version, description, installed_on FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 3;"
# 系统健康检查
curl -s http://localhost:8080/actuator/health | jq
```
### 9.4 缓存优化
```bash
# 监控Redis内存使用
docker exec -it qaup-redis redis-cli INFO memory
# 清理过期缓存
docker exec -it qaup-redis redis-cli FLUSHDB
```
## 10. 现代化部署优势
### 10.1 核心特性
**极简操作**: 解压 → 运行脚本2步完成部署
**自动迁移**: Flyway处理数据库版本管理无需手动SQL
**零停机升级**: 只重启应用服务,数据库持续运行
**PostGIS支持**: 内置地理空间数据处理能力
**健康监控**: 自动化服务依赖检查和状态监控
**离线部署**: 预打包Docker镜像无需互联网
**一键回滚**: 升级失败自动回滚到上一版本
**版本兼容**: Java 21运行时确保版本一致性
### 10.2 与传统方案对比
| 特性 | 传统方案 | 现代化方案 |
|------|----------|------------|
| 部署步骤 | 手动配置SQL、修改配置、重启服务 | 解压脚本、自动迁移、一键完成 |
| 数据库迁移 | 手动执行SQL脚本易出错 | Flyway自动版本管理 |
| 停机时间 | 需要停机维护 | 零停机升级 |
| 回滚复杂度 | 手动恢复数据库和配置 | 自动回滚机制 |
| 环境一致性 | 容易出现环境差异 | 容器化确保一致性 |
| 地理空间 | 需要额外安装PostGIS | 内置PostGIS支持 |
### 10.3 技术架构
- **运行时**: Java 21 + Spring Boot 3
- **数据库**: PostgreSQL 17 + PostGIS 3.5
- **缓存**: Redis 8
- **容器化**: Docker + Docker Compose
- **迁移工具**: Flyway自动化版本管理
- **监控**: Spring Boot Actuator + 健康检查
---
## 支持和帮助
如遇到问题,请检查:
1. 系统要求和依赖软件版本
2. Docker和Docker Compose安装状态
3. 网络连接和防火墙配置
4. 磁盘空间和文件权限
5. 应用日志和数据库日志
更多详细信息请参考:
- [deploy-design.md](./deploy-design.md) - 完整部署设计文档
- [environment.md](./environment.md) - 环境配置指南
- [ADXP_WebSocket.md](./ADXP_WebSocket.md) - WebSocket服务说明
```

View File

@ -0,0 +1,142 @@
# PostgreSQL 数据库迁移指南
## 概述
本文档描述如何将本地 PostgreSQL 数据库完全覆盖导入到远程服务器。
**场景:**
- 本地数据库用户postgres
- 远程数据库用户qaup
- 数据库名称qaup
- 操作方式:完全覆盖
## 第一步:本地数据库导出
### 导出完整数据库
```bash
# 导出数据库(包含结构和数据,处理用户名差异)
pg_dump -h localhost -U postgres -d qaup --no-owner --no-privileges --clean --if-exists -f qaup_database_export.sql
```
**参数说明:**
- `--no-owner`: 不包含对象所有者信息,避免用户名冲突
- `--no-privileges`: 不包含权限设置,导入后重新设置权限
- `--clean --if-exists`: 清理现有对象(如果存在)
- `-f`: 指定输出文件名
## 第二步:远程服务器完全覆盖
```bash
# 1. 删除现有数据库
psql -U qaup -d postgres -c "DROP DATABASE IF EXISTS qaup;"
# 2. 重新创建数据库
psql -U qaup -d postgres -c "CREATE DATABASE qaup OWNER qaup;"
# 3. 设置PostGIS扩展
psql -U qaup -d qaup -c "CREATE EXTENSION IF NOT EXISTS postgis;"
# 4. 导入数据
psql -U qaup -d qaup -f qaup_database_export.sql
```
## 第三步:验证导入结果
### 检查数据库结构
```bash
# 查看所有表
psql -U qaup -d qaup -c "\dt"
# 查看特定表结构
psql -U qaup -d qaup -c "\d traffic_lights"
```
### 验证数据完整性
```bash
# 检查关键表的数据量
psql -U qaup -d qaup -c "SELECT count(*) FROM traffic_lights;"
psql -U qaup -d qaup -c "SELECT count(*) FROM intersections;"
psql -U qaup -d qaup -c "SELECT count(*) FROM sys_vehicle_info;"
```
### 检查用户权限
```bash
# 确保qaup用户拥有所有表的权限
psql -U qaup -d qaup -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO qaup;"
psql -U qaup -d qaup -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO qaup;"
```
## 注意事项
1. **备份重要性**:操作前确保远程数据库已备份(如有重要数据)
2. **网络连接**:确保网络稳定,大数据库传输可能需要较长时间
3. **磁盘空间**:确认远程服务器有足够存储空间
4. **权限检查**确保qaup用户有CREATE DATABASE权限方案A
5. **应用停机**:建议在应用停机时间窗口执行,避免数据不一致
## 故障排除
### 连接失败
```bash
# 检查用户权限
psql -U qaup -d postgres -c "SELECT current_user;"
```
### 导入错误
```bash
# 查看详细错误信息
psql -U qaup -d qaup -f qaup_database_export.sql 2>&1 | tee import.log
```
### 权限问题
```bash
# 重置所有权限
psql -U qaup -d qaup -c "
GRANT ALL PRIVILEGES ON DATABASE qaup TO qaup;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO qaup;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO qaup;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO qaup;
"
```
## 完成后清理
```bash
# 删除本地导出文件(可选)
rm qaup_database_export.sql
# 删除临时脚本如果使用了方案B
rm drop_tables.sql
```
## Java版本兼容性注意事项
如果在执行数据库迁移后更新应用需要注意Java版本兼容性
### 版本匹配检查
- **编译版本**: 确保应用jar文件在Java 21环境下编译
- **运行版本**: Docker容器中运行Java 21
### 版本不匹配错误示例
如果遇到 `UnsupportedClassVersionError` 错误:
```
class file has been compiled by a more recent version of the Java Runtime
(class file version 65.0), this version of the Java Runtime only recognizes
class file versions up to 61.0
```
### 解决方案
1. 确认Docker镜像使用Java 21
```yaml
image: eclipse-temurin:21-jre
```
2. 检查容器Java版本
```bash
docker exec <container-name> java -version
```
---
**创建时间:** 2025-09-25
**适用版本:** PostgreSQL 12+
**测试环境:** QAUP-Management 1.0.1

View File

@ -1,6 +1,5 @@
# QAUP 极简离线部署方案
Spring Boot + Redis + PostgreSQL with PostGIS一键部署、一键升级
Spring Boot + Redis + PostgreSQL一键部署、一键升级
## 1. 设计原则
@ -8,151 +7,103 @@ Spring Boot + Redis + PostgreSQL with PostGIS一键部署、一键升级
- **离线部署**:预打包所有依赖,无需联网
- **一键操作**:部署和升级都是一条命令
- **配置灵活**:关键配置可外部修改
- **自动化迁移**数据库版本自动管理Flyway
## 2. 打包准备(开发环境执行一次)
### 2.1 打包脚本package-all.sh
### 2.1 打包脚本package.sh
```bash
#!/bin/bash
echo "=== QAUP 打包脚本 ==="
# 1. 构建应用在macOS上
# 1. 构建应用
mvn clean package -DskipTests
# 2. 拉取并导出镜像(在服务器上)
docker pull m.daocloud.io/docker.io/postgis/postgis:17-3.5-alpine
docker pull m.daocloud.io/docker.io/library/redis:8.0-alpine
docker pull m.daocloud.io/docker.io/library/eclipse-temurin:21-jre
# 导出镜像
docker save m.daocloud.io/docker.io/postgis/postgis:17-3.5-alpine \
m.daocloud.io/docker.io/library/redis:8.0-alpine \
m.daocloud.io/docker.io/library/eclipse-temurin:21-jre | gzip > images.tar.gz
# 2. 拉取并导出镜像
docker pull openjdk:21-jre-alpine
docker pull redis:8-alpine
docker pull postgres:17-3.5-alpine
docker save openjdk:21-jre-alpine redis:8-alpine postgres:17-3.5-alpine | gzip > images.tar.gz
# 3. 准备部署包
mkdir -p qaup-deploy
cp qaup-admin/target/qaup-admin.jar qaup-deploy/app.jar
cp deploy/docker-compose.yml qaup-deploy/
cp deploy/config.yml qaup-deploy/
cp deploy/simple/* qaup-deploy/
cp images.tar.gz qaup-deploy/
# 4. 打包
tar -czf qaup-deploy-$(date +%Y%m%d-%H%M%S).tar.gz -C qaup-deploy .
echo "打包完成: qaup-deploy-$(date +%Y%m%d-%H%M%S).tar.gz"
cd qaup-deploy && zip -r ../qaup-deploy-$(date +%Y%m%d).zip .
echo "打包完成: qaup-deploy-$(date +%Y%m%d).zip"
```
### 2.2 部署包结构
### 2.2 部署包结构(极简版)
```
qaup-deploy.tar.gz
├── images.tar.gz # Docker镜像包PostGIS + Redis + Java21
├── docker-compose.yml # 服务编排(包含健康检查)
├── app.jar # 应用程序
├── config.yml # 外部配置(可修改)
├── deploy-all.sh # 一键部署脚本
├── deploy-update.sh # 一键升级脚本
├── DeployGuide.md # 详细部署指南
└── qaup_database_export.sql # 完整数据库备份(可选)
qaup-deploy.zip
├── images.tar.gz # Docker镜像包
├── docker-compose.yml # 服务编排
├── app.jar # 应用程序
├── config.yml # 外部配置(可修改)
├── init.sql # 数据库初始化脚本
├── deploy.sh # 一键部署
└── update.sh # 一键升级
```
## 3. docker-compose.yml当前实际版本
## 3. docker-compose.yml极简版
```yaml
services:
# PostgreSQL + PostGIS 数据库服务
qaup-postgres:
image: m.daocloud.io/docker.io/postgis/postgis:17-3.5-alpine
container_name: qaup-postgres
restart: unless-stopped
app:
image: openjdk:21-jre-alpine
container_name: qaup-app
restart: always
ports:
- "8080:8080"
volumes:
- ./app.jar:/app.jar
- ./config.yml:/config.yml
command: ["java", "-jar", "/app.jar", "--spring.config.location=/config.yml"]
depends_on:
- db
- redis
redis:
image: redis:8-alpine
container_name: qaup-redis
restart: always
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
db:
image: postgres:17-3.5-alpine
container_name: qaup-db
restart: always
environment:
POSTGRES_DB: qaup
POSTGRES_USER: qaup
POSTGRES_PASSWORD: qaup123
volumes:
- ./data/postgres:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U qaup"]
interval: 30s
timeout: 10s
retries: 3
# Redis 缓存服务
qaup-redis:
image: m.daocloud.io/docker.io/library/redis:8.0-alpine
container_name: qaup-redis
restart: unless-stopped
command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- ./data/redis:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# QAUP 应用服务Java 21
qaup-app:
image: m.daocloud.io/docker.io/library/eclipse-temurin:21-jre
container_name: qaup-app
restart: unless-stopped
environment:
SPRING_PROFILES_ACTIVE: prod
LOG_PATH: /app/logs
# 数据库连接配置
DB_HOST: qaup-postgres
DB_PORT: 5432
DB_NAME: qaup
DB_USER: qaup
DB_PASSWORD: qaup123
# Flyway配置自动化数据库迁移
SPRING_FLYWAY_ENABLED: true
SPRING_FLYWAY_BASELINE_ON_MIGRATE: true
SPRING_FLYWAY_VALIDATE_ON_MIGRATE: true
SPRING_FLYWAY_CLEAN_DISABLED: true
volumes:
- ./app.jar:/app/app.jar
- ./config.yml:/app/config.yml
- ./logs:/app/logs
- ./backup:/app/backup
ports:
- "8080:8080"
depends_on:
qaup-postgres:
condition: service_healthy
qaup-redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
command: [
"sh", "-c",
"echo 'Waiting for database to be ready...' &&
sleep 10 &&
echo 'Starting QAUP application with Flyway migration...' &&
java -jar /app/app.jar --spring.config.location=/app/config.yml"
]
networks:
default:
name: qaup-network
- ./data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
```
## 4. 外部配置文件config.yml
```yaml
# 服务器配置
server:
port: 8080
# 数据库配置
spring:
datasource:
url: jdbc:postgresql://qaup-db:5432/qaup
username: qaup
password: qaup123
driver-class-name: org.postgresql.Driver
# Redis配置
data:
redis:
host: qaup-redis
port: 6379
database: 0
# 应用配置
qaup:
# 文件上传路径
@ -162,101 +113,64 @@ qaup:
api-host: 192.168.1.100
api-port: 8090
# 日志配置
# 日志配置(简化)
logging:
level:
com.qaup: INFO
com.qaup: info
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: /app/logs/qaup.log
# 注意数据库和Redis配置通过环境变量设置
console: "%d{HH:mm:ss} %-5level %logger{36} - %msg%n"
```
## 5. 自动化数据库迁移Flyway
## 5. 数据库初始化init.sql
```sql
-- 创建基础表结构
CREATE TABLE IF NOT EXISTS sys_user (
user_id SERIAL PRIMARY KEY,
user_name VARCHAR(30) NOT NULL,
nick_name VARCHAR(30) NOT NULL,
password VARCHAR(100) DEFAULT '',
status CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
### 5.1 迁移脚本位置
-- 插入默认管理员
INSERT INTO sys_user (user_name, nick_name, password)
VALUES ('admin', '管理员', '$2a$10$7JB720yubVSOfvam/RtmyO4wV/hOoLsKpHpJI.VqSb/VXaVzqQqhu')
ON CONFLICT DO NOTHING;
```
qaup-admin/src/main/resources/db/migration/
├── V1.0.0__Initial_baseline.sql # 数据库基线结构86KB
├── V1.0.1__Initial_data.sql # 初始数据50KB
└── README.md # 迁移脚本编写规范
-- 其他标准数据...
```
### 5.2 自动迁移机制
- **应用启动时自动执行**无需手动运行SQL脚本
- **版本控制**:所有迁移按版本顺序执行
- **幂等性**:安全重复执行,不会重复创建对象
- **健康检查**:依赖数据库健康状态启动应用
### 5.3 迁移状态查询
```bash
# 查看应用日志中的迁移信息
docker compose logs qaup-app | grep -i flyway
# 直接查询数据库
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT * FROM flyway_schema_history ORDER BY installed_rank;"
```
## 6. 一键部署deploy-all.sh
## 6. 一键部署deploy.sh
```bash
#!/bin/bash
echo "=== QAUP 一键部署 ==="
# 检查Docker
if ! command -v docker &> /dev/null; then
echo "❌ Docker 未安装"
exit 1
fi
# 载入镜像
echo "载入Docker镜像..."
docker load -i images.tar.gz
# 创建数据目录
echo "创建数据目录..."
mkdir -p data/postgres data/redis logs
mkdir -p data
# 启动服务
echo "启动服务..."
docker compose up -d
# 等待服务启动
echo "等待服务启动(60秒..."
sleep 60
# 等待启动
echo "等待服务启动30秒..."
sleep 30
# 检查服务状态
if curl -f -s http://localhost:8080/actuator/health > /dev/null 2>&1; then
echo "✅ 部署成功!"
echo ""
echo "访问地址: http://localhost:8080"
echo "数据库: localhost:5432 (qaup/qaup123)"
echo "Redis: localhost:6379"
echo ""
echo "管理命令:"
echo " 查看状态: docker compose ps"
echo " 查看日志: docker compose logs -f qaup-app"
echo " 停止服务: docker compose down"
echo " 升级应用: ./deploy-update.sh"
else
echo "❌ 服务启动失败,请检查日志:"
docker compose logs
fi
echo "✅ 部署完成!访问: http://localhost:8080"
echo "默认账号: admin / admin123"
```
## 7. 客户部署1条命令
```bash
tar -xzf qaup-deploy.tar.gz && cd qaup-deploy && ./deploy-all.sh
unzip qaup-deploy.zip && cd qaup-deploy && ./deploy.sh
```
## 8. 一键升级deploy-update.sh
## 8. 一键升级update.sh
```bash
#!/bin/bash
echo "=== QAUP 一键升级 ==="
@ -272,190 +186,86 @@ echo "验证Java版本兼容性..."
if command -v java &> /dev/null; then
LOCAL_JAVA_VERSION=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
echo "本地Java版本: $LOCAL_JAVA_VERSION"
echo "容器Java版本: eclipse-temurin:21-jre"
fi
# 备份当前版本
echo "备份当前版本..."
cp app.jar app.jar.backup.$(date +%Y%m%d_%H%M%S)
cp app.jar app.jar.backup
echo "已备份当前版本"
# 数据库备份(重要)
echo "备份数据库..."
docker exec qaup-postgres pg_dump -U qaup qaup > backup/qaup-backup-$(date +%Y%m%d_%H%M%S).sql
# 停止应用(不影响数据库)
# 停止应用
echo "停止应用服务..."
docker compose stop qaup-app
docker compose stop app
# 替换应用
cp new-app.jar app.jar
echo "已更新应用文件"
# 启动应用Flyway自动处理数据库迁移
# 启动应用
echo "启动应用服务..."
docker compose start qaup-app
docker compose start app
# 等待启动和迁移完成
echo "等待应用启动和数据库迁移120秒..."
sleep 120
# 等待启动
echo "等待服务启动30秒..."
sleep 30
# 检查升级结果
if curl -f -s http://localhost:8080/actuator/health > /dev/null 2>&1; then
echo "✅ 升级成功!"
echo ""
echo "Flyway迁移状态:"
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT version, description, installed_rank FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;"
else
echo "❌ 升级失败,开始回滚..."
# 恢复jar文件
LATEST_BACKUP=$(ls -t app.jar.backup.* 2>/dev/null | head -1)
if [ -n "$LATEST_BACKUP" ]; then
cp "$LATEST_BACKUP" app.jar
docker compose restart qaup-app
echo "✅ 已回滚到版本: $LATEST_BACKUP"
else
echo "❌ 未找到备份文件,请手动处理"
fi
fi
echo "✅ 升级完成!"
echo "如有问题,可执行回滚: cp app.jar.backup app.jar && docker compose restart app"
```
## 9. Java版本兼容性注意事项
### 9.1 编译与运行环境匹配
- **编译环境**: 确保项目在Java 21环境下编译
- **运行环境**: Docker镜像使用 `eclipse-temurin:21-jre`
- **运行环境**: Docker镜像必须与编译版本匹配`openjdk:21-jre-alpine` 或 `eclipse-temurin:21-jre`
### 9.2 常见版本错误
如果遇到 `UnsupportedClassVersionError`,确认以下配置:
```yaml
# docker-compose.yml 中必须使用Java 21镜像
services:
qaup-app:
app:
# 错误示例(可能导致版本不匹配)
# image: openjdk:17-jre-alpine
# 正确示例
image: m.daocloud.io/docker.io/library/eclipse-temurin:21-jre
image: openjdk:21-jre-alpine
```
### 9.3 版本验证命令
```bash
# 检查容器中Java版本
docker exec qaup-app java -version
docker exec <container-name> java -version
# 检查jar文件编译版本
javap -cp app.jar -version
# 快速健康检查
curl http://localhost:8080/actuator/health
javap -cp <jar-file> -version <main-class>
```
## 10. 系统维护操作
### 10.1 日常运维命令
```bash
# 查看服务状态
docker compose ps
# 查看应用日志
docker compose logs -f qaup-app
# 查看数据库日志
docker compose logs qaup-postgres
# 查看Redis日志
docker compose logs qaup-redis
# 查看系统资源使用
docker stats
# 数据库连接测试
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT version();"
# Redis连接测试
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;"
# 查看应用日志中的迁移信息
docker compose logs qaup-app | grep -i flyway
# 查看最近的迁移记录
docker exec -it qaup-postgres psql -U qaup -d qaup -c "SELECT version, description, installed_on FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 3;"
```
### 10.3 数据备份和恢复
```bash
# 手动备份数据库
docker exec qaup-postgres pg_dump -U qaup qaup > backup/manual-backup-$(date +%Y%m%d_%H%M%S).sql
# 恢复数据库(需要停止应用)
docker compose stop qaup-app
docker exec -i qaup-postgres psql -U qaup qaup < backup/qaup-backup-20250120_143000.sql
docker compose start qaup-app
```
### 10.4 完全重置(开发测试用)
```bash
# 停止所有服务
docker compose down
# 删除数据目录(⚠️ 注意:这会删除所有数据)
rm -rf data/
# 重新部署
./deploy-all.sh
```
## 11. 操作总结
## 9. 操作总结
### 开发环境(一次性)
```bash
# 在macOS上构建
mvn clean package -DskipTests
./deploy/package-all.sh
# 或在服务器上打包
./deploy/package-all.sh
./package.sh # 打包部署文件
```
### 客户环境
```bash
# 首次部署
tar -xzf qaup-deploy.tar.gz && cd qaup-deploy && ./deploy-all.sh
unzip qaup-deploy.zip && cd qaup-deploy && ./deploy.sh
# 程序更新
# 1. 复制新jar文件并重命名为 new-app.jar
# 2. 执行更新
./deploy-update.sh
# 日常升级
./update.sh
# 查看状态
docker compose ps
# 查看日志
docker compose logs -f qaup-app
docker compose logs -f app
```
## 12. 现代化优势总结
## 10. 优势总结
**极简部署**:解压 → 运行脚本2步完成
**极简升级**替换jar → 运行脚本,自动完成
**离线友好**:所有依赖预打包,无需联网
**配置灵活**:关键配置外部文件,可随时修改
**自动化迁移**Flyway自动处理数据库版本管理
**健康检查**:服务依赖和健康状态监控
**PostGIS支持**:内置地理空间数据库能力
**一键回滚**:升级失败可快速回滚到上一版本
**零停机升级**:只重启应用服务,数据库保持运行
**版本兼容**Java 21运行时确保版本一致性
**自动初始化**:数据库结构和标准数据自动创建
**一键回滚**:升级失败可快速回滚到上一版本

View File

@ -1,696 +0,0 @@
# 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

View File

@ -6,9 +6,9 @@
- 服务器
- 后台
- IP: 10.64.58.228
- IP: 10.100.23.10
- 用户名: root
- 密码: Yaddfepasswd13#$
- 密码: Huawei@1234567890
- 前端
- IP:
- 用户名:
@ -28,7 +28,7 @@
- IP: 10.32.38.3
- 端口: 8090
- 地图服务
- URL: http://10.98.23.81:8090/iserver/services/map-QDJC_DT-GX3/rest/maps
- URL: http://221.215.103.144:8090/iserver/services/map-QDJC_DT-GX3/rest/maps
- 安全设备
- VPN
- IP: 222.173.72.76

View File

@ -5,180 +5,155 @@
## 1. 根目录结构
```
QAUP-Management/
CollisionAvoidanceSystem/
├── doc/ # 文档目录
├── qaup-admin/ # 管理后台模块
├── qaup-collision/ # 碰撞避免系统核心模块
├── qaup-common/ # 公共模块
├── qaup-framework/ # 框架模块
├── qaup-generator/ # 代码生成器
├── qaup-quartz/ # 定时任务模块
├── qaup-system/ # 系统管理模块
├── qaup-ui/ # 前端UI模块
├── deploy/ # 部署脚本
├── scripts/ # 工具脚本
├── sql/ # 数据库脚本
├── .git/ # Git版本控制
├── pom.xml # Maven父项目配置
├── README.md # 项目说明
├── VERSION.md # 版本信息
├── CHANGELOG.md # 变更日志
├── .gitignore # Git忽略配置
└── qaup.sh # 项目启动脚本
```
## 2. 核心模块目录结构 (`qaup-collision`)
```
qaup-collision/
├── src/ # 源代码目录
│ ├── main/ # 主要源代码
│ │ ├── java/ # Java源代码
│ │ │ └── com/
│ │ │ └── qaup/
│ │ │ └── collision/ # 应用程序主包
│ │ └── resources/ # 配置文件和静态资源
│ │ ├── config/ # 特定配置文件 (如 airport_roads.yaml, airport_areas.yaml)
│ │ ├── static/ # 静态Web资源
│ │ └── templates/ # 模板文件
│ └── test/ # 测试源代码
├── libs/ # 本地依赖库
└── pom.xml # Maven模块配置
├── target/ # 编译输出目录
├── .idea/ # IDE配置
├── .mvn/ # Maven包装器配置
├── .git/ # Git版本控制
├── pom.xml # Maven项目配置
├── mvnw / mvnw.cmd # Maven包装器脚本
├── README.md # 项目说明
├── VERSION.txt # 版本信息
├── change_log.md # 变更日志
├── development_log.md # 开发日志
├── .gitignore # Git忽略配置
└── .gitattributes # Git属性配置
```
## 3. 应用程序主包结构 (`com.qaup.collision`)
## 2. 源代码目录结构 (`src`)
```
com.qaup.collision/
├── adapter/ # 外部系统适配器
├── area/ # 机场区域管理模块
│ ├── model/ # 区域相关数据模型
│ └── service/ # 区域管理服务
src/
├── main/ # 主要源代码
│ ├── java/ # Java源代码
│ │ └── com/
│ │ └── dongni/
│ │ └── collisionavoidance/ # 应用程序主包
│ └── resources/ # 配置文件和静态资源
│ ├── config/ # 特定配置文件 (如 airport_roads.yaml, airport_areas.yaml)
│ ├── data/ # 数据文件
│ ├── scripts/ # 脚本文件
│ └── static/ # 静态Web资源
└── test/ # 测试源代码
```
## 3. 应用程序主包结构 (`com.dongni.collisionavoidance`)
```
com.dongni.collisionavoidance/
├── CollisionAvoidanceApplication.java # 应用程序入口类
├── common/ # 通用组件目录
│ ├── adapter/ # 通用适配器
│ ├── config/ # 通用配置
│ ├── exception/ # 通用异常处理
│ ├── model/ # 核心数据模型
│ ├── repository/ # 仓储模式实现
│ └── service/ # 通用服务
│ ├── model/ # 核心移动对象等数据模型
│ │ ├── base/ # 基础类和常量
│ │ ├── dto/ # 数据传输对象
│ │ └── repository/ # 仓储模式实现
│ └── config/ # 通用配置 (较少使用,优先模块内配置)
├── config/ # 应用程序配置
│ ├── properties/ # 配置属性映射类 (POJOs)
│ ├── CollisionAvoidanceConfig.java # 碰撞避免系统配置
│ ├── DatabasePerformanceConfig.java # 数据库性能配置
│ ├── GracefulShutdownConfig.java # 优雅关闭配置
│ ├── RoadNetworkConfig.java # 道路网络配置加载类
│ ├── ThreadPoolConfig.java # 线程池配置
│ └── YamlPropertySourceFactory.java # YAML加载工厂类
│ ├── AirportAreaConfig.java # 机场区域配置加载类
│ ├── YamlPropertySourceFactory.java # YAML加载工厂类
│ ├── RedisConfig.java # Redis配置
│ └── ThreadPoolConfig.java # 线程池配置
├── controller/ # 控制器层 (REST API)
│ ├── DataMonitorController.java # 数据监控控制器
│ ├── HealthController.java # 健康检查控制器
│ ├── IntersectionController.java # 交叉路口控制器
│ ├── SpatialRuleController.java # 空间规则控制器
│ ├── TrafficLightController.java # 交通信号灯控制器
│ ├── UniversalVehicleApiController.java # 通用车辆API控制器
│ └── UnmannedVehicleController.java # 无人车控制器
├── datacollector/ # 数据采集模块
│ ├── config/ # 数据采集配置
│ ├── dao/ # 数据访问对象
│ ├── dto/ # 数据传输对象
│ ├── filter/ # 数据过滤器
├── dataCollector/ # 数据采集模块
│ ├── model/ # 数据采集相关模型
│ ├── repository/ # 数据采集仓储
│ ├── sdk/ # SDK集成
│ ├── server/ # 服务器端实现
│ │ └── enums/ # 枚举类型定义
│ ├── service/ # 数据采集服务
│ ├── util/ # 工具类
│ └── websocket/ # WebSocket数据采集
├── dataprocessing/ # 数据处理模块
│ ├── config/ # 数据处理配置
│ ├── model/ # 数据处理模型
│ ├── parser/ # 数据解析器
│ └── service/ # 数据处理服务
├── event/ # 事件处理模块
│ ├── handler/ # 事件处理器
│ ├── model/ # 事件模型
│ └── service/ # 事件服务
├── geofence/ # 地理围栏模块
│ ├── model/ # 地理围栏模型
│ └── service/ # 地理围栏服务
├── pathconflict/ # 路径冲突模块
│ ├── event/ # 路径冲突事件
│ ├── model/ # 路径冲突模型
│ ├── repository/ # 路径冲突仓储
│ └── service/ # 路径冲突服务
├── road/ # 道路网络模块
│ ├── dao/ # 数据访问对象
│ └── config/ # 数据采集配置
├── dataProcessing/ # 数据处理模块
│ ├── service/ # 数据处理服务
│ └── config/ # 数据处理配置
├── areas/ # 新增:机场区域管理模块
│ ├── model/ # 区域相关数据模型
│ └── service/ # 区域管理服务
├── roads/ # 道路网络模块
│ ├── model/ # 道路网络运行时模型
│ └── service/ # 道路网络服务
├── rule/ # 规则引擎模块
│ ├── event/ # 规则事件
│ ├── model/ # 规则模型
│ ├── repository/ # 规则仓储
│ └── service/ # 规则服务
└── websocket/ # WebSocket通信模块
├── broadcaster/ # 消息广播器
├── cache/ # WebSocket缓存
├── config/ # WebSocket配置
└── webSocket/ # WebSocket通信模块
├── controller/ # WebSocket控制器
├── event/ # WebSocket事件
├── handler/ # WebSocket消息处理器
├── listener/ # WebSocket监听器
└── message/ # WebSocket消息格式
└── config/ # WebSocket配置
```
## 4. 数据模型说明
系统采用模块化的数据模型设计,各模块内部包含自己的数据模型定义。主要数据模型包括:
## 4. 数据模型目录
### 4.1 通用数据模型 (`common/model`)
包含系统核心的、跨模块共享的数据结构,如:
- 地理位置数据模型
- 移动状态数据模型
- 基础常量和枚举
### 4.2 区域数据模型 (`area/model`)
包含系统核心的、跨模块共享的数据结构:
```
common/model/
├── Aircraft.java # 航空器实体类
├── GeoPosition.java # 地理位置数据结构
├── MovementState.java # 移动状态封装
├── MovingObject.java # 移动物体抽象基类
├── MovingObjectType.java # 移动物体类型枚举
├── PositionRecord.java # 位置记录
├── SpecialVehicle.java # 特勤车辆实体类
├── UnmannedVehicle.java # 无人车实体类
├── Velocity.java # 速度和局部坐标系信息
├── base/ # 基础类和常量
│ ├── Constant.java # 系统常量定义
│ └── Response.java # 统一响应格式
├── dto/ # 数据传输对象
│ ├── AircraftDTO.java # 航空器数据传输对象
│ └── SpecialVehicleDTO.java # 特勤车辆数据传输对象
└── repository/ # 仓储模式实现
└── MovingObjectRepository.java # 移动物体仓储接口
```
### 4.2 机场区域数据模型 (`areas/model`)
包含机场区域管理模块的数据结构:
- AreaInfo.java: 区域信息实体类 (含JTS几何对象)
- AreaType.java: 区域类型枚举
### 4.3 道路网络数据模型 (`road/model`)
```
areas/model/
├── AreaInfo.java # 区域信息实体类 (含JTS几何对象)
└── AreaType.java # 区域类型枚举
```
### 4.3 道路网络数据模型 (`roads/model`)
包含道路网络模块内部使用的运行时数据结构:
- RoadInfo.java: 运行时道路信息 (含JTS对象)
### 4.4 数据采集模型 (`datacollector/model`)
包含数据采集模块的专用数据结构,如:
- 车辆位置信息
- 车辆状态信息
- 命令响应
- 各种枚举类型定义
```
roads/model/
├── RoadInfo.java # 运行时道路信息 (含JTS对象)
└── RoadDirectionality.java # 道路方向枚举
```
### 4.5 路径冲突数据模型 (`pathconflict/model`)
包含路径冲突检测模块的数据结构:
- 冲突点信息
- 冲突路径信息
- 冲突解决策略
### 4.4 数据采集模型 (`dataCollector/model`)
### 4.6 地理围栏数据模型 (`geofence/model`)
包含地理围栏模块的数据结构:
- 围栏信息
- 围栏规则
- 围栏触发事件
### 4.7 规则引擎数据模型 (`rule/model`)
包含规则引擎模块的数据结构:
- 规则定义
- 规则条件
- 规则动作
包含数据采集模块的专用数据结构:
```
dataCollector/model/
├── CommandResponse.java # 命令响应
├── VehicleCommand.java # 车辆命令
├── VehicleLocationInfo.java # 车辆位置信息
├── VehicleStateInfo.java # 车辆状态信息
└── enums/ # 枚举类型定义
├── CommandReason.java # 命令原因枚举
├── CommandType.java # 命令类型枚举
└── SignalState.java # 信号状态枚举
```
## 5. 配置属性类目录 (`config/properties`)
包含用于绑定配置文件的POJO类支持
- 机场区域配置绑定
- 道路网络配置绑定
- 几何图形配置绑定
- 系统参数配置
包含用于绑定配置文件的POJO类
实际配置类根据模块需求动态添加和调整。
```
config/properties/
├── AirportAreasProperties.java # 机场区域配置属性
├── AirportRoadsProperties.java # 机场道路配置属性
├── AreaProperties.java # 单个区域配置属性
├── DimensionValue.java # 尺寸值配置
├── GeometryProperties.java # 几何图形配置属性
└── RoadProperties.java # 单个道路配置属性
```
## 6. 资源文件目录 (`resources`)
@ -189,95 +164,103 @@ resources/
│ ├── airport_areas.yaml # 机场区域配置
│ ├── airport_roads.yaml # 机场道路配置
│ └── airport_zones.yaml # 机场区域配置
├── static/ # 静态Web资源
│ ├── index.html # 主页面
│ ├── geoposition-test.html # 地理位置测试页面
│ ├── websocket-test.html # WebSocket测试页面
│ └── js/ # JavaScript文件
└── templates/ # 模板文件目录
├── data/ # 数据文件目录
├── scripts/ # 脚本文件目录
└── static/ # 静态Web资源
├── index.html # 主页面
├── geoposition-test.html # 地理位置测试页面
└── js/ # JavaScript文件
```
## 7. 各模块功能说明
### 7.1 通用组件模块 (common)
提供系统级通用功能和数据结构,包括:
- 通用配置
- 异常处理
- 基础数据模型
- 通用服务
### 7.1 通用数据模型 (common/model)
### 7.2 机场区域管理模块 (area)
负责管理机场内各种功能区域的边界、权限和规则:
- 加载`airport_areas.yaml`配置文件
- 构建空间索引以支持高效的几何查询
- 提供区域查询接口(根据位置查找包含区域、获取限速等)
- 支持区域权限验证和时间有效性检查
- 使用JTS库进行复杂的空间几何计算
数据模型模块定义了系统中使用的所有数据结构:
- **MovingObject**: 所有移动物体的抽象基类,定义共有属性和行为
- **Aircraft**: 代表航空器的具体实现类
- **SpecialVehicle**: 代表特勤车辆的具体实现类
- **UnmannedVehicle**: 代表无人车的具体实现类
- **GeoPosition**: 表示地理位置的数据结构
- **Velocity**: 表示速度和局部坐标系位置信息的数据结构
- **MovementState**: 封装移动物体在特定时刻的完整状态
- **MovingObjectType**: 定义了系统支持的移动物体类型的枚举
- **base/**: 包含系统基础类和常量定义
- **dto/**: 包含数据传输对象用于API交互
- **repository/**: 包含仓储模式实现,提供数据访问抽象
### 7.2 机场区域管理模块 (areas)
**新增模块**,负责管理机场内各种功能区域的边界、权限和规则:
- **areas/model**: 定义区域信息数据结构包含JTS几何对象用于空间计算
- **areas/service**: 提供`AirportAreaService`,负责:
- 加载`airport_areas.yaml`配置文件
- 构建空间索引以支持高效的几何查询
- 提供区域查询接口(根据位置查找包含区域、获取限速等)
- 支持区域权限验证和时间有效性检查
- 使用JTS库进行复杂的空间几何计算
### 7.3 数据采集模块 (dataCollector)
### 7.3 数据采集模块 (datacollector)
负责从各种数据源获取移动物体的实时位置和状态信息:
- 支持多数据源集成
- 提供数据过滤和预处理功能
- 数据持久化和缓存
- WebSocket实时数据采集
### 7.4 数据处理模块 (dataprocessing)
- **model/**: 包含数据采集专用的数据结构
- 车辆位置信息、状态信息、命令响应等
- 枚举类型定义(命令类型、信号状态等)
- **service/**: 数据采集服务实现
- **dao/**: 数据访问对象,处理外部数据源交互
- **config/**: 数据采集相关配置
支持的数据源:
- 航空器数据采集ADS-B、雷达等
- 特勤车辆数据采集GPS、地面雷达等
- 无人车数据采集(车载传感器等)
### 7.4 数据处理模块 (dataProcessing)
处理和分析采集到的数据:
- 数据解析和转换
- 轨迹计算和预测
- 碰撞风险评估
- 数据质量检查和过滤
- 历史数据管理和分析
### 7.5 事件处理模块 (event)
负责系统事件的统一管理和分发:
- 事件定义和注册
- 事件监听和处理
- 事件驱动的业务逻辑
### 7.5 控制器层 (controller)
### 7.6 地理围栏模块 (geofence)
提供地理围栏功能:
- 围栏定义和管理
- 实时位置围栏检测
- 围栏触发事件处理
### 7.7 路径冲突模块 (pathconflict)
负责检测和解决路径冲突:
- 路径冲突检测算法
- 冲突解决策略
- 冲突事件处理
### 7.8 道路网络模块 (road)
负责管理和查询机场静态道路网络信息:
- 加载`airport_roads.yaml`配置
- 初始化道路数据和空间索引
- 提供道路查询接口(如根据位置查找道路、获取限速等)
### 7.9 规则引擎模块 (rule)
提供灵活的规则配置和执行功能:
- 规则定义和管理
- 实时规则执行
- 规则触发事件处理
### 7.10 WebSocket模块 (websocket)
提供实时通信功能:
- 推送实时位置更新
- 发送碰撞警告
- 支持客户端实时监控
- 多客户端消息广播
### 7.11 配置模块 (config)
包含应用程序的配置类和配置加载机制:
- 配置文件加载和解析
- 配置属性绑定
- 模块化配置管理
- 系统参数配置
### 7.12 控制器层 (controller)
提供RESTful API接口用于
- 数据查询和检索
- 系统配置和管理
- 状态报告和监控
- 车辆控制命令发布
### 7.6 WebSocket模块 (webSocket)
提供实时通信功能:
- 推送实时位置更新
- 发送碰撞警告
- 支持客户端实时监控
### 7.7 配置模块 (config)
包含应用程序的配置类和配置加载机制:
- **config/properties**: 存放用于绑定配置文件的POJO类支持
- 机场区域配置绑定(`AirportAreasProperties`、`AreaProperties`
- 道路网络配置绑定(`AirportRoadsProperties`、`RoadProperties`
- 几何图形配置绑定(`GeometryProperties`、`DimensionValue`
- 系统参数配置、Bean配置如`RedisConfig`、`ThreadPoolConfig`
- 特定配置加载器(如`RoadNetworkConfig`、`AirportAreaConfig`、`YamlPropertySourceFactory`
- 服务注册、安全、数据库连接等配置
### 7.8 道路网络模块 (roads)
负责管理和查询机场静态道路网络信息:
- **roads/model**: 定义运行时的道路数据结构(`RoadInfo`包含JTS几何对象和处理过的属性
- **roads/service**: 提供`RoadNetworkService`,负责加载`airport_roads.yaml`配置,初始化道路数据和空间索引,并提供查询接口(如根据位置查找道路、获取限速等)
## 8. 文档目录 (doc)
@ -285,30 +268,10 @@ resources/
```
doc/
├── design/ # 设计文档目录
│ ├── area_design.md # 区域设计文档
│ ├── data_structure_design.md # 系统数据结构设计文档
│ ├── directory_structure.md # 目录结构说明文档(本文档)
│ ├── airport_area_design.md # 机场区域设计方案
│ ├── road_network_design.md # 道路网络设计方案
│ ├── speed_calculation.md # 速度计算设计
│ └── 机场预警系统架构设计方案.md # 机场预警系统架构设计
├── guide/ # 操作指南
│ ├── cad_to_yaml_guide.md # CAD转YAML操作指南
│ ├── configuration_guide.md # 配置指南
│ └── commands.md # 常用命令
├── requirement/ # 需求文档
│ ├── requirements.md # 系统需求
│ ├── area.md # 区域需求
│ └── route.md # 路径需求
├── deploy/ # 部署文档
│ ├── ADXP_WebSocket.md # ADXP WebSocket部署
│ ├── Ubuntu部署指南.md # Ubuntu部署指南
│ └── deploy-design.md # 部署设计
└── work/ # 工作文档
├── API接口修复完成报告.md # API接口修复报告
├── 架构优化报告.md # 架构优化报告
└── 无人车经纬度参数错误修复.md # 无人车参数修复报告
├── design_document.md # 系统数据结构设计文档
├── directory_structure.md # 目录结构说明文档(本文档)
├── cad_to_yaml_guide.md # CAD转YAML操作指南
└── road_network_design.md # 道路网络配置集成设计方案
```
## 9. 主要技术特性
@ -327,39 +290,8 @@ doc/
- 采用继承和组合的面向对象设计
- 支持DTO模式进行数据传输
- 实现仓储模式进行数据访问抽象
- 模块化数据模型设计
### 9.4 实时通信
- WebSocket支持实时数据推送
- 线程池配置支持并发处理
- 消息广播和路由机制
### 9.5 模块化架构
- 清晰的模块划分和职责定义
- 低耦合的模块间交互
- 可扩展性强的系统架构
### 9.6 事件驱动
- 基于事件的系统架构
- 灵活的事件监听和处理机制
- 松耦合的业务逻辑
### 9.7 规则引擎
- 可配置的规则定义
- 实时规则执行
- 灵活的规则扩展机制
### 9.8 数据处理能力
- 高性能的数据处理和分析
- 实时数据解析和转换
- 数据质量保障机制
### 9.9 地理围栏
- 支持多种围栏类型
- 实时围栏检测
- 灵活的围栏规则配置
### 9.10 路径冲突检测
- 高效的路径冲突检测算法
- 智能的冲突解决策略
- 实时冲突处理
- Redis缓存支持高性能数据访问
- 线程池配置支持并发处理

View File

@ -1,9 +1,5 @@
## TODO Lists
### [2025-12-12]
- [x] 功能修改机场中心点经纬度坐标120.08782536483503,36.36236546805307
### [2025-10-09]
- [ ] (功能)模拟无人车平台,返回任务列表

View File

@ -1,131 +0,0 @@
# ============================================================
# 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,7 +43,6 @@ 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

@ -89,6 +89,8 @@ data:
# 机场数据源配置 - 开发环境
airport-api:
base-url: http://localhost:8090
# 滑行路由
glide-url: http://localhost:8099
auth:
username: dianxin
password: dianxin@123

View File

@ -0,0 +1,91 @@
# ============================================================
# QAUP 生产环境配置
# 关键配置项支持环境变量覆盖
# 使用方式:创建 .env 文件,通过启动脚本加载环境变量
# ============================================================
# 服务器配置
server:
port: ${SERVER_PORT:8080}
servlet:
context-path: /
tomcat:
uri-encoding: UTF-8
accept-count: 1000
threads:
max: 800
min-spare: 100
# Spring配置
spring:
# Redis配置支持环境变量
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
database: ${REDIS_DATABASE:0}
password: ${REDIS_PASSWORD:}
timeout: 10s
lettuce:
pool:
min-idle: 0
max-idle: 8
max-active: 8
max-wait: -1ms
# Redis 内存优化配置(生产环境)
redis:
# 最大内存限制生产环境建议1-2GB
max-memory: ${REDIS_MAX_MEMORY:1gb}
# 内存淘汰策略
max-memory-policy: ${REDIS_MAX_MEMORY_POLICY:volatile-lru}
# 日志配置(生产环境简洁日志)
logging:
level:
com.qaup: ${LOG_LEVEL_QAUP:info}
org.springframework: ${LOG_LEVEL_SPRING:warn}
org.hibernate: warn
com.alibaba.druid: warn
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
# 数据采集配置(支持环境变量)
data:
collector:
interval: ${DATA_COLLECTOR_INTERVAL:250}
detection:
interval: ${DATA_DETECTION_INTERVAL:1000}
# 机场数据源配置(支持环境变量)
airport-api:
base-url: ${AIRPORT_API_BASE_URL:http://localhost:8090}
auth:
username: ${AIRPORT_API_USERNAME:dianxin}
password: ${AIRPORT_API_PASSWORD:dianxin@123}
# 无人车厂商数据源配置(支持环境变量)
vehicle-api:
base-url: ${VEHICLE_API_BASE_URL:http://localhost:8091}
timeout: ${VEHICLE_API_TIMEOUT:1000}
retry-attempts: ${VEHICLE_API_RETRY:3}
# ADXP 适配器服务配置(生产环境)
adxp-adapter:
# 适配器主机地址
host: ${ADXP_HOST:localhost}
# 适配器端口
port: ${ADXP_PORT:8086}
# 登录用户名
username: ${ADXP_USERNAME}
# 登录密码
password: ${ADXP_PASSWORD}
# 重连延迟(毫秒)
reconnect-delay-millis: ${RECONNECT_DELAY_MILLIS:3000}
# 红绿灯系统配置
traffic:
light:
tcp:
enabled: ${TRAFFIC_LIGHT_TCP_ENABLED:true}
port: ${TRAFFIC_LIGHT_TCP_PORT:8082}

View File

@ -28,14 +28,10 @@ spring:
timeout: 10s
lettuce:
pool:
# 最小空闲连接数(保底连接)
min-idle: 5
# 最大空闲连接数
max-idle: 20
# 最大活跃连接数
max-active: 50
# 连接最大等待时间(毫秒),防止无限等待
max-wait: 5000ms
min-idle: 0
max-idle: 8
max-active: 8
max-wait: -1ms
# Redis 内存优化配置(生产环境)
redis:
@ -83,9 +79,9 @@ data:
# 适配器端口
port: ${ADXP_PORT:8086}
# 登录用户名
username: ${ADXP_USERNAME:dianxin}
username: ${ADXP_USERNAME}
# 登录密码
password: ${ADXP_PASSWORD:Dianxin#2025}
password: ${ADXP_PASSWORD}
# 重连延迟(毫秒)
reconnect-delay-millis: ${RECONNECT_DELAY_MILLIS:3000}

View File

@ -40,8 +40,8 @@ spring:
# 默认激活的profile开发环境
profiles:
# active: dev,druid
active: prod,druid
active: dev,druid
#active: prod,druid
# 文件上传
servlet:
@ -230,8 +230,8 @@ traffic:
# 坐标系统配置collision模块
coordinate-system:
airport:
center-longitude: 120.08782536
center-latitude: 36.36236547
center-longitude: 120.0834104
center-latitude: 36.35406879
# 性能监控配置collision模块扩展
management:
@ -243,10 +243,11 @@ management:
health:
show-details: always
metrics:
export:
simple:
enabled: true
enable:
hikari: true
jvm: true
simple:
metrics:
export:
enabled: true
jmx:
enabled: true

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,39 +62,91 @@ public class AdxpFlightServiceHttpClient implements org.springframework.beans.fa
}
this.baseUrl = String.format("http://%s:%d/api/adxp", properties.getHost(), properties.getPort());
log.info("ADXP HTTP客户端已初始化等待连接状态确认");
tryLogin();
}
@Override
public void destroy() {
log.info("ADXP HTTP客户端已销毁");
sessionLock.lock();
try {
if (sessionId != null) {
logout();
}
} finally {
sessionId = null;
sessionLock.unlock();
}
}
private void disconnect() {
private void tryLogin() {
sessionLock.lock();
try {
log.info("正在断开 ADXP 适配器连接");
log.info("正在登录 ADXP 适配器服务: url={}", baseUrl);
// 登录适配器服务适配器会使用这些认证信息连接真实数据中台
Map<String, Object> loginRequest = new HashMap<>();
loginRequest.put("username", properties.getUsername());
loginRequest.put("password", properties.getPassword());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(new HashMap<>(), headers);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(loginRequest, headers);
restTemplate.exchange(
baseUrl + "/disconnect",
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
baseUrl + "/login",
HttpMethod.POST,
entity,
new ParameterizedTypeReference<Map<String, Object>>() {}
);
log.info("已断开 ADXP 适配器连接");
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);
}
} catch (Exception e) {
log.warn("断开 ADXP 适配器连接失败", 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);
}
}
public List<FlightNotificationDTO> fetchFlightNotifications() {
if (sessionId == null) {
return Collections.emptyList();
}
sessionLock.lock();
try {
String url = baseUrl + "/messages";
String url = baseUrl + "/messages?sessionId=" + sessionId;
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
url,
HttpMethod.GET,
@ -106,6 +158,11 @@ 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();
}
@ -256,60 +313,6 @@ public class AdxpFlightServiceHttpClient implements org.springframework.beans.fa
}
public boolean isEnabled() {
// 检查适配器是否连接到了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;
}
return sessionId != null;
}
}

View File

@ -13,7 +13,6 @@ import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.lang.NonNull;
import java.net.URI;
import java.util.List;
@ -35,7 +34,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,37 +63,39 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
public void stop() {
if (isRunning.compareAndSet(true, false)) {
log.info("停止ADXP航班通知WebSocket客户端");
disconnectWebSocket();
disconnect();
}
}
/**
* 连接到ADXP适配器的WebSocket服务
*/
@SuppressWarnings("removal") // 抑制WebSocketClient.doHandshake过期警告
public void connect() {
if (!properties.isConfigurationReady()) {
log.warn("数据中台航班 SDK 配置不完整WebSocket客户端将无法正常工作");
return;
}
try {
// 构建WebSocket URL - 新架构下不需要sessionId
String wsUrl = String.format("ws://%s:%d/ws/flight-notifications",
properties.getHost(), properties.getPort());
log.info("正在连接到ADXP适配器WebSocket服务: url={}", wsUrl);
// 连接WebSocket - 使用doHandshake虽过期但仍可用
WebSocketSession newSession = webSocketClient.doHandshake(this, URI.create(wsUrl).toString()).get();
// 在afterConnectionEstablished中也会设置session这里只是避免编译警告
if (newSession != null && newSession.isOpen()) {
session = newSession;
isConnected.set(true);
log.info("✅ 已连接到ADXP适配器WebSocket服务");
// 首先通过HTTP登录获取sessionId
sessionId = loginAndGetSessionId();
if (sessionId == null) {
log.error("无法获取sessionIdWebSocket连接失败");
scheduleReconnect();
return;
}
// 构建WebSocket URL
String wsUrl = String.format("ws://%s:%d/ws/flight-notifications",
properties.getHost(), properties.getPort());
log.info("正在连接到ADXP适配器WebSocket服务: url={}", wsUrl);
// 连接WebSocket
session = webSocketClient.doHandshake(this, URI.create(wsUrl).toString()).get();
isConnected.set(true);
log.info("✅ 已连接到ADXP适配器WebSocket服务");
} catch (Exception e) {
log.error("❌ 连接ADXP适配器WebSocket服务失败", e);
errorCount.incrementAndGet();
@ -103,20 +104,68 @@ 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 disconnectWebSocket() {
public void disconnect() {
try {
if (session != null && session.isOpen()) {
session.close();
log.info("WebSocket连接已关闭");
}
// 断开ADXP适配器连接
disconnectADXP();
// 尝试登出
logout();
} catch (Exception e) {
log.error("断开WebSocket连接时发生异常", e);
errorCount.incrementAndGet();
@ -127,33 +176,42 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
}
/**
* 断开ADXP适配器连接
* 登出
*/
private void disconnectADXP() {
private void logout() {
if (sessionId == null) {
return;
}
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 disconnectUrl = baseUrl + "/disconnect";
String logoutUrl = baseUrl + "/logout";
String requestBody = objectMapper.writeValueAsString(logoutRequest);
java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder()
.uri(URI.create(disconnectUrl))
.uri(URI.create(logoutUrl))
.header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString("{}"))
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(requestBody))
.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();
}
}
@Override
public void afterConnectionEstablished(@NonNull WebSocketSession session) throws Exception {
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("🟢 WebSocket连接已建立: sessionId={}", session.getId());
this.session = session;
isConnected.set(true);
@ -161,7 +219,7 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
}
@Override
public void handleMessage(@NonNull WebSocketSession session, @NonNull WebSocketMessage<?> message) throws Exception {
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
if (message instanceof TextMessage) {
handleTextMessage(session, (TextMessage) message);
}
@ -201,7 +259,7 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
}
@Override
public void handleTransportError(@NonNull WebSocketSession session, @NonNull Throwable exception) throws Exception {
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.error("❌ WebSocket传输错误", exception);
errorCount.incrementAndGet();
isConnected.set(false);
@ -214,7 +272,7 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
}
@Override
public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus closeStatus) throws Exception {
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("🟡 WebSocket连接已关闭: reason={}, code={}", closeStatus.getReason(), closeStatus.getCode());
isConnected.set(false);
this.session = null;
@ -386,14 +444,6 @@ public class AdxpFlightServiceWebSocketClient implements WebSocketHandler {
public boolean isConnected() {
return isConnected.get();
}
/**
* 检查是否启用
* 主动连接架构下返回连接状态
*/
public boolean isEnabled() {
return isConnected();
}
/**
* 安排重新连接

View File

@ -231,42 +231,16 @@ public class WebSocketMessageBroadcaster {
// 使用Jackson ObjectMapper将UniversalMessage序列化为JSON字符串
// 这样前端可以获得消息类型时间戳消息ID等完整信息
String jsonMessage = objectMapper.writeValueAsString(message);
this.collisionWebSocketHandler.broadcastMessage(jsonMessage);
// 根据消息类型决定是否缓存避免高频消息阻塞Redis
// 高频实时消息如位置更新不缓存追求实时性
// 低频重要消息如碰撞预警缓存保证不丢失
if (shouldCacheMessage(message.getType())) {
messageCacheService.cacheMessage(message);
}
this.collisionWebSocketHandler.broadcastMessage(jsonMessage);
// 缓存消息用于重连恢复
messageCacheService.cacheMessage(message);
} catch (Exception e) {
System.err.println("Failed to broadcast message via native WebSocket: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 判断消息是否应该缓存
*
* @param messageType 消息类型
* @return true=需要缓存false=不需要缓存
*/
private boolean shouldCacheMessage(String messageType) {
// 高频实时消息 - 不缓存追求实时性避免Redis性能瓶颈
// 这些消息更新频繁用户关心的是最新状态历史数据价值不高
if (MessageTypeConstants.POSITION_UPDATE.equals(messageType) ||
MessageTypeConstants.TRAFFIC_LIGHT_STATUS.equals(messageType) ||
MessageTypeConstants.HEARTBEAT.equals(messageType) ||
MessageTypeConstants.VEHICLE_STATUS_UPDATE.equals(messageType)) {
return false;
}
// 低频重要消息 - 缓存保证不丢失
// 包括碰撞预警规则违规路径冲突电子围栏航班通知车辆指令等
// 这些事件频率低但重要性高用户需要完整的历史记录
return true;
}
/**
* 生成唯一消息ID

View File

@ -7,8 +7,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import java.util.List;
import java.util.stream.Collectors;
/**
* 冲突检测WebSocket处理器
@ -101,7 +99,7 @@ public class CollisionWebSocketHandler implements WebSocketHandler {
}
/**
* 发送消息给指定会话线程安全- 添加发送失败跳过机制
* 发送消息给指定会话线程安全
*/
private void sendMessage(WebSocketSession session, String message) {
try {
@ -114,66 +112,15 @@ public class CollisionWebSocketHandler implements WebSocketHandler {
}
}
} catch (Exception e) {
// 发送失败时快速清理无效会话避免影响其他会话
// 使用快速失败策略不重试避免加重系统负载
String sessionId = session.getId();
LOGGER.warn("发送消息失败,快速跳过 - 会话ID: {}, 错误: {}", sessionId, e.getMessage());
// 异步清理无效会话不阻塞主流程
try {
sessions.remove(sessionId);
if (session.isOpen()) {
session.close(CloseStatus.SERVICE_RESTARTED);
}
} catch (Exception closeEx) {
LOGGER.debug("关闭无效会话失败 - 会话ID: {}, 错误: {}", sessionId, closeEx.getMessage());
}
LOGGER.error("发送消息失败 - 会话ID: {}", session.getId(), e);
}
}
/**
* 广播消息给所有连接的客户端 - 添加分批发送机制
* 避免一次发送过多消息导致前端卡死
* 广播消息给所有连接的客户端
*/
public void broadcastMessage(String message) {
// 获取所有活跃会话
List<WebSocketSession> activeSessions = sessions.values().stream()
.filter(WebSocketSession::isOpen)
.collect(Collectors.toList());
if (activeSessions.isEmpty()) {
LOGGER.debug("没有活跃会话,跳过广播");
return;
}
// 控制单次发送数量避免前端过载
int batchSize = 10; // 每批最多发送10个会话
int totalSessions = activeSessions.size();
LOGGER.debug("开始广播消息,总会话数: {}, 分批大小: {}", totalSessions, batchSize);
// 分批发送
for (int i = 0; i < totalSessions; i += batchSize) {
int endIndex = Math.min(i + batchSize, totalSessions);
List<WebSocketSession> batch = activeSessions.subList(i, endIndex);
LOGGER.debug("发送批次 {}-{} (共{}个会话)", i + 1, endIndex, batch.size());
// 发送当前批次
batch.forEach(session -> sendMessage(session, message));
// 如果还有更多批次短暂休眠避免过载
if (endIndex < totalSessions) {
try {
Thread.sleep(10); // 休眠10毫秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOGGER.warn("广播线程休眠被中断", e);
}
}
}
LOGGER.debug("广播消息完成,总会话数: {}", totalSessions);
sessions.values().forEach(session -> sendMessage(session, message));
}
/**

View File

@ -1,41 +0,0 @@
#!/bin/bash
# ============================================================
# Flyway 迁移详细信息查看脚本
# 显示迁移历史、当前状态等详细信息
# ============================================================
echo "📊 Flyway 数据库迁移详细信息"
echo "=========================================="
# 切换到项目根目录
cd "$(dirname "$0")/.."
cd qaup-admin
# 显示Flyway Info详细信息
echo "🔍 迁移历史和状态:"
mvn flyway:info \
-Dspring.profiles.active=dev \
-Dflyway.url=jdbc:postgresql://10.0.0.58:5432/qaup \
-Dflyway.user=qaup \
-Dflyway.password=qaup123 \
-q
echo ""
echo "=========================================="
echo "📋 常用 Flyway 命令参考:"
echo ""
echo "1. 查看迁移状态:"
echo " ./check-flyway-info.sh"
echo ""
echo "2. 验证迁移脚本:"
echo " ./check-flyway.sh"
echo ""
echo "3. 执行迁移 (谨慎使用):"
echo " mvn flyway:migrate -Dspring.profiles.active=dev -Dflyway.url=jdbc:postgresql://10.0.0.58:5432/qaup -Dflyway.user=qaup -Dflyway.password=qaup123"
echo ""
echo "4. 修复迁移记录 (谨慎使用):"
echo " mvn flyway:repair -Dspring.profiles.active=dev -Dflyway.url=jdbc:postgresql://10.0.0.58:5432/qaup -Dflyway.user=qaup -Dflyway.password=qaup123"
echo ""
echo "5. 基线迁移 (谨慎使用):"
echo " mvn flyway:baseline -Dspring.profiles.active=dev -Dflyway.url=jdbc:postgresql://10.0.0.58:5432/qaup -Dflyway.user=qaup -Dflyway.password=qaup123"

View File

@ -1,53 +0,0 @@
#!/bin/bash
# ============================================================
# Flyway 数据库迁移状态检查脚本
# 用于验证数据库结构与迁移脚本的一致性
# ============================================================
echo "🔍 正在检查 Flyway 数据库迁移状态..."
echo "📊 数据库连接信息:"
echo " 主机: 10.0.0.58"
echo " 端口: 5432"
echo " 数据库: qaup"
echo " 用户: qaup"
echo ""
# 切换到项目根目录
cd "$(dirname "$0")/.."
cd qaup-admin
# 执行Flyway验证命令
echo "🚀 正在执行 Flyway 验证..."
mvn flyway:validate \
-Dspring.profiles.active=dev \
-Dflyway.url=jdbc:postgresql://10.0.0.58:5432/qaup \
-Dflyway.user=qaup \
-Dflyway.password=qaup123 \
-q
# 检查执行结果
if [ $? -eq 0 ]; then
echo ""
echo "✅ Flyway 验证成功!"
echo "📝 建议:"
echo " - 数据库结构与迁移脚本一致"
echo " - 没有发现 schema drift"
echo " - 可以安全进行部署"
echo ""
echo "🔍 详细信息请查看上方输出"
else
echo ""
echo "❌ Flyway 验证失败!"
echo "🔧 建议:"
echo " - 检查数据库连接"
echo " - 验证迁移脚本是否被修改"
echo " - 查看详细错误信息"
echo " - 联系开发团队进行修复"
echo ""
fi
echo ""
echo "📋 其他有用的Flyway命令"
echo " 查看迁移状态: ./check-flyway-info.sh"
echo " 执行迁移: mvn flyway:migrate -Dspring.profiles.active=dev -Dflyway.url=jdbc:postgresql://10.0.0.58:5432/qaup -Dflyway.user=qaup -Dflyway.password=qaup123"

Binary file not shown.

Binary file not shown.

View File

@ -5,7 +5,6 @@ import logging
import os
import threading
import atexit
import random
from typing import Any, Literal, final, TypedDict
# 导入统一的日志配置
@ -201,7 +200,7 @@ def get_aircraft_route_params():
"data": None
}), 404
logger.info(f"进港路由参数查询: flightNo={flight_no}, params={route_params}")
logging.info(f"进港路由参数查询: flightNo={flight_no}, params={route_params}")
return jsonify({
"status": 200,
"msg": "进港路由参数查询成功",
@ -225,7 +224,7 @@ def get_aircraft_route_params():
"data": None
}), 404
logger.info(f"出港路由参数查询: flightNo={flight_no}, params={route_params}")
logging.info(f"出港路由参数查询: flightNo={flight_no}, params={route_params}")
return jsonify({
"status": 200,
"msg": "出港路由参数查询成功",
@ -959,8 +958,8 @@ class AirportCoordinateSystem:
print("警告: pyproj库未安装将回退到简化转换算法")
self.use_pyproj = False
# 回退到原来的参数
self.center_lon = 120.08782536
self.center_lat = 36.36236547
self.center_lon = 120.0834104
self.center_lat = 36.35406879
self.utm_origin_x = 40507423
self.utm_origin_y = 4026164
self.meters_per_degree_lon = 89932
@ -1069,11 +1068,11 @@ def convert_route_to_cgcs2000(route_data: dict) -> dict:
try:
# 检查当前坐标系类型
current_coordinate_system = route_data.get("coordinateSystem", "WGS84")
logger.info(f"转换路由数据坐标系: 当前={current_coordinate_system} -> 目标=CGCS2000")
logging.info(f"转换路由数据坐标系: 当前={current_coordinate_system} -> 目标=CGCS2000")
# 如果已经是CGCS2000坐标系直接返回
if current_coordinate_system == "CGCS2000":
logger.info("路由数据已经是CGCS2000坐标系无需转换")
logging.info("路由数据已经是CGCS2000坐标系无需转换")
return converted_route
# 如果是WGS84坐标系需要转换为CGCS2000
@ -1081,7 +1080,7 @@ def convert_route_to_cgcs2000(route_data: dict) -> dict:
geo_path = converted_route.get("geoPath", {})
features = geo_path.get("features", [])
logger.info(f"开始转换WGS84坐标到CGCS2000共有 {len(features)} 个feature")
logging.info(f"开始转换WGS84坐标到CGCS2000共有 {len(features)} 个feature")
for feature in features:
geometry = feature.get("geometry", {})
@ -1099,7 +1098,7 @@ def convert_route_to_cgcs2000(route_data: dict) -> dict:
x, y = coordinate_system.wgs84_to_cgcs2000(latitude, longitude)
converted_coordinates.append([x, y])
logger.debug(f"WGS84坐标转换: lon={longitude}, lat={latitude} -> CGCS2000: x={x}, y={y}")
logging.debug(f"WGS84坐标转换: lon={longitude}, lat={latitude} -> CGCS2000: x={x}, y={y}")
# 更新坐标数据
geometry["coordinates"] = converted_coordinates
@ -1107,12 +1106,12 @@ def convert_route_to_cgcs2000(route_data: dict) -> dict:
# 更新坐标系标识
converted_route["coordinateSystem"] = "CGCS2000"
logger.info(f"WGS84到CGCS2000坐标转换完成共转换 {len(features)} 个feature")
logging.info(f"WGS84到CGCS2000坐标转换完成共转换 {len(features)} 个feature")
return converted_route
except Exception as e:
logger.error(f"路由数据坐标转换失败: {str(e)}")
logging.error(f"路由数据坐标转换失败: {str(e)}")
# 转换失败时返回原始数据但确保坐标系标识为CGCS2000
converted_route["coordinateSystem"] = "CGCS2000"
return converted_route
@ -1135,9 +1134,9 @@ def merge_discontinuous_route_for_flight(route_data: dict, flight_no: str) -> di
codes_str = route_data.get("codes", "")
if codes_str:
code_order = [code.strip() for code in codes_str.split(",")]
logger.info(f"航班 {flight_no} codes顺序: {code_order}")
logging.info(f"航班 {flight_no} codes顺序: {code_order}")
else:
logger.warning(f"航班 {flight_no} 缺少codes字段无法验证方向")
logging.warning(f"航班 {flight_no} 缺少codes字段无法验证方向")
code_order = []
# 收集所有LineString路径段并记录每段的code标识
@ -1161,7 +1160,7 @@ def merge_discontinuous_route_for_flight(route_data: dict, flight_no: str) -> di
# 如果成功合并为单条路径
if isinstance(merged_geometry, LineString):
continuous_coords = list(merged_geometry.coords)
logger.info(f"✅ 航班 {flight_no} 合并为连续路径,包含 {len(continuous_coords)} 个坐标点")
logging.info(f"✅ 航班 {flight_no} 合并为连续路径,包含 {len(continuous_coords)} 个坐标点")
# 基于codes字段验证和修正路径方向
if len(code_order) >= 2:
@ -1182,14 +1181,14 @@ def merge_discontinuous_route_for_flight(route_data: dict, flight_no: str) -> di
else:
# 无法合并就报错,不能保持原样掩盖问题
error_msg = f"❌ 航班 {flight_no} 路径无法合并为连续路径,存在不连续段"
logger.error(error_msg)
logging.error(error_msg)
raise RuntimeError(error_msg)
except ImportError:
logger.warning(f"Shapely库未安装跳过路径合并")
logging.warning(f"Shapely库未安装跳过路径合并")
return route_data
except Exception as e:
logger.error(f"航班 {flight_no} 路径合并失败: {str(e)}")
logging.error(f"航班 {flight_no} 路径合并失败: {str(e)}")
return route_data
@ -1224,26 +1223,26 @@ def _verify_and_correct_path_direction(continuous_coords: list, original_feature
start_code = find_point_code(continuous_coords[0])
end_code = find_point_code(continuous_coords[-1])
logger.info(f"航班 {flight_no} 端点检查: 起点在{start_code}, 终点在{end_code}, 期望顺序{code_order}")
logging.info(f"航班 {flight_no} 端点检查: 起点在{start_code}, 终点在{end_code}, 期望顺序{code_order}")
# 检查方向是否正确 - 只使用实际存在的路径段codes进行判断
expected_start = code_order[0] # F1
# 对于MU5123进港应该从F1开始但终点138不是路径段code所以检查起点即可
logger.info(f"航班 {flight_no} 方向检查: 期望从{expected_start}开始, 实际起点{start_code}, 终点{end_code}")
logging.info(f"航班 {flight_no} 方向检查: 期望从{expected_start}开始, 实际起点{start_code}, 终点{end_code}")
# 通用路径方向检查逻辑
if start_code == expected_start:
logger.info(f"✅ 航班 {flight_no} 路径方向正确: 从{expected_start}开始")
logging.info(f"✅ 航班 {flight_no} 路径方向正确: 从{expected_start}开始")
else:
# 方向相反,需要反转
continuous_coords.reverse()
logger.info(f"✅ 航班 {flight_no} 路径方向已修正: 从{expected_start}开始 (原起点: {start_code})")
logging.info(f"✅ 航班 {flight_no} 路径方向已修正: 从{expected_start}开始 (原起点: {start_code})")
return continuous_coords
except Exception as e:
logger.error(f"航班 {flight_no} 路径方向验证失败: {str(e)}")
logging.error(f"航班 {flight_no} 路径方向验证失败: {str(e)}")
return continuous_coords
def validate_route_data_integrity(route_data: dict, flight_no: str) -> dict:
@ -1262,10 +1261,10 @@ def validate_route_data_integrity(route_data: dict, flight_no: str) -> dict:
features = geo_path.get("features", [])
if len(features) == 0:
logger.warning(f"⚠️ 航班 {flight_no} 路径数据为空,无路径段")
logging.warning(f"⚠️ 航班 {flight_no} 路径数据为空,无路径段")
return route_data
logger.info(f"✅ 航班 {flight_no} 路径数据完整:包含 {len(features)} 个路径段")
logging.info(f"✅ 航班 {flight_no} 路径数据完整:包含 {len(features)} 个路径段")
# 验证每个路径段的数据完整性
valid_segments = 0
@ -1282,22 +1281,22 @@ def validate_route_data_integrity(route_data: dict, flight_no: str) -> dict:
if len(coordinates) >= 2:
valid_segments += 1
total_coordinates += len(coordinates)
logger.debug(f" 路径段 {i+1}: 代码={code}, 坐标点数={len(coordinates)}")
logging.debug(f" 路径段 {i+1}: 代码={code}, 坐标点数={len(coordinates)}")
else:
logger.warning(f" 路径段 {i+1}: 代码={code}, 坐标点数不足({len(coordinates)}) ⚠️")
logging.warning(f" 路径段 {i+1}: 代码={code}, 坐标点数不足({len(coordinates)}) ⚠️")
else:
logger.warning(f" 路径段 {i+1}: 非LineString类型 ({geometry.get('type')}) ⚠️")
logging.warning(f" 路径段 {i+1}: 非LineString类型 ({geometry.get('type')}) ⚠️")
logger.info(f"✅ 航班 {flight_no} 数据验证完成:{valid_segments}/{len(features)} 有效路径段,共 {total_coordinates} 个坐标点")
logging.info(f"✅ 航班 {flight_no} 数据验证完成:{valid_segments}/{len(features)} 有效路径段,共 {total_coordinates} 个坐标点")
# 验证坐标系信息
coordinate_system = route_data.get("coordinateSystem", "未知")
logger.info(f"✅ 航班 {flight_no} 坐标系:{coordinate_system}")
logging.info(f"✅ 航班 {flight_no} 坐标系:{coordinate_system}")
return route_data
except Exception as e:
logger.error(f"航班 {flight_no} 路径数据完整性验证异常: {str(e)}")
logging.error(f"航班 {flight_no} 路径数据完整性验证异常: {str(e)}")
return route_data
def parse_route_path(route_data: dict) -> list[tuple[float, float]]:
@ -1315,12 +1314,12 @@ def parse_route_path(route_data: dict) -> list[tuple[float, float]]:
try:
# 检查坐标系类型
coordinate_system_type = route_data.get("coordinateSystem", "CGCS2000")
logger.info(f"路由数据坐标系: {coordinate_system_type}")
logging.info(f"路由数据坐标系: {coordinate_system_type}")
geo_path = route_data.get("geoPath", {})
features = geo_path.get("features", [])
logger.info(f"解析路径,共有 {len(features)} 个feature")
logging.info(f"解析路径,共有 {len(features)} 个feature")
for feature in features:
geometry = feature.get("geometry", {})
@ -1335,23 +1334,23 @@ def parse_route_path(route_data: dict) -> list[tuple[float, float]]:
if coordinate_system_type == "WGS84":
# 如果已经是WGS84坐标直接使用 (经度, 纬度)
lat, lon = y, x
logger.debug(f"WGS84坐标直接使用: lon={lon}, lat={lat}")
logging.debug(f"WGS84坐标直接使用: lon={lon}, lat={lat}")
else:
# 如果是CGCS2000坐标需要转换
lat, lon = coordinate_system.cgcs2000_to_wgs84(x, y)
logger.debug(f"CGCS2000坐标转换: {x},{y} -> lat={lat}, lon={lon}")
logging.debug(f"CGCS2000坐标转换: {x},{y} -> lat={lat}, lon={lon}")
path_points.append((lat, lon))
logger.info(f"路径解析完成,坐标系={coordinate_system_type},共生成 {len(path_points)} 个坐标点")
logging.info(f"路径解析完成,坐标系={coordinate_system_type},共生成 {len(path_points)} 个坐标点")
# 暂时禁用过滤,保留所有路径点用于测试
logger.info(f"保留所有 {len(path_points)} 个路径点")
logging.info(f"保留所有 {len(path_points)} 个路径点")
return path_points
except Exception as e:
logger.error(f"路径解析失败: {str(e)}")
logging.error(f"路径解析失败: {str(e)}")
return []
def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
@ -1398,7 +1397,7 @@ class AircraftRouteFollower:
# 根据航班号获取对应的路由数据
flight_routes = aircraft_routes.get(self.flight_no)
if not flight_routes:
logger.warning(f"未找到航班 {self.flight_no} 的路由数据使用MU5123的路由作为默认")
logging.warning(f"未找到航班 {self.flight_no} 的路由数据使用MU5123的路由作为默认")
flight_routes = aircraft_routes.get("MU5123", {})
# 解析进港路径
@ -1406,20 +1405,20 @@ class AircraftRouteFollower:
# 验证MU5123进港路径的数据完整性然后合并为连续路径
if self.flight_no == "MU5123" and arrival_route:
logger.info(f"验证 {self.flight_no} 进港路径数据完整性")
logging.info(f"验证 {self.flight_no} 进港路径数据完整性")
arrival_route = validate_route_data_integrity(arrival_route, self.flight_no)
# 将31个路径段合并为连续路径让飞机能够完整运行
logger.info(f"{self.flight_no} 的分散路径段合并为连续路径")
logging.info(f"{self.flight_no} 的分散路径段合并为连续路径")
arrival_route = merge_discontinuous_route_for_flight(arrival_route, self.flight_no)
self.arrival_points = parse_route_path(arrival_route)
logger.info(f"航空器 {self.flight_no} 进港路径: {len(self.arrival_points)} 个点")
logging.info(f"航空器 {self.flight_no} 进港路径: {len(self.arrival_points)} 个点")
# 解析出港路径
departure_route = flight_routes.get("departure", {})
self.departure_points = parse_route_path(departure_route)
logger.info(f"航空器 {self.flight_no} 出港路径: {len(self.departure_points)} 个点")
logging.info(f"航空器 {self.flight_no} 出港路径: {len(self.departure_points)} 个点")
# 设置机位坐标138号机位的大概位置
if self.arrival_points:
@ -1431,7 +1430,7 @@ class AircraftRouteFollower:
self.gate_position = (36.354068, 120.083410)
except Exception as e:
logger.error(f"路径解析失败: {str(e)}")
logging.error(f"路径解析失败: {str(e)}")
self.arrival_points = []
self.departure_points = []
self.gate_position = (36.354068, 120.083410)
@ -1446,7 +1445,7 @@ class AircraftRouteFollower:
self.is_at_gate = False
self.flight_status = 'in_route' # 设置为路径运行状态
self.wait_until = 0.0 # 清除等待时间
logger.info(f"航空器 {self.flight_no} 开始进港路径跟随")
logging.info(f"航空器 {self.flight_no} 开始进港路径跟随")
elif route_type == "departure" and self.departure_points:
self.current_route_type = "departure"
@ -1456,10 +1455,10 @@ class AircraftRouteFollower:
self.is_at_gate = False
self.flight_status = 'in_route' # 设置为路径运行状态
self.wait_until = 0.0 # 清除等待时间
logger.info(f"航空器 {self.flight_no} 开始出港路径跟随")
logging.info(f"航空器 {self.flight_no} 开始出港路径跟随")
else:
logger.warning(f"航空器 {self.flight_no} 无效的路径类型: {route_type}")
logging.warning(f"航空器 {self.flight_no} 无效的路径类型: {route_type}")
def update_position_on_route(self, aircraft: dict[str, Any], speed_kmh: float, elapsed_time: float) -> bool:
"""
@ -1522,7 +1521,7 @@ class AircraftRouteFollower:
self.flight_status = 'at_gate' if self.is_at_gate else 'completed'
# 设置5秒等待时间
self.wait_until = current_time + self.waiting_duration
logger.info(f"航空器 {self.flight_no} 完成 {self.current_route_type} 路径,等待{self.waiting_duration}秒后重新开始")
logging.info(f"航空器 {self.flight_no} 完成 {self.current_route_type} 路径,等待{self.waiting_duration}秒后重新开始")
return True
# 更新到下一个目标点
@ -1622,12 +1621,12 @@ class AircraftRouteManager:
if follower.route_points:
aircraft["latitude"] = follower.route_points[0][0]
aircraft["longitude"] = follower.route_points[0][1]
logger.info(f"航空器 {aircraft.get('flightNo')} 等待结束,重新开始进港路径")
logging.info(f"航空器 {aircraft.get('flightNo')} 等待结束,重新开始进港路径")
route_completed = follower.update_position_on_route(aircraft, speed, elapsed_time)
if route_completed and follower.flight_status != 'waiting':
# 路径完成已设置5秒等待时间无需立即重新开始
logger.info(f"航空器 {aircraft.get('flightNo')} 进港完成,将等待{follower.waiting_duration}秒后重新开始")
logging.info(f"航空器 {aircraft.get('flightNo')} 进港完成,将等待{follower.waiting_duration}秒后重新开始")
def _update_departure_only_aircraft(self, aircraft: dict[str, Any], follower: AircraftRouteFollower,
speed: float, elapsed_time: float):
@ -1641,12 +1640,12 @@ class AircraftRouteManager:
if follower.route_points:
aircraft["latitude"] = follower.route_points[0][0]
aircraft["longitude"] = follower.route_points[0][1]
logger.info(f"航空器 {aircraft.get('flightNo')} 等待结束,重新开始出港路径")
logging.info(f"航空器 {aircraft.get('flightNo')} 等待结束,重新开始出港路径")
route_completed = follower.update_position_on_route(aircraft, speed, elapsed_time)
if route_completed and follower.flight_status != 'waiting':
# 路径完成已设置5秒等待时间无需立即重新开始
logger.info(f"航空器 {aircraft.get('flightNo')} 出港完成,将等待{follower.waiting_duration}秒后重新开始")
logging.info(f"航空器 {aircraft.get('flightNo')} 出港完成,将等待{follower.waiting_duration}秒后重新开始")
# 创建全局路径跟随管理器
@ -1703,83 +1702,6 @@ def initialize_aircraft_data():
# 初始化飞机数据
aircraft_data.extend(initialize_aircraft_data())
# 额外测试飞机数据(用于前端性能测试)
additional_aircraft_data = []
def initialize_additional_aircraft():
"""初始化 60 架测试飞机"""
global additional_aircraft_data
# 机场中心坐标(基于 CGCS2000 投影坐标)
airport_center_lat = 36.36236547
airport_center_lon = 120.08782536
# 机场覆盖范围(大约 2km x 2km 区域)
lat_range = 0.02 # 大约 2.2km
lon_range = 0.02 # 大约 1.8km
# 创建 60 架测试飞机
for i in range(1, 61):
# 随机分布在机场范围内
lat_offset = (random.random() - 0.5) * lat_range
lon_offset = (random.random() - 0.5) * lon_range
aircraft_lat = airport_center_lat + lat_offset
aircraft_lon = airport_center_lon + lon_offset
# 前 55 架静止,后 5 架移动
is_moving = i > 55
speed = random.uniform(20.0, 50.0) if is_moving else 0.0
# 为移动的飞机设置简单的往复移动路径
if is_moving:
# 在当前位置周围创建一个小范围的移动路径
move_radius = 0.001 # 大约 100 米范围
start_lat = aircraft_lat - move_radius
start_lon = aircraft_lon - move_radius
end_lat = aircraft_lat + move_radius
end_lon = aircraft_lon + move_radius
moving_to_end = True
else:
# 静止飞机使用当前位置作为起点和终点
start_lat = aircraft_lat
start_lon = aircraft_lon
end_lat = aircraft_lat
end_lon = aircraft_lon
moving_to_end = False
aircraft = {
"flightNo": f"PT{i:03d}", # 测试飞机航班号PT001-PT060
"longitude": aircraft_lon,
"latitude": aircraft_lat,
"time": int(time.time() * 1000),
"altitude": 0.0,
"trackNumber": 2000 + i, # 跟踪号2001-2060
"speed": speed,
"use_route_following": False, # 不使用复杂路径跟随
"start_point": {"latitude": start_lat, "longitude": start_lon},
"end_point": {"latitude": end_lat, "longitude": end_lon},
"moving_to_end": moving_to_end
}
additional_aircraft_data.append(aircraft)
# 统计信息
stationary_count = sum(1 for a in additional_aircraft_data if a["speed"] == 0)
moving_count = len(additional_aircraft_data) - stationary_count
print(f"✅ 已添加测试飞机:")
print(f" - 总飞机数: {len(additional_aircraft_data)}")
print(f" - 静止飞机: {stationary_count}")
print(f" - 移动飞机: {moving_count}")
print(f" - 移动速度: 20-50 km/h")
# 添加到主飞机数据列表
aircraft_data.extend(additional_aircraft_data)
# 初始化测试飞机
initialize_additional_aircraft()
from collections.abc import Mapping
def calculate_distance_to_target(vehicle: Mapping[str, float], target_lat: float, target_lon: float) -> float:
@ -2024,7 +1946,7 @@ class BackgroundUpdateManager:
self.running = True
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
self.update_thread.start()
logger.info("后台更新管理器已启动")
logging.info("后台更新管理器已启动")
def stop(self):
"""停止后台更新线程"""