feat: v2.0.0 done
This commit is contained in:
+2
-9
@@ -4,7 +4,6 @@ __pycache__/
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
downloads/
|
||||
eggs/
|
||||
@@ -36,8 +35,6 @@ env/
|
||||
|
||||
# 项目特定目录
|
||||
cache/
|
||||
bak/
|
||||
CapacityReportData/
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
@@ -69,10 +66,6 @@ uploads/
|
||||
*.xls
|
||||
*.csv
|
||||
|
||||
# 配置文件(如果包含敏感信息)
|
||||
# Configure.json # 如果需要保留配置,注释掉这行
|
||||
|
||||
# dist 目录(保留脚本和配置文件,忽略生成的镜像)
|
||||
dist/images/
|
||||
dist/cache/
|
||||
dist/logs/
|
||||
# build 临时目录
|
||||
build/temp/
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"Update": "2026/01/14 17:10:35",
|
||||
"Update": "2026/01/15 10:32:25",
|
||||
"MySQL_DBInfo": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 3306,
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
# CapacityReport Docker 部署指南
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Docker Engine 20.10+
|
||||
- Docker Compose 2.0+
|
||||
|
||||
## 快速启动
|
||||
|
||||
```bash
|
||||
# 启动所有服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## 内网离线部署
|
||||
|
||||
### 步骤1:准备镜像(在有外网的机器上)
|
||||
|
||||
```bash
|
||||
# 拉取所需镜像
|
||||
docker pull python:3.11-slim
|
||||
docker pull mysql:8.0
|
||||
|
||||
# 导出为文件
|
||||
docker save -o capacity-images.tar python:3.11-slim mysql:8.0
|
||||
|
||||
# 将 capacity-images.tar 传输到目标机器
|
||||
```
|
||||
|
||||
### 步骤2:加载镜像(在目标机器上)
|
||||
|
||||
```bash
|
||||
# 加载镜像
|
||||
docker load -i capacity-images.tar
|
||||
```
|
||||
|
||||
### 步骤3:构建并启动
|
||||
|
||||
```bash
|
||||
# 构建应用镜像
|
||||
docker-compose build
|
||||
|
||||
# 启动服务
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 数据库连接
|
||||
|
||||
Docker 环境下,需要修改 `Configure.json` 中的数据库地址:
|
||||
|
||||
```json
|
||||
{
|
||||
"MySQL_DBInfo": {
|
||||
"host": "mysql",
|
||||
"port": 3306,
|
||||
"user": "root",
|
||||
"passwd": "gmcc123",
|
||||
"dbname": "CapacityReport"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- Docker Compose 中 host 应设置为 `mysql`(服务名),而不是 `localhost` 或 `127.0.0.1`
|
||||
- 数据库 `CapacityReport` 会在容器首次启动时自动创建(使用 utf8mb4 字符集)
|
||||
- 如果数据目录已存在(从持久化卷恢复),数据库不会被重复创建,这是安全的
|
||||
|
||||
### 端口映射
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|-----|------|-----|
|
||||
| app | 9081 | Web 应用 |
|
||||
| mysql | 3306 | MySQL 数据库 |
|
||||
|
||||
## LOAD DATA INFILE 支持
|
||||
|
||||
MySQL 容器已配置支持 `LOAD DATA LOCAL INFILE`:
|
||||
|
||||
1. 服务端配置:`mysql/conf.d/custom.cnf` 中 `local_infile = ON`
|
||||
2. 启动参数:`--local-infile=ON`
|
||||
3. Python 客户端:PyMySQL 连接时 `local_infile=True`
|
||||
|
||||
验证是否启用:
|
||||
|
||||
```bash
|
||||
docker exec -it capacity-mysql mysql -uroot -pgmcc123 -e "SHOW VARIABLES LIKE 'local_infile';"
|
||||
```
|
||||
|
||||
预期输出:
|
||||
```
|
||||
+---------------+-------+
|
||||
| Variable_name | Value |
|
||||
+---------------+-------+
|
||||
| local_infile | ON |
|
||||
+---------------+-------+
|
||||
```
|
||||
|
||||
## 数据持久化
|
||||
|
||||
| 路径 | 说明 |
|
||||
|-----|-----|
|
||||
| ./cache | 处理缓存和历史记录 |
|
||||
| ./logs | 应用日志 |
|
||||
| mysql-data | MySQL 数据(Docker 卷) |
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 查看运行状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看应用日志
|
||||
docker-compose logs -f app
|
||||
|
||||
# 查看 MySQL 日志
|
||||
docker-compose logs -f mysql
|
||||
|
||||
# 重启应用
|
||||
docker-compose restart app
|
||||
|
||||
# 进入应用容器
|
||||
docker exec -it capacity-report bash
|
||||
|
||||
# 进入 MySQL
|
||||
docker exec -it capacity-mysql mysql -uroot -pgmcc123
|
||||
|
||||
# 备份数据库
|
||||
docker exec capacity-mysql mysqldump -uroot -pgmcc123 CapacityReport > backup.sql
|
||||
|
||||
# 恢复数据库
|
||||
docker exec -i capacity-mysql mysql -uroot -pgmcc123 CapacityReport < backup.sql
|
||||
|
||||
# 清空数据重新开始
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 应用无法连接 MySQL
|
||||
|
||||
1. 检查 MySQL 是否就绪:
|
||||
```bash
|
||||
docker-compose logs mysql
|
||||
```
|
||||
|
||||
2. 验证 MySQL 健康状态:
|
||||
```bash
|
||||
docker exec capacity-mysql mysqladmin ping -uroot -pgmcc123
|
||||
```
|
||||
|
||||
3. 检查 Configure.json 中 host 设置是否为 `mysql`
|
||||
|
||||
### LOAD DATA INFILE 不工作
|
||||
|
||||
1. 检查 MySQL 变量:
|
||||
```bash
|
||||
docker exec -it capacity-mysql mysql -uroot -pgmcc123 -e "SHOW VARIABLES LIKE 'local_infile';"
|
||||
```
|
||||
|
||||
2. 检查配置文件:
|
||||
```bash
|
||||
docker exec -it capacity-mysql cat /etc/mysql/conf.d/custom.cnf
|
||||
```
|
||||
|
||||
### cache 目录权限问题
|
||||
|
||||
```bash
|
||||
chmod -R 777 cache/
|
||||
```
|
||||
@@ -1,112 +1,133 @@
|
||||
# CapacityReport - 容量报表处理程序
|
||||
# CapacityReport - 容量报表处理系统
|
||||
|
||||
容量报表数据处理系统,支持 Excel/CSV 文件上传、数据提取、清洗和导入 MySQL 数据库。
|
||||
支持 Excel/CSV 文件上传、数据提取、清洗和导入 MySQL 数据库。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: FastAPI + Uvicorn + Supervisor
|
||||
- **前端**: 原生 JavaScript + HTML/CSS
|
||||
- **数据库**: MySQL (PyMySQL + SQLAlchemy)
|
||||
- **数据库**: MySQL 8.0+ (PyMySQL + SQLAlchemy)
|
||||
- **数据处理**: Pandas + OpenPyXL
|
||||
- **部署**: Docker + Supervisor
|
||||
- **部署**: Docker + Docker Compose
|
||||
|
||||
## 系统架构
|
||||
## 数据库要求
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[浏览器] -->|HTTP| B[FastAPI]
|
||||
B -->|静态文件| C[Static Files]
|
||||
B -->|API请求| D[业务逻辑层]
|
||||
D -->|数据处理| E[DataProcessor]
|
||||
D -->|数据管理| F[DatabaseManager]
|
||||
D -->|历史记录| G[HistoryManager]
|
||||
E -->|批量插入| H[MySQL]
|
||||
F -->|连接池| H
|
||||
I[Supervisor] -->|管理进程| J[Uvicorn]
|
||||
J -->|运行| B
|
||||
- **MySQL**: >= 8.0, < 9.0
|
||||
- **字符集**: utf8mb4
|
||||
- **必需功能**: LOAD DATA LOCAL INFILE
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
CapaReport/
|
||||
├── app/ # 应用代码
|
||||
│ ├── main.py # FastAPI 入口
|
||||
│ ├── processor.py # 数据处理
|
||||
│ ├── database.py # 数据库管理
|
||||
│ ├── history.py # 历史记录
|
||||
│ └── config.py # 配置管理
|
||||
├── build/ # 构建脚本和配置
|
||||
│ ├── build.py # 统一构建脚本
|
||||
│ ├── Dockerfile # Docker 镜像定义
|
||||
│ ├── docker-compose.yml # Docker Compose 编排
|
||||
│ └── mysql/ # MySQL 配置
|
||||
├── static/ # 前端静态文件
|
||||
│ ├── index.html
|
||||
│ ├── css/
|
||||
│ └── js/
|
||||
├── Configure.json # 应用配置
|
||||
├── ReportScript.sql # SQL 处理脚本
|
||||
├── requirements.txt # Python 依赖
|
||||
└── supervisord.conf # Supervisor 配置
|
||||
```
|
||||
|
||||
## 核心模块
|
||||
## 本地运行
|
||||
|
||||
- `app/main.py` - FastAPI 应用入口,API 路由
|
||||
- `app/processor.py` - 数据处理核心(ZIP解压、Excel转CSV、CSV导入)
|
||||
- `app/database.py` - 数据库连接与批量插入
|
||||
- `app/history.py` - 处理历史记录管理
|
||||
- `app/config.py` - 配置管理
|
||||
### 前置要求
|
||||
|
||||
## 数据流程
|
||||
- Python >= 3.10, < 3.14
|
||||
- MySQL >= 8.0, < 9.0
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[上传文件] --> B{文件类型}
|
||||
B -->|ZIP| C[解压ZIP]
|
||||
B -->|Excel| D[Excel转CSV]
|
||||
B -->|CSV| E[直接处理]
|
||||
C --> D
|
||||
D --> F[字段映射]
|
||||
E --> F
|
||||
F --> G[数据清洗]
|
||||
G --> H[批量插入MySQL]
|
||||
H --> I[执行SQL脚本]
|
||||
I --> J[生成结果表]
|
||||
```
|
||||
|
||||
## 上传处理时序
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant F as 前端
|
||||
participant A as FastAPI
|
||||
participant P as DataProcessor
|
||||
participant D as MySQL
|
||||
|
||||
U->>F: 选择文件上传
|
||||
F->>A: POST /api/upload
|
||||
A->>A: 保存文件到 cache/
|
||||
A->>F: 返回 task_id
|
||||
F->>A: POST /api/process/start
|
||||
A->>P: 启动处理任务
|
||||
P->>P: 解压ZIP/处理Excel
|
||||
P->>P: 读取CSV并清洗
|
||||
P->>D: 批量插入数据
|
||||
P->>D: 执行SQL脚本
|
||||
P->>A: 返回处理结果
|
||||
A->>F: 推送日志更新
|
||||
F->>U: 显示处理进度
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 部署
|
||||
|
||||
### Docker 部署
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
docker build -t capareport .
|
||||
docker run -d --restart=always -p 9081:9081 capareport
|
||||
```
|
||||
|
||||
### Windows 部署
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
# 启动服务(自动重启模式)
|
||||
### 配置数据库
|
||||
|
||||
编辑 `Configure.json`,设置 MySQL 连接信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"MySQL_DBInfo": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 3306,
|
||||
"user": "root",
|
||||
"passwd": "your_password",
|
||||
"dbname": "CapacityReport"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 启动服务
|
||||
|
||||
**Windows**:
|
||||
```bash
|
||||
run.bat
|
||||
```
|
||||
|
||||
### Linux 部署
|
||||
|
||||
**Linux**:
|
||||
```bash
|
||||
# 使用 Supervisor
|
||||
supervisord -c supervisord.conf
|
||||
```
|
||||
|
||||
访问: http://localhost:9081
|
||||
|
||||
## Docker 编译与部署
|
||||
|
||||
### 构建部署包
|
||||
|
||||
```bash
|
||||
python build/build.py
|
||||
```
|
||||
|
||||
选择构建类型:
|
||||
- `1` - 完整部署包(MySQL + 应用)
|
||||
- `2` - 更新包(仅应用)
|
||||
|
||||
输出文件在 `dist/` 目录:
|
||||
- `capacity-report-full.tar.gz` - 完整部署包
|
||||
- `capacity-report-update.tar.gz` - 更新包
|
||||
|
||||
### 部署
|
||||
|
||||
**完整部署**:
|
||||
```bash
|
||||
tar -xzf capacity-report-full.tar.gz
|
||||
cd capacity-report-full
|
||||
sh deploy.sh
|
||||
```
|
||||
|
||||
**更新应用**:
|
||||
```bash
|
||||
tar -xzf capacity-report-update.tar.gz
|
||||
cd capacity-report-update
|
||||
sh update.sh
|
||||
```
|
||||
|
||||
### 配置说明
|
||||
|
||||
Docker 环境配置在 `build/build.py` 文件头部:
|
||||
- 默认镜像版本
|
||||
- 数据库账号密码
|
||||
- 端口映射
|
||||
|
||||
部署包中的 `Configure.json` 会自动更新为 Docker 环境配置。
|
||||
|
||||
## 配置
|
||||
|
||||
配置文件: `Configure.json`
|
||||
`Configure.json` 主要配置项:
|
||||
|
||||
- `MySQL_DBInfo` - 数据库连接信息
|
||||
- `SheetFilter` - Excel Sheet 过滤规则
|
||||
@@ -116,24 +137,7 @@ supervisord -c supervisord.conf
|
||||
|
||||
- `POST /api/upload` - 上传文件
|
||||
- `POST /api/process/start` - 启动处理
|
||||
- `POST /api/process/status` - 查询处理状态
|
||||
- `POST /api/history` - 获取历史记录
|
||||
- `GET /api/process/status` - 查询状态
|
||||
- `GET /api/history` - 获取历史记录
|
||||
- `POST /api/service/restart` - 重启服务
|
||||
- `GET /api/service/status` - 服务状态
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
CapaReport/
|
||||
├── app/ # 应用代码
|
||||
│ ├── main.py # FastAPI 入口
|
||||
│ ├── processor.py # 数据处理
|
||||
│ ├── database.py # 数据库管理
|
||||
│ ├── history.py # 历史记录
|
||||
│ └── config.py # 配置管理
|
||||
├── static/ # 前端静态文件
|
||||
├── cache/ # 临时文件目录
|
||||
├── Configure.json # 配置文件
|
||||
├── Dockerfile # Docker 配置
|
||||
└── supervisord.conf # Supervisor 配置
|
||||
```
|
||||
- `GET /api/health` - 健康检查
|
||||
|
||||
+12
-6
@@ -290,9 +290,12 @@ CREATE TABLE IF NOT EXISTS `4G_结果表` (
|
||||
ROUND(AVG(`下行流量_GB`),6) AS `下行流量(GB)`,
|
||||
ROUND(AVG(`用户面平均激活UE数`),6) AS `用户面平均激活UE数`,
|
||||
ROUND(AVG(`RRC连接建立最大用户数`),6) AS `YY-RRC连接建立最大用户数`,
|
||||
ROUND(AVG(`上行PUSCH利用率`) / 100,6) AS `上行PUSCH利用率`,
|
||||
ROUND(AVG(`下行PDSCH利用率`) / 100,6) AS `下行PDSCH利用率`,
|
||||
ROUND(AVG(`PDCCH资源利用率`) / 100,6) AS `PDCCH资源利用率`,
|
||||
-- ROUND(AVG(`上行PUSCH利用率`) / 100,6) AS `上行PUSCH利用率`, -- 旧版本:除以100(数据已在前端处理为小数)
|
||||
ROUND(AVG(`上行PUSCH利用率`),6) AS `上行PUSCH利用率`,
|
||||
-- ROUND(AVG(`下行PDSCH利用率`) / 100,6) AS `下行PDSCH利用率`, -- 旧版本:除以100(数据已在前端处理为小数)
|
||||
ROUND(AVG(`下行PDSCH利用率`),6) AS `下行PDSCH利用率`,
|
||||
-- ROUND(AVG(`PDCCH资源利用率`) / 100,6) AS `PDCCH资源利用率`, -- 旧版本:除以100(数据已在前端处理为小数)
|
||||
ROUND(AVG(`PDCCH资源利用率`),6) AS `PDCCH资源利用率`,
|
||||
ROUND(AVG(`用户面最大激活UE数`),6) AS `用户面最大激活UE数`
|
||||
FROM 4G_MAX GROUP BY CGI, `基站名称`, `小区名称`
|
||||
);
|
||||
@@ -450,9 +453,12 @@ CREATE TABLE IF NOT EXISTS `5G_结果表` (
|
||||
ROUND(AVG(`RRC连接最大连接用户数`),6) AS `RRC连接最大连接用户数`,
|
||||
ROUND(AVG(`RRC连接平均连接用户数`),6) AS `RRC连接平均连接用户数`,
|
||||
ROUND(AVG(`小区最大激活UE数`),6) AS `小区最大激活UE数`,
|
||||
ROUND(AVG(`上行PRB平均利用率`) / 100,6) AS `上行PRB平均利用率`,
|
||||
ROUND(AVG(`下行PRB平均利用率`) / 100,6) AS `下行PRB平均利用率`,
|
||||
ROUND(AVG(`PDCCH信道动态CCE占用率`) / 100,6) AS `PDCCH信道CCE占用率(动态)(%)`,
|
||||
-- ROUND(AVG(`上行PRB平均利用率`) / 100,6) AS `上行PRB平均利用率`, -- 旧版本:除以100(数据已在前端处理为小数)
|
||||
ROUND(AVG(`上行PRB平均利用率`),6) AS `上行PRB平均利用率`,
|
||||
-- ROUND(AVG(`下行PRB平均利用率`) / 100,6) AS `下行PRB平均利用率`, -- 旧版本:除以100(数据已在前端处理为小数)
|
||||
ROUND(AVG(`下行PRB平均利用率`),6) AS `下行PRB平均利用率`,
|
||||
-- ROUND(AVG(`PDCCH信道动态CCE占用率`) / 100,6) AS `PDCCH信道CCE占用率(动态)(%)`, -- 旧版本:除以100(数据已在前端处理为小数)
|
||||
ROUND(AVG(`PDCCH信道动态CCE占用率`),6) AS `PDCCH信道CCE占用率(动态)(%)`,
|
||||
ROUND(AVG(`小区下行RLC_SDU字节数_MByte`),6) AS `小区下行RLC SDU字节数(MByte)`,
|
||||
ROUND(AVG(`小区上行RLC_SDU字节数_MByte`),6) AS `小区上行RLC SDU字节数(MByte)`,
|
||||
ROUND(AVG(`平均单Flow流量_MB`),6) AS `单Flow流量(MB)`
|
||||
|
||||
+88
-22
@@ -783,7 +783,6 @@ async def upload_config(file: UploadFile = File(...)):
|
||||
if not file.filename.endswith('.json'):
|
||||
raise HTTPException(status_code=400, detail="只支持 JSON 格式的配置文件")
|
||||
|
||||
backup_file = None
|
||||
try:
|
||||
# 读取文件内容
|
||||
content = await file.read()
|
||||
@@ -793,28 +792,30 @@ async def upload_config(file: UploadFile = File(...)):
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("配置文件格式错误:必须是 JSON 对象")
|
||||
|
||||
# 备份当前配置
|
||||
config_file = BASE_DIR / "Configure.json"
|
||||
if config_file.exists():
|
||||
backup_file = BASE_DIR / f"Configure.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
shutil.copy2(config_file, backup_file)
|
||||
# 只对比和更新这三个 key:MySQL_DBInfo、SheetFilter、ExtractField
|
||||
# 有则更新,无则沿用旧的
|
||||
|
||||
# 验证并更新配置
|
||||
# 验证 MySQL 配置
|
||||
mysql_data = data.get("MySQL_DBInfo", {})
|
||||
if not all(key in mysql_data for key in ["host", "port", "user", "passwd", "dbname"]):
|
||||
raise ValueError("配置文件缺少必需的 MySQL 配置项")
|
||||
# 更新 MySQL_DBInfo(如果存在)
|
||||
if "MySQL_DBInfo" in data and isinstance(data["MySQL_DBInfo"], dict):
|
||||
mysql_data = data["MySQL_DBInfo"]
|
||||
if "host" in mysql_data:
|
||||
config.mysql.host = mysql_data["host"]
|
||||
if "port" in mysql_data:
|
||||
config.mysql.port = mysql_data["port"]
|
||||
if "user" in mysql_data:
|
||||
config.mysql.user = mysql_data["user"]
|
||||
if "passwd" in mysql_data:
|
||||
config.mysql.passwd = mysql_data["passwd"]
|
||||
if "dbname" in mysql_data:
|
||||
config.mysql.dbname = mysql_data["dbname"]
|
||||
|
||||
# 更新配置
|
||||
config.mysql.host = mysql_data.get("host", "localhost")
|
||||
config.mysql.port = mysql_data.get("port", 3306)
|
||||
config.mysql.user = mysql_data.get("user", "root")
|
||||
config.mysql.passwd = mysql_data.get("passwd", "")
|
||||
config.mysql.dbname = mysql_data.get("dbname", "CapacityReport")
|
||||
# 更新 SheetFilter(如果存在)
|
||||
if "SheetFilter" in data:
|
||||
config.sheet_filter = data["SheetFilter"] if isinstance(data["SheetFilter"], list) else []
|
||||
|
||||
# 更新其他配置
|
||||
config.sheet_filter = data.get("SheetFilter", [])
|
||||
config.extract_fields = data.get("ExtractField", [])
|
||||
# 更新 ExtractField(如果存在)
|
||||
if "ExtractField" in data:
|
||||
config.extract_fields = data["ExtractField"] if isinstance(data["ExtractField"], list) else []
|
||||
|
||||
# 保存配置
|
||||
config.save()
|
||||
@@ -822,8 +823,7 @@ async def upload_config(file: UploadFile = File(...)):
|
||||
return {
|
||||
"success": True,
|
||||
"message": "配置文件上传成功",
|
||||
"update": config.update,
|
||||
"backup": backup_file.name if backup_file and backup_file.exists() else None
|
||||
"update": config.update
|
||||
}
|
||||
|
||||
except json.JSONDecodeError:
|
||||
@@ -1157,6 +1157,72 @@ async def get_script_content():
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@app.post("/api/script/execute")
|
||||
async def execute_script():
|
||||
"""直接执行 SQL 脚本(不经过上传和处理数据)"""
|
||||
import uuid
|
||||
|
||||
# 检查是否有任务在运行
|
||||
if global_task_lock["locked"]:
|
||||
raise HTTPException(status_code=409, detail="已有任务在运行,请等待完成")
|
||||
|
||||
# 生成虚拟 task_id
|
||||
task_id = f"script_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# 锁定任务状态
|
||||
global_task_lock["locked"] = True
|
||||
global_task_lock["task_id"] = task_id
|
||||
global_task_lock["stage"] = "processing"
|
||||
global_task_lock["started_at"] = datetime.now().isoformat()
|
||||
|
||||
# 创建日志记录器(不写入文件,只记录到内存)
|
||||
logs: List[str] = []
|
||||
def log_callback(msg: str):
|
||||
logs.append(msg)
|
||||
processing_tasks[task_id] = {"logs": logs, "status": "processing"}
|
||||
|
||||
logger = ProcessLogger(log_file=None, callback=log_callback)
|
||||
|
||||
# 初始化任务状态
|
||||
processing_tasks[task_id] = {"logs": logs, "status": "processing"}
|
||||
|
||||
# 在后台线程执行脚本
|
||||
def run_script():
|
||||
try:
|
||||
logger.info("开始执行 SQL 脚本...")
|
||||
|
||||
# 创建临时工作目录(虽然不需要,但为了兼容性)
|
||||
temp_work_dir = CACHE_DIR / task_id
|
||||
temp_work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 执行脚本(使用 DataProcessor 的脚本执行方法)
|
||||
processor = DataProcessor(config, temp_work_dir, logger)
|
||||
processor._execute_sql_script()
|
||||
|
||||
# 执行成功
|
||||
logger.success("SQL 脚本执行完成")
|
||||
processing_tasks[task_id] = {"logs": logs, "status": "completed"}
|
||||
|
||||
# 清理临时目录
|
||||
if temp_work_dir.exists():
|
||||
shutil.rmtree(temp_work_dir, ignore_errors=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SQL 脚本执行失败: {str(e)}")
|
||||
processing_tasks[task_id] = {"logs": logs, "status": "failed"}
|
||||
finally:
|
||||
# 解锁全局状态
|
||||
global_task_lock["locked"] = False
|
||||
global_task_lock["task_id"] = None
|
||||
global_task_lock["stage"] = None
|
||||
global_task_lock["started_at"] = None
|
||||
|
||||
thread = Thread(target=run_script)
|
||||
thread.start()
|
||||
|
||||
return {"success": True, "message": "脚本执行任务已启动", "task_id": task_id}
|
||||
|
||||
|
||||
@app.post("/api/script/save")
|
||||
async def save_script_content(content: str = Body(..., embed=True)):
|
||||
"""保存 SQL 脚本内容"""
|
||||
|
||||
+15
-1
@@ -560,10 +560,17 @@ class DataProcessor:
|
||||
def _convert_int_column(self, series: pd.Series) -> pd.Series:
|
||||
"""转换整数列"""
|
||||
try:
|
||||
# 检测是否包含百分号
|
||||
has_percent = series.astype(str).str.contains('%', regex=False, na=False)
|
||||
|
||||
# 去除百分号和逗号
|
||||
cleaned = series.str.replace('%', '', regex=False).str.replace(',', '', regex=False)
|
||||
# 转换为数值,保留为字符串形式(数据库会自动转换)
|
||||
# 转换为数值
|
||||
numeric = pd.to_numeric(cleaned, errors='coerce')
|
||||
|
||||
# 如果有百分号,除以100转换为小数
|
||||
numeric[has_percent] = numeric[has_percent] / 100
|
||||
|
||||
# 四舍五入并转为整数字符串,空值保留为 None
|
||||
return numeric.round().fillna(0).astype(int).astype(str).replace('0', None, regex=False)
|
||||
except Exception:
|
||||
@@ -572,10 +579,17 @@ class DataProcessor:
|
||||
def _convert_float_column(self, series: pd.Series) -> pd.Series:
|
||||
"""转换浮点数列"""
|
||||
try:
|
||||
# 检测是否包含百分号
|
||||
has_percent = series.astype(str).str.contains('%', regex=False, na=False)
|
||||
|
||||
# 去除百分号和逗号
|
||||
cleaned = series.str.replace('%', '', regex=False).str.replace(',', '', regex=False)
|
||||
# 转换为数值
|
||||
numeric = pd.to_numeric(cleaned, errors='coerce')
|
||||
|
||||
# 如果有百分号,除以100转换为小数(例如:95% → 0.95)
|
||||
numeric[has_percent] = numeric[has_percent] / 100
|
||||
|
||||
# 保留小数,空值为 None
|
||||
return numeric.fillna(0).astype(str).replace('0.0', None, regex=False).replace('0', None, regex=False)
|
||||
except Exception:
|
||||
|
||||
@@ -33,6 +33,9 @@ Dockerfile*
|
||||
# MySQL data
|
||||
mysql/
|
||||
|
||||
# Build directory (exclude build files from image)
|
||||
build/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
@@ -0,0 +1,58 @@
|
||||
# 构建脚本
|
||||
|
||||
## 使用方法
|
||||
|
||||
在项目根目录运行:
|
||||
|
||||
```bash
|
||||
python build/build.py
|
||||
```
|
||||
|
||||
选择构建类型:
|
||||
- `1` - 完整部署包(MySQL + 应用)
|
||||
- `2` - 更新包(仅应用)
|
||||
|
||||
## 输出
|
||||
|
||||
构建完成后在 `dist/` 目录生成:
|
||||
- `capacity-report-full.tar.gz` - 完整部署包
|
||||
- `capacity-report-update.tar.gz` - 更新包
|
||||
|
||||
## 部署包内容
|
||||
|
||||
**完整部署包**:
|
||||
- `images/capacity-images.tar` - Docker 镜像
|
||||
- `docker-compose.yml` - 编排文件
|
||||
- `deploy.sh` - 部署脚本
|
||||
- `Configure.json` - 配置文件(已更新为 Docker 环境)
|
||||
- `ReportScript.sql` - SQL 脚本
|
||||
- `mysql/` - MySQL 配置
|
||||
|
||||
**更新包**:
|
||||
- `images/capacity-app-update.tar` - 应用镜像
|
||||
- `docker-compose.yml` - 编排文件
|
||||
- `update.sh` - 更新脚本
|
||||
|
||||
## 部署
|
||||
|
||||
**完整部署**:
|
||||
```bash
|
||||
tar -xzf capacity-report-full.tar.gz
|
||||
cd capacity-report-full
|
||||
sh deploy.sh
|
||||
```
|
||||
|
||||
**更新应用**:
|
||||
```bash
|
||||
tar -xzf capacity-report-update.tar.gz
|
||||
cd capacity-report-update
|
||||
sh update.sh
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
所有配置在 `build/build.py` 文件头部:
|
||||
- 默认镜像版本
|
||||
- 版本要求范围
|
||||
- 数据库账号密码
|
||||
- 端口映射
|
||||
+997
@@ -0,0 +1,997 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CapacityReport 统一构建脚本
|
||||
支持完整部署包和更新包的构建
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import re
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
# ============================================================================
|
||||
# 配置常量 - 可根据需要修改
|
||||
# ============================================================================
|
||||
|
||||
# 默认镜像版本
|
||||
DEFAULT_MYSQL_VERSION = "8.0.44"
|
||||
DEFAULT_PYTHON_VERSION = "3.13.11-slim"
|
||||
|
||||
# 版本要求
|
||||
MYSQL_MIN_VERSION = (8, 0, 0) # MySQL >= 8.0
|
||||
MYSQL_MAX_VERSION = (9, 0, 0) # MySQL < 9.0
|
||||
PYTHON_MIN_VERSION = (3, 10, 0) # Python >= 3.10
|
||||
PYTHON_MAX_VERSION = (3, 14, 0) # Python < 3.14
|
||||
|
||||
# 数据库配置
|
||||
MYSQL_ROOT_USER = "root"
|
||||
MYSQL_ROOT_PASSWORD = "gmcc123"
|
||||
MYSQL_DATABASE = "CapacityReport"
|
||||
MYSQL_HOST = "capacity-mysql"
|
||||
MYSQL_PORT = 3306
|
||||
|
||||
# 应用端口映射
|
||||
APP_PORT_HOST = 19081
|
||||
APP_PORT_CONTAINER = 9081
|
||||
MYSQL_PORT_HOST = 13306
|
||||
MYSQL_PORT_CONTAINER = 3306
|
||||
|
||||
# ============================================================================
|
||||
# 脚本目录配置
|
||||
# ============================================================================
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent.absolute()
|
||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||
DIST_DIR = PROJECT_ROOT / "dist"
|
||||
BUILD_DIR = PROJECT_ROOT / "build"
|
||||
TEMP_BUILD_DIR = BUILD_DIR / "temp"
|
||||
|
||||
# 颜色输出(Windows 支持)
|
||||
import platform
|
||||
if platform.system() == 'Windows':
|
||||
try:
|
||||
import ctypes
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||
except:
|
||||
pass
|
||||
|
||||
class Colors:
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
BLUE = '\033[94m'
|
||||
RESET = '\033[0m'
|
||||
|
||||
def print_step(msg):
|
||||
print(f"\n{Colors.GREEN}{msg}{Colors.RESET}")
|
||||
|
||||
def print_info(msg):
|
||||
print(f" {msg}")
|
||||
|
||||
def print_warning(msg):
|
||||
print(f"{Colors.YELLOW}警告: {msg}{Colors.RESET}")
|
||||
|
||||
def print_error(msg):
|
||||
print(f"{Colors.RED}错误: {msg}{Colors.RESET}")
|
||||
|
||||
def print_choice(msg):
|
||||
print(f"{Colors.BLUE}{msg}{Colors.RESET}")
|
||||
|
||||
def run_cmd(cmd, check=True, capture_output=False):
|
||||
"""执行命令"""
|
||||
if isinstance(cmd, str):
|
||||
cmd = cmd.split()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
check=check,
|
||||
capture_output=capture_output,
|
||||
text=True
|
||||
)
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
if check:
|
||||
print_error(f"命令执行失败: {' '.join(cmd)}")
|
||||
sys.exit(1)
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
print_error(f"命令未找到: {cmd[0]}")
|
||||
sys.exit(1)
|
||||
|
||||
def check_command(cmd):
|
||||
"""检查命令是否存在"""
|
||||
try:
|
||||
if cmd == "docker":
|
||||
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
||||
elif cmd == "docker-compose":
|
||||
subprocess.run([cmd, "version"], capture_output=True, check=True)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def parse_version(version_str):
|
||||
"""解析版本字符串为元组,例如 '8.0.44' -> (8, 0, 44)"""
|
||||
parts = version_str.split('.')
|
||||
try:
|
||||
# 处理带后缀的版本,如 '3.13.11-slim' -> (3, 13, 11)
|
||||
base_version = parts[0] if len(parts) > 0 else '0'
|
||||
major = int(base_version)
|
||||
minor = int(parts[1]) if len(parts) > 1 else 0
|
||||
patch_str = parts[2] if len(parts) > 2 else '0'
|
||||
# 移除后缀,如 '11-slim' -> '11'
|
||||
patch = int(patch_str.split('-')[0])
|
||||
return (major, minor, patch)
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
def check_version_in_range(version_tuple, min_version, max_version):
|
||||
"""检查版本是否在指定范围内 [min, max)"""
|
||||
if version_tuple is None:
|
||||
return False
|
||||
return min_version <= version_tuple < max_version
|
||||
|
||||
def get_python_tag_from_dockerfile():
|
||||
"""从 Dockerfile 中提取 Python 镜像标签"""
|
||||
# 从 build 目录读取 Dockerfile
|
||||
dockerfile_path = BUILD_DIR / "Dockerfile"
|
||||
if not dockerfile_path.exists():
|
||||
print_error(f"Dockerfile 不存在: {dockerfile_path}")
|
||||
print_error("请确保 build/Dockerfile 文件存在")
|
||||
sys.exit(1)
|
||||
|
||||
with open(dockerfile_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
match = re.match(r'^FROM\s+python:(.+?)(?:\s|$)', line, re.IGNORECASE)
|
||||
if match:
|
||||
tag = match.group(1).strip()
|
||||
return tag
|
||||
|
||||
print_error("无法从 Dockerfile 中找到 Python 镜像标签")
|
||||
print_error("请确保 Dockerfile 中包含 'FROM python:xxx' 行")
|
||||
sys.exit(1)
|
||||
|
||||
def check_and_get_python_image():
|
||||
"""检查并获取符合要求的 Python 镜像"""
|
||||
dockerfile_tag = get_python_tag_from_dockerfile()
|
||||
dockerfile_version = parse_version(dockerfile_tag)
|
||||
|
||||
# 检查 Dockerfile 中的版本是否符合要求
|
||||
if dockerfile_version and check_version_in_range(
|
||||
dockerfile_version, PYTHON_MIN_VERSION, PYTHON_MAX_VERSION
|
||||
):
|
||||
print_info(f"Dockerfile 中的 Python 版本符合要求: {dockerfile_tag}")
|
||||
return f"python:{dockerfile_tag}"
|
||||
else:
|
||||
print_warning(f"Dockerfile 中的 Python 版本 {dockerfile_tag} 不符合要求")
|
||||
print_warning(f"要求: Python >= {'.'.join(map(str, PYTHON_MIN_VERSION))} < {'.'.join(map(str, PYTHON_MAX_VERSION))}")
|
||||
print_info(f"将使用默认版本: {DEFAULT_PYTHON_VERSION}")
|
||||
print_info(f"如果镜像不存在,将自动拉取: python:{DEFAULT_PYTHON_VERSION}")
|
||||
return f"python:{DEFAULT_PYTHON_VERSION}"
|
||||
|
||||
def check_and_get_mysql_image():
|
||||
"""检查并获取符合要求的 MySQL 镜像"""
|
||||
# 检查默认版本是否符合要求
|
||||
default_version = parse_version(DEFAULT_MYSQL_VERSION)
|
||||
if default_version and check_version_in_range(
|
||||
default_version, MYSQL_MIN_VERSION, MYSQL_MAX_VERSION
|
||||
):
|
||||
print_info(f"默认 MySQL 版本符合要求: {DEFAULT_MYSQL_VERSION}")
|
||||
return f"mysql:{DEFAULT_MYSQL_VERSION}"
|
||||
else:
|
||||
print_error(f"默认 MySQL 版本 {DEFAULT_MYSQL_VERSION} 不符合要求")
|
||||
print_error(f"要求: MySQL >= {'.'.join(map(str, MYSQL_MIN_VERSION))} < {'.'.join(map(str, MYSQL_MAX_VERSION))}")
|
||||
sys.exit(1)
|
||||
|
||||
def image_exists(image_name):
|
||||
"""检查镜像是否已存在"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return image_name in result.stdout
|
||||
except:
|
||||
return False
|
||||
|
||||
def format_size(size_bytes):
|
||||
"""格式化文件大小"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size_bytes < 1024.0:
|
||||
return f"{size_bytes:.1f} {unit}"
|
||||
size_bytes /= 1024.0
|
||||
return f"{size_bytes:.1f} TB"
|
||||
|
||||
def write_sh_script(file_path, content):
|
||||
"""写入 shell 脚本,确保使用 LF 换行和 UTF-8 编码"""
|
||||
# 确保内容使用 LF 换行
|
||||
content = content.replace('\r\n', '\n').replace('\r', '\n')
|
||||
# 写入文件(二进制模式确保换行符不被转换)
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(content.encode('utf-8'))
|
||||
# 设置执行权限(在 Linux 上)
|
||||
if platform.system() != 'Windows':
|
||||
os.chmod(file_path, 0o755)
|
||||
|
||||
def get_deploy_sh():
|
||||
"""生成 deploy.sh 脚本内容"""
|
||||
mysql_version = DEFAULT_MYSQL_VERSION
|
||||
return f'''#!/bin/bash
|
||||
# CapacityReport Deployment Script
|
||||
# Run this script on offline machine to import images and start services
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Set permissions automatically
|
||||
chmod -R 0777 "$SCRIPT_DIR" 2>/dev/null || true
|
||||
find "$SCRIPT_DIR" -type f \\( -name "*.sh" -o -name "*.py" \\) -exec chmod +x {{}} \\; 2>/dev/null || true
|
||||
|
||||
echo "=========================================="
|
||||
echo "CapacityReport Deployment"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check Docker
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Error: Docker not found, please install Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Docker Compose
|
||||
DOCKER_COMPOSE_CMD=""
|
||||
if docker compose version &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
echo "Error: Docker Compose not found, please install Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check image file
|
||||
if [ ! -f "images/capacity-images.tar" ]; then
|
||||
echo "Error: Image file not found: images/capacity-images.tar"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 1: Checking ports..."
|
||||
check_port() {{
|
||||
local port=${{1}}
|
||||
local name=${{2}}
|
||||
local in_use=false
|
||||
|
||||
if command -v lsof &> /dev/null; then
|
||||
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
|
||||
in_use=true
|
||||
fi
|
||||
elif command -v netstat &> /dev/null; then
|
||||
if netstat -tuln 2>/dev/null | grep -q ":$port "; then
|
||||
in_use=true
|
||||
fi
|
||||
elif command -v ss &> /dev/null; then
|
||||
if ss -tuln 2>/dev/null | grep -q ":$port "; then
|
||||
in_use=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$in_use" = true ]; then
|
||||
echo "Warning: Port $port ($name) is in use"
|
||||
read -p "Continue? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
{{}}
|
||||
|
||||
check_port {APP_PORT_HOST} "App"
|
||||
check_port {MYSQL_PORT_HOST} "MySQL"
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Loading images..."
|
||||
docker load -i images/capacity-images.tar >/dev/null 2>&1
|
||||
|
||||
echo ""
|
||||
echo "Step 3: Verifying images..."
|
||||
if docker images | grep -q "capacity-report-app.*latest"; then
|
||||
echo " App image: OK"
|
||||
else
|
||||
echo " App image: FAILED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if docker images | grep -q "capacity-mysql.*{mysql_version}"; then
|
||||
echo " MySQL image: OK"
|
||||
else
|
||||
echo " MySQL image: FAILED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 4: Creating directories..."
|
||||
mkdir -p cache logs
|
||||
chmod 777 cache logs 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "Step 5: Checking config..."
|
||||
if [ ! -f "Configure.json" ]; then
|
||||
echo " Creating default Configure.json..."
|
||||
cat > Configure.json << 'EOF'
|
||||
{{
|
||||
"MySQL_DBInfo": {{
|
||||
"host": "{MYSQL_HOST}",
|
||||
"port": {MYSQL_PORT},
|
||||
"user": "{MYSQL_ROOT_USER}",
|
||||
"passwd": "{MYSQL_ROOT_PASSWORD}",
|
||||
"dbname": "{MYSQL_DATABASE}"
|
||||
}},
|
||||
"ExtractField": []
|
||||
}}
|
||||
EOF
|
||||
else
|
||||
if command -v python3 &> /dev/null || command -v python &> /dev/null; then
|
||||
(command -v python3 >/dev/null && python3 || python) << 'PYEOF'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
with open('Configure.json', 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
db_info = config.get('MySQL_DBInfo', {{}})
|
||||
needs_update = False
|
||||
|
||||
if db_info.get('host') != '{MYSQL_HOST}':
|
||||
db_info['host'] = '{MYSQL_HOST}'
|
||||
needs_update = True
|
||||
|
||||
if db_info.get('port') != {MYSQL_PORT}:
|
||||
db_info['port'] = {MYSQL_PORT}
|
||||
needs_update = True
|
||||
|
||||
if db_info.get('user') != '{MYSQL_ROOT_USER}':
|
||||
db_info['user'] = '{MYSQL_ROOT_USER}'
|
||||
needs_update = True
|
||||
|
||||
if db_info.get('passwd') != '{MYSQL_ROOT_PASSWORD}':
|
||||
db_info['passwd'] = '{MYSQL_ROOT_PASSWORD}'
|
||||
needs_update = True
|
||||
|
||||
if db_info.get('dbname') != '{MYSQL_DATABASE}':
|
||||
db_info['dbname'] = '{MYSQL_DATABASE}'
|
||||
needs_update = True
|
||||
|
||||
if needs_update:
|
||||
with open('Configure.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
print(" Updated Configure.json")
|
||||
except:
|
||||
pass
|
||||
PYEOF
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "ReportScript.sql" ]; then
|
||||
touch ReportScript.sql
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 6: Starting services..."
|
||||
$DOCKER_COMPOSE_CMD down 2>/dev/null || true
|
||||
$DOCKER_COMPOSE_CMD up -d >/dev/null 2>&1
|
||||
|
||||
echo ""
|
||||
echo "Step 7: Waiting for services..."
|
||||
for i in {{1..30}}; do
|
||||
if docker exec capacity-mysql mysqladmin ping -h localhost -u {MYSQL_ROOT_USER} -p{MYSQL_ROOT_PASSWORD} --silent 2>/dev/null; then
|
||||
echo " MySQL: Ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo " MySQL: Timeout"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if command -v curl &> /dev/null; then
|
||||
for i in {{1..30}}; do
|
||||
if curl -f http://localhost:{APP_PORT_HOST}/health >/dev/null 2>&1; then
|
||||
echo " App: Ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo " App: Timeout"
|
||||
echo " Check logs: $DOCKER_COMPOSE_CMD logs capacity-app"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
else
|
||||
for i in {{1..30}}; do
|
||||
if docker ps --filter "name=capacity-report-app" --filter "status=running" --format "{{.Names}}" | grep -q "capacity-report-app"; then
|
||||
if docker inspect capacity-report-app --format='{{.State.Health.Status}}' 2>/dev/null | grep -q "healthy"; then
|
||||
echo " App: Ready"
|
||||
break
|
||||
elif [ $i -gt 10 ]; then
|
||||
echo " App: Running"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo " App: Timeout"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Deployment completed!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Access: http://localhost:{APP_PORT_HOST}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " Logs: $DOCKER_COMPOSE_CMD logs -f"
|
||||
echo " Stop: $DOCKER_COMPOSE_CMD down"
|
||||
echo " Restart: $DOCKER_COMPOSE_CMD restart"
|
||||
echo " Status: $DOCKER_COMPOSE_CMD ps"
|
||||
echo ""
|
||||
echo "Database:"
|
||||
echo " Host: localhost"
|
||||
echo " Port: {MYSQL_PORT_HOST}"
|
||||
echo " User: {MYSQL_ROOT_USER}"
|
||||
echo " Password: {MYSQL_ROOT_PASSWORD}"
|
||||
echo " Database: {MYSQL_DATABASE}"
|
||||
echo ""
|
||||
'''
|
||||
|
||||
def get_update_sh():
|
||||
"""生成 update.sh 脚本内容"""
|
||||
return f'''#!/bin/bash
|
||||
# CapacityReport Update Script
|
||||
# Update application container without affecting database
|
||||
# Run this script when you need to update the application
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Set permissions automatically
|
||||
chmod -R 0777 "$SCRIPT_DIR" 2>/dev/null || true
|
||||
find "$SCRIPT_DIR" -type f \\( -name "*.sh" -o -name "*.py" \\) -exec chmod +x {{}} \\; 2>/dev/null || true
|
||||
|
||||
echo "=========================================="
|
||||
echo "CapacityReport Application Update"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check Docker
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Error: Docker not found, please install Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Docker Compose
|
||||
DOCKER_COMPOSE_CMD=""
|
||||
if docker compose version &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
echo "Error: Docker Compose not found, please install Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if services are running
|
||||
if ! docker ps --format "{{.Names}}" | grep -q "capacity-report-app"; then
|
||||
echo "Error: Application container is not running"
|
||||
echo "Please use deploy.sh for initial deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 1: Checking current status..."
|
||||
if docker ps --format "{{.Names}}" | grep -q "capacity-mysql"; then
|
||||
echo " MySQL: Running"
|
||||
else
|
||||
echo " Warning: MySQL container is not running"
|
||||
read -p "Continue? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Loading new application image..."
|
||||
if [ -f "images/capacity-app-update.tar" ]; then
|
||||
echo " Loading images from capacity-app-update.tar..."
|
||||
docker load -i images/capacity-app-update.tar >/dev/null 2>&1
|
||||
|
||||
if docker images | grep -q "capacity-report-app.*latest"; then
|
||||
echo " App image: Loaded"
|
||||
else
|
||||
echo " App image: Not found in tar file"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo " Error: Image file not found: images/capacity-app-update.tar"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 3: Stopping application container..."
|
||||
$DOCKER_COMPOSE_CMD stop capacity-app 2>/dev/null || true
|
||||
$DOCKER_COMPOSE_CMD rm -f capacity-app 2>/dev/null || true
|
||||
echo " Application stopped"
|
||||
|
||||
echo ""
|
||||
echo "Step 4: Updating configuration files..."
|
||||
if [ -f "Configure.json" ]; then
|
||||
if command -v python3 &> /dev/null || command -v python &> /dev/null; then
|
||||
(command -v python3 >/dev/null && python3 || python) << 'PYEOF'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
with open('Configure.json', 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
db_info = config.get('MySQL_DBInfo', {{}})
|
||||
needs_update = False
|
||||
|
||||
if db_info.get('host') != '{MYSQL_HOST}':
|
||||
db_info['host'] = '{MYSQL_HOST}'
|
||||
needs_update = True
|
||||
|
||||
if db_info.get('port') != {MYSQL_PORT}:
|
||||
db_info['port'] = {MYSQL_PORT}
|
||||
needs_update = True
|
||||
|
||||
if needs_update:
|
||||
with open('Configure.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
print(" Updated Configure.json")
|
||||
except:
|
||||
pass
|
||||
PYEOF
|
||||
fi
|
||||
else
|
||||
echo " Warning: Configure.json not found, creating default..."
|
||||
cat > Configure.json << 'EOF'
|
||||
{{
|
||||
"MySQL_DBInfo": {{
|
||||
"host": "{MYSQL_HOST}",
|
||||
"port": {MYSQL_PORT},
|
||||
"user": "{MYSQL_ROOT_USER}",
|
||||
"passwd": "{MYSQL_ROOT_PASSWORD}",
|
||||
"dbname": "{MYSQL_DATABASE}"
|
||||
}},
|
||||
"ExtractField": []
|
||||
}}
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ ! -f "ReportScript.sql" ]; then
|
||||
touch ReportScript.sql
|
||||
fi
|
||||
|
||||
mkdir -p cache logs
|
||||
chmod 777 cache logs 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "Step 5: Starting application container..."
|
||||
$DOCKER_COMPOSE_CMD up -d capacity-app >/dev/null 2>&1
|
||||
echo " Application started"
|
||||
|
||||
echo ""
|
||||
echo "Step 6: Waiting for application to be ready..."
|
||||
if command -v curl &> /dev/null; then
|
||||
for i in {{1..30}}; do
|
||||
if curl -f http://localhost:{APP_PORT_HOST}/health >/dev/null 2>&1; then
|
||||
echo " App: Ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo " App: Timeout (may still be starting)"
|
||||
echo " Check logs: $DOCKER_COMPOSE_CMD logs capacity-app"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
else
|
||||
for i in {{1..30}}; do
|
||||
if docker ps --filter "name=capacity-report-app" --filter "status=running" --format "{{.Names}}" | grep -q "capacity-report-app"; then
|
||||
if docker inspect capacity-report-app --format='{{.State.Health.Status}}' 2>/dev/null | grep -q "healthy"; then
|
||||
echo " App: Ready"
|
||||
break
|
||||
elif [ $i -gt 10 ]; then
|
||||
echo " App: Running"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo " App: Timeout"
|
||||
echo " Check logs: $DOCKER_COMPOSE_CMD logs capacity-app"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Update completed!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Application: http://localhost:{APP_PORT_HOST}"
|
||||
echo ""
|
||||
echo "Database status:"
|
||||
if docker ps --format "{{.Names}}" | grep -q "capacity-mysql"; then
|
||||
echo " MySQL: Running (unchanged)"
|
||||
else
|
||||
echo " MySQL: Not running"
|
||||
fi
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " View logs: $DOCKER_COMPOSE_CMD logs -f capacity-app"
|
||||
echo " Restart app: $DOCKER_COMPOSE_CMD restart capacity-app"
|
||||
echo " Status: $DOCKER_COMPOSE_CMD ps"
|
||||
echo ""
|
||||
'''
|
||||
|
||||
def build_full_package():
|
||||
"""构建完整部署包"""
|
||||
print_step("构建完整部署包...")
|
||||
|
||||
# 清理临时目录
|
||||
if TEMP_BUILD_DIR.exists():
|
||||
shutil.rmtree(TEMP_BUILD_DIR)
|
||||
TEMP_BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 检查 Docker
|
||||
if not check_command("docker"):
|
||||
print_error("未检测到 Docker,请先安装 Docker Desktop")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查 Docker Compose
|
||||
docker_compose_cmd = None
|
||||
try:
|
||||
subprocess.run(["docker", "compose", "version"], capture_output=True, check=True)
|
||||
docker_compose_cmd = "docker compose"
|
||||
except:
|
||||
if check_command("docker-compose"):
|
||||
docker_compose_cmd = "docker-compose"
|
||||
else:
|
||||
print_error("未检测到 Docker Compose")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查并获取符合要求的镜像
|
||||
print_step("检查镜像版本...")
|
||||
python_image = check_and_get_python_image()
|
||||
mysql_image = check_and_get_mysql_image()
|
||||
|
||||
# 拉取基础镜像(如果不存在则自动拉取)
|
||||
print_step("拉取基础镜像...")
|
||||
if not image_exists(python_image):
|
||||
print_info(f"Python 镜像不存在,正在拉取: {python_image}")
|
||||
run_cmd(["docker", "pull", python_image])
|
||||
print_info(f"Python 镜像拉取完成")
|
||||
else:
|
||||
print_info(f"Python 镜像已存在: {python_image}")
|
||||
|
||||
if not image_exists(mysql_image):
|
||||
print_info(f"MySQL 镜像不存在,正在拉取: {mysql_image}")
|
||||
run_cmd(["docker", "pull", mysql_image])
|
||||
print_info(f"MySQL 镜像拉取完成")
|
||||
else:
|
||||
print_info(f"MySQL 镜像已存在: {mysql_image}")
|
||||
|
||||
# 构建应用镜像
|
||||
print_step("构建应用镜像...")
|
||||
# 使用 build 目录的 Dockerfile
|
||||
dockerfile_path = BUILD_DIR / "Dockerfile"
|
||||
if not dockerfile_path.exists():
|
||||
print_error(f"Dockerfile 不存在: {dockerfile_path}")
|
||||
sys.exit(1)
|
||||
print_info(f"使用 build 目录的 Dockerfile: {dockerfile_path}")
|
||||
|
||||
# 构建时使用项目根目录作为上下文(因为需要复制应用代码)
|
||||
os.chdir(PROJECT_ROOT)
|
||||
# 但使用 build 目录的 Dockerfile
|
||||
dockerfile_arg = str(dockerfile_path)
|
||||
# .dockerignore 会自动使用构建上下文根目录的(即 PROJECT_ROOT/.dockerignore)
|
||||
# 从 build 目录复制 .dockerignore 到根目录(临时,构建时使用)
|
||||
dockerignore_build = BUILD_DIR / ".dockerignore"
|
||||
dockerignore_root = PROJECT_ROOT / ".dockerignore"
|
||||
if dockerignore_build.exists():
|
||||
shutil.copy2(dockerignore_build, dockerignore_root)
|
||||
print_info("已使用 build 目录的 .dockerignore")
|
||||
run_cmd(["docker", "build", "-t", "capacity-report-app:latest", "-f", dockerfile_arg, "."])
|
||||
# 清理临时复制的 .dockerignore(如果根目录原本没有)
|
||||
if dockerignore_build.exists() and dockerignore_root.exists():
|
||||
try:
|
||||
# 如果内容相同,说明是从 build 目录复制的,可以删除
|
||||
if dockerignore_build.read_text() == dockerignore_root.read_text():
|
||||
dockerignore_root.unlink()
|
||||
print_info("已清理临时 .dockerignore")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 标记 MySQL 镜像(提取版本号)
|
||||
mysql_tag = mysql_image.split(':')[1]
|
||||
mysql_tagged = f"capacity-mysql:{mysql_tag}"
|
||||
run_cmd(["docker", "tag", mysql_image, mysql_tagged])
|
||||
|
||||
# 导出镜像
|
||||
print_step("导出镜像...")
|
||||
images_dir = TEMP_BUILD_DIR / "images"
|
||||
images_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tar_file = images_dir / "capacity-images.tar"
|
||||
# 提取 MySQL 标签
|
||||
mysql_tag = mysql_image.split(':')[1]
|
||||
mysql_tagged = f"capacity-mysql:{mysql_tag}"
|
||||
|
||||
run_cmd([
|
||||
"docker", "save",
|
||||
"capacity-report-app:latest",
|
||||
mysql_tagged,
|
||||
"-o", str(tar_file)
|
||||
])
|
||||
print_info(f"镜像导出完成: {format_size(tar_file.stat().st_size)}")
|
||||
|
||||
# 复制配置文件
|
||||
print_step("复制配置文件...")
|
||||
# 优先从 build 目录读取,如果不存在则从根目录读取
|
||||
def get_source_path(filename, subdir=None):
|
||||
"""获取源文件路径,优先 build 目录,其次根目录"""
|
||||
if subdir:
|
||||
build_path = BUILD_DIR / subdir / filename
|
||||
root_path = PROJECT_ROOT / subdir / filename
|
||||
else:
|
||||
build_path = BUILD_DIR / filename
|
||||
root_path = PROJECT_ROOT / filename
|
||||
return build_path if build_path.exists() else root_path
|
||||
|
||||
# Configure.json 和 ReportScript.sql 从项目根目录读取(应用配置文件)
|
||||
files_to_copy = [
|
||||
(PROJECT_ROOT / "Configure.json", TEMP_BUILD_DIR / "Configure.json"),
|
||||
(PROJECT_ROOT / "ReportScript.sql", TEMP_BUILD_DIR / "ReportScript.sql"),
|
||||
(get_source_path("docker-compose.yml"), TEMP_BUILD_DIR / "docker-compose.yml"),
|
||||
(get_source_path("custom.cnf", "mysql/conf.d"), TEMP_BUILD_DIR / "mysql" / "conf.d" / "custom.cnf"),
|
||||
(get_source_path("01-init-db.sql", "mysql/init"), TEMP_BUILD_DIR / "mysql" / "init" / "01-init-db.sql"),
|
||||
]
|
||||
|
||||
for src, dst in files_to_copy:
|
||||
if src.exists():
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
else:
|
||||
print_warning(f"{src.name} 不存在: {src}")
|
||||
|
||||
# 更新 Configure.json
|
||||
config_file = TEMP_BUILD_DIR / "Configure.json"
|
||||
if config_file.exists():
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
config['MySQL_DBInfo'] = {
|
||||
'host': MYSQL_HOST,
|
||||
'port': MYSQL_PORT,
|
||||
'user': MYSQL_ROOT_USER,
|
||||
'passwd': MYSQL_ROOT_PASSWORD,
|
||||
'dbname': MYSQL_DATABASE
|
||||
}
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 生成 deploy.sh
|
||||
print_step("生成部署脚本...")
|
||||
write_sh_script(TEMP_BUILD_DIR / "deploy.sh", get_deploy_sh())
|
||||
|
||||
# 打包
|
||||
print_step("打包部署包...")
|
||||
tar_output = DIST_DIR / "capacity-report-full.tar.gz"
|
||||
DIST_DIR.mkdir(exist_ok=True)
|
||||
|
||||
with tarfile.open(tar_output, 'w:gz') as tar:
|
||||
tar.add(TEMP_BUILD_DIR, arcname='.')
|
||||
|
||||
print_info(f"部署包已生成: {tar_output}")
|
||||
print_info(f"文件大小: {format_size(tar_output.stat().st_size)}")
|
||||
|
||||
# 清理临时文件和缓存
|
||||
print_step("清理临时文件...")
|
||||
if TEMP_BUILD_DIR.exists():
|
||||
shutil.rmtree(TEMP_BUILD_DIR, ignore_errors=True)
|
||||
print_info("已清理临时构建目录")
|
||||
|
||||
return tar_output
|
||||
|
||||
def build_update_package():
|
||||
"""构建更新包"""
|
||||
print_step("构建更新包...")
|
||||
|
||||
# 清理临时目录
|
||||
if TEMP_BUILD_DIR.exists():
|
||||
shutil.rmtree(TEMP_BUILD_DIR)
|
||||
TEMP_BUILD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 检查 Docker
|
||||
if not check_command("docker"):
|
||||
print_error("未检测到 Docker")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查并获取符合要求的镜像
|
||||
print_step("检查镜像版本...")
|
||||
python_image = check_and_get_python_image()
|
||||
|
||||
# 拉取基础镜像(如果不存在则自动拉取)
|
||||
print_step("拉取基础镜像...")
|
||||
if not image_exists(python_image):
|
||||
print_info(f"Python 镜像不存在,正在拉取: {python_image}")
|
||||
run_cmd(["docker", "pull", python_image])
|
||||
print_info(f"Python 镜像拉取完成")
|
||||
else:
|
||||
print_info(f"Python 镜像已存在: {python_image}")
|
||||
|
||||
# 构建应用镜像
|
||||
print_step("构建应用镜像...")
|
||||
# 使用 build 目录的 Dockerfile
|
||||
dockerfile_path = BUILD_DIR / "Dockerfile"
|
||||
if not dockerfile_path.exists():
|
||||
print_error(f"Dockerfile 不存在: {dockerfile_path}")
|
||||
sys.exit(1)
|
||||
print_info(f"使用 build 目录的 Dockerfile: {dockerfile_path}")
|
||||
|
||||
# 构建时使用项目根目录作为上下文(因为需要复制应用代码)
|
||||
os.chdir(PROJECT_ROOT)
|
||||
# 但使用 build 目录的 Dockerfile
|
||||
dockerfile_arg = str(dockerfile_path)
|
||||
# .dockerignore 会自动使用构建上下文根目录的(即 PROJECT_ROOT/.dockerignore)
|
||||
# 从 build 目录复制 .dockerignore 到根目录(临时,构建时使用)
|
||||
dockerignore_build = BUILD_DIR / ".dockerignore"
|
||||
dockerignore_root = PROJECT_ROOT / ".dockerignore"
|
||||
if dockerignore_build.exists():
|
||||
shutil.copy2(dockerignore_build, dockerignore_root)
|
||||
print_info("已使用 build 目录的 .dockerignore")
|
||||
run_cmd(["docker", "build", "-t", "capacity-report-app:latest", "-f", dockerfile_arg, "."])
|
||||
# 清理临时复制的 .dockerignore(如果根目录原本没有)
|
||||
if dockerignore_build.exists() and dockerignore_root.exists():
|
||||
try:
|
||||
# 如果内容相同,说明是从 build 目录复制的,可以删除
|
||||
if dockerignore_build.read_text() == dockerignore_root.read_text():
|
||||
dockerignore_root.unlink()
|
||||
print_info("已清理临时 .dockerignore")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 导出镜像
|
||||
print_step("导出镜像...")
|
||||
images_dir = TEMP_BUILD_DIR / "images"
|
||||
images_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tar_file = images_dir / "capacity-app-update.tar"
|
||||
run_cmd([
|
||||
"docker", "save",
|
||||
"capacity-report-app:latest",
|
||||
"-o", str(tar_file)
|
||||
])
|
||||
print_info(f"镜像导出完成: {format_size(tar_file.stat().st_size)}")
|
||||
|
||||
# 复制配置文件
|
||||
print_step("复制配置文件...")
|
||||
# 从 build 目录读取配置文件
|
||||
def get_source_path(filename, subdir=None):
|
||||
"""获取源文件路径,从 build 目录读取"""
|
||||
if subdir:
|
||||
return BUILD_DIR / subdir / filename
|
||||
else:
|
||||
return BUILD_DIR / filename
|
||||
|
||||
files_to_copy = [
|
||||
(get_source_path("docker-compose.yml"), TEMP_BUILD_DIR / "docker-compose.yml"),
|
||||
]
|
||||
|
||||
for src, dst in files_to_copy:
|
||||
if src.exists():
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
# 生成 update.sh
|
||||
print_step("生成更新脚本...")
|
||||
write_sh_script(TEMP_BUILD_DIR / "update.sh", get_update_sh())
|
||||
|
||||
# 打包
|
||||
print_step("打包更新包...")
|
||||
tar_output = DIST_DIR / "capacity-report-update.tar.gz"
|
||||
DIST_DIR.mkdir(exist_ok=True)
|
||||
|
||||
with tarfile.open(tar_output, 'w:gz') as tar:
|
||||
tar.add(TEMP_BUILD_DIR, arcname='.')
|
||||
|
||||
print_info(f"更新包已生成: {tar_output}")
|
||||
print_info(f"文件大小: {format_size(tar_output.stat().st_size)}")
|
||||
|
||||
# 清理临时文件和缓存
|
||||
print_step("清理临时文件...")
|
||||
if TEMP_BUILD_DIR.exists():
|
||||
shutil.rmtree(TEMP_BUILD_DIR, ignore_errors=True)
|
||||
print_info("已清理临时构建目录")
|
||||
|
||||
return tar_output
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("CapacityReport 统一构建脚本")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
print_choice("请选择构建类型:")
|
||||
print(" 1. 完整部署包 (包含 MySQL + 应用)")
|
||||
print(" 2. 更新包 (仅应用)")
|
||||
print()
|
||||
|
||||
while True:
|
||||
choice = input("请输入选项 (1/2): ").strip()
|
||||
if choice in ['1', '2']:
|
||||
break
|
||||
print_error("无效选项,请输入 1 或 2")
|
||||
|
||||
print()
|
||||
|
||||
if choice == '1':
|
||||
tar_file = build_full_package()
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("构建完成!")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(f"部署包位置: {tar_file}")
|
||||
print()
|
||||
print("使用方法:")
|
||||
print("1. 将 tar.gz 文件传输到目标 Linux 机器")
|
||||
print("2. 解压: tar -xzf capacity-report-full.tar.gz")
|
||||
print("3. 进入解压目录,执行: sh deploy.sh")
|
||||
print()
|
||||
else:
|
||||
tar_file = build_update_package()
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("构建完成!")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(f"更新包位置: {tar_file}")
|
||||
print()
|
||||
print("使用方法:")
|
||||
print("1. 将 tar.gz 文件传输到目标 Linux 机器")
|
||||
print("2. 解压: tar -xzf capacity-report-update.tar.gz")
|
||||
print("3. 进入解压目录,执行: sh update.sh")
|
||||
print()
|
||||
print("注意: 更新包仅用于更新已部署的应用,不包含 MySQL")
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n构建已取消")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print_error(f"构建失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
# 此文件用于确保 dist 目录被 Git 跟踪
|
||||
# dist 目录用于存放离线部署包
|
||||
Vendored
-243
@@ -1,243 +0,0 @@
|
||||
{
|
||||
"Update": "2026/01/14 09:14:43",
|
||||
"MySQL_DBInfo": {
|
||||
"host": "capacity-mysql",
|
||||
"port": 3306,
|
||||
"user": "root",
|
||||
"passwd": "gmcc123",
|
||||
"dbname": "CapacityReport"
|
||||
},
|
||||
"SheetFilter": [
|
||||
"指标(计数器)",
|
||||
"Template"
|
||||
],
|
||||
"ExtractField": [
|
||||
{
|
||||
"Field": "日期时间",
|
||||
"Extract": [
|
||||
"开始时间"
|
||||
],
|
||||
"Type": "datetime"
|
||||
},
|
||||
{
|
||||
"Field": "网元ID",
|
||||
"Extract": [
|
||||
"网元ID",
|
||||
"网元"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "eNodeBId",
|
||||
"Extract": [
|
||||
"eNodeBId",
|
||||
"eNodeB"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "cellLocalId",
|
||||
"Extract": [
|
||||
"cellId",
|
||||
"小区"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "基站名称",
|
||||
"Extract": [
|
||||
"eNodeB CU名称",
|
||||
"eNodeB名称"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "小区名称",
|
||||
"Extract": [
|
||||
"小区名称",
|
||||
"E-UTRAN FDD小区名称",
|
||||
"E-UTRAN TDD小区名称"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "上下行总流量_GB",
|
||||
"Extract": [
|
||||
"集团-上下行总业务量(GB)",
|
||||
"业务流量上下行(GB)",
|
||||
"YJ_业务流量上下行(GB)",
|
||||
"FDD总流量(G)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "ERAB流量",
|
||||
"Extract": [
|
||||
"ERAB流量(新高负荷)",
|
||||
"ERAB流量",
|
||||
"ERAB流量(新高负荷)",
|
||||
"ERAB流量(新高负荷)-FDD"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "上行流量_GB",
|
||||
"Extract": [
|
||||
"上行流量(新高负荷GB)",
|
||||
"上行流量(新高负荷GB)",
|
||||
"高负荷-上行流量(GB)",
|
||||
"FDD上行流量(G)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "下行流量_GB",
|
||||
"Extract": [
|
||||
"下行流量(新高负荷GB)",
|
||||
"下行流量(新高负荷GB)",
|
||||
"高负荷-下行流量(GB)",
|
||||
"FDD下行流量(G)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "用户面平均激活UE数",
|
||||
"Extract": [
|
||||
"用户面平均激活UE数",
|
||||
"YY-用户面平均激活UE数(个)",
|
||||
"用户面平均激活UE数(unit)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "RRC连接建立最大用户数",
|
||||
"Extract": [
|
||||
"YY-RRC连接建立最大用户数(个)",
|
||||
"RRC连接建立最大用户数(unit)",
|
||||
"RRC连接建立最大用户数"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "上行PUSCH利用率",
|
||||
"Extract": [
|
||||
"上行PUSCH利用率",
|
||||
"YY-上行PUSCH利用率",
|
||||
"上行PUSCH利用率-FDD"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "下行PDSCH利用率",
|
||||
"Extract": [
|
||||
"下行PDSCH利用率",
|
||||
"YY-下行PDSCH利用率",
|
||||
"下行PDSCH利用率-FDD"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "PDCCH资源利用率",
|
||||
"Extract": [
|
||||
"PDCCH资源利用率",
|
||||
"PDCCH资源利用率_1548034713403-33-2",
|
||||
"PDCCH资源使用率(%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "用户面最大激活UE数",
|
||||
"Extract": [
|
||||
"开发者大会保障--最大激活UE数",
|
||||
"用户面最大激活UE数",
|
||||
"YY-用户面最大激活UE数(个)",
|
||||
"用户面最大激活UE数(unit)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "CU小区配置名称",
|
||||
"Extract": [
|
||||
"CU小区配置名称"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "gNBId",
|
||||
"Extract": [
|
||||
"gNBId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "gNBplmn",
|
||||
"Extract": [
|
||||
"gNBplmn"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "5G上下行总流量_GB",
|
||||
"Extract": [
|
||||
"5G上下行总流量(上行PDCP PDU数据量+下行PDCP成功发送数据量)(GB)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "5G上行流量_GB",
|
||||
"Extract": [
|
||||
"5G上行流量(上行PDCP PDU数据量)_1603677232210(GB)",
|
||||
"5G上行流量(上行PDCP PDU数据量)(GB)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "5G下行流量_GB",
|
||||
"Extract": [
|
||||
"5G下行流量(下行PDCP成功发送数据量)_1603677232245(GB)",
|
||||
"5G下行流量(下行PDCP成功发送数据量)(GB)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "小区平均激活UE数",
|
||||
"Extract": [
|
||||
"小区平均激活UE数"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "RRC连接最大连接用户数",
|
||||
"Extract": [
|
||||
"RRC连接最大连接用户数"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "RRC连接平均连接用户数",
|
||||
"Extract": [
|
||||
"RRC连接平均连接用户数"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "小区最大激活UE数",
|
||||
"Extract": [
|
||||
"小区最大激活UE数"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "上行PRB平均利用率",
|
||||
"Extract": [
|
||||
"上行PRB平均利用率"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "下行PRB平均利用率",
|
||||
"Extract": [
|
||||
"下行PRB平均利用率"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "小区下行RLC_SDU字节数_MByte",
|
||||
"Extract": [
|
||||
"小区下行RLC SDU字节数(MByte)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "小区上行RLC_SDU字节数_MByte",
|
||||
"Extract": [
|
||||
"小区上行RLC SDU字节数(MByte)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "PDCCH信道动态CCE占用率",
|
||||
"Extract": [
|
||||
"PDCCH信道CCE占用率(动态)(%)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Field": "平均单Flow流量_MB",
|
||||
"Extract": [
|
||||
"平均单Flow流量(MB)",
|
||||
"平均每flow流量(流量/flow(MB/flow))"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-471
@@ -1,471 +0,0 @@
|
||||
# Update: 2024/11/25 09:30:00
|
||||
|
||||
SET GLOBAL wait_timeout = 86400;
|
||||
DROP TABLE IF EXISTS `4G`;
|
||||
DROP TABLE IF EXISTS `5G`;
|
||||
DROP TABLE IF EXISTS `4G_Date`;
|
||||
DROP TABLE IF EXISTS `4G_MAX`;
|
||||
DROP TABLE IF EXISTS `CCE_MAX`;
|
||||
DROP TABLE IF EXISTS `ULPRB_MAX`;
|
||||
DROP TABLE IF EXISTS `DLPRB_MAX`;
|
||||
DROP TABLE IF EXISTS `4G_MAX_IDX`;
|
||||
DROP TABLE IF EXISTS `CCE_MAX_IDX`;
|
||||
DROP TABLE IF EXISTS `ULPRB_MAX_IDX`;
|
||||
DROP TABLE IF EXISTS `DLPRB_MAX_IDX`;
|
||||
DROP TABLE IF EXISTS `日流量`;
|
||||
DROP TABLE IF EXISTS `4G日均流量`;
|
||||
DROP TABLE IF EXISTS `4G_结果表`;
|
||||
|
||||
DROP TABLE IF EXISTS `5G_Date`;
|
||||
DROP TABLE IF EXISTS `5G_MAX`;
|
||||
DROP TABLE IF EXISTS `5G_MAX_IDX`;
|
||||
DROP TABLE IF EXISTS `5G日均流量`;
|
||||
DROP TABLE IF EXISTS `5G_结果表`;
|
||||
|
||||
|
||||
CREATE TABLE `4G` AS SELECT * FROM `4G_UD`;
|
||||
CREATE TABLE `5G` AS SELECT * FROM `5G_UD`;
|
||||
|
||||
UPDATE 4G SET `日期时间` = DATE_FORMAT(`日期时间`, '%Y-%m-%d %H:%i:%s');
|
||||
UPDATE 5G SET `日期时间` = DATE_FORMAT(`日期时间`, '%Y-%m-%d %H:%i:%s');
|
||||
|
||||
ALTER TABLE `4G`
|
||||
ADD COLUMN `日期` date NULL FIRST,
|
||||
MODIFY COLUMN `日期时间` datetime,
|
||||
ADD COLUMN `CGI` varchar(100) NULL AFTER `日期时间`,
|
||||
MODIFY COLUMN `网元ID` int,
|
||||
MODIFY COLUMN `基站名称` varchar(200),
|
||||
MODIFY COLUMN `小区名称` varchar(200),
|
||||
MODIFY COLUMN `cellLocalId` int,
|
||||
MODIFY COLUMN `eNodeBId` int,
|
||||
MODIFY COLUMN `上下行总流量_GB` float,
|
||||
MODIFY COLUMN `ERAB流量` float,
|
||||
MODIFY COLUMN `上行流量_GB` float,
|
||||
MODIFY COLUMN `下行流量_GB` float,
|
||||
MODIFY COLUMN `用户面平均激活UE数` float,
|
||||
MODIFY COLUMN `RRC连接建立最大用户数` int,
|
||||
MODIFY COLUMN `上行PUSCH利用率` float,
|
||||
MODIFY COLUMN `下行PDSCH利用率` float,
|
||||
MODIFY COLUMN `PDCCH资源利用率` float,
|
||||
MODIFY COLUMN `用户面最大激活UE数` int;
|
||||
|
||||
ALTER TABLE `5G`
|
||||
ADD COLUMN `日期` date NULL FIRST,
|
||||
MODIFY COLUMN `日期时间` datetime,
|
||||
ADD COLUMN `NCGI` varchar(100) NULL AFTER `日期时间`,
|
||||
MODIFY COLUMN `网元ID` int,
|
||||
MODIFY COLUMN `CU小区配置名称` varchar(200),
|
||||
MODIFY COLUMN `cellLocalId` int,
|
||||
MODIFY COLUMN `gNBId` int,
|
||||
MODIFY COLUMN `gNBplmn` varchar(200),
|
||||
MODIFY COLUMN `5G上下行总流量_GB` float,
|
||||
MODIFY COLUMN `5G上行流量_GB` float,
|
||||
MODIFY COLUMN `5G下行流量_GB` float,
|
||||
MODIFY COLUMN `小区平均激活UE数` float,
|
||||
MODIFY COLUMN `RRC连接最大连接用户数` int,
|
||||
MODIFY COLUMN `RRC连接平均连接用户数` float,
|
||||
MODIFY COLUMN `小区最大激活UE数` float,
|
||||
MODIFY COLUMN `上行PRB平均利用率` float,
|
||||
MODIFY COLUMN `下行PRB平均利用率` float,
|
||||
MODIFY COLUMN `小区下行RLC_SDU字节数_MByte` float,
|
||||
MODIFY COLUMN `小区上行RLC_SDU字节数_MByte` float,
|
||||
MODIFY COLUMN `PDCCH信道动态CCE占用率` float,
|
||||
MODIFY COLUMN `平均单Flow流量_MB` float;
|
||||
|
||||
UPDATE 4G SET `日期` = DATE_FORMAT(`日期时间`, '%Y-%m-%d'), CGI = CONCAT("460-00-",eNodeBId,"-", cellLocalId);
|
||||
UPDATE 5G SET gNBplmn = "460-00" WHERE gNBplmn IS NULL;
|
||||
UPDATE 5G SET `日期` = DATE_FORMAT(`日期时间`, '%Y-%m-%d'), NCGI = CONCAT(gNBplmn, "-",gNBId,"-", cellLocalId);
|
||||
|
||||
ALTER TABLE 4G
|
||||
ADD INDEX(`日期`),
|
||||
ADD INDEX(`日期时间`),
|
||||
ADD INDEX(`CGI`),
|
||||
ADD INDEX `TC`(`日期时间`, `CGI`),
|
||||
ADD INDEX `DC`(`日期`, `CGI`);
|
||||
|
||||
ALTER TABLE 5G
|
||||
ADD INDEX(`日期`),
|
||||
ADD INDEX(`日期时间`),
|
||||
ADD INDEX(`NCGI`),
|
||||
ADD INDEX `TC`(`日期时间`, `NCGI`),
|
||||
ADD INDEX `DC`(`日期`, `NCGI`);
|
||||
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `CCE_MAX_IDX`(
|
||||
SELECT
|
||||
`日期`,
|
||||
CGI,
|
||||
MAX( `PDCCH资源利用率`) AS `PDCCH资源利用率_忙时`
|
||||
FROM 4G
|
||||
GROUP BY `日期`, CGI
|
||||
);
|
||||
|
||||
ALTER TABLE `CCE_MAX_IDX`
|
||||
ADD COLUMN `日期时间` datetime,
|
||||
ADD INDEX `DC`(`日期`, `CGI`, `PDCCH资源利用率_忙时`),
|
||||
ADD INDEX `TC`(`日期时间`, `CGI`);
|
||||
|
||||
UPDATE `CCE_MAX_IDX` INNER JOIN 4G
|
||||
ON
|
||||
`CCE_MAX_IDX`.`日期` = 4G.`日期` AND
|
||||
`CCE_MAX_IDX`.CGI=4G.CGI AND
|
||||
`CCE_MAX_IDX`.`PDCCH资源利用率_忙时`=4G.`PDCCH资源利用率`
|
||||
SET
|
||||
`CCE_MAX_IDX`.`日期时间`=4G.`日期时间`;
|
||||
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `CCE_MAX` (
|
||||
SELECT 4G.*
|
||||
FROM `CCE_MAX_IDX` LEFT JOIN 4G
|
||||
ON `CCE_MAX_IDX`.CGI=4G.CGI AND
|
||||
`CCE_MAX_IDX`.`日期时间`=4G.`日期时间`
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS `CCE_MAX_IDX`;
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `ULPRB_MAX_IDX`(
|
||||
SELECT
|
||||
`日期`,
|
||||
CGI,
|
||||
MAX( `上行PUSCH利用率`) AS `上行PUSCH利用率_忙时`
|
||||
FROM 4G
|
||||
GROUP BY `日期`, CGI
|
||||
);
|
||||
|
||||
ALTER TABLE `ULPRB_MAX_IDX`
|
||||
ADD COLUMN `日期时间` datetime,
|
||||
ADD INDEX `DC`(`日期`, `CGI`, `上行PUSCH利用率_忙时`),
|
||||
ADD INDEX `TC`(`日期时间`, `CGI`);
|
||||
|
||||
UPDATE `ULPRB_MAX_IDX` INNER JOIN 4G
|
||||
ON
|
||||
`ULPRB_MAX_IDX`.`日期` = 4G.`日期` AND
|
||||
`ULPRB_MAX_IDX`.CGI=4G.CGI AND
|
||||
`ULPRB_MAX_IDX`.`上行PUSCH利用率_忙时`=4G.`上行PUSCH利用率`
|
||||
SET
|
||||
`ULPRB_MAX_IDX`.`日期时间`=4G.`日期时间`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ULPRB_MAX` (
|
||||
SELECT 4G.*
|
||||
FROM `ULPRB_MAX_IDX` LEFT JOIN 4G
|
||||
ON `ULPRB_MAX_IDX`.CGI=4G.CGI AND
|
||||
`ULPRB_MAX_IDX`.`日期时间`=4G.`日期时间`
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS `ULPRB_MAX_IDX`;
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `DLPRB_MAX_IDX`(
|
||||
SELECT
|
||||
`日期`,
|
||||
CGI,
|
||||
MAX( `下行PDSCH利用率`) AS `下行PDSCH利用率_忙时`
|
||||
FROM 4G
|
||||
GROUP BY `日期`, CGI
|
||||
);
|
||||
|
||||
ALTER TABLE `DLPRB_MAX_IDX`
|
||||
ADD COLUMN `日期时间` datetime,
|
||||
ADD INDEX `DC`(`日期`, `CGI`, `下行PDSCH利用率_忙时`),
|
||||
ADD INDEX `TC`(`日期时间`, `CGI`);
|
||||
|
||||
UPDATE `DLPRB_MAX_IDX` INNER JOIN 4G
|
||||
ON
|
||||
`DLPRB_MAX_IDX`.`日期` = 4G.`日期` AND
|
||||
`DLPRB_MAX_IDX`.CGI=4G.CGI AND
|
||||
`DLPRB_MAX_IDX`.`下行PDSCH利用率_忙时`=4G.`下行PDSCH利用率`
|
||||
SET
|
||||
`DLPRB_MAX_IDX`.`日期时间`=4G.`日期时间`;
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `DLPRB_MAX` (
|
||||
SELECT 4G.*
|
||||
FROM `DLPRB_MAX_IDX` LEFT JOIN 4G
|
||||
ON `DLPRB_MAX_IDX`.CGI=4G.CGI AND
|
||||
`DLPRB_MAX_IDX`.`日期时间`=4G.`日期时间`
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS `DLPRB_MAX_IDX`;
|
||||
|
||||
ALTER TABLE `CCE_MAX` ADD INDEX `DC`(`日期`, `CGI`);
|
||||
ALTER TABLE `DLPRB_MAX` ADD INDEX `DC`(`日期`, `CGI`);
|
||||
ALTER TABLE `ULPRB_MAX` ADD INDEX `DC`(`日期`, `CGI`);
|
||||
|
||||
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `4G_Date` (
|
||||
SELECT `日期`, CGI FROM 4G GROUP BY `日期`, CGI
|
||||
);
|
||||
ALTER TABLE `4G_Date` ADD INDEX `DC`(`日期`, CGI);
|
||||
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `4G_MAX_IDX` (
|
||||
SELECT
|
||||
4G_Date.`日期`,
|
||||
4G_Date.CGI,
|
||||
GREATEST(`ULPRB_MAX`.`上行PUSCH利用率`, DLPRB_MAX.`下行PDSCH利用率`,CCE_MAX.`PDCCH资源利用率`) AS `最大值`
|
||||
FROM
|
||||
4G_Date
|
||||
INNER JOIN
|
||||
DLPRB_MAX
|
||||
ON
|
||||
4G_Date.`日期` = DLPRB_MAX.`日期` AND
|
||||
4G_Date.CGI = DLPRB_MAX.CGI
|
||||
INNER JOIN
|
||||
CCE_MAX
|
||||
ON
|
||||
4G_Date.`日期` = CCE_MAX.`日期` AND
|
||||
4G_Date.CGI = CCE_MAX.CGI
|
||||
INNER JOIN
|
||||
ULPRB_MAX
|
||||
ON
|
||||
4G_Date.`日期` = ULPRB_MAX.`日期` AND
|
||||
4G_Date.CGI = ULPRB_MAX.CGI
|
||||
);
|
||||
DROP TABLE IF EXISTS `4G_Date`;
|
||||
ALTER TABLE `4G_MAX_IDX` ADD INDEX `DC`(`日期`, CGI), ADD INDEX (`最大值`);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `4G_MAX` (
|
||||
SELECT * FROM 4G LIMIT 0
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE `4G_MAX` ADD INDEX (`日期`), ADD INDEX (`CGI`), ADD INDEX `DC` (`日期`,CGI), ADD INDEX `idx`(`CGI`, `基站名称`, `小区名称`);
|
||||
|
||||
ALTER TABLE DLPRB_MAX ADD COLUMN `insert` INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE CCE_MAX ADD COLUMN `insert` INT NOT NULL DEFAULT 0;
|
||||
|
||||
|
||||
|
||||
|
||||
TRUNCATE `4G_MAX`;
|
||||
INSERT INTO `4G_MAX`
|
||||
SELECT ULPRB_MAX.* FROM ULPRB_MAX INNER JOIN `4G_MAX_IDX`
|
||||
ON
|
||||
ULPRB_MAX.`日期` = 4G_MAX_IDX.`日期` AND
|
||||
ULPRB_MAX.CGI = 4G_MAX_IDX.`CGI` AND
|
||||
ULPRB_MAX.`上行PUSCH利用率` = 4G_MAX_IDX.`最大值`;
|
||||
|
||||
UPDATE DLPRB_MAX INNER JOIN 4G_MAX ON DLPRB_MAX.`日期` = 4G_MAX.`日期` AND DLPRB_MAX.CGI = 4G_MAX.CGI SET DLPRB_MAX.`insert` = 1;
|
||||
ALTER TABLE DLPRB_MAX ADD INDEX (`insert`);
|
||||
DELETE FROM DLPRB_MAX WHERE `insert` = 1;
|
||||
ALTER TABLE DLPRB_MAX DROP COLUMN `insert`;
|
||||
INSERT INTO `4G_MAX`
|
||||
SELECT DLPRB_MAX.* FROM DLPRB_MAX INNER JOIN `4G_MAX_IDX`
|
||||
ON
|
||||
DLPRB_MAX.`日期` = 4G_MAX_IDX.`日期` AND
|
||||
DLPRB_MAX.CGI = 4G_MAX_IDX.`CGI` AND
|
||||
DLPRB_MAX.`下行PDSCH利用率` = 4G_MAX_IDX.`最大值`;
|
||||
|
||||
UPDATE CCE_MAX INNER JOIN 4G_MAX ON CCE_MAX.`日期` = 4G_MAX.`日期` AND CCE_MAX.CGI = 4G_MAX.CGI SET CCE_MAX.`insert` = 1;
|
||||
ALTER TABLE CCE_MAX ADD INDEX (`insert`);
|
||||
DELETE FROM CCE_MAX WHERE `insert` = 1;
|
||||
ALTER TABLE CCE_MAX DROP COLUMN `insert`;
|
||||
INSERT INTO `4G_MAX`
|
||||
SELECT CCE_MAX.* FROM CCE_MAX INNER JOIN `4G_MAX_IDX`
|
||||
ON
|
||||
CCE_MAX.`日期` = 4G_MAX_IDX.`日期` AND
|
||||
CCE_MAX.CGI = 4G_MAX_IDX.`CGI` AND
|
||||
CCE_MAX.`PDCCH资源利用率` = 4G_MAX_IDX.`最大值`;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `CCE_MAX`;
|
||||
DROP TABLE IF EXISTS `ULPRB_MAX`;
|
||||
DROP TABLE IF EXISTS `DLPRB_MAX`;
|
||||
DROP TABLE IF EXISTS `4G_MAX_IDX`;
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `日流量`(SELECT `日期`,CGI, SUM(`上下行总流量_GB`) AS `日流量` FROM 4G GROUP BY `日期`,CGI);
|
||||
ALTER TABLE `日流量` ADD INDEX (`CGI`);
|
||||
DROP TABLE IF EXISTS `4G日均流量`;
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `4G日均流量` (
|
||||
SELECT CGI, AVG(`日流量`) AS `日均流量`, COUNT(`日期`) AS `流量有效天数` FROM `日流量` WHERE `日流量` > 0 GROUP BY CGI
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `4G_结果表` (
|
||||
SELECT CGI, `基站名称`, `小区名称`,
|
||||
ROUND(AVG(`上下行总流量_GB`),6) AS `自忙时流量(GB)`,
|
||||
ROUND(AVG(`ERAB流量`),6) AS `ERAB流量`,
|
||||
ROUND(AVG(`上行流量_GB`),6) AS `上行流量(GB)`,
|
||||
ROUND(AVG(`下行流量_GB`),6) AS `下行流量(GB)`,
|
||||
ROUND(AVG(`用户面平均激活UE数`),6) AS `用户面平均激活UE数`,
|
||||
ROUND(AVG(`RRC连接建立最大用户数`),6) AS `YY-RRC连接建立最大用户数`,
|
||||
ROUND(AVG(`上行PUSCH利用率`) / 100,6) AS `上行PUSCH利用率`,
|
||||
ROUND(AVG(`下行PDSCH利用率`) / 100,6) AS `下行PDSCH利用率`,
|
||||
ROUND(AVG(`PDCCH资源利用率`) / 100,6) AS `PDCCH资源利用率`,
|
||||
ROUND(AVG(`用户面最大激活UE数`),6) AS `用户面最大激活UE数`
|
||||
FROM 4G_MAX GROUP BY CGI, `基站名称`, `小区名称`
|
||||
);
|
||||
DROP TABLE IF EXISTS `4G_MAX`;
|
||||
ALTER TABLE `4G_结果表` ADD COLUMN `日均流量(GB)` DOUBLE AFTER `小区名称`, ADD INDEX (`CGI`);
|
||||
ALTER TABLE `4G日均流量` ADD INDEX (CGI);
|
||||
UPDATE `4G_结果表` INNER JOIN `4G日均流量`
|
||||
ON `4G_结果表`.CGI=`4G日均流量`.CGI
|
||||
SET `4G_结果表`.`日均流量(GB)` = ROUND(`4G日均流量`.`日均流量`,6);
|
||||
|
||||
DROP TABLE IF EXISTS `日流量`;
|
||||
DROP TABLE IF EXISTS `4G日均流量`;
|
||||
DROP TABLE IF EXISTS `4G`;
|
||||
|
||||
|
||||
# 5G脚本
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `ULPRB_MAX_IDX`(
|
||||
SELECT
|
||||
`日期`,
|
||||
NCGI,
|
||||
MAX(`上行PRB平均利用率`) AS `上行PRB平均利用率_忙时`
|
||||
FROM 5G
|
||||
GROUP BY `日期`, NCGI
|
||||
);
|
||||
|
||||
ALTER TABLE `ULPRB_MAX_IDX`
|
||||
ADD COLUMN `日期时间` datetime,
|
||||
ADD INDEX `DC`(`日期`, `NCGI`, `上行PRB平均利用率_忙时`),
|
||||
ADD INDEX `TC`(`日期时间`, `NCGI`);
|
||||
|
||||
UPDATE `ULPRB_MAX_IDX` INNER JOIN 5G
|
||||
ON
|
||||
`ULPRB_MAX_IDX`.`日期` = 5G.`日期` AND
|
||||
`ULPRB_MAX_IDX`.NCGI=5G.NCGI AND
|
||||
`ULPRB_MAX_IDX`.`上行PRB平均利用率_忙时`=5G.`上行PRB平均利用率`
|
||||
SET
|
||||
`ULPRB_MAX_IDX`.`日期时间`=5G.`日期时间`;
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `ULPRB_MAX` (
|
||||
SELECT 5G.*
|
||||
FROM `ULPRB_MAX_IDX` LEFT JOIN 5G
|
||||
ON `ULPRB_MAX_IDX`.NCGI=5G.NCGI AND
|
||||
`ULPRB_MAX_IDX`.`日期时间`=5G.`日期时间`
|
||||
);
|
||||
|
||||
# 下行PRB MAX表
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `DLPRB_MAX_IDX`(
|
||||
SELECT
|
||||
`日期`,
|
||||
NCGI,
|
||||
MAX( `下行PRB平均利用率`) AS `下行PRB平均利用率_忙时`
|
||||
FROM 5G
|
||||
GROUP BY `日期`, NCGI
|
||||
);
|
||||
|
||||
ALTER TABLE `DLPRB_MAX_IDX`
|
||||
ADD COLUMN `日期时间` datetime,
|
||||
ADD INDEX `DC`(`日期`, `NCGI`, `下行PRB平均利用率_忙时`),
|
||||
ADD INDEX `TC`(`日期时间`, `NCGI`);
|
||||
|
||||
UPDATE `DLPRB_MAX_IDX` INNER JOIN 5G
|
||||
ON
|
||||
`DLPRB_MAX_IDX`.`日期` = 5G.`日期` AND
|
||||
`DLPRB_MAX_IDX`.NCGI=5G.NCGI AND
|
||||
`DLPRB_MAX_IDX`.`下行PRB平均利用率_忙时`=5G.`下行PRB平均利用率`
|
||||
SET
|
||||
`DLPRB_MAX_IDX`.`日期时间`=5G.`日期时间`;
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `DLPRB_MAX` (
|
||||
SELECT 5G.*
|
||||
FROM `DLPRB_MAX_IDX` LEFT JOIN 5G
|
||||
ON `DLPRB_MAX_IDX`.NCGI=5G.NCGI AND
|
||||
`DLPRB_MAX_IDX`.`日期时间`=5G.`日期时间`
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS `DLPRB_MAX_IDX`;
|
||||
ALTER TABLE `DLPRB_MAX` ADD INDEX `DC`(`日期`, `NCGI`);
|
||||
ALTER TABLE `ULPRB_MAX` ADD INDEX `DC`(`日期`, `NCGI`);
|
||||
|
||||
# 5G 统计MAX数据
|
||||
|
||||
DROP TABLE IF EXISTS `5G_Date`;
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `5G_Date` (
|
||||
SELECT `日期`, NCGI FROM 5G GROUP BY `日期`, NCGI
|
||||
);
|
||||
ALTER TABLE `5G_Date` ADD INDEX `DC`(`日期`, NCGI);
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `5G_MAX_IDX` (
|
||||
SELECT
|
||||
5G_Date.`日期`,
|
||||
5G_Date.NCGI,
|
||||
GREATEST(ULPRB_MAX.`上行PRB平均利用率`, DLPRB_MAX.`下行PRB平均利用率`) AS `最大值`
|
||||
FROM
|
||||
5G_Date
|
||||
INNER JOIN
|
||||
DLPRB_MAX
|
||||
ON
|
||||
5G_Date.`日期` = DLPRB_MAX.`日期` AND
|
||||
5G_Date.NCGI = DLPRB_MAX.NCGI
|
||||
INNER JOIN
|
||||
ULPRB_MAX
|
||||
ON
|
||||
5G_Date.`日期` = ULPRB_MAX.`日期` AND
|
||||
5G_Date.NCGI = ULPRB_MAX.NCGI
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS `5G_Date`;
|
||||
ALTER TABLE `5G_MAX_IDX` ADD INDEX `DC`(`日期`, NCGI), ADD INDEX (`最大值`);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `5G_MAX` (
|
||||
SELECT * FROM 5G LIMIT 0
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE `5G_MAX` ADD INDEX (`日期`), ADD INDEX (`NCGI`), ADD INDEX `DC` (`日期`,NCGI), ADD INDEX `idx`(`NCGI`, `CU小区配置名称`);
|
||||
|
||||
ALTER TABLE DLPRB_MAX ADD COLUMN `insert` INT NOT NULL DEFAULT 0;
|
||||
TRUNCATE `5G_MAX`;
|
||||
INSERT INTO `5G_MAX`
|
||||
SELECT ULPRB_MAX.* FROM ULPRB_MAX INNER JOIN `5G_MAX_IDX`
|
||||
ON
|
||||
ULPRB_MAX.`日期` = 5G_MAX_IDX.`日期` AND
|
||||
ULPRB_MAX.NCGI = 5G_MAX_IDX.`NCGI` AND
|
||||
ULPRB_MAX.`上行PRB平均利用率` = 5G_MAX_IDX.`最大值`;
|
||||
|
||||
UPDATE DLPRB_MAX INNER JOIN 5G_MAX ON DLPRB_MAX.`日期` = 5G_MAX.`日期` AND DLPRB_MAX.NCGI = 5G_MAX.NCGI SET DLPRB_MAX.`insert` = 1;
|
||||
ALTER TABLE DLPRB_MAX ADD INDEX (`insert`);
|
||||
DELETE FROM DLPRB_MAX WHERE `insert` = 1;
|
||||
ALTER TABLE DLPRB_MAX DROP COLUMN `insert`;
|
||||
INSERT INTO `5G_MAX`
|
||||
SELECT DLPRB_MAX.* FROM DLPRB_MAX INNER JOIN `5G_MAX_IDX`
|
||||
ON
|
||||
DLPRB_MAX.`日期` = 5G_MAX_IDX.`日期` AND
|
||||
DLPRB_MAX.NCGI = 5G_MAX_IDX.`NCGI` AND
|
||||
DLPRB_MAX.`下行PRB平均利用率` = 5G_MAX_IDX.`最大值`;
|
||||
|
||||
DROP TABLE IF EXISTS `ULPRB_MAX`;
|
||||
DROP TABLE IF EXISTS `DLPRB_MAX`;
|
||||
DROP TABLE IF EXISTS `5G_MAX_IDX`;
|
||||
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `日流量`(SELECT `日期`,NCGI, SUM(`5G上下行总流量_GB`) AS `日流量` FROM 5G GROUP BY `日期`,NCGI);
|
||||
ALTER TABLE `日流量` ADD INDEX (`NCGI`);
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS `5G日均流量` (
|
||||
SELECT NCGI, AVG(`日流量`) AS `日均流量`, COUNT(`日期`) AS `流量有效天数` FROM `日流量` WHERE `日流量` > 0 GROUP BY NCGI
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `5G_结果表` (
|
||||
SELECT NCGI, `CU小区配置名称`,
|
||||
ROUND(AVG(`5G上下行总流量_GB`),6) AS `5G上下行总流量(上行PDCP PDU数据量+下行PDCP成功发送数据量)(GB)`,
|
||||
ROUND(AVG(`5G上行流量_GB`),6) AS `5G上行流量(上行PDCP PDU数据量)(GB)`,
|
||||
ROUND(AVG(`5G下行流量_GB`),6) AS `5G下行流量(下行PDCP成功发送数据量)(GB)`,
|
||||
ROUND(AVG(`小区平均激活UE数`),6) AS `小区平均激活UE数`,
|
||||
ROUND(AVG(`RRC连接最大连接用户数`),6) AS `RRC连接最大连接用户数`,
|
||||
ROUND(AVG(`RRC连接平均连接用户数`),6) AS `RRC连接平均连接用户数`,
|
||||
ROUND(AVG(`小区最大激活UE数`),6) AS `小区最大激活UE数`,
|
||||
ROUND(AVG(`上行PRB平均利用率`) / 100,6) AS `上行PRB平均利用率`,
|
||||
ROUND(AVG(`下行PRB平均利用率`) / 100,6) AS `下行PRB平均利用率`,
|
||||
ROUND(AVG(`PDCCH信道动态CCE占用率`) / 100,6) AS `PDCCH信道CCE占用率(动态)(%)`,
|
||||
ROUND(AVG(`小区下行RLC_SDU字节数_MByte`),6) AS `小区下行RLC SDU字节数(MByte)`,
|
||||
ROUND(AVG(`小区上行RLC_SDU字节数_MByte`),6) AS `小区上行RLC SDU字节数(MByte)`,
|
||||
ROUND(AVG(`平均单Flow流量_MB`),6) AS `单Flow流量(MB)`
|
||||
FROM 5G_MAX GROUP BY NCGI, `CU小区配置名称`
|
||||
);
|
||||
|
||||
ALTER TABLE `5G_结果表` ADD COLUMN `日均流量(GB)` DOUBLE AFTER `CU小区配置名称`, ADD INDEX (`NCGI`);
|
||||
ALTER TABLE `5G日均流量` ADD INDEX (NCGI);
|
||||
UPDATE `5G_结果表` INNER JOIN `5G日均流量`
|
||||
ON `5G_结果表`.NCGI=`5G日均流量`.NCGI
|
||||
SET `5G_结果表`.`日均流量(GB)` = ROUND(`5G日均流量`.`日均流量`,6);
|
||||
|
||||
DROP TABLE IF EXISTS `日流量`;
|
||||
DROP TABLE IF EXISTS `5G日均流量`;
|
||||
DROP TABLE IF EXISTS `4G`;
|
||||
DROP TABLE IF EXISTS `5G`;
|
||||
Vendored
-220
@@ -1,220 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
REM CapacityReport 镜像构建和导出脚本 (Windows)
|
||||
REM 在有外网的 Windows 机器上运行此脚本,生成离线部署包
|
||||
@REM set http_proxy=http://127.0.0.1:7897
|
||||
@REM set https_proxy=http://127.0.0.1:7897
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set SCRIPT_DIR=%~dp0
|
||||
set "PROJECT_ROOT=%~dp0..\"
|
||||
set DIST_DIR=%~dp0
|
||||
REM 规范化路径(去掉末尾的反斜杠,如果有的话)
|
||||
if "%PROJECT_ROOT:~-1%"=="\" set "PROJECT_ROOT=%PROJECT_ROOT:~0,-1%"
|
||||
|
||||
echo ==========================================
|
||||
echo CapacityReport 离线部署包构建脚本
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
REM 清理旧的构建文件
|
||||
echo 清理旧的构建文件...
|
||||
if exist "%DIST_DIR%images\capacity-images.tar" (
|
||||
del /f /q "%DIST_DIR%images\capacity-images.tar"
|
||||
echo - 已删除旧镜像文件
|
||||
)
|
||||
if exist "%DIST_DIR%Configure.json" (
|
||||
del /f /q "%DIST_DIR%Configure.json"
|
||||
echo - 已删除旧 Configure.json
|
||||
)
|
||||
if exist "%DIST_DIR%ReportScript.sql" (
|
||||
del /f /q "%DIST_DIR%ReportScript.sql"
|
||||
echo - 已删除旧 ReportScript.sql
|
||||
)
|
||||
if exist "%DIST_DIR%mysql\conf.d\custom.cnf" (
|
||||
del /f /q "%DIST_DIR%mysql\conf.d\custom.cnf"
|
||||
echo - 已删除旧 mysql\conf.d\custom.cnf
|
||||
)
|
||||
if exist "%DIST_DIR%mysql\init\01-init-db.sql" (
|
||||
del /f /q "%DIST_DIR%mysql\init\01-init-db.sql"
|
||||
echo - 已删除旧 mysql\init\01-init-db.sql
|
||||
)
|
||||
echo 清理完成
|
||||
echo.
|
||||
|
||||
REM 检查 Docker 是否安装
|
||||
where docker >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 未检测到 Docker,请先安装 Docker Desktop
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 检查 Docker Compose 是否安装
|
||||
docker compose version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
docker-compose version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 未检测到 Docker Compose,请先安装 Docker Compose
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo 步骤 1: 拉取基础镜像...
|
||||
REM 检查 Dockerfile 中的 Python 版本
|
||||
set PYTHON_TAG=
|
||||
for /f "tokens=2 delims=:" %%a in ('findstr /R "^FROM python:" "!PROJECT_ROOT!\Dockerfile"') do (
|
||||
set "PYTHON_TAG=%%a"
|
||||
goto :found_python
|
||||
)
|
||||
:found_python
|
||||
if defined PYTHON_TAG (
|
||||
REM 去掉可能的空格
|
||||
for /f "tokens=1 delims= " %%b in ("!PYTHON_TAG!") do (
|
||||
set "PYTHON_TAG=%%b"
|
||||
)
|
||||
echo 从 Dockerfile 检测到 Python 镜像标签: !PYTHON_TAG!
|
||||
) else (
|
||||
echo 错误: 无法从 Dockerfile 中找到 Python 镜像标签
|
||||
echo 请确保 Dockerfile 中包含 "FROM python:xxx" 行
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 检查 Python 镜像是否已存在
|
||||
docker images --format "{{.Repository}}:{{.Tag}}" | findstr /C:"python:!PYTHON_TAG!" >nul
|
||||
if %errorlevel% equ 0 (
|
||||
echo Python 镜像 python:!PYTHON_TAG! 已存在,跳过拉取
|
||||
) else (
|
||||
echo 正在拉取 python:!PYTHON_TAG!...
|
||||
docker pull python:!PYTHON_TAG!
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 拉取 Python 镜像失败
|
||||
echo 提示: 如果遇到网络问题,请检查网络连接或稍后重试
|
||||
echo 镜像标签: python:!PYTHON_TAG!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
REM 检查 MySQL 镜像是否已存在
|
||||
docker images --format "{{.Repository}}:{{.Tag}}" | findstr /C:"mysql:8.0.44" >nul
|
||||
if %errorlevel% equ 0 (
|
||||
echo MySQL 镜像 mysql:8.0.44 已存在,跳过拉取
|
||||
) else (
|
||||
echo 正在拉取 mysql:8.0.44...
|
||||
docker pull mysql:8.0.44
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 拉取 MySQL 镜像失败
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 步骤 2: 构建应用镜像...
|
||||
cd /d "!PROJECT_ROOT!"
|
||||
docker build -t capacity-report-app:latest -f Dockerfile .
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 构建应用镜像失败
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 步骤 3: 标记 MySQL 镜像...
|
||||
docker tag mysql:8.0.44 capacity-mysql:8.0.44
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 标记 MySQL 镜像失败
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 步骤 4: 导出镜像到 dist 目录...
|
||||
if not exist "%DIST_DIR%images" mkdir "%DIST_DIR%images"
|
||||
echo 正在导出镜像到单个 tar 文件:
|
||||
echo - capacity-report-app:latest (应用镜像)
|
||||
echo - capacity-mysql:8.0.44 (MySQL 镜像)
|
||||
docker save capacity-report-app:latest capacity-mysql:8.0.44 -o "%DIST_DIR%images\capacity-images.tar"
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 导出镜像失败
|
||||
exit /b 1
|
||||
)
|
||||
echo 镜像导出完成: %DIST_DIR%images\capacity-images.tar
|
||||
echo 注意: 此文件包含应用镜像和 MySQL 镜像,部署时会自动加载所有镜像
|
||||
|
||||
echo.
|
||||
echo 步骤 5: 复制必要文件到 dist 目录...
|
||||
REM 创建必要的目录结构
|
||||
if not exist "%DIST_DIR%mysql\conf.d" mkdir "%DIST_DIR%mysql\conf.d"
|
||||
if not exist "%DIST_DIR%mysql\init" mkdir "%DIST_DIR%mysql\init"
|
||||
|
||||
REM 复制配置文件
|
||||
echo 正在复制配置文件...
|
||||
if exist "!PROJECT_ROOT!\Configure.json" (
|
||||
copy "!PROJECT_ROOT!\Configure.json" "!DIST_DIR!" >nul
|
||||
echo - Configure.json 已复制
|
||||
REM 更新为 Docker 环境配置
|
||||
echo 正在更新 Configure.json 为 Docker 环境配置...
|
||||
powershell -NoProfile -Command "$json = Get-Content \"%DIST_DIR%Configure.json\" -Raw -Encoding UTF8 | ConvertFrom-Json; $json.MySQL_DBInfo.host = 'capacity-mysql'; $json.MySQL_DBInfo.port = 3306; $json.MySQL_DBInfo.user = 'root'; $json.MySQL_DBInfo.passwd = 'gmcc123'; $json.MySQL_DBInfo.dbname = 'CapacityReport'; $json | ConvertTo-Json -Depth 10 | Set-Content \"%DIST_DIR%Configure.json\" -Encoding UTF8"
|
||||
if %errorlevel% equ 0 (
|
||||
echo - 已更新数据库配置: host=capacity-mysql, port=3306, user=root, passwd=gmcc123
|
||||
) else (
|
||||
echo 警告: 无法自动更新 Configure.json,请手动修改数据库配置
|
||||
)
|
||||
) else (
|
||||
echo 警告: Configure.json 不存在: !PROJECT_ROOT!\Configure.json
|
||||
)
|
||||
|
||||
if exist "!PROJECT_ROOT!\ReportScript.sql" (
|
||||
copy "!PROJECT_ROOT!\ReportScript.sql" "!DIST_DIR!" >nul
|
||||
echo - ReportScript.sql 已复制
|
||||
) else (
|
||||
echo 警告: ReportScript.sql 不存在: !PROJECT_ROOT!\ReportScript.sql
|
||||
)
|
||||
|
||||
if exist "!PROJECT_ROOT!\mysql\conf.d\custom.cnf" (
|
||||
copy "!PROJECT_ROOT!\mysql\conf.d\custom.cnf" "!DIST_DIR!mysql\conf.d\" >nul
|
||||
echo - mysql\conf.d\custom.cnf 已复制
|
||||
) else (
|
||||
echo 警告: mysql\conf.d\custom.cnf 不存在: !PROJECT_ROOT!\mysql\conf.d\custom.cnf
|
||||
)
|
||||
|
||||
if exist "!PROJECT_ROOT!\mysql\init\01-init-db.sql" (
|
||||
copy "!PROJECT_ROOT!\mysql\init\01-init-db.sql" "!DIST_DIR!mysql\init\" >nul
|
||||
echo - mysql\init\01-init-db.sql 已复制
|
||||
) else (
|
||||
echo 警告: mysql\init\01-init-db.sql 不存在: !PROJECT_ROOT!\mysql\init\01-init-db.sql
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 步骤 6: 计算文件大小...
|
||||
for %%A in ("%DIST_DIR%images\capacity-images.tar") do (
|
||||
set SIZE=%%~zA
|
||||
set /a SIZE_MB=!SIZE!/1024/1024
|
||||
echo 镜像文件大小: !SIZE_MB! MB
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 步骤 7: 清理临时目录...
|
||||
if exist "%DIST_DIR%cache" (
|
||||
rd /s /q "%DIST_DIR%cache" 2>nul
|
||||
echo - 已删除 cache 目录
|
||||
)
|
||||
if exist "%DIST_DIR%logs" (
|
||||
rd /s /q "%DIST_DIR%logs" 2>nul
|
||||
echo - 已删除 logs 目录
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo 构建完成!
|
||||
echo ==========================================
|
||||
echo.
|
||||
echo 离线部署包位置: %DIST_DIR%
|
||||
echo.
|
||||
echo 部署步骤:
|
||||
echo 1. 将整个 dist 目录传输到目标 Linux 机器
|
||||
echo 2. 在目标机器上执行: sh dist/deploy.sh
|
||||
echo.
|
||||
echo 端口映射:
|
||||
echo - 应用端口: 19081 (原 9081)
|
||||
echo - MySQL 端口: 13306 (原 3306)
|
||||
echo.
|
||||
|
||||
endlocal
|
||||
Vendored
-313
@@ -1,313 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CapacityReport 镜像构建和导出脚本 (Windows)
|
||||
在有外网的 Windows 机器上运行此脚本,生成离线部署包
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# 脚本所在目录
|
||||
SCRIPT_DIR = Path(__file__).parent.absolute()
|
||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||
DIST_DIR = SCRIPT_DIR
|
||||
|
||||
# 颜色输出(Windows 支持)
|
||||
import platform
|
||||
if platform.system() == 'Windows':
|
||||
try:
|
||||
import ctypes
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||
except:
|
||||
pass
|
||||
|
||||
class Colors:
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
RESET = '\033[0m'
|
||||
|
||||
def print_step(msg):
|
||||
print(f"\n{Colors.GREEN}{msg}{Colors.RESET}")
|
||||
|
||||
def print_info(msg):
|
||||
print(f" {msg}")
|
||||
|
||||
def print_warning(msg):
|
||||
print(f"{Colors.YELLOW}警告: {msg}{Colors.RESET}")
|
||||
|
||||
def print_error(msg):
|
||||
print(f"{Colors.RED}错误: {msg}{Colors.RESET}")
|
||||
|
||||
def run_cmd(cmd, check=True, capture_output=False):
|
||||
"""执行命令"""
|
||||
if isinstance(cmd, str):
|
||||
cmd = cmd.split()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
check=check,
|
||||
capture_output=capture_output,
|
||||
text=True
|
||||
)
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
if check:
|
||||
print_error(f"命令执行失败: {' '.join(cmd)}")
|
||||
sys.exit(1)
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
print_error(f"命令未找到: {cmd[0]}")
|
||||
sys.exit(1)
|
||||
|
||||
def check_command(cmd):
|
||||
"""检查命令是否存在"""
|
||||
try:
|
||||
if cmd == "docker":
|
||||
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
||||
elif cmd == "docker-compose":
|
||||
subprocess.run([cmd, "version"], capture_output=True, check=True)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def get_python_tag_from_dockerfile():
|
||||
"""从 Dockerfile 中提取 Python 镜像标签"""
|
||||
dockerfile_path = PROJECT_ROOT / "Dockerfile"
|
||||
if not dockerfile_path.exists():
|
||||
print_error(f"Dockerfile 不存在: {dockerfile_path}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(dockerfile_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
match = re.match(r'^FROM\s+python:(.+?)(?:\s|$)', line, re.IGNORECASE)
|
||||
if match:
|
||||
tag = match.group(1).strip()
|
||||
return tag
|
||||
|
||||
print_error("无法从 Dockerfile 中找到 Python 镜像标签")
|
||||
print_error("请确保 Dockerfile 中包含 'FROM python:xxx' 行")
|
||||
sys.exit(1)
|
||||
|
||||
def image_exists(image_name):
|
||||
"""检查镜像是否已存在"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return image_name in result.stdout
|
||||
except:
|
||||
return False
|
||||
|
||||
def format_size(size_bytes):
|
||||
"""格式化文件大小"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size_bytes < 1024.0:
|
||||
return f"{size_bytes:.1f} {unit}"
|
||||
size_bytes /= 1024.0
|
||||
return f"{size_bytes:.1f} TB"
|
||||
|
||||
def main():
|
||||
print("=" * 50)
|
||||
print("CapacityReport 离线部署包构建脚本")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
# 清理旧的构建文件
|
||||
print_step("清理旧的构建文件...")
|
||||
files_to_clean = [
|
||||
DIST_DIR / "images" / "capacity-images.tar",
|
||||
DIST_DIR / "Configure.json",
|
||||
DIST_DIR / "ReportScript.sql",
|
||||
DIST_DIR / "mysql" / "conf.d" / "custom.cnf",
|
||||
DIST_DIR / "mysql" / "init" / "01-init-db.sql",
|
||||
]
|
||||
|
||||
for file_path in files_to_clean:
|
||||
if file_path.exists():
|
||||
if file_path.is_file():
|
||||
file_path.unlink()
|
||||
print_info(f"已删除旧 {file_path.name}")
|
||||
elif file_path.is_dir():
|
||||
shutil.rmtree(file_path)
|
||||
print_info(f"已删除旧 {file_path.name}")
|
||||
|
||||
print_info("清理完成")
|
||||
|
||||
# 检查 Docker
|
||||
print_step("检查 Docker 环境...")
|
||||
if not check_command("docker"):
|
||||
print_error("未检测到 Docker,请先安装 Docker Desktop")
|
||||
sys.exit(1)
|
||||
print_info("Docker: OK")
|
||||
|
||||
# 检查 Docker Compose
|
||||
docker_compose_cmd = None
|
||||
try:
|
||||
subprocess.run(["docker", "compose", "version"], capture_output=True, check=True)
|
||||
docker_compose_cmd = "docker compose"
|
||||
print_info("Docker Compose: OK (docker compose)")
|
||||
except:
|
||||
if check_command("docker-compose"):
|
||||
docker_compose_cmd = "docker-compose"
|
||||
print_info("Docker Compose: OK (docker-compose)")
|
||||
else:
|
||||
print_error("未检测到 Docker Compose,请先安装 Docker Compose")
|
||||
sys.exit(1)
|
||||
|
||||
# 步骤 1: 拉取基础镜像
|
||||
print_step("步骤 1: 拉取基础镜像...")
|
||||
|
||||
# 获取 Python 版本
|
||||
python_tag = get_python_tag_from_dockerfile()
|
||||
python_image = f"python:{python_tag}"
|
||||
print_info(f"从 Dockerfile 检测到 Python 镜像标签: {python_tag}")
|
||||
|
||||
# 检查并拉取 Python 镜像
|
||||
if image_exists(python_image):
|
||||
print_info(f"Python 镜像 {python_image} 已存在,跳过拉取")
|
||||
else:
|
||||
print_info(f"正在拉取 {python_image}...")
|
||||
run_cmd(["docker", "pull", python_image])
|
||||
print_info(f"Python 镜像拉取完成")
|
||||
|
||||
# 检查并拉取 MySQL 镜像
|
||||
mysql_image = "mysql:8.0.44"
|
||||
if image_exists(mysql_image):
|
||||
print_info(f"MySQL 镜像 {mysql_image} 已存在,跳过拉取")
|
||||
else:
|
||||
print_info(f"正在拉取 {mysql_image}...")
|
||||
run_cmd(["docker", "pull", mysql_image])
|
||||
print_info(f"MySQL 镜像拉取完成")
|
||||
|
||||
# 步骤 2: 构建应用镜像
|
||||
print_step("步骤 2: 构建应用镜像...")
|
||||
os.chdir(PROJECT_ROOT)
|
||||
run_cmd(["docker", "build", "-t", "capacity-report-app:latest", "-f", "Dockerfile", "."])
|
||||
print_info("应用镜像构建完成")
|
||||
|
||||
# 步骤 3: 标记 MySQL 镜像
|
||||
print_step("步骤 3: 标记 MySQL 镜像...")
|
||||
run_cmd(["docker", "tag", mysql_image, "capacity-mysql:8.0.44"])
|
||||
print_info("MySQL 镜像标记完成")
|
||||
|
||||
# 步骤 4: 导出镜像
|
||||
print_step("步骤 4: 导出镜像到 dist 目录...")
|
||||
images_dir = DIST_DIR / "images"
|
||||
images_dir.mkdir(exist_ok=True)
|
||||
|
||||
tar_file = images_dir / "capacity-images.tar"
|
||||
print_info("正在导出镜像到单个 tar 文件:")
|
||||
print_info(" - capacity-report-app:latest (应用镜像)")
|
||||
print_info(" - capacity-mysql:8.0.44 (MySQL 镜像)")
|
||||
|
||||
run_cmd([
|
||||
"docker", "save",
|
||||
"capacity-report-app:latest",
|
||||
"capacity-mysql:8.0.44",
|
||||
"-o", str(tar_file)
|
||||
])
|
||||
|
||||
print_info(f"镜像导出完成: {tar_file}")
|
||||
print_info("注意: 此文件包含应用镜像和 MySQL 镜像,部署时会自动加载所有镜像")
|
||||
|
||||
# 步骤 5: 复制必要文件
|
||||
print_step("步骤 5: 复制必要文件到 dist 目录...")
|
||||
|
||||
# 创建必要的目录结构
|
||||
(DIST_DIR / "mysql" / "conf.d").mkdir(parents=True, exist_ok=True)
|
||||
(DIST_DIR / "mysql" / "init").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 复制配置文件
|
||||
print_info("正在复制配置文件...")
|
||||
|
||||
files_to_copy = [
|
||||
(PROJECT_ROOT / "Configure.json", DIST_DIR / "Configure.json"),
|
||||
(PROJECT_ROOT / "ReportScript.sql", DIST_DIR / "ReportScript.sql"),
|
||||
(PROJECT_ROOT / "mysql" / "conf.d" / "custom.cnf", DIST_DIR / "mysql" / "conf.d" / "custom.cnf"),
|
||||
(PROJECT_ROOT / "mysql" / "init" / "01-init-db.sql", DIST_DIR / "mysql" / "init" / "01-init-db.sql"),
|
||||
]
|
||||
|
||||
for src, dst in files_to_copy:
|
||||
if src.exists():
|
||||
shutil.copy2(src, dst)
|
||||
print_info(f"{src.name} 已复制")
|
||||
else:
|
||||
print_warning(f"{src.name} 不存在: {src}")
|
||||
|
||||
# 更新 Configure.json 为 Docker 环境配置
|
||||
config_file = DIST_DIR / "Configure.json"
|
||||
if config_file.exists():
|
||||
print_info("正在更新 Configure.json 为 Docker 环境配置...")
|
||||
try:
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
config['MySQL_DBInfo'] = {
|
||||
'host': 'capacity-mysql',
|
||||
'port': 3306,
|
||||
'user': 'root',
|
||||
'passwd': 'gmcc123',
|
||||
'dbname': 'CapacityReport'
|
||||
}
|
||||
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print_info("已更新数据库配置: host=capacity-mysql, port=3306, user=root, passwd=gmcc123")
|
||||
except Exception as e:
|
||||
print_warning(f"无法自动更新 Configure.json: {e}")
|
||||
print_warning("请手动修改数据库配置")
|
||||
|
||||
# 步骤 6: 计算文件大小
|
||||
print_step("步骤 6: 计算文件大小...")
|
||||
if tar_file.exists():
|
||||
size = tar_file.stat().st_size
|
||||
print_info(f"镜像文件大小: {format_size(size)}")
|
||||
|
||||
# 步骤 7: 清理临时目录
|
||||
print_step("步骤 7: 清理临时目录...")
|
||||
temp_dirs = [DIST_DIR / "cache", DIST_DIR / "logs"]
|
||||
for temp_dir in temp_dirs:
|
||||
if temp_dir.exists():
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
print_info(f"已删除 {temp_dir.name} 目录")
|
||||
|
||||
# 完成
|
||||
print()
|
||||
print("=" * 50)
|
||||
print("构建完成!")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print(f"离线部署包位置: {DIST_DIR}")
|
||||
print()
|
||||
print("部署步骤:")
|
||||
print("1. 将整个 dist 目录传输到目标 Linux 机器")
|
||||
print("2. 在目标机器上执行: sh dist/deploy.sh")
|
||||
print()
|
||||
print("端口映射:")
|
||||
print(" - 应用端口: 19081 (原 9081)")
|
||||
print(" - MySQL 端口: 13306 (原 3306)")
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n构建已取消")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print_error(f"构建失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Vendored
-240
@@ -1,240 +0,0 @@
|
||||
#!/bin/bash
|
||||
# CapacityReport Deployment Script
|
||||
# Run this script on offline machine to import images and start services
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=========================================="
|
||||
echo "CapacityReport Deployment"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Set permissions for dist directory and make scripts executable
|
||||
echo "Setting permissions..."
|
||||
chmod -R 0777 "$SCRIPT_DIR" 2>/dev/null || true
|
||||
find "$SCRIPT_DIR" -type f \( -name "*.sh" -o -name "*.py" \) -exec chmod +x {} \; 2>/dev/null || true
|
||||
echo " Permissions set"
|
||||
|
||||
echo ""
|
||||
|
||||
# Check Docker
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Error: Docker not found, please install Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Docker Compose
|
||||
DOCKER_COMPOSE_CMD=""
|
||||
if docker compose version &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
DOCKER_COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
echo "Error: Docker Compose not found, please install Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check image file
|
||||
if [ ! -f "images/capacity-images.tar" ]; then
|
||||
echo "Error: Image file not found: images/capacity-images.tar"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Step 1: Checking ports..."
|
||||
# 检查端口是否被占用
|
||||
check_port() {
|
||||
local port=$1
|
||||
local name=$2
|
||||
local in_use=false
|
||||
|
||||
# 尝试多种方法检查端口
|
||||
if command -v lsof &> /dev/null; then
|
||||
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
|
||||
in_use=true
|
||||
fi
|
||||
elif command -v netstat &> /dev/null; then
|
||||
if netstat -tuln 2>/dev/null | grep -q ":$port "; then
|
||||
in_use=true
|
||||
fi
|
||||
elif command -v ss &> /dev/null; then
|
||||
if ss -tuln 2>/dev/null | grep -q ":$port "; then
|
||||
in_use=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$in_use" = true ]; then
|
||||
echo "Warning: Port $port ($name) is in use"
|
||||
read -p "Continue? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_port 19081 "App"
|
||||
check_port 13306 "MySQL"
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Loading images..."
|
||||
docker load -i images/capacity-images.tar >/dev/null 2>&1
|
||||
|
||||
echo ""
|
||||
echo "Step 3: Verifying images..."
|
||||
if docker images | grep -q "capacity-report-app.*latest"; then
|
||||
echo " App image: OK"
|
||||
else
|
||||
echo " App image: FAILED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if docker images | grep -q "capacity-mysql.*8.0.44"; then
|
||||
echo " MySQL image: OK"
|
||||
else
|
||||
echo " MySQL image: FAILED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 4: Creating directories..."
|
||||
mkdir -p cache logs
|
||||
chmod 777 cache logs 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "Step 5: Checking config..."
|
||||
if [ ! -f "Configure.json" ]; then
|
||||
echo " Creating default Configure.json..."
|
||||
cat > Configure.json << 'EOF'
|
||||
{
|
||||
"MySQL_DBInfo": {
|
||||
"host": "capacity-mysql",
|
||||
"port": 3306,
|
||||
"user": "root",
|
||||
"passwd": "gmcc123",
|
||||
"dbname": "CapacityReport"
|
||||
},
|
||||
"ExtractField": []
|
||||
}
|
||||
EOF
|
||||
else
|
||||
# Update config for Docker environment
|
||||
if command -v python3 &> /dev/null || command -v python &> /dev/null; then
|
||||
(command -v python3 >/dev/null && python3 || python) << 'PYEOF'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
with open('Configure.json', 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
db_info = config.get('MySQL_DBInfo', {})
|
||||
needs_update = False
|
||||
|
||||
if db_info.get('host') != 'capacity-mysql':
|
||||
db_info['host'] = 'capacity-mysql'
|
||||
needs_update = True
|
||||
|
||||
if db_info.get('port') != 3306:
|
||||
db_info['port'] = 3306
|
||||
needs_update = True
|
||||
|
||||
if db_info.get('user') != 'root':
|
||||
db_info['user'] = 'root'
|
||||
needs_update = True
|
||||
|
||||
if db_info.get('passwd') != 'gmcc123':
|
||||
db_info['passwd'] = 'gmcc123'
|
||||
needs_update = True
|
||||
|
||||
if db_info.get('dbname') != 'CapacityReport':
|
||||
db_info['dbname'] = 'CapacityReport'
|
||||
needs_update = True
|
||||
|
||||
if needs_update:
|
||||
with open('Configure.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
print(" Updated Configure.json")
|
||||
except:
|
||||
pass
|
||||
PYEOF
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "ReportScript.sql" ]; then
|
||||
touch ReportScript.sql
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 6: Starting services..."
|
||||
$DOCKER_COMPOSE_CMD down 2>/dev/null || true
|
||||
$DOCKER_COMPOSE_CMD up -d >/dev/null 2>&1
|
||||
|
||||
echo ""
|
||||
echo "Step 7: Waiting for services..."
|
||||
for i in {1..30}; do
|
||||
if docker exec capacity-mysql mysqladmin ping -h localhost -u root -pgmcc123 --silent 2>/dev/null; then
|
||||
echo " MySQL: Ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo " MySQL: Timeout"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if command -v curl &> /dev/null; then
|
||||
for i in {1..30}; do
|
||||
if curl -f http://localhost:19081/health >/dev/null 2>&1; then
|
||||
echo " App: Ready"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo " App: Timeout"
|
||||
echo " Check logs: $DOCKER_COMPOSE_CMD logs capacity-app"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
else
|
||||
for i in {1..30}; do
|
||||
if docker ps --filter "name=capacity-report-app" --filter "status=running" --format "{{.Names}}" | grep -q "capacity-report-app"; then
|
||||
if docker inspect capacity-report-app --format='{{.State.Health.Status}}' 2>/dev/null | grep -q "healthy"; then
|
||||
echo " App: Ready"
|
||||
break
|
||||
elif [ $i -gt 10 ]; then
|
||||
echo " App: Running"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo " App: Timeout"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Deployment completed!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Access: http://localhost:19081"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " Logs: $DOCKER_COMPOSE_CMD logs -f"
|
||||
echo " Stop: $DOCKER_COMPOSE_CMD down"
|
||||
echo " Restart: $DOCKER_COMPOSE_CMD restart"
|
||||
echo " Status: $DOCKER_COMPOSE_CMD ps"
|
||||
echo ""
|
||||
echo "Database:"
|
||||
echo " Host: localhost"
|
||||
echo " Port: 13306"
|
||||
echo " User: root"
|
||||
echo " Password: gmcc123"
|
||||
echo " Database: CapacityReport"
|
||||
echo ""
|
||||
@@ -1,98 +0,0 @@
|
||||
# CapacityReport Docker 编排文件
|
||||
# 用于内网离线部署
|
||||
#
|
||||
# 使用方法:
|
||||
# docker-compose up -d # 启动所有服务
|
||||
# docker-compose down # 停止所有服务
|
||||
# docker-compose logs -f app # 查看应用日志
|
||||
# docker-compose logs -f mysql # 查看 MySQL 日志
|
||||
#
|
||||
# 内网部署准备(在有外网的机器上):
|
||||
# docker pull python:3.11-slim
|
||||
# docker pull mysql:8.0 # 或指定版本: mysql:8.0.44
|
||||
# docker save -o capacity-images.tar python:3.11-slim mysql:8.0
|
||||
# # 将 capacity-images.tar 传到目标机器后执行:
|
||||
# docker load -i capacity-images.tar
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# 容量报表应用
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: capacity-report
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9081:9081"
|
||||
volumes:
|
||||
# 持久化数据
|
||||
- ./cache:/app/cache
|
||||
- ./Configure.json:/app/Configure.json
|
||||
- ./ReportScript.sql:/app/ReportScript.sql
|
||||
# 日志目录
|
||||
- ./logs:/var/log/supervisor
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- PYTHONUNBUFFERED=1
|
||||
- SUPERVISOR_ENABLED=1
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- capacity-network
|
||||
|
||||
# MySQL 数据库
|
||||
# 版本说明:
|
||||
# - mysql:8.0 # 自动使用最新的 8.0.x 版本(推荐,当前最新为 8.0.44)
|
||||
# - mysql:8.0.44 # 固定版本,适合生产环境(2025-11-04 发布)
|
||||
# - mysql:8.0.43 # 其他稳定版本
|
||||
# 注意:MySQL 9.5 目前没有官方 Docker 镜像,建议使用 8.0 系列
|
||||
# LOAD DATA INFILE 支持:MySQL 8.0 完全支持,已通过配置启用
|
||||
mysql:
|
||||
image: mysql:8.0.44
|
||||
container_name: capacity-mysql
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
# 数据持久化
|
||||
- mysql-data:/var/lib/mysql
|
||||
# 自定义配置(启用 LOAD DATA INFILE)
|
||||
- ./mysql/conf.d:/etc/mysql/conf.d:ro
|
||||
# 初始化脚本(仅在首次启动时执行)
|
||||
- ./mysql/init:/docker-entrypoint-initdb.d:ro
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- MYSQL_ROOT_PASSWORD=gmcc123
|
||||
# MYSQL_DATABASE: 仅在首次初始化时创建数据库
|
||||
# 如果数据目录已存在,此变量会被忽略(安全)
|
||||
# 初始化脚本 mysql/init/01-init-db.sql 会确保数据库以 utf8mb4 字符集创建
|
||||
- MYSQL_DATABASE=CapacityReport
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
- --local-infile=ON
|
||||
- --secure-file-priv=
|
||||
- --max_allowed_packet=256M
|
||||
- --innodb_buffer_pool_size=512M
|
||||
- --innodb_log_file_size=128M
|
||||
- --innodb_flush_log_at_trx_commit=2
|
||||
- --sync_binlog=0
|
||||
networks:
|
||||
- capacity-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pgmcc123"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
capacity-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
driver: local
|
||||
@@ -1,35 +0,0 @@
|
||||
# MySQL 自定义配置 - CapacityReport
|
||||
# 启用 LOAD DATA LOCAL INFILE 支持
|
||||
|
||||
[mysqld]
|
||||
# 字符集
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
# 启用 LOAD DATA LOCAL INFILE
|
||||
local_infile = ON
|
||||
secure_file_priv = ""
|
||||
|
||||
# 性能优化
|
||||
max_allowed_packet = 256M
|
||||
innodb_buffer_pool_size = 512M
|
||||
innodb_log_file_size = 128M
|
||||
innodb_flush_log_at_trx_commit = 2
|
||||
sync_binlog = 0
|
||||
|
||||
# 连接设置
|
||||
max_connections = 200
|
||||
wait_timeout = 28800
|
||||
interactive_timeout = 28800
|
||||
|
||||
# 日志(可选,生产环境建议注释掉)
|
||||
# general_log = 1
|
||||
# general_log_file = /var/lib/mysql/general.log
|
||||
|
||||
[mysql]
|
||||
# 客户端设置
|
||||
local_infile = ON
|
||||
default-character-set = utf8mb4
|
||||
|
||||
[client]
|
||||
default-character-set = utf8mb4
|
||||
@@ -1,13 +0,0 @@
|
||||
-- MySQL 初始化脚本 - 确保数据库以正确的字符集创建
|
||||
-- 此脚本仅在首次初始化时执行(数据目录为空时)
|
||||
|
||||
-- 如果数据库不存在,则创建(使用 utf8mb4 字符集)
|
||||
CREATE DATABASE IF NOT EXISTS CapacityReport
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 切换到数据库
|
||||
USE CapacityReport;
|
||||
|
||||
-- 输出确认信息
|
||||
SELECT 'CapacityReport database initialized with utf8mb4 charset' AS status;
|
||||
@@ -855,7 +855,7 @@ body {
|
||||
|
||||
/* ==================== 处理区域 ==================== */
|
||||
.process-section {
|
||||
margin-top: 24px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.process-header {
|
||||
@@ -892,8 +892,7 @@ body {
|
||||
|
||||
.log-container {
|
||||
height: calc(100vh - 280px);
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
min-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: var(--td-bg-color-page);
|
||||
border-radius: var(--td-radius-default);
|
||||
|
||||
+32
-26
@@ -61,6 +61,9 @@
|
||||
<h1>数据上传与处理</h1>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn btn-sm btn-outline" id="newProcess" style="display: none;" title="新建任务">
|
||||
<span>🔄</span> 新建任务
|
||||
</button>
|
||||
<button class="theme-toggle" id="themeToggle" title="切换主题">
|
||||
<span>🌓</span>
|
||||
</button>
|
||||
@@ -153,14 +156,6 @@
|
||||
<div class="log-content" id="logContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer process-actions" id="processActions" style="display: none;">
|
||||
<button class="btn btn-success" id="downloadResult">
|
||||
<span>📥</span> 下载结果
|
||||
</button>
|
||||
<button class="btn btn-outline" id="newProcess">
|
||||
<span>🔄</span> 新建任务
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,6 +308,9 @@
|
||||
<button class="btn btn-sm btn-outline" id="formatScript" title="格式化 SQL">
|
||||
✨ 格式化
|
||||
</button>
|
||||
<button class="btn btn-sm btn-success" id="runScript" title="运行脚本">
|
||||
▶️ 运行脚本
|
||||
</button>
|
||||
<button class="theme-toggle" id="themeToggle" title="切换主题">
|
||||
<span>🌓</span>
|
||||
</button>
|
||||
@@ -332,6 +330,32 @@
|
||||
<span id="editorCursor">行 1, 列 1</span>
|
||||
<span id="editorStatus">就绪</span>
|
||||
</div>
|
||||
|
||||
<!-- 脚本执行状态 -->
|
||||
<div class="process-section" id="scriptProcessSection" style="display: none; margin-top: 20px;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-header-left">
|
||||
<span class="card-title">脚本执行进度</span>
|
||||
<span class="process-status processing" id="scriptProcessStatus">执行中...</span>
|
||||
</div>
|
||||
<div class="card-header-right">
|
||||
<label class="auto-refresh-toggle">
|
||||
<input type="checkbox" id="scriptAutoRefreshLog" checked>
|
||||
<span>自动刷新</span>
|
||||
</label>
|
||||
<button class="btn btn-sm btn-outline" id="scriptManualRefreshLog" title="手动刷新日志">
|
||||
<span>🔄</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="log-container" id="scriptLogContainer">
|
||||
<div class="log-content" id="scriptLogContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -483,24 +507,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下载选择对话框 -->
|
||||
<div class="modal" id="downloadModal">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-content">
|
||||
<h3>选择下载的数据表</h3>
|
||||
<div class="download-options">
|
||||
<button class="btn btn-lg btn-primary download-option" data-table="4G_结果表">
|
||||
<span>📊</span> 4G 结果表
|
||||
</button>
|
||||
<button class="btn btn-lg btn-primary download-option" data-table="5G_结果表">
|
||||
<span>📊</span> 5G 结果表
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-outline" id="downloadCancel">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
+182
-54
@@ -331,56 +331,6 @@ class FileUploader {
|
||||
// 不在页面加载时自动检查任务,只在用户导航到上传页面时检查
|
||||
// 避免无意义的轮询请求
|
||||
|
||||
// 下载结果按钮
|
||||
$('#downloadResult').addEventListener('click', () => {
|
||||
$('#downloadModal').classList.add('active');
|
||||
});
|
||||
|
||||
// 下载选项
|
||||
$$('.download-option').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const table = btn.dataset.table;
|
||||
try {
|
||||
const response = await fetch('/api/download', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
table_name: table,
|
||||
format: 'xlsx'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('下载失败');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${table}_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
showToast(`下载失败: ${error.message}`, 'error');
|
||||
}
|
||||
$('#downloadModal').classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
$('#downloadCancel').addEventListener('click', () => {
|
||||
$('#downloadModal').classList.remove('active');
|
||||
});
|
||||
|
||||
// 关闭模态框点击背景
|
||||
$('#downloadModal .modal-backdrop').addEventListener('click', () => {
|
||||
$('#downloadModal').classList.remove('active');
|
||||
});
|
||||
|
||||
// 新建任务
|
||||
$('#newProcess').addEventListener('click', () => {
|
||||
this.reset();
|
||||
@@ -555,7 +505,7 @@ class FileUploader {
|
||||
$('#uploadZone').style.display = 'none';
|
||||
$('#fileList').style.display = 'none';
|
||||
$('#processSection').style.display = 'block';
|
||||
$('#processActions').style.display = 'none';
|
||||
$('#newProcess').style.display = 'none';
|
||||
$('#processStatus').className = 'process-status processing';
|
||||
$('#processStatus').textContent = statusCheck.stage === 'uploading' ? '上传中...' : '处理中...';
|
||||
if (statusCheck.logs && statusCheck.logs.length > 0) {
|
||||
@@ -834,7 +784,7 @@ class FileUploader {
|
||||
if (status.status === 'completed') {
|
||||
statusEl.textContent = '处理完成';
|
||||
statusEl.className = 'process-status completed';
|
||||
$('#processActions').style.display = 'flex';
|
||||
$('#newProcess').style.display = 'inline-flex';
|
||||
// 任务完成,立即停止轮询
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
@@ -847,7 +797,7 @@ class FileUploader {
|
||||
} else if (status.status === 'failed') {
|
||||
statusEl.textContent = '处理失败';
|
||||
statusEl.className = 'process-status failed';
|
||||
$('#processActions').style.display = 'flex';
|
||||
$('#newProcess').style.display = 'inline-flex';
|
||||
// 任务失败,立即停止轮询
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
@@ -954,7 +904,7 @@ class FileUploader {
|
||||
$('#uploadStats').style.display = 'none';
|
||||
this.taskId = result.task_id;
|
||||
$('#processSection').style.display = 'block';
|
||||
$('#processActions').style.display = 'none';
|
||||
$('#newProcess').style.display = 'none';
|
||||
$('#processStatus').className = 'process-status processing';
|
||||
|
||||
// 根据阶段显示不同状态
|
||||
@@ -2295,6 +2245,8 @@ class ScriptEditor {
|
||||
this.monacoReady = false;
|
||||
this.monacoLoading = false;
|
||||
this.initAttempted = false;
|
||||
this.taskId = null;
|
||||
this.scriptPollInterval = null;
|
||||
|
||||
// 只绑定事件,不立即初始化编辑器
|
||||
this.bindEvents();
|
||||
@@ -2664,6 +2616,179 @@ class ScriptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
async runScript() {
|
||||
// 检查是否有未保存的修改
|
||||
if (this.isModified) {
|
||||
const confirmed = await showConfirm(
|
||||
'运行脚本',
|
||||
'脚本有未保存的修改,是否先保存再运行?\n\n点击"确定"将先保存脚本,点击"取消"将直接运行当前内容。'
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
await this.saveScript();
|
||||
// 等待保存完成
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用执行脚本 API
|
||||
const result = await api('/script/execute', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.taskId = result.task_id;
|
||||
showToast('脚本执行任务已启动', 'success');
|
||||
|
||||
// 跳转到上传页面显示处理进度
|
||||
const navItem = document.querySelector('[data-page="upload"]');
|
||||
if (navItem) {
|
||||
navItem.click();
|
||||
}
|
||||
|
||||
// 等待页面切换完成后再设置处理进度
|
||||
setTimeout(() => {
|
||||
// 使用上传页面的处理逻辑
|
||||
if (window.fileUploader) {
|
||||
window.fileUploader.taskId = this.taskId;
|
||||
|
||||
// 显示处理进度区域(使用上传页面的处理区域)
|
||||
const processSection = document.getElementById('processSection');
|
||||
if (processSection) {
|
||||
processSection.style.display = 'block';
|
||||
const processStatus = document.getElementById('processStatus');
|
||||
if (processStatus) {
|
||||
processStatus.className = 'process-status processing';
|
||||
processStatus.textContent = '执行中...';
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
const logContent = document.getElementById('logContent');
|
||||
if (logContent) {
|
||||
logContent.innerHTML = '';
|
||||
}
|
||||
|
||||
// 隐藏上传区域和文件列表
|
||||
const uploadZone = document.getElementById('uploadZone');
|
||||
const fileList = document.getElementById('fileList');
|
||||
if (uploadZone) uploadZone.style.display = 'none';
|
||||
if (fileList) fileList.style.display = 'none';
|
||||
|
||||
// 使用上传页面的轮询逻辑
|
||||
if (window.fileUploader.pollStatus) {
|
||||
window.fileUploader.pollStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
showToast('启动脚本执行失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message.includes('已有任务在运行')) {
|
||||
showToast('已有任务在运行,请等待完成', 'warning');
|
||||
} else {
|
||||
showToast(`启动脚本执行失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pollScriptStatus() {
|
||||
if (this.scriptPollInterval) {
|
||||
clearInterval(this.scriptPollInterval);
|
||||
}
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
// 检查自动刷新开关
|
||||
const autoRefreshCheckbox = document.getElementById('scriptAutoRefreshLog');
|
||||
if (autoRefreshCheckbox && !autoRefreshCheckbox.checked) {
|
||||
if (this.scriptPollInterval) {
|
||||
clearInterval(this.scriptPollInterval);
|
||||
this.scriptPollInterval = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查全局任务状态
|
||||
const statusCheck = await api('/task/status');
|
||||
|
||||
if (!statusCheck.has_active) {
|
||||
// 任务已完成,停止轮询
|
||||
if (this.scriptPollInterval) {
|
||||
clearInterval(this.scriptPollInterval);
|
||||
this.scriptPollInterval = null;
|
||||
}
|
||||
|
||||
// 获取最终状态
|
||||
if (this.taskId) {
|
||||
const finalStatus = await api('/process/status', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task_id: this.taskId })
|
||||
});
|
||||
|
||||
const processStatus = document.getElementById('scriptProcessStatus');
|
||||
if (processStatus) {
|
||||
if (finalStatus.status === 'completed') {
|
||||
processStatus.className = 'process-status completed';
|
||||
processStatus.textContent = '执行完成';
|
||||
showToast('脚本执行完成', 'success');
|
||||
} else {
|
||||
processStatus.className = 'process-status failed';
|
||||
processStatus.textContent = '执行失败';
|
||||
showToast('脚本执行失败', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.taskId = null;
|
||||
} else if (statusCheck.stage === 'processing' && this.taskId) {
|
||||
// 获取处理状态和日志
|
||||
const status = await api('/process/status', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task_id: this.taskId })
|
||||
});
|
||||
|
||||
// 更新日志显示
|
||||
const logContent = document.getElementById('scriptLogContent');
|
||||
if (logContent && status.logs) {
|
||||
logContent.innerHTML = status.logs.map(log => {
|
||||
let level = 'info';
|
||||
if (log.includes('[ERROR]')) level = 'error';
|
||||
else if (log.includes('[WARN]')) level = 'warn';
|
||||
else if (log.includes('[SUCCESS]')) level = 'success';
|
||||
return `<div class="log-line ${level}">${log}</div>`;
|
||||
}).join('');
|
||||
|
||||
// 滚动到底部
|
||||
logContent.scrollTop = logContent.scrollHeight;
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
const processStatus = document.getElementById('scriptProcessStatus');
|
||||
if (processStatus) {
|
||||
if (status.status === 'completed') {
|
||||
processStatus.className = 'process-status completed';
|
||||
processStatus.textContent = '执行完成';
|
||||
} else if (status.status === 'failed') {
|
||||
processStatus.className = 'process-status failed';
|
||||
processStatus.textContent = '执行失败';
|
||||
} else {
|
||||
processStatus.className = 'process-status processing';
|
||||
processStatus.textContent = '执行中...';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('轮询脚本执行状态失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
this.scriptPollInterval = setInterval(poll, 1000);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 保存按钮
|
||||
document.addEventListener('click', (e) => {
|
||||
@@ -2673,6 +2798,9 @@ class ScriptEditor {
|
||||
if (e.target.closest('#formatScript')) {
|
||||
this.formatScript();
|
||||
}
|
||||
if (e.target.closest('#runScript')) {
|
||||
this.runScript();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听主题变化
|
||||
|
||||
Reference in New Issue
Block a user