feat: v2.0.0 done

This commit is contained in:
2026-01-15 15:59:38 +08:00
parent 5b50c9750f
commit 9fa12cb7f5
26 changed files with 1498 additions and 2037 deletions
+2 -9
View File
@@ -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
View File
@@ -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,
-178
View File
@@ -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/
```
+106 -102
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+3
View File
@@ -33,6 +33,9 @@ Dockerfile*
# MySQL data
mysql/
# Build directory (exclude build files from image)
build/
# Temporary files
*.tmp
*.temp
View File
+58
View File
@@ -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
View File
@@ -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)
-2
View File
@@ -1,2 +0,0 @@
# 此文件用于确保 dist 目录被 Git 跟踪
# dist 目录用于存放离线部署包
-243
View File
@@ -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))"
]
}
]
}
-471
View File
@@ -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`;
-220
View File
@@ -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
-313
View File
@@ -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)
-240
View File
@@ -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 ""
-98
View File
@@ -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
-35
View File
@@ -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
-13
View File
@@ -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;
+2 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
});
// 监听主题变化