diff --git a/.gitignore b/.gitignore index cd20011..7f993b8 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ \ No newline at end of file +# build 临时目录 +build/temp/ \ No newline at end of file diff --git a/Configure.json b/Configure.json index 835f555..33e4d76 100644 --- a/Configure.json +++ b/Configure.json @@ -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, diff --git a/DEPLOY.md b/DEPLOY.md deleted file mode 100644 index db06dca..0000000 --- a/DEPLOY.md +++ /dev/null @@ -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/ -``` diff --git a/README.md b/README.md index 9e8c81a..f3e13e3 100644 --- a/README.md +++ b/README.md @@ -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` - 健康检查 diff --git a/ReportScript.sql b/ReportScript.sql index 5a1f6c6..4f0e70d 100644 --- a/ReportScript.sql +++ b/ReportScript.sql @@ -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)` diff --git a/app/main.py b/app/main.py index 1f18828..9d21901 100644 --- a/app/main.py +++ b/app/main.py @@ -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 脚本内容""" diff --git a/app/processor.py b/app/processor.py index f03b59b..982050f 100644 --- a/app/processor.py +++ b/app/processor.py @@ -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: diff --git a/.dockerignore b/build/.dockerignore similarity index 86% rename from .dockerignore rename to build/.dockerignore index 72fe3e4..b6293bc 100644 --- a/.dockerignore +++ b/build/.dockerignore @@ -33,6 +33,9 @@ Dockerfile* # MySQL data mysql/ +# Build directory (exclude build files from image) +build/ + # Temporary files *.tmp *.temp diff --git a/Dockerfile b/build/Dockerfile similarity index 100% rename from Dockerfile rename to build/Dockerfile diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..cba84e0 --- /dev/null +++ b/build/README.md @@ -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` 文件头部: +- 默认镜像版本 +- 版本要求范围 +- 数据库账号密码 +- 端口映射 diff --git a/build/build.py b/build/build.py new file mode 100644 index 0000000..d68cc70 --- /dev/null +++ b/build/build.py @@ -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) diff --git a/dist/docker-compose.yml b/build/docker-compose.yml similarity index 100% rename from dist/docker-compose.yml rename to build/docker-compose.yml diff --git a/dist/mysql/conf.d/custom.cnf b/build/mysql/conf.d/custom.cnf similarity index 100% rename from dist/mysql/conf.d/custom.cnf rename to build/mysql/conf.d/custom.cnf diff --git a/dist/mysql/init/01-init-db.sql b/build/mysql/init/01-init-db.sql similarity index 100% rename from dist/mysql/init/01-init-db.sql rename to build/mysql/init/01-init-db.sql diff --git a/dist/.gitkeep b/dist/.gitkeep deleted file mode 100644 index 7433af7..0000000 --- a/dist/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ -# 此文件用于确保 dist 目录被 Git 跟踪 -# dist 目录用于存放离线部署包 diff --git a/dist/Configure.json b/dist/Configure.json deleted file mode 100644 index cd097ba..0000000 --- a/dist/Configure.json +++ /dev/null @@ -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))" - ] - } - ] -} \ No newline at end of file diff --git a/dist/ReportScript.sql b/dist/ReportScript.sql deleted file mode 100644 index 5a1f6c6..0000000 --- a/dist/ReportScript.sql +++ /dev/null @@ -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`; \ No newline at end of file diff --git a/dist/build.bat b/dist/build.bat deleted file mode 100644 index 341b8f9..0000000 --- a/dist/build.bat +++ /dev/null @@ -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 diff --git a/dist/build.py b/dist/build.py deleted file mode 100644 index 918cc3b..0000000 --- a/dist/build.py +++ /dev/null @@ -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) diff --git a/dist/deploy.sh b/dist/deploy.sh deleted file mode 100644 index ec33a84..0000000 --- a/dist/deploy.sh +++ /dev/null @@ -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 "" diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 99225cd..0000000 --- a/docker-compose.yml +++ /dev/null @@ -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 diff --git a/mysql/conf.d/custom.cnf b/mysql/conf.d/custom.cnf deleted file mode 100644 index 80ea686..0000000 --- a/mysql/conf.d/custom.cnf +++ /dev/null @@ -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 diff --git a/mysql/init/01-init-db.sql b/mysql/init/01-init-db.sql deleted file mode 100644 index afef3b5..0000000 --- a/mysql/init/01-init-db.sql +++ /dev/null @@ -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; diff --git a/static/css/style.css b/static/css/style.css index 544ae22..bc4f84f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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); diff --git a/static/index.html b/static/index.html index cbd797b..04edc1f 100644 --- a/static/index.html +++ b/static/index.html @@ -61,6 +61,9 @@