提交 4cff19b6 authored 作者: 陈泽健's avatar 陈泽健

feat(deploy): V2部署架构升级 — 合一容器+MySQL

完成服务器部署架构升级(V1三容器→V2双容器),解决代码更新需重建镜像、前端无法热更新、SQLite并发锁等问题。

变更内容:
- 新建合一Dockerfile(仅装环境,代码volume挂载)
- 重写docker-compose.yml(2服务:app+mysql)
- 改造main.py(StaticFiles挂载前端+SPA路由回退)
- 改造database.py(区分SQLite/MySQL连接参数)
- 7个model ID字段String(32)→String(64)(适配MySQL长度校验)
- 新增aiomysql+cryptography依赖
- 编写migrate_sqlite_to_mysql.py(断点续传+外键处理)
- 新增init.sql、deploy.sh、.env.example
- 删除旧Dockerfile.backend/frontend
- 新增PRD需求文档、执行计划、部署方案、HANDOFF_V2文档
- CLAUDE.md新增MySQL踩坑记录#10-#13
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 c3417d53
...@@ -147,8 +147,12 @@ Windows 上 Playwright 异步 API 在 FastAPI asyncio 循环中无法启动。** ...@@ -147,8 +147,12 @@ Windows 上 Playwright 异步 API 在 FastAPI asyncio 循环中无法启动。**
| 7 | 前端页面空白 | `useRoute()` 在 computed 内调用 | 移到 setup 顶层 | | 7 | 前端页面空白 | `useRoute()` 在 computed 内调用 | 移到 setup 顶层 |
| 8 | `NOT NULL constraint failed: error_message` | 字段 nullable=False 但成功时为 None | 改为 nullable=True | | 8 | `NOT NULL constraint failed: error_message` | 字段 nullable=False 但成功时为 None | 改为 nullable=True |
| 9 | Playwright Sync API inside asyncio loop | `run_in_executor` 线程中 Playwright 检测到 asyncio 循环 | `asyncio.set_event_loop(None)` 在工作线程中 | | 9 | Playwright Sync API inside asyncio loop | `run_in_executor` 线程中 Playwright 检测到 asyncio 循环 | `asyncio.set_event_loop(None)` 在工作线程中 |
| 10 | **MySQL 连接 `Can't connect on '2026@mysql'`** | DATABASE_URL 密码含 `@`,被 URL 解析为主机分隔符 | MySQL 密码不要含 `@` 等 URL 特殊字符(已用 `PlatApp2026`) |
| 11 | **MySQL `Data too long for column 'id'`** | 模型 `String(32)` 容不下带前缀 ID(如 `module_xxx` 39 字符),SQLite 不校验长度但 MySQL 校验 | ID 类字段统一用 `String(64)` |
| 12 | **MySQL 迁移 `foreign key constraint fails`** | 自引用外键(如 `modules.parent_id`)插入顺序问题,SQLite 不强制外键但 MySQL 强制 | 迁移脚本先 `SET FOREIGN_KEY_CHECKS=0`,完成后再开启 |
| 13 | **MySQL `ping() missing 'reconnect'` → API 500** | aiomysql 的 `ping()` 与 SQLAlchemy `pool_pre_ping=True` 不兼容 | MySQL 连接禁用 `pool_pre_ping`,改用 `pool_recycle=3600`(见 `database.py`) |
详见 `HANDOFF.md` 第五节。 详见 `HANDOFF.md` 第五节,V2 部署踩坑详见 `HANDOFF_V2部署升级.md`
--- ---
......
# 部署架构升级 V2:前后端合一容器 + MySQL 需求文档
> **文档版本**: v1.0
> **创建日期**: 2026-07-28
> **文档状态**: 初稿待评审
> **负责人**: czj
> **目标服务器**: 192.168.5.60 / Ubuntu 26.04 LTS
> **部署目录**: `/data/third_party/plat-auto-test/`
---
## 目录
1. [背景与问题](#一背景与问题)
2. [需求目标](#二需求目标)
3. [架构设计](#三架构设计)
4. [详细需求](#四详细需求)
5. [非功能需求](#五非功能需求)
6. [验收标准](#六验收标准)
7. [风险评估](#七风险评估)
8. [附录](#八附录)
---
## 一、背景与问题
### 1.1 当前部署现状(V1)
当前系统已通过 Docker 容器化部署在 `192.168.5.60`,架构为:
```
┌──────────────┐ ┌──────────────┐ ┌──────────┐
│ Frontend │ │ Backend │ │ SQLite │
│ Nginx 容器 │───▶│ uvicorn │───▶│ 文件 │
│ :80 │ │ :8001 │ │ │
│ 静态文件 │ │ Playwright │ │ volume │
└──────────────┘ └──────────────┘ └──────────┘
3 个容器独立运行 代码 COPY 进镜像
```
### 1.2 存在的问题
| 问题 | 详细描述 | 影响程度 |
|------|---------|---------|
| **1. 前端独立容器冗余** | 前端是 Vue 构建的纯静态文件,Nginx 容器仅 serve 静态文件,增加资源占用和管理复杂度 | ⭐⭐⭐ |
| **2. 代码更新需重建镜像** | 代码 COPY 进镜像,每次修改代码必须 `docker compose build`(3-5 分钟),开发迭代效率低 | ⭐⭐⭐ |
| **3. 前端更新需重启容器** | 修改前端后需重建镜像并重启 Nginx 容器,无法热更新 | ⭐⭐ |
| **4. SQLite 并发瓶颈** | SQLite 不支持高并发写入,多用户/批量执行时容易 `database is locked` | ⭐⭐⭐ |
| **5. 项目目录与另一服务共用** | 当前在 `~/ubains-module-test/` git 目录下,与 8088 端口维护平台共用仓库,存在分支冲突风险 | ⭐⭐ |
| **6. 数据无独立备份方案** | SQLite 文件数据库难以增量备份,迁移困难 | ⭐⭐ |
### 1.3 目标
1. **前后端合一容器**:前端静态文件由 FastAPI 直接 serve,去掉独立 Nginx 容器
2. **前端热更新**:前端构建产物通过 volume 挂载,替换文件直接生效,无需重启容器
3. **后端 volume 挂载**:后端代码通过 volume 挂载,修改后重启容器(秒级)即可生效,无需重建镜像
4. **MySQL 替代 SQLite**:数据库升级为 MySQL 8.0,独立容器运行,数据持久化,支持并发写入
5. **独立部署目录**:迁移到 `/data/third_party/plat-auto-test/`,与另一服务完全隔离
6. **数据独立存储**:MySQL 数据文件单独 volume 持久化,便于备份和迁移
---
## 二、需求目标
### 2.1 业务需求
| 需求 ID | 需求描述 | 优先级 |
|---------|---------|--------|
| REQ-01 | 前后端合并为同一容器运行,减少容器数量 | P0 |
| REQ-02 | 前端代码更新后实现热替换,无需重启容器 | P0 |
| REQ-03 | 后端代码更新后只需重启容器(秒级),无需重建镜像 | P0 |
| REQ-04 | 数据库从 SQLite 迁移至 MySQL 8.0,支持并发写入 | P0 |
| REQ-05 | 项目部署到独立目录 `/data/third_party/plat-auto-test/`,与另一服务隔离 | P0 |
| REQ-06 | MySQL 数据独立 volume 持久化,支持备份和恢复 | P1 |
| REQ-07 | 保留 SQLite 兼容性,本地开发环境仍可用 SQLite | P1 |
| REQ-08 | 现有 283 个用例数据完整迁移到 MySQL | P0 |
### 2.2 架构对比
| 维度 | V1(当前) | V2(目标) |
|------|-----------|-----------|
| **容器数量** | 3 个(前端 Nginx + 后端 + SQLite 文件) | **2 个(合一应用 + MySQL 8.0)** |
| **前端运行方式** | 独立 Nginx 容器 serve 静态文件 | **FastAPI StaticFiles 直接 serve** |
| **前端更新** | 重建镜像 + 重启容器(3-5 分钟) | **替换 dist/ 文件,热生效(无需重启)** |
| **后端更新** | 重建镜像 + 重启容器(3-5 分钟) | **重启容器(5-10 秒,无需重建)** |
| **数据库** | SQLite 文件(volume 挂载) | **MySQL 8.0 独立容器(volume 持久化)** |
| **部署目录** | `~/ubains-module-test/`(与另一服务共用) | **`/data/third_party/plat-auto-test/`(独立)** |
| **代码与镜像关系** | 代码 COPY 进镜像 | **代码 volume 挂载,镜像只装环境** |
| **数据备份** | 手动复制 .db 文件 | **mysqldump 标准备份流程** |
---
## 三、架构设计
### 3.1 整体架构图
```
用户浏览器
┌───────────────────────────────────────┐
│ 服务器 192.168.5.60 (Ubuntu) │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Docker Compose 网络 │ │
│ │ (plat-auto-test-net) │ │
│ │ │ │
│ │ ┌────────────────────┐ ┌──────┐│ │
│ │ │ app 容器 │ │mysql ││ │
│ │ │ (合一) │◀─│ 容器 ││ │
│ │ │ │ │ ││ │
│ │ │ ┌──────────────┐ │ │:3306 ││ │
│ │ │ │ FastAPI │ │ └──────┘│ │
│ │ │ │ ├─ API 路由 │ │ │ │
│ │ │ │ ├─ StaticFiles│ │ │ │
│ │ │ │ ├─ Playwright │ │ │ │
│ │ │ │ └─ WebSocket │ │ │ │
│ │ │ └──────────────┘ │ │ │
│ │ │ Port: 80 │ │ │
│ │ └────────┬─────────┘ └────┬───┘ │
│ │ │ │ │
│ │ Volume 挂载 Volume │
│ │ │ │ │
│ └───────────┼──────────────────┼──────┘
│ │ │
│ ┌───────────▼──────────────────▼──────┐
│ │ /data/third_party/plat-auto-test/ │
│ │ ├── backend/ (源码 volume) │
│ │ ├── frontend/dist/ (构建产物 volume)│
│ │ ├── data/ (截图/报告) │
│ │ ├── mysql/data/ (MySQL 数据) │
│ │ └── logs/ (日志) │
│ └──────────────────────────────────────┘
│ │
│ 另一维护平台 (8088) ← 独立,不受影响 │
└───────────────────────────────────────────┘
```
### 3.2 容器职责
| 容器 | 数量 | 职责 | 端口 | 镜像 |
|------|------|------|------|------|
| **app** | 1 | FastAPI(API + 前端静态文件 + Playwright + WebSocket) | 80 | 自构建(python:3.10-slim) |
| **mysql** | 1 | MySQL 8.0 数据库 | 3306(仅容器内,宿主机映射 3307) | mysql:8.0 |
### 3.3 关键设计决策
**决策 1:前后端合一 — 为什么去掉独立 Nginx?**
| 理由 | 说明 |
|------|------|
| 前端是静态文件 | Vue 构建产物是纯静态 HTML/JS/CSS,FastAPI 的 `StaticFiles` 可直接 serve |
| 同源访问 | 前端 API 请求走 `/api/*` 相对路径,无需跨域配置 |
| 减少资源 | 从 3 个容器减到 2 个,降低内存和运维复杂度 |
| 热更新 | FastAPI 静态文件服务实时读取文件系统,替换 dist/ 立即生效 |
**决策 2:MySQL 独立容器 — 为什么不是宿主机直装?**
| 理由 | 说明 |
|------|------|
| 环境隔离 | 数据库版本、配置与宿主机解耦,不影响其他服务 |
| 易于备份 | `mysqldump` 标准备份,Docker volume 持久化 |
| 易于迁移 | 容器化数据库可整体迁移到其他 Docker 主机 |
| 版本管理 | 可独立升级 MySQL 版本,不影响应用 |
**决策 3:代码 volume 挂载 — 为什么代码不 COPY 进镜像?**
| 理由 | 说明 |
|------|------|
| 更新速度快 | 改后端代码只需重启容器(5-10s),无需重建镜像(3-5min) |
| 前端热更新 | 替换 dist/ 文件即时生效,完全不需重启 |
| 镜像体积小 | 镜像只包含 Python 环境 + Playwright + pip 依赖,约 1.2GB(不含业务代码) |
| 版本回退快 | 保留旧代码目录即可快速回退 |
---
## 四、详细需求
### 4.1 服务器目录结构
```
/data/third_party/plat-auto-test/ ← 项目根目录(独立,无 git 仓库)
├── deploy/ ← 部署配置文件
│ ├── docker-compose.yml ← 服务编排(app + mysql)
│ ├── Dockerfile ← 合一应用镜像
│ ├── init.sql ← MySQL 初始化脚本
│ ├── nginx.conf ← Nginx 配置(可选,未来 HTTPS 用)
│ ├── .env ← 环境变量(敏感信息,不提交 git)
│ └── deploy.sh ← 一键部署脚本
├── backend/ ← 后端源码(volume 挂载到容器 /app)
│ ├── app/
│ │ ├── main.py ← 改造:新增 StaticFiles 挂载前端
│ │ ├── config.py ← 改造:DATABASE_URL 支持 MySQL
│ │ ├── database.py ← 无需改动(已有条件判断)
│ │ ├── models/
│ │ ├── routers/
│ │ ├── services/
│ │ ├── executors/
│ │ └── websocket/
│ ├── scripts/
│ ├── requirements.txt ← 改造:新增 aiomysql + cryptography
│ └── ...
├── frontend/
│ └── dist/ ← 前端构建产物(volume 挂载)
│ ├── index.html
│ ├── assets/
│ └── ...
├── data/ ← 应用数据(volume 挂载到 /app/data)
│ ├── screenshots/ ← 执行截图
│ └── reports/ ← 测试报告
├── mysql/
│ └── data/ ← MySQL 数据持久化(volume 挂载)
└── logs/ ← 应用日志(volume 挂载到 /app/logs)
```
### 4.2 目录权限要求
| 目录 | 容器内路径 | 宿主机权限 | 容器内 UID | 说明 |
|------|-----------|-----------|-----------|------|
| `backend/` | `/app` | 755 / ubains:ubains | 1000 | 后端源码 |
| `frontend/dist/` | `/app/frontend/dist` | 755 / ubains:ubains | 1000 | 前端构建产物 |
| `data/` | `/app/data` | 777 | 1000 | 截图/报告(需写入) |
| `mysql/data/` | `/var/lib/mysql` | 999:999 (mysql) | 999 | MySQL 数据 |
| `logs/` | `/app/logs` | 777 | 1000 | 日志 |
### 4.3 Dockerfile 需求
**文件**: `deploy/Dockerfile`
```dockerfile
FROM python:3.10-slim
WORKDIR /app
# 安装系统依赖(Playwright 需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
wget gnupg curl \
libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 \
libcups2t64 libdrm2 libdbus-1-3 libxkbcommon0 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libpango-1.0-0 libcairo2 libasound2t64 \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖(使用国内镜像源)
COPY requirements.txt .
RUN pip install --no-cache-dir \
-i https://pypi.tuna.tsinghua.edu.cn/simple \
--trusted-host pypi.tuna.tsinghua.edu.cn \
--retries 5 --timeout 120 -r requirements.txt
# 安装 Playwright Chromium
RUN playwright install chromium
# 创建必要目录
RUN mkdir -p /app/data/screenshots /app/data/reports \
/app/frontend/dist /app/logs
# ⚠️ 以下内容通过 volume 挂载,不 COPY 进镜像:
# - backend/ 源码 → /app
# - frontend/dist/ → /app/frontend/dist
# - data/ → /app/data
# - logs/ → /app/logs
EXPOSE 80
# 启动 uvicorn,监听 80 端口(同时提供 API + 前端静态文件)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
```
**关键要求**
- 镜像只装环境(Python + Playwright + pip 依赖),**不打包业务代码**
- 首次构建后,后续更新代码**无需重建镜像**
- 使用国内镜像源加速构建
### 4.4 requirements.txt 改造
```diff
fastapi==0.110.0
uvicorn[standard]==0.27.0
sqlalchemy==2.0.25
aiosqlite==0.19.0 # 保留,本地开发仍用 SQLite
+ aiomysql==0.2.0 # 新增,MySQL 异步驱动
+ cryptography==42.0.0 # 新增,MySQL 认证需要
pydantic==2.5.3
...
```
### 4.5 main.py 改造(前端静态文件服务)
`main.py` 末尾添加前端静态文件挂载:
```python
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pathlib import Path
# ==================== 前端静态文件服务 ====================
FRONTEND_DIST = Path(__file__).parent.parent / "frontend" / "dist"
if FRONTEND_DIST.exists():
# 挂载静态资源目录
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
# SPA 路由:所有非 /api 路径回退到 index.html
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""前端 SPA 入口,所有非 API 路径返回 index.html"""
file_path = FRONTEND_DIST / full_path
if file_path.exists() and file_path.is_file():
return FileResponse(str(file_path))
return FileResponse(str(FRONTEND_DIST / "index.html"))
```
**关键要求**
- 仅在 `frontend/dist/` 存在时才挂载(本地开发时不干扰)
- SPA 路由回退:所有非 `/api` 路径返回 `index.html`
- 前端热更新:FastAPI 实时读取文件系统,替换 dist/ 文件后立即生效
### 4.6 Docker Compose 编排
**文件**: `deploy/docker-compose.yml`
```yaml
version: "3.8"
services:
mysql:
image: mysql:8.0
container_name: plat-auto-test-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE:-plat_auto_test}
MYSQL_USER: ${MYSQL_USER:-platapp}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Shanghai
volumes:
- /data/third_party/plat-auto-test/mysql/data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "3307:3306" # 宿主机 3307,避免与本地 MySQL 冲突
networks:
- plat-auto-test-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-authentication-plugin=mysql_native_password
- --max_connections=200
app:
build:
context: ../backend
dockerfile: ../deploy/Dockerfile
container_name: plat-auto-test-app
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
environment:
- DATABASE_URL=mysql+aiomysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}?charset=utf8mb4
- PLAYWRIGHT_HEADLESS=true
- PLAYWRIGHT_TIMEOUT=30000
- LOG_LEVEL=INFO
- TZ=Asia/Shanghai
volumes:
- /data/third_party/plat-auto-test/backend:/app
- /data/third_party/plat-auto-test/frontend/dist:/app/frontend/dist
- /data/third_party/plat-auto-test/data:/app/data
- /data/third_party/plat-auto-test/logs:/app/logs
ports:
- "80:80"
networks:
- plat-auto-test-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
plat-auto-test-net:
driver: bridge
```
**关键设计**
- `mysql` 先启动并通过健康检查后,`app` 才启动(`depends_on` + `condition: service_healthy`
- `app``DATABASE_URL` 用容器服务名 `mysql` 作为主机名(Docker 内部 DNS)
- MySQL 宿主机映射 `3307` 端口(避免与服务器本地 3306 冲突)
- 全部代码和数据通过 volume 挂载
### 4.7 MySQL 初始化脚本
**文件**: `deploy/init.sql`
```sql
-- 确保字符集
ALTER DATABASE plat_auto_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 授予权限
GRANT ALL PRIVILEGES ON plat_auto_test.* TO 'platapp'@'%';
FLUSH PRIVILEGES;
-- 时区设置
SET GLOBAL time_zone = '+08:00';
SET SESSION time_zone = '+08:00';
```
### 4.8 环境变量配置
**文件**: `deploy/.env`
```bash
# ==================== MySQL 配置 ====================
MYSQL_ROOT_PASSWORD=Ubains@13579
MYSQL_DATABASE=plat_auto_test
MYSQL_USER=platapp
MYSQL_PASSWORD=PlatApp@2026
# ==================== 应用配置 ====================
PLAYWRIGHT_HEADLESS=true
PLAYWRIGHT_TIMEOUT=30000
LOG_LEVEL=INFO
```
> ⚠️ **安全提示**:生产环境请修改 `MYSQL_ROOT_PASSWORD` 和 `MYSQL_PASSWORD` 为强密码。
> `.env` 文件不要提交到 git。
### 4.9 数据迁移方案
#### 4.9.1 表结构迁移
应用首次启动时,`main.py``lifespan` 调用 `init_db()``Base.metadata.create_all()`,根据 SQLAlchemy 模型自动在 MySQL 中创建所有表。
#### 4.9.2 数据迁移
需要编写迁移脚本 `backend/scripts/migrate_sqlite_to_mysql.py`,将本地 SQLite 数据迁移到 MySQL:
| 表名 | 行数 | 说明 |
|------|------|------|
| `modules` | ~28 | 测试模块 |
| `test_cases` | ~283 | 测试用例 |
| `executions` | 若干 | 执行记录 |
| `case_results` | 若干 | 用例结果 |
| `case_dependencies` | 若干 | 用例依赖 |
| `security_configs` | 若干 | 安全测试配置 |
| `vulnerability_results` | 若干 | 漏洞结果 |
### 4.10 更新流程
#### 场景 1:更新前端(热更新,无需重启)
```bash
# 本地构建
cd frontend && npm run build
# 上传 dist/ 到服务器(直接覆盖)
scp -r dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
```
**效果**:替换文件后,刷新浏览器即刻生效,**无需重启容器**
#### 场景 2:更新后端(需重启容器,秒级)
```bash
# 上传修改后的代码
scp -r backend/app ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/
# 重启容器(5-10 秒)
ssh ubains@192.168.5.60
cd /data/third_party/plat-auto-test/deploy
docker compose restart app
```
#### 场景 3:更新依赖(需重建镜像)
仅在 `requirements.txt` 发生变更时才需要:
```bash
cd /data/third_party/plat-auto-test/deploy
docker compose build app
docker compose up -d app
```
---
## 五、非功能需求
### 5.1 性能要求
| 指标 | 要求 |
|------|------|
| 应用容器启动时间 | < 15 秒(等待 MySQL 健康检查后) |
| API 健康检查响应 | < 100ms |
| 后端重启时间 | < 10 秒 |
| 前端首屏加载 | < 3 秒 |
| 数据库连接池 | 最大 200 连接 |
### 5.2 安全要求
| 要求 | 说明 |
|------|------|
| MySQL 密码 | 生产环境使用强密码,`.env` 不提交 git |
| 端口暴露 | 宿主机只暴露 80(前端)和 3307(MySQL 调试),3306 仅在容器内 |
| 文件权限 | `data/``logs/` 目录容器内可写 |
| 容器隔离 | 应用容器与数据库容器网络隔离,仅通过内部网络通信 |
### 5.3 兼容性要求
| 场景 | 数据库 | 说明 |
|------|--------|------|
| 生产环境(服务器) | MySQL 8.0 | 通过 `DATABASE_URL` 环境变量配置 |
| 本地开发(Windows) | SQLite | 保留 `aiosqlite` 依赖,默认使用 SQLite |
| 本地测试 | SQLite | 无需额外配置 |
### 5.4 数据持久化要求
| 数据 | 存储方式 | 备份方式 |
|------|---------|---------|
| MySQL 数据库 | Volume 挂载到 `mysql/data/` | `mysqldump` 每日备份 |
| 执行截图 | Volume 挂载到 `data/screenshots/` | 定期清理 30 天前的 |
| 测试报告 | Volume 挂载到 `data/reports/` | 定期清理 30 天前的 |
| 应用日志 | Volume 挂载到 `logs/` | 日志轮转 |
---
## 六、验收标准
### 6.1 功能验收
| 验收项 | 操作 | 期望结果 |
|--------|------|---------|
| 目录独立 | 检查 `/data/third_party/plat-auto-test/` | 文件结构完整,与 `/opt/troubleshoot/` 独立 |
| 容器运行 | `docker compose ps` | app + mysql 状态 running/healthy |
| API 健康 | `curl http://localhost/health` | 返回 `{"status":"healthy"}` |
| 前端访问 | 浏览器打开 `http://192.168.5.60` | 显示登录页或首页 |
| 数据库连接 | 查看应用日志 | 无 MySQL 连接错误,表自动创建 |
| 数据完整 | 查询用例数 | 283 个用例完整迁移 |
| 前端热更新 | 替换 dist/ 后刷新浏览器 | 新版本生效,无需重启容器 |
| 后端更新 | 重启 app 容器后调用 API | 新代码生效 |
| 已有服务 | 访问 8088 端口 | 另一维护平台正常 |
| 本地开发 | 本地 `uvicorn` 启动 | 使用 SQLite 正常运行 |
### 6.2 性能验收
| 验收项 | 标准 |
|--------|------|
| 容器启动时间 | 总部署时间 < 5 分钟 |
| 后端重启时间 | < 10 秒 |
| 前端热更新 | 替换文件后,浏览器刷新即生效,无需等待 |
| 批量执行 | 同时执行 10 个用例,无 `database is locked` |
### 6.3 数据迁移验收
| 验收项 | 操作 | 期望结果 |
|--------|------|---------|
| 模块数据 | `curl /api/modules?case_type=ui` | 返回 16 个 UI 模块 |
| 用例数据 | `curl /api/cases?limit=500` | 返回 283 个用例 |
| 执行记录 | 查看执行历史 | 历史执行记录可查询 |
---
## 七、风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| MySQL 连接失败 | 低 | 高 | 健康检查依赖确保启动顺序 |
| 数据迁移丢失 | 低 | 高 | 迁移前备份 SQLite,迁移后对比行数 |
| 容器内路径不一致 | 中 | 高 | 确保 WORKDIR 和 volume 路径严格匹配 |
| 旧容器端口冲突 | 低 | 中 | 先停旧容器再启动新容器 |
| Playwright 执行异常 | 低 | 中 | 容器内已安装所有系统依赖 |
| 前端热更新不生效 | 低 | 低 | 浏览器缓存问题,强制刷新解决 |
| 代码 volume 挂载性能 | 低 | 低 | 绑定挂载性能接近原生 |
---
## 八、附录
### 8.1 相关文档
| 文档 | 路径 | 说明 |
|------|------|------|
| V1 部署方案 | `Docs/部署方案/Linux服务器部署方案.md` | 原始部署方案 |
| V1 执行计划 | `Docs/部署方案/Linux容器化部署_执行计划.md` | 原始部署执行计划 |
| V2 设计方案 | `Docs/部署方案/Linux容器化部署方案_v2.md` | V2 实施方案初稿 |
| Volume 优化 PRD | `Docs/PRD/需求文档/部署运维/_PRD_需求文档_容器化部署Volume挂载优化.md` | 前置需求文档 |
| Volume 优化计划 | `Docs/PRD/需求文档/部署运维/_执行计划_容器化部署Volume挂载优化.md` | 前置计划文档 |
| UI 自动化交接 | `HANDOFF_UI自动化.md` | 项目交接 |
| 项目指南 | `CLAUDE.md` | 项目指导 |
### 8.2 服务器信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 192.168.5.60 |
| SSH 用户 | ubains |
| 操作系统 | Ubuntu 26.04 LTS |
| 已有服务 | 8088 端口(`/opt/troubleshoot/`) |
| 部署目录 | `/data/third_party/plat-auto-test/` |
| 前端地址 | http://192.168.5.60 |
| API 文档 | http://192.168.5.60/docs |
| MySQL 外部访问 | 192.168.5.60:3307(仅调试用) |
### 8.3 部署架构演进路线
| 阶段 | 架构 | 状态 |
|------|------|------|
| V1 | 前端 Nginx + 后端 + SQLite(代码 COPY 进镜像) | ✅ 已部署 |
| **V2** | **合一应用 + MySQL 8.0(代码 volume 挂载)** | 📋 **本需求文档** |
| V3(远期) | 应用 + MySQL + Redis + HTTPS + 监控告警 | 🔮 规划中 |
### 8.4 涉及改造的文件清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `deploy/Dockerfile` | 🆕 新建 | 合一应用镜像(替代原 backend + frontend 两个 Dockerfile) |
| `deploy/docker-compose.yml` | 🔄 重写 | 从 3 服务改为 2 服务(app + mysql) |
| `deploy/init.sql` | 🆕 新建 | MySQL 初始化 |
| `deploy/.env.example` | 🔄 更新 | 新增 MySQL 配置项 |
| `deploy/deploy.sh` | 🔄 重写 | 适配新目录和新架构 |
| `backend/app/main.py` | 🔄 改造 | 新增 StaticFiles 挂载前端静态文件 |
| `backend/requirements.txt` | 🔄 改造 | 新增 aiomysql + cryptography |
| `backend/scripts/migrate_sqlite_to_mysql.py` | 🆕 新建 | 数据迁移脚本 |
---
*本文档由 Claude Code 编写,供评审后进入 prd-plan 阶段生成执行计划。*
\ No newline at end of file
# 执行计划:V2 部署架构升级 — 前后端合一容器 + MySQL
> **文档版本**: v1.0
> **编写日期**: 2026-07-28
> **当前分支**: `platform-auto-test`
> **目标服务器**: 192.168.5.60 / Ubuntu 26.04 LTS / 用户 ubains
> **部署目录**: `/data/third_party/plat-auto-test/`
> **关联 PRD**: `_PRD_需求文档_V2部署架构升级_合一容器MySQL.md` v1.0
> **预计工时**: 1.5 天
---
## 一、执行概述
### 1.1 任务背景
当前系统以 Docker 容器化部署在 `192.168.5.60`,架构为 3 容器(前端 Nginx + 后端 + SQLite),存在以下问题:
1. **前端独立容器冗余** — 纯静态文件用 Nginx 容器 serve,资源浪费
2. **代码更新需重建镜像** — 每次修改代码必须 `docker compose build`(3-5 分钟)
3. **前端更新需重启容器** — 无法热更新
4. **SQLite 并发瓶颈** — 批量执行时 `database is locked`
5. **部署目录与另一服务共用** — 存在分支冲突风险
### 1.2 执行目标
| 目标 | 描述 | 验收标准 |
|------|------|---------|
| 前后端合一容器 | FastAPI 直接 serve 前端静态文件,去掉独立 Nginx 容器 | 访问 `http://192.168.5.60` 正常显示前端 |
| 前端热更新 | 前端构建产物通过 volume 挂载,替换文件立即生效 | 替换 dist/ 后刷新浏览器即生效,无需重启 |
| 后端 volume 挂载 | 后端代码通过 volume 挂载,改代码重启容器(秒级)生效 | 修改代码后 `docker compose restart app` 生效 |
| MySQL 替代 SQLite | 数据库升级为 MySQL 8.0,独立容器 | API 正常读写,批量执行无 `database is locked` |
| 数据迁移 | 283 个用例数据完整迁移到 MySQL | 查询返回 283 个用例 |
| 独立部署目录 | 迁移到 `/data/third_party/plat-auto-test/` | 与另一服务完全隔离,8088 不受影响 |
### 1.3 预计工期
| 阶段 | 内容 | 预计时间 |
|------|------|---------|
| Phase 1 | 本地代码改造(Dockerfile + main.py + requirements.txt) | 0.5 天 |
| Phase 2 | 数据迁移脚本编写与测试 | 0.5 天 |
| Phase 3 | 服务器部署实施(目录创建、文件上传、镜像构建) | 0.5 天 |
| Phase 4 | 数据迁移与验证 | 0.5 天 |
| **总计** | | **2 天** |
---
## 二、任务分解与实施计划
### Phase 1: 本地代码改造(0.5 天)
#### 1.1 新建合一 Dockerfile
**文件**: `deploy/Dockerfile`(新建)
**目标**:镜像只装 Python 环境 + Playwright + pip 依赖,不打包业务代码,代码通过 volume 挂载。
```dockerfile
FROM python:3.10-slim
WORKDIR /app
# 安装 Playwright 系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
wget gnupg curl \
libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 \
libcups2t64 libdrm2 libdbus-1-3 libxkbcommon0 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libpango-1.0-0 libcairo2 libasound2t64 \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖(国内镜像源)
COPY requirements.txt .
RUN pip install --no-cache-dir \
-i https://pypi.tuna.tsinghua.edu.cn/simple \
--trusted-host pypi.tuna.tsinghua.edu.cn \
--retries 5 --timeout 120 -r requirements.txt
# 安装 Playwright Chromium
RUN playwright install chromium
# 创建必要目录
RUN mkdir -p /app/data/screenshots /app/data/reports \
/app/frontend/dist /app/logs
EXPOSE 80
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
```
**检查要点**
- [ ] WORKDIR 为 `/app`,与 volume 挂载路径一致
- [ ] 使用国内镜像源加速
- [ ] 未包含 `COPY backend/ .` — 代码从 volume 挂载
- [ ] 端口改为 80(合一容器监听 80 端口)
#### 1.2 改造 requirements.txt
**文件**: `backend/requirements.txt`
**变更**:新增 aiomysql 和 cryptography 依赖
```diff
aiosqlite==0.19.0
+ aiomysql==0.2.0
+ cryptography==42.0.0
```
**检查要点**
- [ ] aiomysql 版本兼容 SQLAlchemy 2.0
- [ ] cryptography 用于 MySQL 认证
- [ ] 保留 aiosqlite(本地开发仍用 SQLite)
#### 1.3 改造 main.py(前端静态文件服务)
**文件**: `backend/app/main.py`
**变更**:在文件末尾添加前端静态文件挂载和 SPA 路由回退
```python
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pathlib import Path
# ==================== 前端静态文件服务 ====================
FRONTEND_DIST = Path(__file__).parent.parent / "frontend" / "dist"
if FRONTEND_DIST.exists():
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
file_path = FRONTEND_DIST / full_path
if file_path.exists() and file_path.is_file():
return FileResponse(str(file_path))
return FileResponse(str(FRONTEND_DIST / "index.html"))
```
**检查要点**
- [ ] 仅在 `frontend/dist/` 存在时挂载(本地开发时不干扰)
- [ ] SPA 路由回退正确(非 `/api` 路径返回 `index.html`
- [ ] 不影响现有 API 路由(`/api/*` 路由优先匹配)
- [ ] 路径引用正确:`parent.parent``app/` 到项目根
#### 1.4 重写 docker-compose.yml
**文件**: `deploy/docker-compose.yml`
**变更**:从 3 服务改为 2 服务(app + mysql),全部使用 volume 挂载
```yaml
version: "3.8"
services:
mysql:
image: mysql:8.0
container_name: plat-auto-test-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE:-plat_auto_test}
MYSQL_USER: ${MYSQL_USER:-platapp}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Shanghai
volumes:
- /data/third_party/plat-auto-test/mysql/data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "3307:3306"
networks:
- plat-auto-test-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-authentication-plugin=mysql_native_password
- --max_connections=200
app:
build:
context: ../backend
dockerfile: ../deploy/Dockerfile
container_name: plat-auto-test-app
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
environment:
- DATABASE_URL=mysql+aiomysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}?charset=utf8mb4
- PLAYWRIGHT_HEADLESS=true
- PLAYWRIGHT_TIMEOUT=30000
- LOG_LEVEL=INFO
- TZ=Asia/Shanghai
volumes:
- /data/third_party/plat-auto-test/backend:/app
- /data/third_party/plat-auto-test/frontend/dist:/app/frontend/dist
- /data/third_party/plat-auto-test/data:/app/data
- /data/third_party/plat-auto-test/logs:/app/logs
ports:
- "80:80"
networks:
- plat-auto-test-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
plat-auto-test-net:
driver: bridge
```
**检查要点**
- [ ] `build.context` 指向 `../backend`(确保 requirements.txt 路径正确)
- [ ] `build.dockerfile` 指向 `../deploy/Dockerfile`
- [ ] MySQL 宿主机端口用 3307,避免冲突
- [ ] app 依赖 mysql 健康检查
- [ ] 4 个 volume 挂载路径正确
- [ ] DATABASE_URL 使用容器服务名 `mysql` 作为主机名
#### 1.5 新建 MySQL 初始化脚本
**文件**: `deploy/init.sql`
```sql
ALTER DATABASE plat_auto_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON plat_auto_test.* TO 'platapp'@'%';
FLUSH PRIVILEGES;
SET GLOBAL time_zone = '+08:00';
SET SESSION time_zone = '+08:00';
```
#### 1.6 更新 .env.example
**文件**: `deploy/.env.example`
```bash
# MySQL 配置
MYSQL_ROOT_PASSWORD=Ubains@13579
MYSQL_DATABASE=plat_auto_test
MYSQL_USER=platapp
MYSQL_PASSWORD=PlatApp@2026
# 应用配置
PLAYWRIGHT_HEADLESS=true
PLAYWRIGHT_TIMEOUT=30000
LOG_LEVEL=INFO
```
#### 1.7 编写数据迁移脚本
**文件**: `backend/scripts/migrate_sqlite_to_mysql.py`
**功能**:从本地 SQLite 数据库读取数据,写入 MySQL
**表迁移顺序**
1. `modules` — 先迁移模块(无外键依赖)
2. `test_cases` — 依赖 module.id
3. `executions` — 独立
4. `case_results` — 依赖 execution.id
5. `case_dependencies` — 依赖 test_case.id
6. `security_configs` — 独立
7. `vulnerability_results` — 依赖 security_configs.id
**迁移逻辑**
```python
#!/usr/bin/env python
"""
SQLite → MySQL 数据迁移脚本
将本地 SQLite 数据迁移到远程 MySQL
"""
import asyncio
from sqlalchemy import create_engine, text
from sqlalchemy.ext.asyncio import create_async_engine
SQLITE_URL = "sqlite+aiosqlite:///./data/test_platform.db"
MYSQL_URL = "mysql+aiomysql://platapp:PlatApp@2026@192.168.5.60:3307/plat_auto_test?charset=utf8mb4"
TABLES_ORDER = [
"modules",
"test_cases",
"executions",
"case_results",
"case_dependencies",
"security_configs",
"vulnerability_results",
]
async def migrate():
# 1. 连接 SQLite 读取数据
sqlite_engine = create_async_engine(SQLITE_URL)
# 2. 连接 MySQL
mysql_engine = create_async_engine(MYSQL_URL)
# 3. 逐表迁移
for table in TABLES_ORDER:
# 读取 SQLite 数据
async with sqlite_engine.connect() as conn:
rows = await conn.execute(text(f"SELECT * FROM {table}"))
columns = rows.keys()
data = [dict(zip(columns, row)) for row in rows]
# 写入 MySQL
if data:
async with mysql_engine.connect() as conn:
for row in data:
keys = ", ".join(row.keys())
values = ", ".join([f":{k}" for k in row.keys()])
await conn.execute(
text(f"INSERT INTO {table} ({keys}) VALUES ({values})"),
row
)
await conn.commit()
print(f"✅ {table}: {len(data)} 行迁移完成")
print("🎉 所有数据迁移完成!")
if __name__ == "__main__":
asyncio.run(migrate())
```
**检查要点**
- [ ] 迁移前确认 MySQL 表已创建(应用首次启动自动建表)
- [ ] 处理 UUID 主键兼容性(SQLite 和 MySQL 的 hex 字符串一致)
- [ ] 处理 JSON 字段序列化(SQLite 的 JSON 和 MySQL 的 JSON 兼容)
- [ ] 迁移完成后对比行数
#### 1.8 更新 deploy.sh
**文件**: `deploy/deploy.sh`
**变更**:适配新目录 `/data/third_party/plat-auto-test/` 和新架构(2 容器)
---
### Phase 2: 数据迁移脚本测试(0.5 天)
#### 2.1 本地测试
```bash
# 1. 本地启动 MySQL(Docker)
docker run --name mysql-test -e MYSQL_ROOT_PASSWORD=Ubains@13579 \
-e MYSQL_DATABASE=plat_auto_test -e MYSQL_USER=platapp \
-e MYSQL_PASSWORD=PlatApp@2026 -p 3307:3306 -d mysql:8.0 \
--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci \
--default-authentication-plugin=mysql_native_password
# 2. 安装依赖
pip install aiomysql cryptography
# 3. 创建表结构(启动应用或手动建表)
# 4. 运行迁移脚本
cd backend
python scripts/migrate_sqlite_to_mysql.py
# 5. 验证数据
mysql -h 127.0.0.1 -P 3307 -uplatapp -pPlatApp@2026 plat_auto_test -e "SELECT COUNT(*) FROM test_cases;"
# 期望: 283
```
#### 2.2 验证要点
- [ ] 所有表数据完整迁移
- [ ] 主键无冲突
- [ ] 外键关系正确
- [ ] JSON 字段内容正确
- [ ] 迁移后本地 SQLite 可继续使用(本地开发)
---
### Phase 3: 服务器部署实施(0.5 天)
#### 3.1 停掉旧容器
```bash
ssh ubains@192.168.5.60
# 停掉 V1 容器
cd ~/ubains-module-test/deploy 2>/dev/null
docker compose down 2>/dev/null
# 确认容器已停止
docker compose ps
```
#### 3.2 创建独立目录
```bash
# 创建目录结构
sudo mkdir -p /data/third_party/plat-auto-test/deploy
sudo mkdir -p /data/third_party/plat-auto-test/backend
sudo mkdir -p /data/third_party/plat-auto-test/frontend/dist
sudo mkdir -p /data/third_party/plat-auto-test/data/screenshots
sudo mkdir -p /data/third_party/plat-auto-test/data/reports
sudo mkdir -p /data/third_party/plat-auto-test/mysql/data
sudo mkdir -p /data/third_party/plat-auto-test/logs
# 设置权限
sudo chown -R ubains:ubains /data/third_party/plat-auto-test
```
#### 3.3 上传部署配置
```powershell
# 本地执行 - 上传部署相关文件
scp deploy/Dockerfile ubains@192.168.5.60:/data/third_party/plat-auto-test/deploy/
scp deploy/docker-compose.yml ubains@192.168.5.60:/data/third_party/plat-auto-test/deploy/
scp deploy/init.sql ubains@192.168.5.60:/data/third_party/plat-auto-test/deploy/
scp deploy/.env.example ubains@192.168.5.60:/data/third_party/plat-auto-test/deploy/.env
scp deploy/deploy.sh ubains@192.168.5.60:/data/third_party/plat-auto-test/deploy/
```
#### 3.4 上传后端代码
```powershell
# 本地执行 - 上传后端源码(排除 pycache)
scp -r backend/app ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/
scp -r backend/scripts ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/
scp backend/requirements.txt ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/
```
#### 3.5 构建前端并上传
```powershell
# 本地执行 - 构建前端
cd frontend
npm run build
# 上传构建产物
scp -r dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
```
#### 3.6 构建镜像并启动
```bash
# 服务器上执行
ssh ubains@192.168.5.60
cd /data/third_party/plat-auto-test/deploy
# 构建镜像(首次约 3-5 分钟)
docker compose build
# 启动服务
docker compose up -d
# 查看启动状态
docker compose ps
docker compose logs -f app # 看应用日志,确认连上 MySQL
```
---
### Phase 4: 数据迁移与验证(0.5 天)
#### 4.1 确认 MySQL 表已创建
```bash
# 检查 MySQL 表是否自动创建
docker exec plat-auto-test-mysql mysql -uplatapp -pPlatApp@2026 \
plat_auto_test -e "SHOW TABLES;"
```
#### 4.2 运行数据迁移
```bash
# 本地运行迁移脚本(连接服务器 MySQL)
cd backend
python scripts/migrate_sqlite_to_mysql.py
```
#### 4.3 验证数据完整性
```bash
# 验证用例数
curl -s "http://192.168.5.60/api/cases?limit=500" | python3 -c "
import sys,json
d=json.load(sys.stdin)
print(f'总用例数: {len(d.get(\"items\", d))}')"
# 验证模块数
curl -s "http://192.168.5.60/api/modules?case_type=ui" | python3 -c "
import sys,json
d=json.load(sys.stdin)
print(f'UI模块数: {len(d)}')"
# 验证前端可访问
curl -s -o /dev/null -w "%{http_code}" http://192.168.5.60/
# 期望: 200
# 验证 API 健康
curl -s http://192.168.5.60/health
# 期望: {"status":"healthy"}
# 验证已有服务不受影响
curl -s -o /dev/null -w "%{http_code}" http://192.168.5.60:8088/
# 期望: 200
```
#### 4.4 验证前端热更新
```bash
# 1. 修改本地前端代码并构建
cd frontend
# 修改某个文件 ...
npm run build
# 2. 上传新构建产物
scp -r dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
# 3. 浏览器刷新 - 验证新版本生效(无需重启容器)
```
#### 4.5 验证后端更新
```bash
# 1. 修改本地后端代码
# 修改某个 .py 文件 ...
# 2. 上传修改的文件
scp -r backend/app ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/
# 3. 重启容器(秒级)
ssh ubains@192.168.5.60
cd /data/third_party/plat-auto-test/deploy
docker compose restart app
# 4. 验证新代码生效
curl -s http://192.168.5.60/health
```
---
## 三、验收标准
### 3.1 功能验收
| 验收项 | 操作 | 期望结果 |
|--------|------|---------|
| 目录独立 | 检查 `/data/third_party/plat-auto-test/` | 文件结构完整,与 `/opt/troubleshoot/` 独立 |
| 容器运行 | `docker compose ps` | app + mysql 状态 running/healthy |
| API 健康 | `curl http://localhost/health` | 返回 `{"status":"healthy"}` |
| 前端访问 | 浏览器打开 `http://192.168.5.60` | 显示登录页或首页 |
| 数据库连接 | 应用日志无 MySQL 连接错误 | 表自动创建 |
| 数据完整 | 查询用例数 | 283 个用例 |
| 前端热更新 | 替换 dist/ 后刷新浏览器 | 新版本生效,无需重启容器 |
| 后端更新 | 重启 app 容器后调用 API | 新代码生效 |
| 已有服务 | 访问 8088 端口 | 另一维护平台正常 |
| 本地开发 | 本地 `uvicorn` 启动 | 使用 SQLite 正常运行 |
### 3.2 性能验收
| 验收项 | 标准 |
|--------|------|
| 容器启动时间 | 总部署时间 < 5 分钟 |
| 后端重启时间 | < 10 秒 |
| 前端热更新 | 替换文件后刷新即生效 |
### 3.3 数据迁移验收
| 表名 | 期望行数 | 是否完整 |
|------|---------|---------|
| modules | ~28 | ✅ |
| test_cases | 283 | ✅ |
| 其他表 | 完整迁移 | ✅ |
---
## 四、测试计划
### 4.1 本地测试
| 测试项 | 方法 | 预期 |
|--------|------|------|
| 本地 SQLite 模式 | `uvicorn app.main:app --port 8001` | 正常启动,使用本地数据库 |
| 前端挂载 | 访问 `http://localhost:8001` | 前端页面正常显示 |
| API 路由 | 访问 `http://localhost:8001/api/cases` | 返回用例列表 |
| 数据迁移脚本 | 运行迁移脚本 | 数据完整迁移 |
### 4.2 服务器测试
| 测试项 | 方法 | 预期 |
|--------|------|------|
| 服务启动 | `docker compose up -d` | 2 容器均正常运行 |
| 健康检查 | `curl http://localhost/health` | 200 OK |
| 前端访问 | 浏览器访问 `http://192.168.5.60` | 正常显示 |
| 用例查询 | `curl /api/cases?limit=10` | 返回数据 |
| 批量执行 | 执行 10 个用例 | 无 `database is locked` |
---
## 五、风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| MySQL 连接失败 | 低 | 高 | 健康检查依赖确保启动顺序 |
| 数据迁移丢失 | 低 | 高 | 迁移前备份 SQLite,迁移后对比行数 |
| 容器内路径不一致 | 中 | 高 | 确保 WORKDIR 和 volume 路径严格匹配 |
| 旧容器端口冲突 | 低 | 中 | 先 `docker compose down` 再启动 |
| Dockerfile 构建失败 | 低 | 中 | 使用国内镜像源,已有成功经验 |
| 前端热更新不生效 | 低 | 低 | 浏览器缓存问题,强制刷新解决 |
---
## 六、实施记录
| 日期 | 操作 | 负责人 | 备注 |
|------|------|--------|------|
| 2026-07-28 | 编写需求文档和计划执行文档 | czj | 待实施 |
---
## 七、后续工作
| 优先级 | 待办 | 说明 |
|--------|------|------|
| P1 | 实施 Phase 1-4 | 按计划执行代码改造和部署 |
| P2 | 全量用例执行验证 | 迁移后执行 241 个 UI 用例验证 |
| P3 | 清理旧部署目录 | 确认新部署稳定后清理 `~/ubains-module-test/` 下的旧部署 |
| P4 | HTTPS 配置 | 为前端配置 Let's Encrypt 证书 |
| P5 | 编写自动化更新脚本 | 简化后续更新操作 |
---
## 八、附录
### 8.1 涉及改造的文件清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `deploy/Dockerfile` | 🆕 新建 | 合一应用镜像 |
| `deploy/docker-compose.yml` | 🔄 重写 | 2 服务编排 |
| `deploy/init.sql` | 🆕 新建 | MySQL 初始化 |
| `deploy/.env.example` | 🔄 更新 | 新增 MySQL 配置项 |
| `deploy/deploy.sh` | 🔄 重写 | 适配新目录 |
| `backend/app/main.py` | 🔄 改造 | 新增 StaticFiles 挂载前端 |
| `backend/requirements.txt` | 🔄 改造 | 新增 aiomysql + cryptography |
| `backend/scripts/migrate_sqlite_to_mysql.py` | 🆕 新建 | 数据迁移脚本 |
### 8.2 服务器目录结构
```
/data/third_party/plat-auto-test/
├── deploy/
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── init.sql
│ ├── .env
│ └── deploy.sh
├── backend/
│ ├── app/
│ ├── scripts/
│ └── requirements.txt
├── frontend/dist/
│ ├── index.html
│ └── assets/
├── data/
│ ├── screenshots/
│ └── reports/
├── mysql/data/
└── logs/
```
### 8.3 更新流程速查
| 场景 | 命令 |
|------|------|
| 更新前端 | `npm run build && scp -r dist/* ubains@5.60:/data/third_party/plat-auto-test/frontend/dist/` |
| 更新后端 | `scp -r backend/app ubains@5.60:.../backend/ && ssh ubains@5.60 'cd .../deploy && docker compose restart app'` |
| 更新依赖 | `docker compose build app && docker compose up -d app` |
| 查看日志 | `docker compose logs -f app` |
| 重启服务 | `docker compose restart app` |
| 备份数据库 | `docker exec plat-auto-test-mysql mysqldump -uroot -pXXX plat_auto_test > backup.sql` |
### 8.4 相关文档
| 文档 | 路径 |
|------|------|
| V2 部署升级 PRD | `Docs/PRD/需求文档/部署运维/_PRD_需求文档_V2部署架构升级_合一容器MySQL.md` |
| V1 部署方案 | `Docs/部署方案/Linux服务器部署方案.md` |
| V2 设计方案 | `Docs/部署方案/Linux容器化部署方案_v2.md` |
| 项目指南 | `CLAUDE.md` |
| UI 自动化交接 | `HANDOFF_UI自动化.md` |
---
*本文档由 Claude Code 生成,建议评审后按 Phase 1-4 顺序实施。*
\ No newline at end of file
# Linux 容器化部署方案(V2 - 合一容器 + MySQL)
> **文档版本**: v2.0
> **创建日期**: 2026-07-27
> **文档状态**: 初稿待评审
> **适用版本**: platform-auto-test v1.0.0
> **目标服务器**: 192.168.5.60 / Ubuntu 26.04 LTS
> **部署目录**: `/data/third_party/plat-auto-test/`
> **负责人**: czj
---
## 目录
1. [架构设计](#一架构设计)
2. [目录规划](#二目录规划)
3. [部署文件清单](#三部署文件清单)
4. [Dockerfile(前后端合一)](#四dockerfile前后端合一)
5. [Docker Compose 编排](#五docker-compose-编排)
6. [MySQL 配置](#六mysql-配置)
7. [Nginx 配置(可选)](#七nginx-配置可选)
8. [环境变量](#八环境变量)
9. [部署步骤](#九部署步骤)
10. [更新流程](#十更新流程)
11. [MySQL 迁移改造](#十一mysql-迁移改造)
12. [验收标准](#十二验收标准)
13. [运维指南](#十三运维指南)
14. [常见问题](#十四常见问题)
---
## 一、架构设计
### 1.1 架构对比
| 维度 | V1(原方案) | V2(本方案) |
|------|-------------|-------------|
| 容器数量 | 3 个(前端 Nginx + 后端 + SQLite) | **2 个(合一应用 + MySQL)** |
| 前端运行方式 | 独立 Nginx 容器 | **应用容器内 serve 静态文件** |
| 数据库 | SQLite(文件) | **MySQL 8.0(独立容器)** |
| 前端更新 | 需重启前端容器 | **热替换,无需重启** |
| 后端更新 | 需重启后端容器 | **重启应用容器(秒级)** |
| 部署目录 | `~/ubains-module-test/`(git 目录) | **`/data/third_party/plat-auto-test/`**(独立目录) |
| 与其他服务隔离 | 共用 git 目录 | **完全独立目录,无冲突** |
### 1.2 整体架构图
```
用户浏览器
┌───────────────────────────────────┐
│ 服务器 192.168.5.60 (Ubuntu) │
│ │
│ ┌─────────────────────────────┐ │
│ │ Docker Compose 网络 │ │
│ │ (plat-auto-test-net) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌────────┐ │ │
│ │ │ app 容器 │ │ mysql │ │ │
│ │ │ (合一) │──│ 容器 │ │ │
│ │ │ │ │ │ │ │
│ │ │ :8001 API │ │ :3306 │ │ │
│ │ │ :80 前端 │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌─────────┐ │ │ ┌────┐ │ │ │
│ │ │ │uvicorn │ │ │ │MySQL│ │ │ │
│ │ │ │(API) │ │ │ │ 8.0 │ │ │ │
│ │ │ ├─────────┤ │ │ └────┘ │ │ │
│ │ │ │静态文件 │ │ │ │ │ │
│ │ │ │serve │ │ │ │ │ │
│ │ │ ├─────────┤ │ │ │ │ │
│ │ │ │Playwright│ │ │ │ │ │
│ │ │ │Chromium │ │ │ │ │ │
│ │ │ └─────────┘ │ │ │ │ │
│ │ └──────┬──────┘ └────┬───┘ │ │
│ │ │ │ │ │
│ │ Volume 挂载 Volume 挂载 │ │
│ │ │ │ │ │
│ └─────────┼──────────────┼──────┘ │
│ │ │ │
│ ┌─────────▼──────────────▼──────┐ │
│ │ /data/third_party/ │ │
│ │ plat-auto-test/ │ │
│ │ ├── backend/ (源码) │ │
│ │ ├── frontend/dist/ (构建产物) │ │
│ │ ├── data/ (截图/报告) │ │
│ │ └── mysql/data/ (DB 数据) │ │
│ └────────────────────────────────┘ │
│ │
│ 另一维护平台 (8088) ← 独立,不受影响 │
└───────────────────────────────────────┘
```
### 1.3 容器职责划分
| 容器 | 职责 | 端口 | 镜像 |
|------|------|------|------|
| **app** | 运行 FastAPI(API + 前端静态文件 + Playwright) | 80 | 自构建(python:3.10-slim) |
| **mysql** | 运行 MySQL 8.0 数据库 | 3306(仅容器内) | mysql:8.0 |
### 1.4 为什么这样设计
**① 前后端合一容器**
- 前端是 Vue 构建后的静态文件(`dist/`),FastAPI 内置 `StaticFiles` 即可提供静态服务
- 无需独立 Nginx 容器,减少资源占用和管理复杂度
- 前端 API 调用走 `/api/*` 相对路径,同源访问,无跨域问题
**② MySQL 独立容器**
- 数据库是核心数据资产,独立运行便于备份、迁移、升级
- MySQL 8.0 支持原生 JSON 字段(项目大量使用 JSON 列)
- 数据持久化通过 volume 挂载,容器重建不丢数据
- 项目 ORM 用 SQLAlchemy 2.0,跨数据库兼容,迁移成本低
**③ 代码 volume 挂载**
- 后端代码、前端 `dist/` 都通过 volume 挂载到容器
- 更新前端:替换 `dist/` 文件即可,**无需重启容器**(FastAPI 静态服务实时读取文件系统)
- 更新后端:替换代码后重启容器,**秒级生效,无需重建镜像**
- 镜像只包含 Python 环境 + Playwright + 依赖,不打包业务代码
---
## 二、目录规划
### 2.1 服务器目录结构
```
/data/third_party/plat-auto-test/ ← 项目根目录(独立,无 git)
├── deploy/ ← 部署配置
│ ├── docker-compose.yml ← 服务编排
│ ├── Dockerfile ← 应用镜像(合一)
│ ├── init.sql ← MySQL 初始化脚本
│ ├── nginx.conf ← Nginx 配置(可选,见第七节)
│ ├── .env ← 环境变量(敏感信息)
│ └── deploy.sh ← 一键部署脚本
├── backend/ ← 后端源码(volume 挂载到 /app)
│ ├── app/
│ ├── scripts/
│ ├── requirements.txt
│ └── ...
├── frontend/
│ └── dist/ ← 前端构建产物(volume 挂载)
│ ├── index.html
│ ├── assets/
│ └── ...
├── data/ ← 应用数据(volume 挂载)
│ ├── screenshots/ ← 执行截图
│ └── reports/ ← 测试报告
├── mysql/
│ └── data/ ← MySQL 数据持久化
└── logs/ ← 日志(volume 挂载)
```
### 2.2 目录用途与权限
| 目录 | 用途 | 挂载到容器 | 权限 |
|------|------|-----------|------|
| `deploy/` | 部署配置文件 | 不挂载 | root |
| `backend/` | 后端源码 | `/app` | ubains |
| `frontend/dist/` | 前端构建产物 | `/app/frontend/dist` | ubains |
| `data/` | 截图、报告 | `/app/data` | ubains(1000:1000) |
| `mysql/data/` | MySQL 数据 | `/var/lib/mysql` | mysql(999:999) |
| `logs/` | 应用日志 | `/app/logs` | ubains |
---
## 三、部署文件清单
| 文件 | 说明 | 状态 |
|------|------|------|
| `deploy/Dockerfile` | 合一应用镜像 | 🆕 新建 |
| `deploy/docker-compose.yml` | 服务编排(app + mysql) | 🆕 重写 |
| `deploy/init.sql` | MySQL 数据库初始化 | 🆕 新建 |
| `deploy/.env` | 环境变量 | 🆕 新建 |
| `deploy/deploy.sh` | 一键部署脚本 | 🔄 改写 |
| `backend/app/main.py` | 挂载前端静态文件 | 🔄 改造 |
| `backend/app/config.py` | MySQL 连接配置 | 🔄 改造 |
| `backend/requirements.txt` | 新增 aiomysql 依赖 | 🔄 改造 |
---
## 四、Dockerfile(前后端合一)
**文件**: `deploy/Dockerfile`
```dockerfile
FROM python:3.10-slim
WORKDIR /app
# 安装系统依赖(Playwright 需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
wget gnupg curl \
libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 \
libcups2t64 libdrm2 libdbus-1-3 libxkbcommon0 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libpango-1.0-0 libcairo2 libasound2t64 \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖(使用国内镜像源)
COPY requirements.txt .
RUN pip install --no-cache-dir \
-i https://pypi.tuna.tsinghua.edu.cn/simple \
--trusted-host pypi.tuna.tsinghua.edu.cn \
--retries 5 --timeout 120 -r requirements.txt
# 安装 Playwright Chromium
RUN playwright install chromium
# 创建必要目录
RUN mkdir -p /app/data/screenshots /app/data/reports \
/app/frontend/dist /app/logs
# 注意:以下内容通过 volume 挂载,不 COPY
# - backend/ 源码 → /app
# - frontend/dist/ → /app/frontend/dist
# - data/ → /app/data
# - logs/ → /app/logs
EXPOSE 80
# 启动 uvicorn,监听 80 端口(同时提供 API + 静态文件)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
```
**关键点**
- 镜像只装环境(Python + Playwright + 依赖),**不打包业务代码**
- 业务代码、前端 dist、数据、日志全部 volume 挂载
- uvicorn 监听 80 端口,FastAPI 同时提供 API 和前端静态服务
- 首次构建后,**后续更新代码无需重建镜像**
---
## 五、Docker Compose 编排
**文件**: `deploy/docker-compose.yml`
```yaml
version: "3.8"
services:
# ==================== MySQL 数据库 ====================
mysql:
image: mysql:8.0
container_name: plat-auto-test-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-Ubains@13579}
MYSQL_DATABASE: ${MYSQL_DATABASE:-plat_auto_test}
MYSQL_USER: ${MYSQL_USER:-platapp}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-PlatApp@2026}
TZ: Asia/Shanghai
volumes:
# 数据持久化
- /data/third_party/plat-auto-test/mysql/data:/var/lib/mysql
# 初始化脚本(仅首次启动执行)
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "3307:3306" # 映射到宿主机 3307,避免与本地 MySQL 冲突
networks:
- plat-auto-test-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-Ubains@13579}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-authentication-plugin=mysql_native_password
- --max_connections=200
# ==================== 应用容器(前后端合一) ====================
app:
build:
context: ../backend
dockerfile: ../deploy/Dockerfile
container_name: plat-auto-test-app
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
environment:
- DATABASE_URL=mysql+aiomysql://${MYSQL_USER:-platapp}:${MYSQL_PASSWORD:-PlatApp@2026}@mysql:3306/${MYSQL_DATABASE:-plat_auto_test}?charset=utf8mb4
- PLAYWRIGHT_HEADLESS=true
- PLAYWRIGHT_TIMEOUT=30000
- LOG_LEVEL=INFO
- TZ=Asia/Shanghai
volumes:
# 后端源码(更新后端只需重启容器)
- /data/third_party/plat-auto-test/backend:/app
# 前端构建产物(更新前端无需重启,热替换)
- /data/third_party/plat-auto-test/frontend/dist:/app/frontend/dist
# 应用数据(截图、报告)
- /data/third_party/plat-auto-test/data:/app/data
# 日志
- /data/third_party/plat-auto-test/logs:/app/logs
ports:
- "80:80"
networks:
- plat-auto-test-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
plat-auto-test-net:
driver: bridge
```
**关键设计**
- `mysql` 容器先启动并通过健康检查后,`app` 才启动(`depends_on` + `condition: service_healthy`
- `app``DATABASE_URL` 用容器服务名 `mysql` 作为主机名(Docker 内部 DNS)
- MySQL 宿主机映射 `3307` 端口(避免与服务器本地 3306 冲突,方便外部调试)
- `build.context` 指向 `backend/`,这样 `requirements.txt` 路径正确
---
## 六、MySQL 配置
### 6.1 初始化脚本
**文件**: `deploy/init.sql`
```sql
-- 平台自动化测试系统 - MySQL 初始化脚本
-- 注意:MYSQL_DATABASE 环境变量会自动创建数据库,此脚本用于补充设置
-- 确保字符集正确
ALTER DATABASE plat_auto_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 授予 platapp 用户完整权限(MYSQL_USER 已自动创建并授予当前库权限,此处确认)
GRANT ALL PRIVILEGES ON plat_auto_test.* TO 'platapp'@'%';
FLUSH PRIVILEGES;
-- 时区设置(避免 SQLAlchemy 时间字段时区问题)
SET GLOBAL time_zone = '+08:00';
SET SESSION time_zone = '+08:00';
```
### 6.2 表结构创建
**无需手动建表**。应用首次启动时,`main.py``lifespan` 会调用 `init_db()``Base.metadata.create_all()`,根据 SQLAlchemy 模型自动在 MySQL 中创建所有表:
| 表名 | 说明 |
|------|------|
| `modules` | 测试模块 |
| `test_cases` | 测试用例 |
| `executions` | 执行记录 |
| `case_results` | 用例结果 |
| `case_dependencies` | 用例依赖 |
| `security_configs` | 安全测试配置 |
| `vulnerability_results` | 漏洞结果 |
### 6.3 MySQL 字段兼容性分析
| 字段类型 | SQLite | MySQL 8.0 | 兼容性 |
|---------|--------|-----------|--------|
| `String(32)` 主键(UUID hex) | TEXT | VARCHAR(32) | ✅ 兼容 |
| `String(200)` | TEXT | VARCHAR(200) | ✅ 兼容 |
| `Text` | TEXT | TEXT | ✅ 兼容 |
| `JSON` | JSON | JSON | ✅ 兼容(MySQL 8.0+ 原生支持) |
| `Integer` | INTEGER | INT | ✅ 兼容 |
| `Float` | REAL | FLOAT | ✅ 兼容 |
| `Boolean` | BOOLEAN | TINYINT(1) | ✅ 兼容 |
| `DateTime` | DATETIME | DATETIME | ✅ 兼容 |
**唯一需要适配的代码**(已自动处理):
- `cleanup_service.py:350``VACUUM`(SQLite 专用)→ 已有 `if "sqlite" not in` 保护,MySQL 自动跳过 ✅
- `database.py:32``check_same_thread`(SQLite 专用)→ 已有条件判断 ✅
---
## 七、Nginx 配置(可选)
> **说明**:本方案默认用 FastAPI 直接 serve 静态文件,**无需 Nginx**。
> 如果未来需要 HTTPS、负载均衡、或更高的静态文件性能,可再加 Nginx 反向代理。
如果需要,**文件**: `deploy/nginx.conf`
```nginx
server {
listen 80;
server_name _;
# 前端静态文件(如需 Nginx 托管)
location / {
root /app/frontend/dist;
try_files $uri $uri/ /index.html;
}
# API 反向代理
location /api/ {
proxy_pass http://app:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 300s;
}
# WebSocket 代理
location /api/executions/ws/ {
proxy_pass http://app:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
}
}
```
---
## 八、环境变量
**文件**: `deploy/.env`
```bash
# ==================== MySQL 配置 ====================
MYSQL_ROOT_PASSWORD=Ubains@13579
MYSQL_DATABASE=plat_auto_test
MYSQL_USER=platapp
MYSQL_PASSWORD=PlatApp@2026
# ==================== 应用配置 ====================
# DATABASE_URL 由 docker-compose 根据 MYSQL_* 变量自动拼接,无需手动设置
PLAYWRIGHT_HEADLESS=true
PLAYWRIGHT_TIMEOUT=30000
LOG_LEVEL=INFO
```
> ⚠️ **安全提示**:生产环境请修改 `MYSQL_ROOT_PASSWORD` 和 `MYSQL_PASSWORD` 为强密码。
> `.env` 文件不要提交到 git。
---
## 九、部署步骤
### Phase 1: 服务器准备(首次)
```bash
# SSH 到服务器
ssh ubains@192.168.5.60
# 停掉旧的 V1 容器(如存在)
cd ~/ubains-module-test/deploy 2>/dev/null && docker compose down 2>/dev/null
# 创建项目目录
sudo mkdir -p /data/third_party/plat-auto-test/{deploy,backend,frontend/dist,data/screenshots,data/reports,mysql/data,logs}
# 设置权限
sudo chown -R ubains:ubains /data/third_party/plat-auto-test
```
### Phase 2: 上传部署配置
从本地(Windows)上传 `deploy/` 目录下所有文件到服务器:
```powershell
# 本地执行
scp -r deploy/* ubains@192.168.5.60:/data/third_party/plat-auto-test/deploy/
```
### Phase 3: 上传后端源码
```powershell
# 本地:上传 backend/ 到服务器(排除 pycache)
scp -r backend/app backend/scripts backend/requirements.txt ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/
```
### Phase 4: 构建并上传前端
```powershell
# 本地:构建前端
cd frontend
npm run build
# 上传构建产物
scp -r dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
```
### Phase 5: 构建镜像并启动
```bash
# 服务器上执行
ssh ubains@192.168.5.60
cd /data/third_party/plat-auto-test/deploy
# 构建镜像(首次需要,约 3-5 分钟)
docker compose build
# 启动服务
docker compose up -d
# 查看启动状态
docker compose ps
docker compose logs -f app # 看应用日志,确认连上 MySQL
```
### Phase 6: 验证
```bash
# 健康检查
curl http://localhost/health
# 期望: {"status":"healthy",...}
# 前端可访问
curl -s -o /dev/null -w "%{http_code}" http://localhost/
# 期望: 200
# 数据库连接验证(查看表是否自动创建)
docker exec plat-auto-test-mysql mysql -uplatapp -pPlatApp@2026 plat_auto_test -e "SHOW TABLES;"
```
### Phase 7: 数据迁移(从本地 SQLite)
由于从 SQLite 迁移到 MySQL,需要导出本地数据并导入:
```bash
# 本地:从 SQLite 导出数据为 SQL
# (后续编写迁移脚本,见第十一节)
```
---
## 十、更新流程
> **核心优势**:镜像构建后,更新代码无需重建镜像,秒级生效。
### 10.1 更新前端(无需重启服务)
```powershell
# 本地改完前端代码后,构建
cd frontend
npm run build
# 上传到服务器(直接覆盖 dist/)
scp -r dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
```
**前端无需重启**:FastAPI 静态文件服务实时读取文件系统,浏览器刷新即可看到新版本。
### 10.2 更新后端(需重启服务,秒级)
```powershell
# 本地改完后端代码后,上传
scp -r backend/app ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/
```
```bash
# 服务器:重启应用容器(约 5-10 秒)
ssh ubains@192.168.5.60
cd /data/third_party/plat-auto-test/deploy
docker compose restart app
```
### 10.3 更新数据库配置(需重启 MySQL)
```bash
# 修改 .env 后重启 MySQL
nano .env
docker compose restart mysql app
```
### 10.4 更新依赖(需重建镜像)
仅当 `requirements.txt` 改变时才需要:
```bash
cd /data/third_party/plat-auto-test/deploy
docker compose build app
docker compose up -d app
```
---
## 十一、MySQL 迁移改造
### 11.1 后端代码改造清单
| 文件 | 改造内容 | 说明 |
|------|---------|------|
| `backend/app/main.py` | 新增挂载前端静态文件 | FastAPI serve SPA |
| `backend/app/config.py` | DATABASE_URL 默认改 MySQL(或保持环境变量) | 兼容 SQLite/MySQL |
| `backend/requirements.txt` | 新增 `aiomysql``cryptography` | MySQL 异步驱动 |
| `backend/app/database.py` | 无需改动 | 已做条件判断 |
### 11.2 main.py 改造(挂载前端静态文件)
`main.py` 末尾添加:
```python
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pathlib import Path
# ==================== 前端静态文件服务 ====================
FRONTEND_DIST = Path(__file__).parent.parent / "frontend" / "dist"
if FRONTEND_DIST.exists():
# 挂载静态资源目录
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
# SPA 路由:所有非 /api 路径回退到 index.html
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""前端 SPA 入口,所有非 API 路径返回 index.html"""
file_path = FRONTEND_DIST / full_path
if file_path.exists() and file_path.is_file():
return FileResponse(str(file_path))
return FileResponse(str(FRONTEND_DIST / "index.html"))
```
### 11.3 requirements.txt 改造
```diff
fastapi==0.110.0
uvicorn[standard]==0.27.0
sqlalchemy==2.0.25
aiosqlite==0.19.0 # 保留,开发环境仍用 SQLite
+ aiomysql==0.2.0 # 新增,MySQL 异步驱动
+ cryptography==42.0.0 # 新增,MySQL 认证需要
pydantic==2.5.3
python-multipart==0.0.6
playwright==1.40.0
websockets==12.0
requests==2.31.0
```
### 11.4 数据迁移脚本
从本地 SQLite 导出到 MySQL,编写脚本 `backend/scripts/migrate_sqlite_to_mysql.py`
```python
#!/usr/bin/env python
"""
SQLite → MySQL 数据迁移脚本
将本地 backend/data/test_platform.db 的数据迁移到 MySQL
"""
# 详见后续实现
```
---
## 十二、验收标准
### 12.1 功能验收
| 验收项 | 操作 | 期望结果 |
|--------|------|---------|
| 目录独立 | 检查 `/data/third_party/plat-auto-test/` | 文件结构完整 |
| 容器运行 | `docker compose ps` | app + mysql 状态 running/healthy |
| API 健康 | `curl http://localhost/health` | 返回 `{"status":"healthy"}` |
| 前端访问 | 浏览器打开 `http://192.168.5.60` | 显示登录页/首页 |
| 数据库连接 | 应用日志无连接错误 | 表自动创建 |
| 前端更新 | 替换 dist/ 后刷新浏览器 | 新版本生效(无需重启) |
| 后端更新 | 重启 app 容器后调用 API | 新代码生效 |
| 已有服务 | 访问 8088 端口 | 另一维护平台正常 |
### 12.2 性能验收
| 验收项 | 标准 |
|--------|------|
| 应用容器启动时间 | < 15 秒(等待 MySQL 健康检查) |
| API 响应时间 | 健康检查 < 100ms |
| 后端重启时间 | < 10 秒 |
| 前端首屏加载 | < 3 秒 |
---
## 十三、运维指南
### 13.1 常用命令
```bash
cd /data/third_party/plat-auto-test/deploy
# 查看状态
docker compose ps
# 查看日志
docker compose logs -f app # 应用日志
docker compose logs -f mysql # 数据库日志
# 重启服务
docker compose restart app # 重启应用(更新后端后用)
docker compose restart mysql # 重启数据库
# 停止/启动
docker compose down # 停止所有服务
docker compose up -d # 启动所有服务
# 进入容器
docker exec -it plat-auto-test-app bash
docker exec -it plat-auto-test-mysql mysql -uplatapp -pPlatApp@2026 plat_auto_test
```
### 13.2 数据备份
```bash
# MySQL 备份
docker exec plat-auto-test-mysql mysqldump -uroot -pUbains@13579 \
plat_auto_test > /data/third_party/plat-auto-test/backup_$(date +%Y%m%d).sql
# 定时备份(crontab)
# 每天凌晨 3 点备份
0 3 * * * docker exec plat-auto-test-mysql mysqldump -uroot -pUbains@13579 plat_auto_test > /data/third_party/plat-auto-test/backup/backup_$(date +\%Y\%m\%d).sql 2>/dev/null
```
### 13.3 数据恢复
```bash
# 从备份恢复
docker exec -i plat-auto-test-mysql mysql -uroot -pUbains@13579 plat_auto_test < backup_20260727.sql
```
### 13.4 截图/报告清理
```bash
# 清理 30 天前的截图和报告
find /data/third_party/plat-auto-test/data/screenshots -type f -mtime +30 -delete
find /data/third_party/plat-auto-test/data/reports -type f -mtime +30 -delete
```
---
## 十四、常见问题
### 14.1 应用连不上 MySQL
**现象**:app 容器日志报 `Can't connect to MySQL server`
**排查**
```bash
# 1. 检查 MySQL 容器状态
docker compose ps mysql
# 2. 检查 MySQL 健康状态
docker exec plat-auto-test-mysql mysqladmin ping -uroot -pUbains@13579
# 3. 检查网络
docker exec plat-auto-test-app ping mysql
# 4. 检查 DATABASE_URL
docker exec plat-auto-test-app env | grep DATABASE_URL
```
### 14.2 表未自动创建
**现象**:API 报错表不存在
**原因**:MySQL 启动慢,app 连接时数据库未就绪
**解决**
```bash
# 手动重启 app(MySQL 就绪后)
docker compose restart app
```
### 14.3 Playwright 在容器中执行失败
**现象**:执行用例报 `Missing libraries`
**解决**:确认 Dockerfile 中系统依赖完整(已包含所有 Playwright 依赖库)
### 14.4 前端更新不生效
**现象**:替换 dist/ 后浏览器还是旧版本
**解决**
- 浏览器强制刷新(Ctrl+F5)清除缓存
- 或在浏览器开发者工具禁用缓存
### 14.5 MySQL 端口冲突
**现象**:MySQL 容器启动失败,提示 3306 被占用
**解决**:修改 `docker-compose.yml``ports: "3307:3306"` 为其他端口
### 14.6 WebSocket 连接失败
**现象**:执行页面进度不实时更新
**原因**:如使用 Nginx,需配置 WebSocket 升级头(见第七节)
---
## 附录
### A. 部署架构演进路线
| 阶段 | 架构 | 状态 |
|------|------|------|
| V1 | 前端 Nginx + 后端 + SQLite(代码 COPY 进镜像) | ✅ 已部署 |
| V2 | 合一应用 + MySQL(代码 volume 挂载) | 📋 本方案 |
| V3(远期) | 应用 + MySQL + Redis 缓存 + HTTPS + 监控 | 🔮 规划中 |
### B. 相关文档
| 文档 | 路径 |
|------|------|
| V1 部署方案 | `Docs/部署方案/Linux服务器部署方案.md` |
| V1 执行计划 | `Docs/部署方案/Linux容器化部署_执行计划.md` |
| 容器化优化 PRD | `Docs/PRD/需求文档/部署运维/_PRD_需求文档_容器化部署Volume挂载优化.md` |
| 容器化优化计划 | `Docs/PRD/需求文档/部署运维/_执行计划_容器化部署Volume挂载优化.md` |
| UI 自动化交接 | `HANDOFF_UI自动化.md` |
| 项目指南 | `CLAUDE.md` |
### C. 服务器信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 192.168.5.60 |
| SSH 用户 | ubains |
| 操作系统 | Ubuntu 26.04 LTS |
| 已有服务 | 8088 端口(另一维护平台) |
| 新部署目录 | `/data/third_party/plat-auto-test/` |
| 前端地址 | http://192.168.5.60 |
| API 文档 | http://192.168.5.60/docs |
| MySQL 外部访问 | 192.168.5.60:3307 |
---
*本文档由 Claude Code 编写,建议根据实际服务器环境调整参数。部署前请评审确认架构方案。*
# HANDOFF — 平台自动化测试交接文档(UI自动化窗口 # HANDOFF — 平台自动化测试系统交接文档(V2 部署架构升级
> **生成时间**: 2026-07-22 > **生成时间**: 2026-07-28
> **当前分支**: `platform-auto-test` > **当前分支**: `platform-auto-test`
> **最近提交**: `a9074f8c` fix(executor): 修复FastAPI端点执行Playwright报asyncio检测错误 > **最近提交**: `c3417d53` docs(deploy): 新增容器化部署Volume挂载优化需求文档和执行计划文档
> **状态**: ✅ 深层交互用例步骤优化完成,通过率从 30% 提升到 76% > **状态**: ✅ V2 部署架构升级完成并上线(app + mysql 双容器运行中,数据迁移完成,未提交 git)
--- ---
...@@ -15,9 +15,13 @@ ...@@ -15,9 +15,13 @@
- Playwright 执行互斥(不可同时执行用例) - Playwright 执行互斥(不可同时执行用例)
- Git 提交协作 - Git 提交协作
**当前多窗口状态**:本项目有两个窗口同时在开发: **当前多窗口状态**:本项目有多个窗口分工开发,交接文档分散在:
- **本窗口**:UI 自动化测试模块(会议管理、数据分析、运维管理、管理看板) - `HANDOFF.md`(本文档)— **V2 部署架构升级**(最新,本次会话)
- **另一窗口**:安全测试模块(API安全测试、华为红线检查、漏洞回归等)— 交接文档见 `HANDOFF_安全测试.md` - `HANDOFF_V2部署升级.md` — V2 部署详细交接(与本文档同步,含完整运维命令)
- `HANDOFF_UI自动化.md` — UI 自动化测试模块
- `HANDOFF_安全测试.md` — 安全测试模块
**下次会话优先读本文档**(V2 部署已完成上线,但有未提交变更需处理)。
--- ---
...@@ -25,139 +29,111 @@ ...@@ -25,139 +29,111 @@
**平台自动化测试可视化系统 (platform-auto-test)** — Web 可视化自动化测试平台,核心创新是用例录制器。 **平台自动化测试可视化系统 (platform-auto-test)** — Web 可视化自动化测试平台,核心创新是用例录制器。
- **技术栈**: FastAPI + SQLAlchemy(async) + SQLite + Playwright(同步API) + WebSocket / Vue 3 + Element Plus + Vite 5 + TypeScript + ECharts - **技术栈**: FastAPI + SQLAlchemy(async) + MySQL 8.0(生产)/ SQLite(本地)+ Playwright(同步API) + WebSocket / Vue 3 + Element Plus + Vite 5 + TypeScript
- **被测系统**: 统一管理平台 (https://192.168.5.44) - **被测系统**: 统一管理平台 (https://192.168.5.44)
- **登录凭据**: admin@xty / Ubains@13579 · 验证码固定 `csba` - **登录凭据**: admin@xty / Ubains@13579 · 验证码固定 `csba`
- **远程仓库**: http://git.ubainsyun.com/bing/ubains-module-test.git - **远程仓库**: http://git.ubainsyun.com/bing/ubains-module-test.git
- **当前分支**: `platform-auto-test`(主分支:`main` - **当前分支**: `platform-auto-test`(主分支:`master`
### 目录结构(关键路径 ### V2 部署架构(本次会话上线
``` ```
platform-auto-test/ 服务器 192.168.5.60 (Ubuntu)
├── backend/app/ └── Docker Compose 网络 (plat-auto-test-net)
│ ├── main.py # FastAPI 入口(Windows ProactorEventLoop) ├── app 容器 (合一) :80 FastAPI(API+前端静态+Playwright+WebSocket)
│ ├── executors/ │ ├── backend/ → volume 挂载源码(重启容器秒级生效)
│ │ └── playwright_executor.py # UI测试执行引擎(13种动作+11种断言,支持iframe) │ ├── frontend/dist/ → volume 挂载(替换文件热更新)
│ ├── services/execution_service.py # 执行调度(含 run_all_cases_sync) │ ├── data/ → 截图/报告
│ └── scripts/ │ └── logs/ → 日志
│ ├── create_deep_interaction_cases_v2.py # 深层用例创建脚本(已优化) └── mysql 容器 :3307(外部)/3306(内部)
│ ├── test_executor_direct.py # 执行器直接调用测试 └── mysql/data/ → volume 持久化
│ └── test_single_case.py # FastAPI端点单用例测试
├── frontend/src/ 部署目录: /data/third_party/plat-auto-test/(独立,与 8088 维护平台隔离)
│ ├── views/Cases.vue # 用例管理
│ ├── views/Execution.vue # 执行中心
│ └── ...
├── Docs/PRD/
│ ├── 需求文档/用例管理/
│ │ ├── _PRD_需求文档_深层交互用例步骤优化.md # 本次P0需求文档
│ │ └── _执行计划_深层交互用例步骤优化.md # 本次P0计划执行文档
│ └── ...
└── HANDOFF.md # 本文档
``` ```
--- ---
## 二、本次会话任务(已完成) ## 二、本次会话任务(已完成)
### 深层交互用例步骤优化 ✅ ### V2 部署架构升级 — 合一容器 + MySQL ✅
**问题背景**:25 个深层交互用例执行通过率仅约 30%,主要问题:canvas 等待超时、重复登录、选择器不精确、导航等待不足。
**完成的优化**
1. **删除冗余登录步骤** **任务背景**:将部署从 V1(3 容器:前端 Nginx + 后端 + SQLite)升级到 V2(2 容器:合一 app + MySQL 8.0),解决代码更新需重建镜像、前端无法热更新、SQLite 并发锁、部署目录与 8088 维护平台共用等问题。
- 移除了 `build_base_steps()` 函数(8步登录流程)
- 用例步骤从"导航到目标菜单"开始,`auto_login=True` 由执行器处理
- 步骤 order 从 1 开始连续编号
2. **优化 canvas 等待策略** **完成的工作**
- canvas 等待超时从 15s 提升到 30s
- 增加 ECharts 特定选择器回退链:`[".echarts", ".chart-container canvas", "canvas", "[class*='chart']"]`
- 增加"等待图表渲染完成"步骤(3s 延时确保 ECharts 完成绘制)
3. **优化导航等待策略** 1. **本地代码改造**
- 导航后等待目标页面特征元素(canvas/.el-table),而非仅 `wait body` - 新建合一 Dockerfile(镜像仅装环境,代码 volume 挂载)
- 增加"等待页面渲染完成"步骤(3s 延时应对动画) - 重写 docker-compose.yml(2 服务:app + mysql)
- 新建 init.sql(MySQL 字符集 + 权限 + 时区)
- 改造 main.py(新增 StaticFiles 挂载前端 + SPA 路由回退)
- 改造 requirements.txt(新增 aiomysql + cryptography)
- 改造 database.py(区分 SQLite/MySQL 连接参数)
- 编写 migrate_sqlite_to_mysql.py(断点续传 + 外键处理)
4. **优化选择器精确度** 2. **服务器部署**
- 筛选器选择器增加上下文限定(`.el-select:nth-child(n) .el-input` - 停旧容器、创建独立目录、上传部署配置与源码、构建镜像并启动
- 下拉选项选择器增加可见性限定(`.el-select-dropdown:visible`
- 使用 `selectors` 列表提供多选器回退链
5. **修复执行器 `_do_wait` 方法** 3. **数据迁移**
- 增加快速检查阶段:先 1s 超时快速检查每个选择器 - 7 张表全部迁移:modules(28) + test_cases(283) + executions(636) + case_results(926) + case_dependencies(0) + security_configs(1) + vulnerability_results(69)
- 将总超时均分给每个选择器,避免 4 个选择器 × 30s = 120s
- 效果:canvas 等待从 120s 降到 4s
6. **修复执行器 `element_count_min` 断言** 4. **清理旧部署**
- 支持 `params.min_count` 参数(之前仅支持 `params.expected` - 已删除服务器旧目录 `~/ubains-module-test/deploy/`
- 向后兼容:`min_count` 不存在时回退到 `expected`
7. **脚本支持更新已有用例** ### 最终验证结果(全部通过)
- 新增 `update_or_create_case()` 函数
- 按名称查找已有用例,找到则更新,否则创建
**执行结果** | 验收项 | 结果 |
|--------|------|
| 容器状态 | app + mysql 双容器 **healthy** |
| 前端访问 (http://192.168.5.60) | **HTTP 200** |
| SPA 路由 (/login /modules /cases 等) | **HTTP 200**(回退 index.html) |
| API 路由 (/api/modules /api/cases /docs) | **HTTP 200** |
| 健康检查 (/health) | `{"status":"healthy"}` |
| 模块 / 用例数据 | **28 模块 / 283 用例** ✅ |
| 8088 维护平台隔离 | **HTTP 302**(不受影响) |
| 指标 | 优化前 | 优化后 | ---
|------|--------|--------|
| 通过率 | ~30% | **76%** (19/25) |
| 单用例耗时 | 120-130s | **10-20s** |
| 重复登录 | 有 | **无** |
**失败用例分析**(6 个,均为业务数据/页面结构问题): ## 三、本次会话解决的 4 个关键问题(踩坑记录)
| 用例 | 失败原因 | 类型 | ### 问题 1:MySQL 密码含 `@` 导致连接失败 🔴
|------|---------|------|
| 管理看板-表格数据验证 | 表格数据行 0 行(被测系统数据为空) | 数据问题 |
| 会议概览-筛选器交互验证 | canvas 等待超时(页面无 canvas) | 页面结构 |
| 会议室概览-筛选器验证 | canvas 等待超时(页面无 canvas) | 页面结构 |
| 会议统计-下拉筛选验证 | 筛选器选择器不匹配 | 选择器 |
| 历史记录-RSE-表格数据验证 | 菜单不存在(名称不正确) | 菜单名称 |
| 通知统计-筛选器验证 | 页面无 .el-select 元素 | 页面结构 |
**相关文档** - **现象**`Can't connect to MySQL server on '2026@mysql'`
- `Docs/PRD/需求文档/用例管理/_PRD_需求文档_深层交互用例步骤优化.md` - **根因**:密码 `PlatApp@2026` 中的 `@` 被 DATABASE_URL 当作密码/主机分隔符,主机名被解析为 `2026@mysql`
- `Docs/PRD/需求文档/用例管理/_执行计划_深层交互用例步骤优化.md` - **修复**:密码改为不含 URL 特殊字符的 `PlatApp2026`(改 `.env` + `docker-compose.yml` 默认值 + 迁移脚本)。改密码后**必须清理 `mysql/data/` 目录重新初始化**,否则旧密码用户残留 → Access denied
--- ### 问题 2:ID 字段 `varchar(32)` 装不下实际 ID 🔴
## 三、历史已完成事项 - **现象**`Data too long for column 'id' at row 1`
- **根因**:模型 `String(32)`,但 `id_generator.py` 带前缀 ID(如 `module_432a65347d624b638f9623fe303af957`)有 39 字符。SQLite 不校验长度,MySQL 严格校验
- **修复**:所有 ID/外键字段 `String(32)``String(64)`(7 个 model 文件,15 处)
### 3.1 用例创建 ### 问题 3:外键约束导致迁移中途失败 🔴
1. ✅ 创建了 25 个深层交互用例脚本 `create_deep_interaction_cases_v2.py` - **现象**`foreign key constraint fails (modules.parent_id REFERENCES modules.id)`
2. ✅ 用例成功写入数据库(数据库总用例数:141个) - **根因**:自引用外键插入顺序问题。SQLite 默认不强制外键,MySQL 强制
- 管理看板:13个(+6) - **修复**:迁移脚本迁移前 `SET FOREIGN_KEY_CHECKS=0`,完成后再开启
- 运维管理:15个(+7)
- 数据分析:34个(+12)
### 3.2 用例步骤修复 ### 问题 4:aiomysql 与 pool_pre_ping 不兼容 → 部分 API 500 🔴
1.**删除了"点击展开功能中心"步骤**:登录后 .block 菜单已可见,无需额外点击 - **现象**`AsyncAdapt_aiomysql_connection.ping() missing 'reconnect'``/api/modules` 等 API 500
2.**删除了登录步骤**`auto_login=True` 时执行器会自动登录,用例步骤不应包含登录步骤 - **根因**:SQLAlchemy 的 `pool_pre_ping=True` 调 ping() 时未传 aiomysql 必需的 `reconnect` 参数
- **修复**`database.py` 中 MySQL 连接**禁用 `pool_pre_ping`**,改用 `pool_recycle=3600`;SQLite 仍保留 `pool_pre_ping`。按 DATABASE_URL 是否含 `sqlite` 自动判断
### 3.3 首页探索发现 ---
- 首页有 29 个 .block 菜单卡片(前24个和后5个重复) ## 四、卡点和问题
- 登录后 .block 元素已可见,不需要点击"展开功能中心"
- 点击 `.block:has-text("管理看板")` 可正常跳转
--- ### 4.1 ⚠️ 本次所有变更尚未 git commit(最紧急)
## 四、待解决问题 V2 部署的代码改造(7 个 model + database.py + main.py + 部署配置 + 迁移脚本)**全部未提交**。下次会话首要任务是用 `/GitCommit` 提交。未提交文件清单见第六节。
### 4.1 6 个失败用例需针对性修复 ### 4.2 新 MySQL 环境的批量执行验证未做
1. **会议概览/会议室概览**:页面可能没有 canvas 图表,需探索实际 DOM 结构 部署和数据迁移已验证,但**未在新环境批量执行 UI 用例**验证 Playwright 在容器内运行 + MySQL 并发写入无 `database is locked`
2. **会议统计-下拉筛选验证**:筛选器选择器需调整
3. **历史记录-RSE**:菜单名称可能不准确,需确认实际菜单文本
4. **通知统计/管理看板表格**:页面结构/数据可能与预期不符
### 4.2 执行器端口问题 ### 4.3 前端热更新未实测
8001 端口有僵尸进程(旧 uvicorn --reload),强制 kill 后仍会重生。当前通过 8002 端口绕过。需要找到并清理所有相关进程 热更新机制已实现(volume 挂载 dist/),但未实际「改前端→build→上传→刷新」验证
--- ---
...@@ -165,15 +141,57 @@ platform-auto-test/ ...@@ -165,15 +141,57 @@ platform-auto-test/
| 优先级 | 待办 | 说明 | | 优先级 | 待办 | 说明 |
|--------|------|------| |--------|------|------|
| **P0** | 修复 6 个失败用例 | 探索实际 DOM 结构,调整选择器和等待策略 | | **P0** | **git commit 提交本次变更** | model 字段改造 + 部署配置 + 迁移脚本,用 `/GitCommit` skill |
| **P1** | 提交代码 | 脚本优化 + 执行器修复 + 文档 待 git commit | | **P1** | 新环境批量执行 UI 用例 | 验证 Playwright 容器内运行 + MySQL 无并发锁 |
| **P1** | 清理 8001 端口僵尸进程 | 旧 uvicorn --reload 进程未清理 | | **P1** | 实测前端热更新 | 改前端→build→上传→刷新验证 |
| **P2** | 批量执行全量用例 | 验证 117 个 UI 用例的执行情况 | | **P2** | HTTPS 配置 | 为前端配置 Let's Encrypt 证书 |
| **P3** | 清理临时脚本 | 根目录有多个临时探索脚本待清理 | | **P3** | 编写自动化更新脚本 | 简化后续前端/后端更新操作 |
---
## 六、未提交变更(待 git commit)
### 修改的文件
| 文件 | 变更说明 |
|------|---------|
| `backend/app/main.py` | 新增 StaticFiles 挂载前端 + SPA 路由回退(serve_spa) |
| `backend/app/database.py` | 区分 SQLite/MySQL 连接参数,MySQL 用 pool_recycle 替代 pool_pre_ping |
| `backend/app/models/module.py` | ID 字段 `String(32)``String(64)` |
| `backend/app/models/test_case.py` | ID/外键字段 `String(32)``String(64)` |
| `backend/app/models/execution.py` | ID 字段 `String(32)``String(64)` |
| `backend/app/models/case_result.py` | ID/外键字段 `String(32)``String(64)` |
| `backend/app/models/case_dependency.py` | ID/外键字段 `String(32)``String(64)` |
| `backend/app/models/security_config.py` | ID 字段 `String(32)``String(64)` |
| `backend/app/models/vulnerability_result.py` | ID/外键字段 `String(32)``String(64)` |
| `backend/requirements.txt` | 新增 `aiomysql==0.2.0` + `cryptography==42.0.0` |
| `deploy/docker-compose.yml` | 重写为 2 服务(app + mysql),密码改为 `PlatApp2026` |
| `deploy/.env.example` | 新增 MySQL 配置,密码改为 `PlatApp2026` |
| `deploy/deploy.sh` | 重写,适配新目录和新架构 |
| `CLAUDE.md` | 踩坑记录新增 #10-#13(MySQL 相关 4 条) |
### 新增的文件
| 文件 | 说明 |
|------|------|
| `deploy/Dockerfile` | 合一应用镜像(仅装环境,代码 volume 挂载) |
| `deploy/init.sql` | MySQL 初始化脚本 |
| `backend/scripts/migrate_sqlite_to_mysql.py` | SQLite → MySQL 数据迁移脚本 |
| `Docs/PRD/需求文档/部署运维/_PRD_..._V2部署架构升级_合一容器MySQL.md` | V2 需求文档 |
| `Docs/PRD/需求文档/部署运维/_PRD_..._合一容器MySQL_计划执行.md` | V2 执行计划 |
| `Docs/部署方案/Linux容器化部署方案_v2.md` | V2 设计方案 |
| `HANDOFF_V2部署升级.md` | V2 部署详细交接文档 |
### 删除的文件
| 文件 | 说明 |
|------|------|
| `deploy/Dockerfile.backend` | 旧后端 Dockerfile(已合并到合一 Dockerfile) |
| `deploy/Dockerfile.frontend` | 旧前端 Dockerfile(已合并) |
--- ---
## 、关键踩坑记录(绝对不要重复踩) ## 、关键踩坑记录(绝对不要重复踩)
| # | 现象 | 根因 | 正确做法 | | # | 现象 | 根因 | 正确做法 |
|---|------|------|---------| |---|------|------|---------|
...@@ -182,20 +200,22 @@ platform-auto-test/ ...@@ -182,20 +200,22 @@ platform-auto-test/
| 3 | 请求 `/api/api/cases` 404 | `baseURL:'/api'` 与路径叠加 | baseURL 留空,路径写完整 `/api/...` | | 3 | 请求 `/api/api/cases` 404 | `baseURL:'/api'` 与路径叠加 | baseURL 留空,路径写完整 `/api/...` |
| 4 | `SQLite database is locked` | 同步执行并发写入 | check_same_thread=False + pool_pre_ping | | 4 | `SQLite database is locked` | 同步执行并发写入 | check_same_thread=False + pool_pre_ping |
| 5 | bash 中 `start` 启动 CMD 失败 | MSYS 封装损坏 `&&` 引号 | `MSYS_NO_PATHCONV=1 cmd.exe /c start ...` | | 5 | bash 中 `start` 启动 CMD 失败 | MSYS 封装损坏 `&&` 引号 | `MSYS_NO_PATHCONV=1 cmd.exe /c start ...` |
| 6 | 前端页面空白 | `useRoute()` 在 computed 内调用 | 移到 setup 顶层 | | 6 | `claude` 命令不在 PATH | Windows 未配全局 PATH | `which claude` 找路径,补 `.cmd` 后缀 |
| 7 | `NOT NULL constraint failed: error_message` | 字段 nullable=False 但成功时为 None | 改为 nullable=True | | 7 | 前端页面空白 | `useRoute()` 在 computed 内调用 | 移到 setup 顶层 |
| 8 | 菜单text=选择器失败 | 被测系统菜单是.block>p | `.block:has-text("菜单名")` | | 8 | `NOT NULL constraint failed: error_message` | 字段 nullable=False 但成功时为 None | 改为 nullable=True |
| 9 | **auto_login+重复登录** | 用例含登录步骤 | **auto_login=True时不要包含登录步骤** | | 9 | Playwright Sync API inside asyncio loop | `run_in_executor` 线程中 Playwright 检测到 asyncio 循环 | `asyncio.set_event_loop(None)` 在工作线程中 |
| 10 | **"点击展开功能中心"无效** | 登录后.block已可见 | **删除此步骤** | | 10 | **MySQL `Can't connect on '2026@mysql'`** | DATABASE_URL 密码含 `@`,被解析为主机分隔符 | MySQL 密码不要含 `@` 等 URL 特殊字符(用 `PlatApp2026` |
| 11 | **Playwright asyncio检测错误** | run_in_executor中调用 | **`asyncio.set_event_loop(None)` 绕过检测** | | 11 | **MySQL `Data too long for column 'id'`** | 模型 `String(32)` 容不下带前缀 ID,SQLite 不校验但 MySQL 校验 | ID 类字段统一用 `String(64)` |
| 12 | **FastAPI端点执行失败** | Playwright检测到asyncio循环 | **在工作线程中设置 event_loop=None** | | 12 | **MySQL 迁移 `foreign key constraint fails`** | 自引用外键插入顺序,SQLite 不强制但 MySQL 强制 | 迁移脚本先 `SET FOREIGN_KEY_CHECKS=0` |
| 13 | **_do_wait 多选器超时过长** | 4选择器×30s=120s | **快速检查+均分超时** | | 13 | **MySQL `ping() missing 'reconnect'` → API 500** | aiomysql 的 ping() 与 SQLAlchemy `pool_pre_ping` 不兼容 | MySQL 禁用 `pool_pre_ping`,改用 `pool_recycle=3600` |
| 14 | **element_count_min断言失败** | `params.min_count`未支持 | **修复执行器支持min_count参数** |
| 15 | **uvicorn --reload僵尸进程** | Windows进程管理异常 | **清除__pycache__,kill所有进程,换端口** | 详见 `HANDOFF_V2部署升级.md` 第三节。
--- ---
## 七、启动与验证 ## 八、启动与运维指南
### 本地开发(仍用 SQLite,保持不变)
```bash ```bash
# 后端(端口 8001) # 后端(端口 8001)
...@@ -210,74 +230,70 @@ npm install ...@@ -210,74 +230,70 @@ npm install
npm run dev npm run dev
``` ```
- 前端: http://localhost:3000 - `database.py` 自动按 DATABASE_URL 判断:含 `sqlite` 用 SQLite 配置,否则用 MySQL 配置
- API 文档: http://localhost:8001/docs - `main.py` 仅在 `frontend/dist/` 存在时挂载前端(本地 dev 时 Vite 代理处理)
- 被测系统: https://192.168.5.44 (admin@xty / Ubains@13579 / csba)
### 快速验证执行引擎 ### 服务器运维(V2 部署)
```bash ```bash
cd backend # 更新前端(热更新,无需重启)
PYTHONIOENCODING=utf-8 python scripts/test_executor_direct.py # 直接调用 cd frontend && npm run build
PYTHONIOENCODING=utf-8 python scripts/test_single_case.py # API端点测试 scp -r dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
``` # 刷新浏览器即生效
### 验证深层用例优化效果 # 更新后端(需重启容器,秒级)
scp -r backend/app ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/
ssh ubains@192.168.5.60 'cd /data/third_party/plat-auto-test/deploy && docker compose restart app'
```bash # 更新依赖(需重建镜像)
cd backend ssh ubains@192.168.5.60 'cd /data/third_party/plat-auto-test/deploy && docker compose build app && docker compose up -d app'
PYTHONIOENCODING=utf-8 python scripts/create_deep_interaction_cases_v2.py # 更新用例步骤
# 然后在前端执行中心批量执行深层交互用例
```
---
## 八、未提交变更(待 git commit) # 数据迁移(本地执行,连接服务器 MySQL)
cd backend && python scripts/migrate_sqlite_to_mysql.py
### 修改的文件 # 备份数据库
docker exec plat-auto-test-mysql mysqldump -uroot -pUbains@13579 plat_auto_test > backup.sql
| 文件 | 变更说明 | # 查看日志 / 重启
|------|---------| ssh ubains@192.168.5.60 'cd /data/third_party/plat-auto-test/deploy && docker compose logs -f app'
| `backend/scripts/create_deep_interaction_cases_v2.py` | 重写步骤构建逻辑,删除冗余登录,优化模板 | ```
| `backend/app/executors/playwright_executor.py` | 修复 _do_wait 多选器超时 + element_count_min 支持 min_count |
| `Docs/PRD/需求文档/用例管理/_PRD_需求文档_深层交互用例步骤优化.md` | 新增需求文档 |
| `Docs/PRD/需求文档/用例管理/_执行计划_深层交互用例步骤优化.md` | 新增计划执行文档 |
| `HANDOFF.md` | 更新交接文档 |
### 数据库变更 ---
- `backend/data/test_platform.db` — 25 个深层用例的 steps 已更新 ## 九、关键配置信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 192.168.5.60 |
| SSH 用户 | ubains |
| 部署目录 | `/data/third_party/plat-auto-test/` |
| 前端地址 | http://192.168.5.60 |
| API 文档 | http://192.168.5.60/docs |
| 健康检查 | http://192.168.5.60/health |
| MySQL 外部访问 | 192.168.5.60:3307(仅调试) |
| MySQL 用户/密码 | platapp / PlatApp2026 |
| MySQL root 密码 | Ubains@13579 |
| MySQL 数据库 | plat_auto_test |
| 本地开发 | SQLite(http://localhost:3000 前端 / http://localhost:8001 API) |
--- ---
## 、文档索引 ## 、文档索引
| 文档 | 路径 | | 文档 | 路径 |
|------|------| |------|------|
| CLAUDE.md | `CLAUDE.md` | | 项目指南 | `CLAUDE.md` |
| 多窗口并行开发指南 | `Docs/多窗口并行开发指南.md` | | 多窗口并行开发指南 | `Docs/多窗口并行开发指南.md` |
| 深层用例优化需求文档 | `Docs/PRD/需求文档/用例管理/_PRD_需求文档_深层交互用例步骤优化.md` | | V2 部署升级 PRD | `Docs/PRD/需求文档/部署运维/_PRD_需求文档_V2部署架构升级_合一容器MySQL.md` |
| 深层用例优化执行计划 | `Docs/PRD/需求文档/用例管理/_执行计划_深层交互用例步骤优化.md` | | V2 部署升级执行计划 | `Docs/PRD/需求文档/部署运维/_PRD_需求文档_V2部署架构升级_合一容器MySQL_计划执行.md` |
| 问题处理文档 | `Docs/PRD/问题处理/` | | V2 设计方案 | `Docs/部署方案/Linux容器化部署方案_v2.md` |
| 需求文档 | `Docs/PRD/需求文档/` | | V2 部署详细交接 | `HANDOFF_V2部署升级.md` |
| 安全测试交接文档 | `HANDOFF_安全测试.md` | | UI 自动化交接 | `HANDOFF_UI自动化.md` |
| HANDOFF.md | `HANDOFF.md`(本文档) | | 安全测试交接 | `HANDOFF_安全测试.md` |
| 主 PRD | `Docs/PRD/需求文档/总览/_PRD_平台自动化测试可视化系统需求文档.md` |
--- | 当前进度记录 | `Docs/PRD/需求文档/总览/当前进度记录.md` |
| 本文档 | `HANDOFF.md` |
## 十、用例统计
| 模块 | 用例数 | 深层用例通过率 |
|------|--------|--------------|
| 会议管理 | 50 | - |
| 数据分析 | 34 | - (深层12个,9通过3失败) |
| 运维管理 | 15 | - (深层7个,6通过1失败) |
| 管理看板 | 13 | - (深层6个,4通过2失败) |
| 登录模块 | 5 | - |
| **UI测试小计** | **117** | |
| 安全测试模块 | 24 | - |
| **总计** | **141** | **深层25个,19通过6失败(76%)** |
--- ---
*本文档由 Claude Code 维护,供下一次会话快速恢复上下文。* *本文档由 Claude Code 于 2026-07-28 生成,记录 V2 部署架构升级会话进度,供下次会话快速恢复上下文。*
# HANDOFF — V2 部署架构升级(合一容器 + MySQL)
> **生成时间**: 2026-07-28
> **当前分支**: `platform-auto-test`
> **状态**: ✅ V2 部署完成并验证通过(app + mysql 双容器已上线)
> **关联文档**: `Docs/PRD/需求文档/部署运维/_PRD_需求文档_V2部署架构升级_合一容器MySQL.md`
---
## 一、本次任务概要
将部署架构从 V1(3 容器:前端 Nginx + 后端 + SQLite 文件)升级到 V2(2 容器:合一应用 + MySQL 8.0),实现:
- 前后端合一容器(FastAPI 直接 serve 前端静态文件,去掉独立 Nginx)
- 前端热更新(volume 挂载 dist/,替换文件即生效,无需重启)
- 后端秒级更新(volume 挂载源码,重启容器即生效,无需重建镜像)
- MySQL 8.0 替代 SQLite(独立容器,支持并发写入)
- 独立部署目录 `/data/third_party/plat-auto-test/`(与 8088 维护平台隔离)
---
## 二、当前状态:✅ 全部完成
| 阶段 | 状态 | 说明 |
|------|------|------|
| Phase 1: 本地代码改造 | ✅ 完成 | Dockerfile / docker-compose / main.py / requirements / models |
| Phase 2: 数据迁移脚本 | ✅ 完成 | `migrate_sqlite_to_mysql.py`(含断点续传 + 外键处理) |
| Phase 3: 服务器部署 | ✅ 完成 | 双容器 running/healthy |
| Phase 4: 数据迁移验证 | ✅ 完成 | 283 用例 + 28 模块 + 636 执行记录全部迁移 |
### 服务器最终状态
```
容器状态:
plat-auto-test-app Up (healthy) 0.0.0.0:80->80/tcp
plat-auto-test-mysql Up (healthy) 0.0.0.0:3307->3306/tcp
验证结果:
前端访问 http://192.168.5.60 → HTTP 200 ✅
API 健康 http://192.168.5.60/health → {"status":"healthy"} ✅
模块 API /api/modules → 28 个模块 ✅
用例 API /api/cases?limit=500 → 283 个用例 ✅
SPA 路由 /login /modules /cases → HTTP 200(回退 index.html)✅
已有服务 http://192.168.5.60:8088 → HTTP 302(不受影响)✅
```
---
## 三、部署过程中解决的 4 个关键问题(踩坑记录)
### 问题 1:MySQL 密码含 `@` 导致连接失败 🔴
- **现象**`Can't connect to MySQL server on '2026@mysql'`
- **根因**:密码 `PlatApp@2026` 中的 `@` 被 URL 解析器当作"密码/主机分隔符",导致主机名被解析为 `2026@mysql`
- **修复**:将 MySQL 密码改为不含特殊字符的 `PlatApp2026`(修改 `.env` + `docker-compose.yml` 默认值 + 迁移脚本)
- **注意**:修改密码后**必须清理 `mysql/data/` 目录**让 MySQL 重新初始化,否则旧密码用户会残留导致 `Access denied`
### 问题 2:ID 字段 `varchar(32)` 容纳不下实际 ID 🔴
- **现象**`Data too long for column 'id' at row 1`
- **根因**:SQLAlchemy 模型定义 `String(32)`,但 `id_generator.py` 生成的带前缀 ID(如 `module_432a65347d624b638f9623fe303af957`)实际有 39 字符。SQLite 不强制列长度,MySQL 严格检查
- **修复**:将所有模型中 ID 相关字段的 `String(32)` 改为 `String(64)`(7 个 model 文件,共 15 处)
- **涉及文件**`module.py / test_case.py / execution.py / case_result.py / case_dependency.py / security_config.py / vulnerability_result.py`
### 问题 3:外键约束导致 modules 表迁移失败 🔴
- **现象**`Cannot add or update a child row: a foreign key constraint fails (modules.parent_id REFERENCES modules.id)`
- **根因**`modules.parent_id` 是自引用外键,部分模块的 `parent_id` 指向的父模块在 SQLite 中存在,但插入顺序导致父记录尚未写入。SQLite 默认不强制外键,MySQL 强制
- **修复**:迁移脚本在 `main()` 中迁移前 `SET FOREIGN_KEY_CHECKS = 0`,迁移完成后再 `SET FOREIGN_KEY_CHECKS = 1`
### 问题 4:aiomysql 与 pool_pre_ping 不兼容 🔴
- **现象**`AsyncAdapt_aiomysql_connection.ping() missing 1 required positional argument: 'reconnect'`,导致 `/api/modules` 等部分 API 返回 500
- **根因**:SQLAlchemy 的 `pool_pre_ping=True` 调用连接的 `ping()` 时未传 aiomysql 必需的 `reconnect` 参数
- **修复**`database.py` 中对 MySQL 连接**禁用 `pool_pre_ping`**,改用 `pool_recycle=3600` 定期回收连接;SQLite 仍保留 `pool_pre_ping`
- **关键代码**
```python
_is_sqlite = "sqlite" in settings.DATABASE_URL
engine = create_async_engine(
settings.DATABASE_URL,
pool_pre_ping=_is_sqlite, # MySQL 关闭,SQLite 开启
pool_recycle=3600 if not _is_sqlite else -1, # MySQL 用 recycle 替代
)
```
---
## 四、本次修改的文件清单(待 git commit)
### 修改的文件
| 文件 | 变更说明 |
|------|---------|
| `backend/app/main.py` | 新增 StaticFiles 挂载前端 + SPA 路由回退(serve_spa |
| `backend/app/database.py` | 区分 SQLite/MySQL 连接参数,MySQL pool_recycle 替代 pool_pre_ping |
| `backend/app/models/module.py` | ID 字段 `String(32)` `String(64)` |
| `backend/app/models/test_case.py` | ID 字段 `String(32)` `String(64)` |
| `backend/app/models/execution.py` | ID 字段 `String(32)` `String(64)` |
| `backend/app/models/case_result.py` | ID/外键字段 `String(32)` `String(64)` |
| `backend/app/models/case_dependency.py` | ID/外键字段 `String(32)` `String(64)` |
| `backend/app/models/security_config.py` | ID 字段 `String(32)` `String(64)` |
| `backend/app/models/vulnerability_result.py` | ID/外键字段 `String(32)` `String(64)` |
| `backend/requirements.txt` | 新增 `aiomysql==0.2.0` + `cryptography==42.0.0` |
| `deploy/docker-compose.yml` | 重写为 2 服务(app + mysql),密码改为 `PlatApp2026` |
| `deploy/.env.example` | 新增 MySQL 配置,密码改为 `PlatApp2026` |
| `deploy/deploy.sh` | 重写,适配新目录和新架构 |
| `backend/scripts/migrate_sqlite_to_mysql.py` | 新建,含断点续传 + 外键处理 + JSON 序列化 |
### 新增的文件
| 文件 | 说明 |
|------|------|
| `deploy/Dockerfile` | 合一应用镜像(仅装环境,代码 volume 挂载) |
| `deploy/init.sql` | MySQL 初始化脚本(字符集 + 权限 + 时区) |
| `backend/scripts/migrate_sqlite_to_mysql.py` | SQLite MySQL 数据迁移脚本 |
### 删除的文件
| 文件 | 说明 |
|------|------|
| `deploy/Dockerfile.backend` | 旧的后端 Dockerfile(已合并) |
| `deploy/Dockerfile.frontend` | 旧的前端 Dockerfile(已合并) |
### 服务器端清理
- 已删除旧部署目录 `~/ubains-module-test/deploy/`(含旧 Dockerfile / nginx 配置)
---
## 五、数据迁移结果
| 表名 | SQLite 行数 | MySQL 行数 | 状态 |
|------|------------|------------|------|
| modules | 28 | 28 | |
| test_cases | 283 | 283 | |
| executions | 636 | 636 | |
| case_results | 926 | 926 | |
| case_dependencies | 0 | 0 | |
| security_configs | 1 | 1 | |
| vulnerability_results | 69 | 69 | |
迁移命令(本地执行,连接服务器 MySQL):
```bash
cd backend
python scripts/migrate_sqlite_to_mysql.py
# 环境变量默认值:MYSQL_HOST=192.168.5.60 MYSQL_PORT=3307
```
---
## 六、日常运维命令
### 更新前端(热更新,无需重启)
```bash
cd frontend && npm run build
scp -r dist/* ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/dist/
# 刷新浏览器即生效
```
### 更新后端(需重启容器,秒级)
```bash
scp -r backend/app ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/
ssh ubains@192.168.5.60
cd /data/third_party/plat-auto-test/deploy
docker compose restart app
```
### 更新依赖(需重建镜像)
```bash
ssh ubains@192.168.5.60
cd /data/third_party/plat-auto-test/deploy
docker compose build app && docker compose up -d app
```
### 备份数据库
```bash
docker exec plat-auto-test-mysql mysqldump -uroot -pUbains@13579 plat_auto_test > backup.sql
```
### 查看日志 / 重启
```bash
cd /data/third_party/plat-auto-test/deploy
docker compose logs -f app # 看应用日志
docker compose restart app # 重启应用
docker compose down # 停止所有服务
docker compose up -d # 启动所有服务
```
---
## 七、本地开发环境(保持不变)
本地开发仍使用 SQLite,无需 MySQL:
```bash
cd backend
uvicorn app.main:app --reload --port 8001
cd frontend
npm run dev # http://localhost:3000
```
- `database.py` 已自动判断:DATABASE_URL 含 `sqlite` 用 SQLite 配置,否则用 MySQL 配置
- `main.py` 仅在 `frontend/dist/` 存在时挂载前端(本地 dev 时 Vite 代理处理前端)
---
## 八、关键配置信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 192.168.5.60 |
| SSH 用户 | ubains |
| 部署目录 | `/data/third_party/plat-auto-test/` |
| 前端地址 | http://192.168.5.60 |
| API 文档 | http://192.168.5.60/docs |
| 健康检查 | http://192.168.5.60/health |
| MySQL 外部访问 | 192.168.5.60:3307(仅调试) |
| MySQL 用户/密码 | platapp / PlatApp2026 |
| MySQL root 密码 | Ubains@13579 |
| MySQL 数据库 | plat_auto_test |
---
## 九、待办与后续工作
| 优先级 | 待办 | 说明 |
|--------|------|------|
| **P0** | git commit 提交本次变更 | 模型字段改造 + 部署配置 + 迁移脚本 |
| **P1** | 全量用例执行验证 | 在新 MySQL 环境批量执行 UI 用例,确认无 `database is locked` |
| **P2** | 前端热更新验证 | 实际修改前端代码构建上传,验证热生效 |
| **P3** | HTTPS 配置 | 为前端配置 Let's Encrypt 证书 |
| **P4** | 编写自动化更新脚本 | 简化后续前端/后端更新操作 |
---
## 十、文档索引
| 文档 | 路径 |
|------|------|
| V2 部署升级 PRD | `Docs/PRD/需求文档/部署运维/_PRD_需求文档_V2部署架构升级_合一容器MySQL.md` |
| V2 部署升级执行计划 | `Docs/PRD/需求文档/部署运维/_PRD_需求文档_V2部署架构升级_合一容器MySQL_计划执行.md` |
| V2 设计方案 | `Docs/部署方案/Linux容器化部署方案_v2.md` |
| 项目指南 | `CLAUDE.md` |
| UI 自动化交接 | `HANDOFF_UI自动化.md` |
| 安全测试交接 | `HANDOFF_安全测试.md` |
| 本文档 | `HANDOFF_V2部署升级.md` |
---
*本文档由 Claude Code 生成,记录 V2 部署架构升级的完整进度,供下次会话快速恢复上下文。*
...@@ -25,12 +25,17 @@ from app.config import settings ...@@ -25,12 +25,17 @@ from app.config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 创建异步引擎(SQLite 需要特殊配置避免锁定) # 创建异步引擎(SQLite 需要特殊配置避免锁定)
# 注意:aiomysql 的 ping() 方法与 SQLAlchemy 的 pool_pre_ping 不兼容
# (AsyncAdapt_aiomysql_connection.ping() 缺少 reconnect 参数),
# 因此 MySQL 改用 pool_recycle 定期回收连接,SQLite 仍用 pool_pre_ping。
_is_sqlite = "sqlite" in settings.DATABASE_URL
engine = create_async_engine( engine = create_async_engine(
settings.DATABASE_URL, settings.DATABASE_URL,
echo=settings.DEBUG, echo=settings.DEBUG,
future=True, future=True,
connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {}, connect_args={"check_same_thread": False} if _is_sqlite else {},
pool_pre_ping=True, pool_pre_ping=_is_sqlite,
pool_recycle=3600 if not _is_sqlite else -1,
) )
# 创建异步会话工厂 # 创建异步会话工厂
......
...@@ -215,3 +215,27 @@ async def serve_file(file_path: str): ...@@ -215,3 +215,27 @@ async def serve_file(file_path: str):
status_code=404, status_code=404,
detail=f"文件不存在: {file_path}" detail=f"文件不存在: {file_path}"
) )
# ==================== 前端静态文件服务(容器化部署用) ====================
FRONTEND_DIST = Path(__file__).parent.parent / "frontend" / "dist"
if FRONTEND_DIST.exists():
logger.info(f"挂载前端静态文件: {FRONTEND_DIST}")
# 挂载静态资源目录(JS/CSS/图片等)
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
# 前端 SPA 路由:所有非 API 路径回退到 index.html
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""
前端 SPA 入口,所有非 API 路径返回 index.html
注意:此路由必须注册在所有 API 路由之后,避免覆盖 API 路由。
"""
file_path = FRONTEND_DIST / full_path
if file_path.exists() and file_path.is_file():
return FileResponse(str(file_path))
return FileResponse(str(FRONTEND_DIST / "index.html"))
\ No newline at end of file
...@@ -43,16 +43,16 @@ class CaseDependency(Base): ...@@ -43,16 +43,16 @@ class CaseDependency(Base):
__tablename__ = "case_dependencies" __tablename__ = "case_dependencies"
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="依赖关系ID") id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="依赖关系ID")
case_id: Mapped[str] = mapped_column( case_id: Mapped[str] = mapped_column(
String(32), String(64),
ForeignKey("test_cases.id", ondelete="CASCADE"), ForeignKey("test_cases.id", ondelete="CASCADE"),
nullable=False, nullable=False,
index=True, index=True,
comment="用例ID(依赖方)" comment="用例ID(依赖方)"
) )
depends_on_case_id: Mapped[str] = mapped_column( depends_on_case_id: Mapped[str] = mapped_column(
String(32), String(64),
ForeignKey("test_cases.id", ondelete="CASCADE"), ForeignKey("test_cases.id", ondelete="CASCADE"),
nullable=False, nullable=False,
index=True, index=True,
......
...@@ -48,15 +48,15 @@ class CaseResult(Base): ...@@ -48,15 +48,15 @@ class CaseResult(Base):
__tablename__ = "case_results" __tablename__ = "case_results"
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="结果ID") id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="结果ID")
execution_id: Mapped[str] = mapped_column( execution_id: Mapped[str] = mapped_column(
String(32), String(64),
ForeignKey("executions.id", ondelete="CASCADE"), ForeignKey("executions.id", ondelete="CASCADE"),
nullable=False, nullable=False,
comment="执行记录ID" comment="执行记录ID"
) )
case_id: Mapped[str] = mapped_column( case_id: Mapped[str] = mapped_column(
String(32), String(64),
ForeignKey("test_cases.id", ondelete="SET NULL"), ForeignKey("test_cases.id", ondelete="SET NULL"),
nullable=True, nullable=True,
comment="用例ID" comment="用例ID"
......
...@@ -51,7 +51,7 @@ class Execution(Base): ...@@ -51,7 +51,7 @@ class Execution(Base):
__tablename__ = "executions" __tablename__ = "executions"
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="执行ID") id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="执行ID")
name: Mapped[str] = mapped_column(String(200), default="", comment="执行名称") name: Mapped[str] = mapped_column(String(200), default="", comment="执行名称")
trigger_type: Mapped[str] = mapped_column( trigger_type: Mapped[str] = mapped_column(
String(20), String(20),
......
...@@ -42,7 +42,7 @@ class Module(Base): ...@@ -42,7 +42,7 @@ class Module(Base):
__tablename__ = "modules" __tablename__ = "modules"
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="模块ID") id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="模块ID")
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="模块名称") name: Mapped[str] = mapped_column(String(100), nullable=False, comment="模块名称")
description: Mapped[str] = mapped_column(Text, default="", comment="模块描述") description: Mapped[str] = mapped_column(Text, default="", comment="模块描述")
icon: Mapped[str] = mapped_column(String(50), default="folder", comment="模块图标") icon: Mapped[str] = mapped_column(String(50), default="folder", comment="模块图标")
...@@ -66,7 +66,7 @@ class Module(Base): ...@@ -66,7 +66,7 @@ class Module(Base):
# 父模块ID(支持层级结构) # 父模块ID(支持层级结构)
parent_id: Mapped[Optional[str]] = mapped_column( parent_id: Mapped[Optional[str]] = mapped_column(
String(32), String(64),
ForeignKey("modules.id"), ForeignKey("modules.id"),
nullable=True, nullable=True,
default=None, default=None,
......
...@@ -39,7 +39,7 @@ class SecurityConfig(Base): ...@@ -39,7 +39,7 @@ class SecurityConfig(Base):
__tablename__ = "security_configs" __tablename__ = "security_configs"
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="配置ID") id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="配置ID")
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="配置名称") name: Mapped[str] = mapped_column(String(100), nullable=False, comment="配置名称")
target_url: Mapped[str] = mapped_column(String(500), nullable=False, comment="目标服务器地址") target_url: Mapped[str] = mapped_column(String(500), nullable=False, comment="目标服务器地址")
server_ip: Mapped[Optional[str]] = mapped_column(String(50), default="", comment="服务器IP") server_ip: Mapped[Optional[str]] = mapped_column(String(50), default="", comment="服务器IP")
......
...@@ -50,9 +50,9 @@ class TestCase(Base): ...@@ -50,9 +50,9 @@ class TestCase(Base):
__tablename__ = "test_cases" __tablename__ = "test_cases"
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="用例ID") id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="用例ID")
module_id: Mapped[str] = mapped_column( module_id: Mapped[str] = mapped_column(
String(32), String(64),
ForeignKey("modules.id", ondelete="CASCADE"), ForeignKey("modules.id", ondelete="CASCADE"),
nullable=False, nullable=False,
comment="所属模块ID" comment="所属模块ID"
......
...@@ -51,12 +51,12 @@ class VulnerabilityResult(Base): ...@@ -51,12 +51,12 @@ class VulnerabilityResult(Base):
LEVEL_LOW = "low" LEVEL_LOW = "low"
LEVEL_INFO = "info" LEVEL_INFO = "info"
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="结果ID") id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="结果ID")
execution_id: Mapped[str] = mapped_column( execution_id: Mapped[str] = mapped_column(
String(32), ForeignKey("executions.id", ondelete="CASCADE"), String(64), ForeignKey("executions.id", ondelete="CASCADE"),
nullable=False, comment="关联执行ID" nullable=False, comment="关联执行ID"
) )
case_id: Mapped[str] = mapped_column(String(32), default="", comment="关联用例ID") case_id: Mapped[str] = mapped_column(String(64), default="", comment="关联用例ID")
# 漏洞信息 # 漏洞信息
test_id: Mapped[str] = mapped_column(String(20), default="", comment="OWASP测试编号(如2.1.1)") test_id: Mapped[str] = mapped_column(String(20), default="", comment="OWASP测试编号(如2.1.1)")
......
...@@ -2,6 +2,8 @@ fastapi==0.110.0 ...@@ -2,6 +2,8 @@ fastapi==0.110.0
uvicorn[standard]==0.27.0 uvicorn[standard]==0.27.0
sqlalchemy==2.0.25 sqlalchemy==2.0.25
aiosqlite==0.19.0 aiosqlite==0.19.0
aiomysql==0.2.0
cryptography==42.0.0
pydantic==2.5.3 pydantic==2.5.3
python-multipart==0.0.6 python-multipart==0.0.6
playwright==1.40.0 playwright==1.40.0
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:migrate_sqlite_to_mysql.py
模块描述:SQLite → MySQL 数据迁移脚本
将本地 SQLite 数据库(backend/data/test_platform.db)的数据迁移到远程 MySQL。
支持断点续传:已存在的记录自动跳过(ON DUPLICATE KEY UPDATE)。
作者:czj
创建日期:2026-07-28
"""
import asyncio
import json
import logging
import os
import sys
from datetime import datetime
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# ==================== 数据库连接配置 ====================
# SQLite(本地数据源)
SQLITE_PATH = os.getenv("SQLITE_PATH", "sqlite+aiosqlite:///./data/test_platform.db")
# MySQL(目标数据库)
MYSQL_HOST = os.getenv("MYSQL_HOST", "192.168.5.60")
MYSQL_PORT = os.getenv("MYSQL_PORT", "3307")
MYSQL_USER = os.getenv("MYSQL_USER", "platapp")
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "PlatApp2026")
MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "plat_auto_test")
MYSQL_URL = (
f"mysql+aiomysql://{MYSQL_USER}:{MYSQL_PASSWORD}"
f"@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4"
)
# ==================== 表迁移顺序(按外键依赖排序) ====================
TABLES = [
# (表名, 主键字段, 是否需要处理 JSON)
("modules", "id", False),
("test_cases", "id", True), # 含 JSON 字段(steps, selectors 等)
("executions", "id", True), # 含 JSON 字段
("case_results", "id", False), # 依赖 execution.id
("case_dependencies", "id", False), # 依赖 test_case.id
("security_configs", "id", False),
("vulnerability_results", "id", False), # 依赖 security_configs.id
]
async def get_sqlite_data(sqlite_engine, table: str) -> list:
"""从 SQLite 读取表数据"""
async with sqlite_engine.connect() as conn:
result = await conn.execute(text(f"SELECT * FROM {table}"))
columns = result.keys()
rows = []
for row in result.fetchall():
rows.append(dict(zip(columns, row)))
logger.info(f" SQLite [{table}]: {len(rows)} 行")
return rows
def serialize_value(value, is_json: bool = False):
"""序列化值,处理特殊类型"""
if value is None:
return None
if is_json and not isinstance(value, (str, int, float, bool)):
# JSON 字段:如果是 dict/list 则序列化,否则保持原样
if isinstance(value, (dict, list)):
return json.dumps(value, ensure_ascii=False)
# datetime 类型转为字符串
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %H:%M:%S")
# bytes 类型转为字符串
if isinstance(value, bytes):
return value.decode("utf-8")
return value
async def migrate_table(mysql_engine, table: str, pk_field: str, rows: list, is_json_table: bool) -> int:
"""将数据写入 MySQL"""
if not rows:
return 0
# 获取列名(从第一行数据)
columns = list(rows[0].keys())
# 构建 INSERT ... ON DUPLICATE KEY UPDATE 语句
col_list = ", ".join([f"`{c}`" for c in columns])
placeholders = ", ".join([f":{c}" for c in columns])
update_parts = ", ".join([f"`{c}`=VALUES(`{c}`)" for c in columns if c != pk_field])
sql = f"""
INSERT INTO `{table}` ({col_list})
VALUES ({placeholders})
ON DUPLICATE KEY UPDATE {update_parts}
"""
# 序列化数据
serialized_rows = []
for row in rows:
serialized = {}
for col, val in row.items():
# 判断是否是 JSON 字段 - 通过表名 + 列名判断
is_json_col = is_json_table and col in ("steps", "selectors", "params", "result", "config", "tags", "metadata", "extra")
serialized[col] = serialize_value(val, is_json_col)
serialized_rows.append(serialized)
async with mysql_engine.connect() as conn:
for row in serialized_rows:
await conn.execute(text(sql), row)
await conn.commit()
logger.info(f" MySQL [{table}]: {len(rows)} 行写入成功")
return len(rows)
async def verify_migration(mysql_engine):
"""验证迁移后的数据"""
logger.info("")
logger.info("=" * 50)
logger.info("验证迁移结果")
logger.info("=" * 50)
async with mysql_engine.connect() as conn:
for table, pk_field, _ in TABLES:
result = await conn.execute(text(f"SELECT COUNT(*) FROM `{table}`"))
count = result.scalar()
logger.info(f" {table}: {count} 行")
# 验证用例完整性
result = await conn.execute(text("SELECT COUNT(*) FROM test_cases"))
case_count = result.scalar()
result = await conn.execute(text("SELECT COUNT(*) FROM modules"))
module_count = result.scalar()
logger.info("")
logger.info(f"✅ 迁移完成: {case_count} 个用例, {module_count} 个模块")
async def main():
"""主迁移流程"""
start_time = datetime.now()
logger.info("=" * 50)
logger.info("SQLite → MySQL 数据迁移")
logger.info(f"来源: {SQLITE_PATH}")
logger.info(f"目标: {MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}")
logger.info("=" * 50)
# 创建引擎
sqlite_engine = create_async_engine(SQLITE_PATH)
mysql_engine = create_async_engine(MYSQL_URL)
# 迁移前:关闭外键检查(避免自引用外键如 modules.parent_id 的顺序问题)
async with mysql_engine.connect() as conn:
await conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
await conn.commit()
logger.info("已关闭 MySQL 外键检查(迁移期间)")
total_rows = 0
try:
# 逐表迁移
for table, pk_field, is_json in TABLES:
logger.info("")
logger.info(f"📦 迁移表: {table}")
# 1. 从 SQLite 读取
rows = await get_sqlite_data(sqlite_engine, table)
# 2. 写入 MySQL
count = await migrate_table(mysql_engine, table, pk_field, rows, is_json)
total_rows += count
# 重新开启外键检查
async with mysql_engine.connect() as conn:
await conn.execute(text("SET FOREIGN_KEY_CHECKS = 1"))
await conn.commit()
logger.info("已重新开启 MySQL 外键检查")
# 3. 验证
await verify_migration(mysql_engine)
except Exception as e:
logger.error(f"❌ 迁移失败: {str(e)}")
raise
finally:
await sqlite_engine.dispose()
await mysql_engine.dispose()
elapsed = (datetime.now() - start_time).total_seconds()
logger.info("")
logger.info(f"⏱ 总耗时: {elapsed:.1f} 秒")
logger.info(f"📊 总行数: {total_rows}")
return total_rows
if __name__ == "__main__":
asyncio.run(main())
\ No newline at end of file
# 环境变量配置 # 环境变量配置
# 复制此文件为 .env 并根据需要修改 # 复制此文件为 .env 并根据需要修改
# 注意:.env 文件不要提交到 git
# 数据库连接 # ==================== MySQL 配置 ====================
DATABASE_URL=sqlite+aiosqlite:///./data/test_platform.db MYSQL_ROOT_PASSWORD=Ubains@13579
MYSQL_DATABASE=plat_auto_test
MYSQL_USER=platapp
MYSQL_PASSWORD=PlatApp2026
# Playwright 配置 # ==================== 应用配置 ====================
# DATABASE_URL 由 docker-compose 根据 MYSQL_* 变量自动拼接,无需手动设置
PLAYWRIGHT_HEADLESS=true PLAYWRIGHT_HEADLESS=true
PLAYWRIGHT_TIMEOUT=30000 PLAYWRIGHT_TIMEOUT=30000
# 日志级别
LOG_LEVEL=INFO LOG_LEVEL=INFO
\ No newline at end of file
FROM python:3.10-slim
WORKDIR /app
# 安装系统依赖(Playwright 需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
wget gnupg curl \
libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 \
libcups2t64 libdrm2 libdbus-1-3 libxkbcommon0 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libpango-1.0-0 libcairo2 libasound2t64 \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖(使用国内镜像源加速)
COPY requirements.txt .
RUN pip install --no-cache-dir \
-i https://pypi.tuna.tsinghua.edu.cn/simple \
--trusted-host pypi.tuna.tsinghua.edu.cn \
--retries 5 --timeout 120 -r requirements.txt
# 安装 Playwright Chromium(Linux 版本)
RUN playwright install chromium
# 创建必要目录
RUN mkdir -p /app/data/screenshots /app/data/reports \
/app/frontend/dist /app/logs
# 注意:以下内容通过 volume 挂载,不 COPY 进镜像
# - backend/ 源码 → /app
# - frontend/dist/ → /app/frontend/dist
# - data/ → /app/data
# - logs/ → /app/logs
EXPOSE 80
# 启动 uvicorn,监听 80 端口(同时提供 API + 前端静态文件)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
\ No newline at end of file
FROM python:3.10-slim
WORKDIR /app
# 安装 Playwright 系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
gnupg \
curl \
libnss3 \
libnspr4 \
libatk1.0-0t64 \
libatk-bridge2.0-0t64 \
libcups2t64 \
libdrm2 \
libdbus-1-3 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libpango-1.0-0 \
libcairo2 \
libasound2t64 \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖(使用国内镜像源加速,支持重试)
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn --retries 5 --timeout 120 -r requirements.txt
# 安装 Playwright Chromium(Linux 版本)
RUN playwright install chromium
# 复制后端代码
COPY backend/ .
# 创建数据目录
RUN mkdir -p /app/data/screenshots /app/data/reports
# 暴露端口
EXPOSE 8001
# 启动命令(单 worker,SQLite 不支持多 worker 并发)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]
\ No newline at end of file
# ---- 构建阶段 ----
FROM node:20-alpine AS build
WORKDIR /app
# 安装依赖(使用国内镜像)
COPY frontend/package.json frontend/package-lock.json ./
RUN npm config set registry https://registry.npmmirror.com && npm ci
# 复制源码并构建
COPY frontend/ .
RUN npm run build
# ---- 运行阶段 ----
FROM nginx:alpine
# 复制构建产物
COPY --from=build /app/dist /usr/share/nginx/html
# 复制 Nginx 配置
COPY deploy/nginx/default.conf /etc/nginx/conf.d/default.conf
# 暴露端口
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
...@@ -2,15 +2,14 @@ ...@@ -2,15 +2,14 @@
set -e set -e
echo "============================================" echo "============================================"
echo " 平台自动化测试系统 - Docker 部署脚本" echo " 平台自动化测试系统 - V2 Docker 部署脚本"
echo " 架构: 合一应用容器 + MySQL"
echo "============================================" echo "============================================"
echo "" echo ""
# 检查 Docker # 检查 Docker
if ! command -v docker &> /dev/null; then if ! command -v docker &> /dev/null; then
echo "❌ 错误: Docker 未安装" echo "❌ 错误: Docker 未安装"
echo " 请先安装 Docker: sudo apt-get install -y docker.io docker-compose-v2"
echo " 或参考: https://docs.docker.com/engine/install/"
exit 1 exit 1
fi fi
...@@ -22,16 +21,39 @@ fi ...@@ -22,16 +21,39 @@ fi
# 获取脚本所在目录 # 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")" PROJECT_DIR="/data/third_party/plat-auto-test"
echo "📁 项目目录: $PROJECT_DIR" echo "📁 部署目录: $PROJECT_DIR"
echo "" echo ""
# 创建数据目录 # 创建数据目录
echo "📂 创建数据目录..." echo "📂 创建数据目录..."
mkdir -p "$PROJECT_DIR/data/screenshots" "$PROJECT_DIR/data/reports" mkdir -p "$PROJECT_DIR/data/screenshots" "$PROJECT_DIR/data/reports"
mkdir -p "$PROJECT_DIR/mysql/data"
mkdir -p "$PROJECT_DIR/logs"
echo " ✅ 数据目录已就绪" echo " ✅ 数据目录已就绪"
# 检查 .env 文件
if [ ! -f "$SCRIPT_DIR/.env" ]; then
echo "⚠️ .env 文件不存在,从 .env.example 复制..."
cp "$SCRIPT_DIR/.env.example" "$SCRIPT_DIR/.env"
echo " ⚠️ 请检查 .env 中的数据库密码配置"
fi
# 检查后端代码是否存在
if [ ! -d "$PROJECT_DIR/backend/app" ]; then
echo "⚠️ 后端代码不存在,请先上传后端代码到 $PROJECT_DIR/backend/"
exit 1
fi
# 检查前端构建产物是否存在
if [ ! -f "$PROJECT_DIR/frontend/dist/index.html" ]; then
echo "⚠️ 前端构建产物不存在,请先上传 dist/ 到 $PROJECT_DIR/frontend/dist/"
echo " 本地构建: cd frontend && npm run build"
echo " 上传: scp -r dist/* ubains@192.168.5.60:$PROJECT_DIR/frontend/dist/"
exit 1
fi
# 构建镜像 # 构建镜像
echo "" echo ""
echo "📦 构建 Docker 镜像..." echo "📦 构建 Docker 镜像..."
...@@ -48,28 +70,28 @@ echo " ✅ 服务已启动" ...@@ -48,28 +70,28 @@ echo " ✅ 服务已启动"
# 等待服务就绪 # 等待服务就绪
echo "" echo ""
echo "⏳ 等待服务就绪..." echo "⏳ 等待服务就绪..."
sleep 10 sleep 15
# 健康检查 # 健康检查
echo "" echo ""
echo "🔍 执行健康检查..." echo "🔍 执行健康检查..."
MAX_RETRIES=6 MAX_RETRIES=12
RETRY_COUNT=0 RETRY_COUNT=0
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8001/health 2>/dev/null || echo "000") HEALTH=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/health 2>/dev/null || echo "000")
if [ "$HEALTH" = "200" ]; then if [ "$HEALTH" = "200" ]; then
echo " ✅ 后端健康检查通过 (HTTP 200)" echo " ✅ 应用健康检查通过 (HTTP 200)"
break break
fi fi
RETRY_COUNT=$((RETRY_COUNT + 1)) RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo " ⏳ 等待后端就绪... ($RETRY_COUNT/$MAX_RETRIES)" echo " ⏳ 等待应用就绪... ($RETRY_COUNT/$MAX_RETRIES)"
sleep 5 sleep 5
fi fi
done done
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo " ⚠️ 后端健康检查超时,请检查日志: docker compose logs backend" echo " ⚠️ 应用健康检查超时,请检查日志: docker compose logs app"
fi fi
# 检查前端 # 检查前端
...@@ -80,9 +102,17 @@ else ...@@ -80,9 +102,17 @@ else
echo " ⚠️ 前端响应码: $FRONTEND_CODE" echo " ⚠️ 前端响应码: $FRONTEND_CODE"
fi fi
# 检查 MySQL
MYSQL_OK=$(docker exec plat-auto-test-mysql mysqladmin ping -h localhost -uroot -p"${MYSQL_ROOT_PASSWORD:-Ubains@13579}" 2>/dev/null || echo "failed")
if [ "$MYSQL_OK" != "failed" ]; then
echo " ✅ MySQL 运行正常"
else
echo " ⚠️ MySQL 状态异常,请检查: docker compose logs mysql"
fi
# 检查已有服务是否受影响 # 检查已有服务是否受影响
TROUBLESHOOT_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8088/ 2>/dev/null || echo "000") TROUBLESHOOT_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8088/ 2>/dev/null || echo "000")
echo " ℹ️ 已有故障排查平台 (8088): HTTP $TROUBLESHOOT_CODE" echo " ℹ️ 已有维护平台 (8088): HTTP $TROUBLESHOOT_CODE"
# 输出访问信息 # 输出访问信息
echo "" echo ""
...@@ -90,14 +120,14 @@ echo "============================================" ...@@ -90,14 +120,14 @@ echo "============================================"
echo " ✅ 部署完成!" echo " ✅ 部署完成!"
echo "============================================" echo "============================================"
echo "" echo ""
echo " 前端: http://$(curl -s ifconfig.me 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || echo 'localhost')" echo " 前端: http://192.168.5.60"
echo " API 文档: http://localhost:8001/docs" echo " API 文档: http://192.168.5.60/docs"
echo "" echo ""
echo " 已有故障排查平台: http://localhost:8088(不受影响)" echo " 已有维护平台: http://192.168.5.60:8088(不受影响)"
echo "" echo ""
echo " 📋 查看日志: docker compose logs -f" echo " 📋 查看日志: docker compose logs -f"
echo " 🛑 停止服务: docker compose down" echo " 🛑 停止服务: docker compose down"
echo " 🔄 重启服务: docker compose restart" echo " 🔄 重启应用: docker compose restart app"
echo "" echo ""
# 检查容器状态 # 检查容器状态
......
version: "3.8" version: "3.8"
services: services:
backend: # ==================== MySQL 数据库 ====================
build: mysql:
context: .. image: mysql:8.0
dockerfile: deploy/Dockerfile.backend container_name: plat-auto-test-mysql
container_name: plat-auto-test-backend restart: unless-stopped
ports: environment:
- "8001:8001" MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-Ubains@13579}
MYSQL_DATABASE: ${MYSQL_DATABASE:-plat_auto_test}
MYSQL_USER: ${MYSQL_USER:-platapp}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-PlatApp2026}
TZ: Asia/Shanghai
volumes: volumes:
- ../data:/app/data # MySQL 数据持久化
- /data/third_party/plat-auto-test/mysql/data:/var/lib/mysql
# 初始化脚本(仅首次启动执行)
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "3307:3306" # 映射到宿主机 3307,避免与本地 MySQL 冲突
networks:
- plat-auto-test-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-Ubains@13579}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-authentication-plugin=mysql_native_password
- --max_connections=200
# ==================== 应用容器(前后端合一) ====================
app:
build:
context: ../backend
dockerfile: ../deploy/Dockerfile
container_name: plat-auto-test-app
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
environment: environment:
- DATABASE_URL=sqlite+aiosqlite:///./data/test_platform.db - DATABASE_URL=mysql+aiomysql://${MYSQL_USER:-platapp}:${MYSQL_PASSWORD:-PlatApp2026}@mysql:3306/${MYSQL_DATABASE:-plat_auto_test}?charset=utf8mb4
- PLAYWRIGHT_HEADLESS=true - PLAYWRIGHT_HEADLESS=true
- PLAYWRIGHT_TIMEOUT=30000 - PLAYWRIGHT_TIMEOUT=30000
- LOG_LEVEL=INFO - LOG_LEVEL=INFO
restart: unless-stopped - TZ=Asia/Shanghai
volumes:
# 后端源码(更新后端只需重启容器,无需重建镜像)
- /data/third_party/plat-auto-test/backend:/app
# 前端构建产物(更新前端无需重启,替换文件即热生效)
- /data/third_party/plat-auto-test/frontend/dist:/app/frontend/dist
# 应用数据(截图、报告)
- /data/third_party/plat-auto-test/data:/app/data
# 日志
- /data/third_party/plat-auto-test/logs:/app/logs
ports:
- "80:80"
networks:
- plat-auto-test-net
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8001/health"] test: ["CMD", "curl", "-f", "http://localhost:80/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 20s start_period: 40s
frontend: networks:
build: plat-auto-test-net:
context: .. driver: bridge
dockerfile: deploy/Dockerfile.frontend \ No newline at end of file
container_name: plat-auto-test-frontend
ports:
- "80:80"
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
\ No newline at end of file
-- 平台自动化测试系统 - MySQL 初始化脚本
-- 注意:MYSQL_DATABASE 环境变量会自动创建数据库,此脚本用于补充设置
-- 确保字符集正确
ALTER DATABASE plat_auto_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 授予 platapp 用户完整权限
GRANT ALL PRIVILEGES ON plat_auto_test.* TO 'platapp'@'%';
FLUSH PRIVILEGES;
-- 时区设置(避免 SQLAlchemy 时间字段时区问题)
SET GLOBAL time_zone = '+08:00';
SET SESSION time_zone = '+08:00';
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论