# 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 部署架构升级的完整进度，供下次会话快速恢复上下文。*
