提交 3f368601 authored 作者: 陈泽健's avatar 陈泽健

docs: 同步各模块HANDOFF文档 & 性能测试文档迁移至新目录

- 功能测试报告 HANDOFF:更新模板优化(T1-T5)状态、部署记录、待办
- 安全测试 HANDOFF:更新 task-preview 接口实测通过状态
- V2 部署升级 HANDOFF:新增离线部署包验证(5.69 KylinOS)与实施记录
- 功能测试报告路由:新增 FileResponse/HTMLResponse 导入
- 性能测试文档迁移:从 Docs/PRD/需求文档/性能测试/ 迁移至 Docs/PRD/性能测试/
- 新增功能测试报告模板优化 + Excel 适配 PRD 文档(4 份)
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 f3035587
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
> **生成时间**: 2026-08-17 > **生成时间**: 2026-08-17
> **当前分支**: `platform-auto-test` > **当前分支**: `platform-auto-test`
> **最近提交**: `ce2c75de` feat: 功能测试报告新增模板配置功能(多模板管理) > **最近提交**: `ce2c75de` feat: 功能测试报告新增模板配置功能(多模板管理)
> **状态**: ✅ 功能测试报告模块全部完成(后端服务 + API + 前端页面 + ERP 上传 + ERP 配置持久化 + **报告模板配置多模板管理** + **Excel 格式适配**) > **未提交变更**: 报告模板优化(T1-T5)+ Excel 格式适配的代码与文档尚未 git commit,部署是直接 scp 到 5.60 的(本地改动脉冲式同步,需尽快补提交)
> **部署状态**: ✅ 已部署至 192.168.5.60(Docker 容器 plat-auto-test-app,端口 80),前后端均已更新,**模板创建 500 错误已修复并验证通过** > **状态**: ✅ 功能测试报告模块全部完成(后端服务 + API + 前端页面 + ERP 上传 + ERP 配置持久化 + **报告模板配置多模板管理** + **Excel 格式适配** + **报告模板优化(章节编号动态化 + 描述字段 + 复制功能)** + **下载/预览 500 已修复**)
> **部署状态**: ✅ 已部署至 192.168.5.60(Docker 容器 plat-auto-test-app,端口 80),前后端均已更新,**模板创建 500 错误已修复**,**模板优化(编号动态化/描述/复制)已验证通过**,**报告下载/预览 500 已修复并部署**
--- ---
...@@ -141,6 +142,40 @@ PASS 3A-3F: description 创建/更新/列表持久化 + 复制(_副本/_副本 ...@@ -141,6 +142,40 @@ PASS 3A-3F: description 创建/更新/列表持久化 + 复制(_副本/_副本
``` ```
前端 `vue-tsc` 类型检查:修改文件无错误 ✅ 前端 `vue-tsc` 类型检查:修改文件无错误 ✅
**🚀 已部署至 5.60 服务器**(2026-08-17,paramiko scp + docker restart):
| 部署项 | 说明 |
|--------|------|
| 后端 6 文件 | scp 覆盖到 `/data/third_party/plat-auto-test/backend/app/...` |
| 前端 dist/ | 本地 `npx vite build`(跳过 vue-tsc,因 ApiCaseList.vue 预存类型错误与本任务无关)→ 全量上传 |
| 容器重启 | `docker restart plat-auto-test-app`,重启后 `_ensure_columns()` 自动补齐 `report_templates.description` 列 |
| 服务器 API 实测 | 创建带描述 / 复制(`_副本`、`_副本2`、描述继承、is_default=False)/ 更新描述 / 列表含描述 / 旧模板回退 `""` — 全部通过 |
| 验证后状态 | 2 个模板(精简模板 id=2、默认模板 id=1 默认),测试模板已清理 |
**⚠️ 部署注意**:`npm run build` 会因 `ApiCaseList.vue` / `Execution.vue` 的预存类型错误(TS2339/TS2345 等,非本模块代码)而失败,需用 `npx vite build` 直接构建。这些类型错误待 API 测试模块窗口修复。
### 任务 6:报告下载/预览"内部服务器错误"修复 — 2026-08-17
**现象**:前端点击「下载报告」/「预览报告」提示"内部服务器错误"(HTTP 500)。
**根因**:`backend/app/routers/functional_report.py` 使用了 `FileResponse`(download 端点)和 `HTMLResponse`(preview 端点),但**未在文件顶部导入**。本地开发环境因 Starlette 顶层重导出"碰巧能跑",但 5.60 服务器 Docker 环境的 FastAPI 版本不导出这两个类 → `NameError` → 500。
**修复**:import 区增加一行:
```python
from fastapi.responses import FileResponse, HTMLResponse
```
**验证**(5.60 服务器实测):
- `import app.routers.functional_report` 本地通过 ✅
- scp 覆盖 + `docker restart plat-auto-test-app` ✅
- 下载/预览不存在会话 → 返回 404(不再 500)✅
- 容器日志无 NameError / Error ✅
**⚠️ 同类风险排查**:全局 grep 确认其他 router(`document.py` / `reports.py` / `security.py` / `device_sim.py`)均已正确从 `fastapi.responses` 导入或函数内局部导入,无同类问题。
**详细问题处理文档**:`Docs/PRD/功能测试/问题处理/BUG-2026-08-17-001_下载文件内部服务器错误.md`
### 参考工具 bug 修复记录 ### 参考工具 bug 修复记录
| # | 问题 | 根因 | 修复 | | # | 问题 | 根因 | 修复 |
...@@ -234,15 +269,16 @@ GET /api/functional-report/config → 获取 ERP 配置(DB 优先, ...@@ -234,15 +269,16 @@ GET /api/functional-report/config → 获取 ERP 配置(DB 优先,
PUT /api/functional-report/config → 更新 ERP 配置(持久化到数据库) PUT /api/functional-report/config → 更新 ERP 配置(持久化到数据库)
``` ```
### 报告模板配置端点(2026-08-13 新增,前缀 `/api/functional-report/templates`) ### 报告模板配置端点(2026-08-13 新增,2026-08-17 扩展 description + duplicate,前缀 `/api/functional-report/templates`)
``` ```
GET /templates → 获取模板列表(含默认标记,不含 config) GET /templates → 获取模板列表(含默认标记 + description,不含 config)
GET /templates/{id} → 获取单个模板完整配置 GET /templates/{id} → 获取单个模板完整配置
POST /templates → 创建模板(首个模板自动设为默认) POST /templates → 创建模板(首个模板自动设为默认;支持 description ≤200 字符
PUT /templates/{id} → 更新模板(name/config/is_default 可独立更新) PUT /templates/{id} → 更新模板(name/description/config/is_default 可独立更新)
DELETE /templates/{id} → 删除模板(不允许删除最后一个) DELETE /templates/{id} → 删除模板(不允许删除最后一个)
PUT /templates/{id}/default → 设为默认模板(全局唯一,自动切换) PUT /templates/{id}/default → 设为默认模板(全局唯一,自动切换)
POST /templates/{id}/duplicate → 复制模板(2026-08-17 新增;名称自动 _副本/_副本N,description 与 config 继承,is_default=False)
``` ```
**config JSON 结构**(创建/更新时与默认配置合并,`merge_template_config` 保证结构完整): **config JSON 结构**(创建/更新时与默认配置合并,`merge_template_config` 保证结构完整):
...@@ -521,6 +557,7 @@ normalize_test_result("功能验证") → '通过' ✅ ...@@ -521,6 +557,7 @@ normalize_test_result("功能验证") → '通过' ✅
| F12 | **容器启动失败 ImportError** | 服务器后端缺少本地已有的 `performance_output.py`/`api_preset.py` 及 `api_preset` schema/service(routers/__init__.py 不 export,靠 main.py 直接导入) | 补传缺失文件后再 `docker restart`,成功恢复 healthy | | F12 | **容器启动失败 ImportError** | 服务器后端缺少本地已有的 `performance_output.py`/`api_preset.py` 及 `api_preset` schema/service(routers/__init__.py 不 export,靠 main.py 直接导入) | 补传缺失文件后再 `docker restart`,成功恢复 healthy |
| F13 | **创建模板 500 `TypeError: dict can not be used as parameter`** | MySQL 的 aiomysql 驱动不接受 Python dict 作为 SQL 参数:`_validate_config()` 返回 dict,直接赋给 `config` Text 列。SQLite 不校验类型所以本地从未报错 | 赋模型前 `json.dumps(config, ensure_ascii=False)` 序列化为 JSON 字符串(create 和 update 两处都要改),读侧 `to_dict()` 已 `json.loads` 无需改 | | F13 | **创建模板 500 `TypeError: dict can not be used as parameter`** | MySQL 的 aiomysql 驱动不接受 Python dict 作为 SQL 参数:`_validate_config()` 返回 dict,直接赋给 `config` Text 列。SQLite 不校验类型所以本地从未报错 | 赋模型前 `json.dumps(config, ensure_ascii=False)` 序列化为 JSON 字符串(create 和 update 两处都要改),读侧 `to_dict()` 已 `json.loads` 无需改 |
| F14 | **本地 main.py 未注册 report_template 路由** | 之前只改了服务器上的 main.py(已注册),本地 `main.py` 和 `models/__init__.py` 忘记同步注册 | 本地/服务器都要注册:main.py import + include_router,models/__init__.py import + __all__ | | F14 | **本地 main.py 未注册 report_template 路由** | 之前只改了服务器上的 main.py(已注册),本地 `main.py` 和 `models/__init__.py` 忘记同步注册 | 本地/服务器都要注册:main.py import + include_router,models/__init__.py import + __all__ |
| F15 | **下载/预览报告 500 `NameError: FileResponse`** | `functional_report.py` 使用 `FileResponse`/`HTMLResponse` 但未导入;本地因 Starlette 顶层重导出碰巧能跑,服务器 FastAPI 版本严格不导出 | `from fastapi.responses import FileResponse, HTMLResponse` 显式导入(已修复 + 部署 + 排查其他 router 无同类问题) |
--- ---
...@@ -627,15 +664,37 @@ frontend/src/views/functional-report/FunctionalReport.vue — 2026-08-13 新增 ...@@ -627,15 +664,37 @@ frontend/src/views/functional-report/FunctionalReport.vue — 2026-08-13 新增
frontend/src/views/functional-report/index.vue — 2026-08-13 注释更新 frontend/src/views/functional-report/index.vue — 2026-08-13 注释更新
``` ```
### 文档(4 个文件) ### 文档(6 个文件)
``` ```
Docs/PRD/功能测试/需求文档/_PRD_功能测试报告模块.md Docs/PRD/功能测试/需求文档/_PRD_功能测试报告模块.md
Docs/PRD/功能测试/需求文档/_PRD_功能测试报告模块_计划执行.md Docs/PRD/功能测试/需求文档/_PRD_功能测试报告模块_计划执行.md
Docs/PRD/功能测试/需求文档/_PRD_功能测试用例Excel适配.md — 2026-08-17 新增 Docs/PRD/功能测试/需求文档/_PRD_功能测试用例Excel适配.md — 2026-08-17 新增
Docs/PRD/功能测试/需求文档/_PRD_功能测试用例Excel适配_计划执行.md — 2026-08-17 新增 Docs/PRD/功能测试/需求文档/_PRD_功能测试用例Excel适配_计划执行.md — 2026-08-17 新增
Docs/PRD/功能测试/需求文档/_PRD_功能测试报告模板优化.md — 2026-08-17 新增(T1-T5,P2/P3 待办项见文内 backlog)
Docs/PRD/功能测试/需求文档/_PRD_功能测试报告模板优化_计划执行.md — 2026-08-17 新增
``` ```
--- ---
## 七、待办与 Backlog
### 7.1 模板优化 P2/P3 待办(本轮未实施,见 PRD backlog)
| # | 优先级 | 内容 |
|---|--------|------|
| T6 | P2 | Word 字体下拉框(当前为文本输入,改为系统字体枚举) |
| T7 | P2 | 一键恢复默认配置按钮 |
| T8 | P3 | 图表颜色标签显示(当前仅颜色选择器,无语义标签预览) |
| T9 | P3 | 模板导出/导入(JSON 文件) |
| T10 | P3 | 报告生成页模板下拉显示章节摘要(如"精简模板(8/10 章)") |
### 7.2 其他待办
- **P3 大亚湾真实 Excel 报告验证**:新格式用例文件(`临时目录/大亚湾一体化会议系统测试用例-260811.xlsx`)的测试结果列(C13)大部分为公式未缓存值(353/487 空),需业务方填写实际结果后再上传生成完整报告验证
- **前端类型错误**`ApiCaseList.vue` / `Execution.vue` 存在预存 TS 类型错误(TS2339/TS2345),导致 `npm run build` 失败,当前部署用 `npx vite build` 绕过;待 API 测试模块窗口修复
- **临时脚本清理**`临时目录/` 下有 `_fix_md_subs.py``_fix_md_gen.py``_fix_md_gen2.py`(markdown_generator 修复脚本,已完成使命)、`_verify_numbering.py`(验证脚本,可保留供回归)可择机清理
---
*本文档由功能测试报告开发窗口生成,供下一次会话快速恢复上下文。其他窗口的交接见 `HANDOFF.md`(主文档)、`HANDOFF_安全测试.md`、`HANDOFF_设备模拟.md` 等。* *本文档由功能测试报告开发窗口生成,供下一次会话快速恢复上下文。其他窗口的交接见 `HANDOFF.md`(主文档)、`HANDOFF_安全测试.md`、`HANDOFF_设备模拟.md` 等。*
\ No newline at end of file
# 功能测试报告模板优化 · 需求文档(PRD)
> **文档状态**:已定稿 · **版本**:v1.0 · **最后更新**:2026-08-17
> **维护者**:czj · **所属模块**:报告中心-功能测试
> **对应计划执行**:`_PRD_功能测试报告模板优化_计划执行.md`
---
## 1. 背景与目标
### 1.1 背景
功能测试报告模块已于 2026-08-13 完成模板配置功能(多模板管理:章节开关、Word 样式、图表样式、报告默认值)。在代码审查中发现以下问题:
| 问题 | 严重性 | 说明 |
|------|--------|------|
| **章节编号硬编码** | 🔴 P0 | `word_generator.py` 中 10 个章节标题编号(一、二、三…十)为硬编码字符串;`markdown_generator.py``_title()` 中中文数字同样硬编码。**当用户关闭前置章节后,后续章节编号错误**(如关闭"报告基本信息"后,下一章仍显示"二、测试执行摘要",实际应为"一、") |
| **子章节编号硬编码** | 🔴 P0 | 子章节(如 `1.1 报告标识``2.1 测试结果统计`)的前缀数字硬编码,与主章节编号脱节,关闭章节后同样错乱 |
| **模板无描述** | 🟡 P1 | 模板列表只显示名称,多模板(5+)时难以区分用途 |
| **模板无法复制** | 🟡 P1 | 基于现有模板创建变体需手动新建再逐一配置,效率低 |
### 1.2 目标
1. **P0 章节编号动态化**:Word 与 Markdown 报告的主章节、子章节编号均随启用章节动态生成,关闭任意章节后编号连续正确
2. **P1 模板描述字段**:模板支持可选描述(≤200 字),列表与表单均可维护
3. **P1 模板复制功能**:一键复制现有模板为新模板(名称自动加"副本"后缀),配置完整继承
### 1.3 参考文件
| 文件 | 说明 |
|------|------|
| `backend/app/services/functional/word_generator.py` | Word 报告生成器(编号硬编码,需改造) |
| `backend/app/services/functional/markdown_generator.py` | Markdown 报告生成器(编号部分动态,需改造) |
| `backend/app/models/report_template.py` | 模板 ORM 模型(需新增 description 字段) |
| `backend/app/schemas/report_template.py` | 模板 Pydantic 模型(需新增 description) |
| `backend/app/routers/report_template.py` | 模板 API 路由(需新增复制端点) |
| `frontend/src/views/functional-report/TemplateConfig.vue` | 模板配置页面(需新增描述输入与复制按钮) |
| `frontend/src/api/reportTemplate.ts` | 模板 API 封装(需新增 description 与 duplicate) |
---
## 2. 功能需求
### 2.1 功能清单
| 编号 | 功能 | 说明 | 优先级 |
|------|------|------|--------|
| T1 | 主章节编号动态化 | Word/Markdown 主章节编号(一、二…十)按启用顺序动态生成 | **P0** |
| T2 | 子章节编号动态化 | 子章节前缀数字(`{ch_idx}.1`)与主章节编号联动 | **P0** |
| T3 | 模板描述字段 | 模板新增 `description` 可选字段(≤200 字),列表副标题展示 | **P1** |
| T4 | 模板复制功能 | 后端新增复制端点 + 前端"复制"按钮 | **P1** |
| T5 | 向后兼容 | 开启全部章节时,报告编号与现状完全一致;已有模板数据处理兼容 | P0 |
### 2.2 功能详细说明
#### T1. 主章节编号动态化
**现状**
- `word_generator.py``_add_title_local("一、报告基本信息", level=2)` 硬编码,虽有 `ch_idx` 计数器但未使用
- `markdown_generator.py``_title(ch_idx, "一", "报告基本信息")` 中第一个参数 `ch_idx` 已动态,但中文数字 `"一"` 硬编码
**改造**
- 两文件中新增 `_zh_num(n)` 工具函数:整数 → 中文数字(1→一、2→二…10→十),>10 回退为阿拉伯数字
- 主章节标题统一为 `_add_title_local(f"{_zh_num(ch_idx)}、报告基本信息")` / `_title(ch_idx, _zh_num(ch_idx), "报告基本信息")`
**结果**:关闭"报告基本信息"后,"测试执行摘要"自动变为"一、测试执行摘要"。
#### T2. 子章节编号动态化
**现状**:子章节标题硬编码为 `1.1 报告标识``2.1 测试结果统计`…与主章节编号脱节。
**改造**:子章节标题改为 `f"{ch_idx}.1 {子章节名}"`,其中 `ch_idx` 为当前主章节序号(注意:需在 `ch_idx += 1` 之后取值)。
**结果**:关闭章节后主章节编号变化,子章节前缀自动跟随(如原"四、测试用例执行详情"变为"三、"后,其子章节 4.1→3.1、4.2→3.2、4.3→3.3)。
#### T3. 模板描述字段
**后端**
- `models/report_template.py`:新增 `description: Mapped[str] = mapped_column(String(200), nullable=True, default="")``to_dict()` / `to_list_item()` 增加 `description`
- `schemas/report_template.py`:Create / Update / ListItem / Response 增加 `description: Optional[str]`
- 路由 create/update 处理 `description`
**前端**
- `reportTemplate.ts`:接口类型增加 `description`
- `TemplateConfig.vue`:列表中显示描述副标题(截断省略),表单基本信息区新增"模板描述"输入框(`el-input type="textarea"`,maxlength=200)
**数据库迁移**`report_templates` 表为 SQLite/MySQL 通用,需新增一列。提供迁移 SQL / 启动时自动校验(`ALTER TABLE ... ADD COLUMN`,幂等)。
#### T4. 模板复制功能
**后端**:新增端点 `POST /templates/{template_id}/duplicate`
- 逻辑:读取原模板 config + description → 新建模板,name = `原名称_副本`(若已存在同名追加序号),is_default=False,config 深拷贝
- 返回:新模板完整信息
**前端**
- `reportTemplate.ts`:新增 `duplicate(id)` 方法
- `TemplateConfig.vue`:模板列表操作区新增"复制"按钮(CopyDocument 图标),点击后调接口,成功后刷新列表并选中新模板
#### T5. 向后兼容
- 全部章节开启时,动态生成的编号与现状完全一致(一、二…十 / 1.1、2.1…)
- 已有模板 config 无 `description` 字段:读取时默认 `""`,不报错
- 数据库新增列采用幂等 ALTER,已存在列不重复添加
---
## 3. 非功能需求
| 项 | 说明 |
|----|------|
| 性能 | 编号动态化仅影响标题字符串生成,无额外 IO |
| 兼容 | Word/Markdown 两种格式同步改造,行为一致 |
| 回归 | 全章节开启报告与改造前输出一致(仅编号生成方式变化,内容不变) |
| 数据库 | 迁移幂等,SQLite/MySQL 均适用 |
---
## 4. 验收标准
| # | 验收点 | 预期结果 |
|---|--------|----------|
| 1 | 全章节开启生成报告 | 主章节编号一~十、子章节 1.1/2.1…10.4 与改造前一致 |
| 2 | 关闭"报告基本信息"生成报告 | 原"二、测试执行摘要"显示为"一、",其子章节 2.1→1.1、2.2→1.2、2.3→1.3 |
| 3 | 关闭中间任意章节(如"图表分析") | 后续章节编号自动前移,无跳号/重号 |
| 4 | Word 与 Markdown 编号行为一致 | 同一模板配置下两种格式章节编号相同 |
| 5 | 创建模板带描述 | 列表展示描述,编辑可修改,保存后持久化 |
| 6 | 复制模板 | 新模板名称带"副本",config/description 完整继承,不改变原模板 |
| 7 | 旧模板兼容 | 无 description 的旧模板读取正常,列表 description 显示为空 |
---
## 5. 潜在风险与回退
| 风险 | 概率 | 应对 |
|------|------|------|
| 数据库 ALTER 在 MySQL 上失败 | 低 | 仅 ADD COLUMN 单列,幂等检查 |
| 子章节前缀遗漏遗漏某些硬编码处 | 低 | 逐处 grep 核对(`^\d+\.` 模式) |
| 编号改造引入内容变化 | 低 | 全章节开启时与旧输出逐条比对 |
回退方案:`git checkout -- backend/app/services/functional/word_generator.py backend/app/services/functional/markdown_generator.py backend/app/models/report_template.py backend/app/schemas/report_template.py backend/app/routers/report_template.py`
---
## 6. 后续优化(本次不做,列入 backlog)
| 编号 | 说明 | 优先级 |
|------|------|--------|
| T6 | Word 字体下拉选择器(宋体/微软雅黑/黑体等常见字体) | P2 |
| T7 | 一键恢复系统默认配置按钮 | P2 |
| T8 | 图表颜色标签与 BUG 等级对齐(BUG-严重→1级-致命 等) | P2 |
| T9 | 模板配置导出/导入(跨环境迁移) | P2 |
| T10 | 生成报告页模板下拉显示章节摘要 | P3 |
---
*本 PRD 由 Claude Code 编写,供 prd-plan / prd-code 流程使用。*
\ No newline at end of file
# 功能测试报告模板优化 · 计划执行文档
> **文档状态**:已定稿 · **版本**:v1.0 · **最后更新**:2026-08-17
> **维护者**:czj · **所属模块**:报告中心-功能测试
> **对应 PRD**:`_PRD_功能测试报告模板优化.md`
---
## 一、执行计划总览
| 阶段 | 内容 | 预估工时 | 产出物 |
|------|------|----------|--------|
| Phase 1 | 后端:P0 章节编号动态化(word/markdown 生成器) | 20min | 两个生成器改造 |
| Phase 2 | 后端:P1 模板描述字段 + 复制端点 + 数据库补齐 | 15min | 模型/schema/路由/database 修改 |
| Phase 3 | 前端:P1 描述输入 + 复制按钮 + API 封装 | 15min | reportTemplate.ts / TemplateConfig.vue |
| Phase 4 | 验证:编号动态化 + 描述 + 复制全流程 | 10min | 控制台验证输出 |
**总预估工时:60 分钟**
---
## 二、Phase 1:编号动态化(P0)
### 2.1 修改 `backend/app/services/functional/word_generator.py`
**改动点 1**:新增 `_zh_num()` 工具函数(在 `_set_cell_border` 之前):
```python
_ZH_NUM_MAP = {1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "七", 8: "八", 9: "九", 10: "十"}
def _zh_num(n: int) -> str:
"""整数转中文数字(1-10),超出返回阿拉伯数字"""
return _ZH_NUM_MAP.get(n, str(n))
```
**改动点 2**:10 处主章节标题由硬编码改为 `ch_idx` 动态生成:
| 原代码 | 新代码 |
|--------|--------|
| `_add_title_local("一、报告基本信息", level=2)` | `_add_title_local(f"{_zh_num(ch_idx)}、报告基本信息", level=2)` |
| `_add_title_local("二、测试执行摘要", level=2)` | `_add_title_local(f"{_zh_num(ch_idx)}、测试执行摘要", level=2)` |
| ...(其余 8 处同模式) | ... |
> 注意:所有 `_add_title_local(...)` 调用点必须位于对应的 `ch_idx += 1` **之后**。
**改动点 3**:主章节内的子章节标题前缀数字与 `ch_idx` 联动(约 22 处 `x.y` 前缀):
| 原代码 | 新代码 |
|--------|--------|
| `_add_title_local("1.1 报告标识", level=3)` | `_add_title_local(f"{ch_idx}.1 报告标识", level=3)` |
| `_add_title_local("1.2 测试概览", level=3)` | `_add_title_local(f"{ch_idx}.2 测试概览", level=3)` |
| `_add_title_local("1.3 被测系统信息", level=3)` | `_add_title_local(f"{ch_idx}.3 被测系统信息", level=3)` |
| ...(其余同模式,见验证清单) | ... |
### 2.2 修改 `backend/app/services/functional/markdown_generator.py`
**改动点 1**:同样新增 `_zh_num()` 工具函数(模块级)。
**改动点 2**`_title()` 函数的中文数字参数改为动态:
```python
def _title(num: int, name: str, level: int = 2) -> str:
"""生成章节标题(序号动态生成中文数字)"""
return f"{'#' * level} {_zh_num(num)} {name}"
```
调用点由 `_title(ch_idx, "一", "报告基本信息")` 改为 `_title(ch_idx, "报告基本信息")`(10 处)。
**改动点 3**:子章节标题 `### 1.1 报告标识` 等约 22 处改为 `### {ch_idx}.1 报告标识` 动态格式。
### 2.3 验证清单(子章节硬编码排查)
```bash
# 在改造后确认无残留硬编码
grep -n '"一、\|"二、\|"三、\|"四、\|"五、\|"六、\|"七、\|"八、\|"九、\|"十、' word_generator.py
grep -n '"### 1\.\|"### 2\.\|"### 3\.' markdown_generator.py
# 确认子章节前缀全部为 f"{ch_idx}." 动态格式
grep -n 'f"{ch_idx}\.' word_generator.py markdown_generator.py
```
---
## 三、Phase 2:模板描述 + 复制 + 数据库(P1)
### 3.1 修改 `backend/app/models/report_template.py`
```python
description: Mapped[str] = mapped_column(String(200), nullable=True, default="", comment="模板描述")
```
- `to_dict()` 增加 `"description": self.description or ""`
- `to_list_item()` 增加 `"description": self.description or ""`
### 3.2 修改 `backend/app/schemas/report_template.py`
- `ReportTemplateCreateRequest` 增加 `description: Optional[str] = Field(None, max_length=200)`
- `ReportTemplateUpdateRequest` 增加 `description: Optional[str] = Field(None, max_length=200)`
- `ReportTemplateListItem` 增加 `description: Optional[str] = Field(None)`
- `ReportTemplateResponse` 增加 `description: Optional[str] = Field(None)`
### 3.3 修改 `backend/app/routers/report_template.py`
**改动点 1**:create_template 处理 description:
```python
template = ReportTemplate(
name=request.name,
is_default=is_default,
config=json.dumps(config, ensure_ascii=False),
description=request.description or "",
)
```
**改动点 2**:update_template 处理 description:
```python
if request.description is not None:
template.description = request.description
```
**改动点 3**:新增复制端点:
```python
@router.post("/{template_id}/duplicate", response_model=ReportTemplateResponse, summary="复制模板")
async def duplicate_template(template_id: int, db: AsyncSession = Depends(get_db)):
"""
复制报告模板:config 与 description 完整继承,名称加"副本"后缀
"""
template = await db.get(ReportTemplate, template_id)
if not template:
raise HTTPException(status_code=404, detail=f"模板不存在: {template_id}")
# 生成不重复的名称
base_name = template.name
new_name = f"{base_name}_副本"
num = 2
while True:
result = await db.execute(select(ReportTemplate).where(ReportTemplate.name == new_name))
if not result.scalar_one_or_none():
break
new_name = f"{base_name}_副本{num}"
num += 1
new_template = ReportTemplate(
name=new_name,
is_default=False,
config=template.config, # JSON 字符串直接沿用
description=template.description or "",
)
db.add(new_template)
await db.commit()
await db.refresh(new_template)
logger.info(f"复制报告模板: {template.name} → {new_template.name} (id={new_template.id})")
return new_template.to_dict()
```
### 3.4 修改 `backend/app/database.py` — 数据库补齐
`_ensure_columns()``columns_to_add` 列表新增:
```python
# 功能测试报告模板:描述字段(旧库升级)
("report_templates", "description", "VARCHAR(200) DEFAULT ''"),
```
> 该机制对 SQLite / MySQL 均适用(幂等:列已存在则跳过)。
---
## 四、Phase 3:前端(P1)
### 4.1 修改 `frontend/src/api/reportTemplate.ts`
- 各接口类型增加 `description?: string`
- 新增方法:
```typescript
/**
* 复制模板
* @param id 原模板 ID
*/
async duplicate(id: number): Promise<ReportTemplateResponse> {
const response = await request.post(`/api/functional-report/templates/${id}/duplicate`)
return response as any
}
```
### 4.2 修改 `frontend/src/views/functional-report/TemplateConfig.vue`
**改动点 1**:模板列表项显示描述副标题:
```vue
<div class="template-name">{{ tpl.name }}</div>
<span v-if="tpl.description" class="template-desc">{{ tpl.description }}</span>
```
**改动点 2**:列表操作区新增"复制"按钮(`CopyDocument` 图标):
```vue
<el-tooltip content="复制模板" placement="top">
<el-button size="small" circle text @click.stop="handleDuplicate(tpl)">
<el-icon><CopyDocument /></el-icon>
</el-button>
</el-tooltip>
```
**改动点 3**:编辑表单基本信息区新增"模板描述":
```vue
<el-col :span="24">
<el-form-item label="模板描述">
<el-input v-model="editForm.description" type="textarea" :rows="2" maxlength="200" show-word-limit placeholder="模板用途说明(可选)" />
</el-form-item>
</el-col>
```
**改动点 4**`EditForm` 接口增加 `description: string``loadTemplateDetail()` 赋值 `editForm.description = detail.description || ''``handleSave()` 提交 `description: editForm.description`
**改动点 5**:新增 `handleDuplicate(tpl)` 方法:
```typescript
/** 复制模板 */
async function handleDuplicate(tpl: ReportTemplateListItem) {
try {
const result = await reportTemplateApi.duplicate(tpl.id)
ElMessage.success(`已复制模板「${result.name}」`)
await loadTemplates()
selectedId.value = result.id
await loadTemplateDetail(result.id)
} catch (err: any) {
ElMessage.error('复制模板失败:' + (err?.message || '未知错误'))
}
}
```
---
## 五、Phase 4:验证
### 5.1 编号动态化验证脚本
```python
import sys, os
sys.path.insert(0, "backend")
os.chdir("backend")
sys.path.insert(0, "app")
# 1. _zh_num 工具函数
from app.services.functional.word_generator import _zh_num
assert [_zh_num(i) for i in range(1, 11)] == ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"]
assert _zh_num(11) == "11" # 超出回退阿拉伯数字
# 2. 生成器仍可正常 import(无语法错误)
from app.services.functional.markdown_generator import generate_markdown_report
# 3. 用最小数据生成报告,验证编号
# 见下方完整脚本
```
### 5.2 生成报告编号结构验证
用测试 Excel(3 Sheet 新版 + 空 BUG 列表)生成 docx 与 md,检查标题文本:
```python
# md 文件直接读文本断言
content = Path(md_path).read_text(encoding="utf-8")
assert "## 一、报告基本信息" in content
assert "### 1.1 报告标识" in content
# 构造关闭 report_basic_info 的模板配置
tc = {"chapters": {"report_basic_info": False}}
# 重新生成,断言:
assert "## 一、测试执行摘要" in content # 原"二、" → "一、"
assert "### 1.1 测试结果统计" in content # 原"2.1" → "1.1"
```
### 5.3 描述 + 复制验证
```bash
# 后端 API 验证(用 uvicorn 起来后手动测,或直接 SQLite 库操作)
POST /templates {"name":"带描述模板","description":"测试用","config":{}} → 200,返回含 description
POST /templates/1/duplicate → 200,name="带描述模板_副本",description 继承
GET /templates → 列表项含 description
DELETE 副本验证不影响原模板
```
### 5.4 回归验证
| 验证项 | 检查方法 | 预期 |
|--------|---------|------|
| 全章节开启编号 | 生成报告读标题 | 一~十 / 1.1~10.4 与改造前一致 |
| Word 报告可打开 | python-docx 读段落 | 无异常 |
| 旧模板数据兼容 | 读无 description 的旧模板 | description="" 不报错 |
---
## 六、遇坑预案
| 风险 | 应对 |
|------|------|
| 子章节前缀遗漏某处硬编码 | 改造后 grep `"1\.\|"2\.` 等前缀,逐一核对 |
| MySQL ALTER 失败 | 仅单列 ADD COLUMN + DEFAULT '',若失败看具体报错 |
| 复制名称撞名 | 循环加序号 `_副本``_副本2``_副本3`… |
| description 未持久化 | 检查 create/update 两处赋值 + to_dict 两处序列化 |
回退:`git checkout -- backend/app/services/functional/word_generator.py backend/app/services/functional/markdown_generator.py backend/app/models/report_template.py backend/app/schemas/report_template.py backend/app/routers/report_template.py backend/app/database.py frontend/src/api/reportTemplate.ts frontend/src/views/functional-report/TemplateConfig.vue`
---
*本文档由 Claude Code 生成,供 prd-code 流程使用。*
\ No newline at end of file
# 功能测试用例 Excel 格式适配 · 需求文档(PRD)
> **文档状态**:已定稿 · **版本**:v1.0 · **最后更新**:2026-08-17
> **维护者**:czj · **所属模块**:报告中心-功能测试
---
## 1. 背景与目标
### 1.1 背景
功能测试报告模块当前**只读取 Excel 的 active sheet**(Sheet 1),且测试结果归一化映射表覆盖的枚举值有限:
| 现状 | 说明 |
|------|------|
| 单 Sheet 读取 | `read_test_cases()` 使用 `wb.active`,只读第一个 Sheet |
| 测试结果映射不全 | `pass_aliases` 未包含"功能验证"等新枚举值 |
近期收到新的测试用例文件 **「大亚湾一体化会议系统测试用例-260811.xlsx」**,其格式与现有模板格式存在差异,当前代码无法正确处理:
1. **多 Sheet 结构**:文件包含 3 个 Sheet(预约会议 / 会议看板 / 会议室管理),每个 Sheet 都是完整独立的测试用例表,需要全部读取合并
2. **新增测试结果枚举**:Sheet 中出现 `"功能验证"` 值(语义 = 通过),当前映射表未识别,会作为原始字符串返回导致统计错误
3. **其他格式要素与现有模板一致**:列结构(17 列)、表头行(Row 3)、数据起始行(Row 4)均与现有模板相同
### 1.2 目标
使功能测试报告模块能够**无缝适配新格式**的测试用例 Excel 文件:
```
读取全部 Sheet → 合并所有测试用例 → 识别新增测试结果枚举 → 正常统计分析 → 生成报告
```
具体目标:
1. **多 Sheet 读取**`read_test_cases()` 遍历工作簿所有 Sheet,合并读取所有测试用例
2. **新增结果映射**`normalize_test_result()` 增加 `"功能验证"``通过` 映射
3. **兼容性**:不破坏现有单 Sheet 格式文件的读取行为
### 1.3 参考文件
| 文件 | 说明 |
|------|------|
| `E:\GithubData\ubains-module-test\platform-auto-test\临时目录\大亚湾一体化会议系统测试用例-260811.xlsx` | 新格式样例(3 Sheet / 280 行数据) |
| `backend/app/services/functional/excel_reader.py` | 现有读取逻辑(单 Sheet) |
| `backend/app/services/functional/config.py` | 现有常量与映射配置 |
---
## 2. 功能需求
### 2.1 功能清单
| 编号 | 功能 | 说明 | 优先级 |
|------|------|------|--------|
| E1 | 多 Sheet 读取 | `read_test_cases()` 遍历所有 Sheet 合并读取 | **P0** |
| E2 | 新增"功能验证"映射 | `normalize_test_result()` 将"功能验证"归一化为"通过" | **P0** |
| E3 | 向后兼容 | 单 Sheet 旧格式文件读取行为不改变 | P0 |
| E4 | 数据验证 | 对 3 个 Sheet 均执行现有行跳过逻辑(序号为空跳过) | P1 |
### 2.2 功能详细说明
#### E1. 多 Sheet 读取
**现状**`read_test_cases()``ws = wb.active` 只读取第一个 Sheet。
**改造**:遍历 `wb.sheetnames` 中所有 Sheet,对每个 Sheet 执行相同的数据行读取逻辑(`min_row=CASE_DATA_START_ROW`,序号为 0 跳过),按 Sheet 顺序拼接所有测试用例。
**结果**:3 个 Sheet 合计 277 条数据全部读入,`module` 字段自动分组统计(报告按模块分布统计不受影响)。
#### E2. 新增测试结果映射
**现状**`pass_aliases` 包含 `{"通过", "Pass", "PASS", "pass", "成功", "P", "√", "✓", "是", "yes", "Yes", "YES"}`
**改造**:新增 `"功能验证"` 别名。
**结果**`"功能验证"``TEST_RESULT_PASS`("通过"),通过率统计正确。
#### E3. 向后兼容
- 单 Sheet 文件:遍历只有一个 Sheet,行为与现在完全一致
- 多参数 `skip` 逻辑不变:仅对 `serial_number` 为 0 的行跳过
#### E4. 数据验证
- 每个 Sheet 独立判断数据起始行(Row 4)
- 遇空行继续向下(不做提前 break,避免 Sheet 间数据遗漏)
- 用例编号生成规则不变(如 `HYKB-001``YYHY-001`),供 BUG 关联使用
---
## 3. 非功能需求
| 项 | 说明 |
|----|------|
| 性能 | 多 Sheet 合并后仍为单次读取,无额外 IO |
| 兼容 | 不影响 BUG 列表 Excel 读取(仍为单 Sheet) |
| 回归 | 现有 17 列模板文件(`data/input/用例模板.xlsx`)可正常读取 |
---
## 4. 验收标准
| # | 验收点 | 预期结果 |
|---|--------|----------|
| 1 | 上传新格式文件(3 Sheet) | 后端成功解析,读取全部 277 条用例 |
| 2 | 数据统计 | 通过/失败/未验证/未开发四类统计正确,"功能验证"计入通过 |
| 3 | 生成报告 | 10 章节报告正常生成,模块分布覆盖 3 个 Sheet 的模块 |
| 4 | 旧格式文件回归 | 原单 Sheet 模板文件读取与统计结果不变 |
---
*本 PRD 由 Claude Code 编写,供 prd-plan / prd-code 流程使用。*
\ No newline at end of file
# 功能测试用例 Excel 格式适配 · 计划执行文档
> **文档状态**:已定稿 · **版本**:v1.0 · **最后更新**:2026-08-17
> **维护者**:czj · **所属模块**:报告中心-功能测试
> **对应 PRD**:`_PRD_功能测试用例Excel适配.md`
---
## 一、执行计划总览
| 阶段 | 内容 | 预估工时 | 产出物 |
|------|------|----------|--------|
| Phase 1 | 后端:excel_reader.py 多 Sheet 遍历改造 | 15min | `excel_reader.py` 修改 |
| Phase 2 | 后端:config.py 新增测试结果映射 | 5min | `config.py` 修改 |
| Phase 3 | 验证:用新格式 Excel 文件测试 | 10min | 控制台验证输出 |
**总预估工时:30 分钟**
---
## 二、Phase 1:excel_reader.py 多 Sheet 遍历改造
### 修改文件:`backend/app/services/functional/excel_reader.py`
**改动点**`read_test_cases()` 函数中,将 `ws = wb.active` 改为遍历 `wb.sheetnames` 所有 Sheet。
### 当前代码(第 164-165 行)
```python
def read_test_cases(file_path: str) -> List[TestCase]:
wb = openpyxl.load_workbook(file_path, data_only=True)
ws = wb.active
```
### 目标代码
```python
def read_test_cases(file_path: str) -> List[TestCase]:
wb = openpyxl.load_workbook(file_path, data_only=True)
test_cases = []
for sheet_name in wb.sheetnames: # ← 遍历所有 Sheet
ws = wb[sheet_name]
for row in ws.iter_rows(min_row=CASE_DATA_START_ROW):
serial_number = _get_int_cell_value(row, CASE_COL_SERIAL_NUMBER)
if serial_number == 0:
continue
case = TestCase(
serial_number=serial_number,
# ... 其余字段不变 ...
)
test_cases.append(case)
wb.close()
return test_cases
```
### 不变的部分
- `CASE_DATA_START_ROW` 常量(仍为 4)
- 所有列索引常量(`CASE_COL_*`
- 数据行的`serial_number == 0` 跳过逻辑
- `_get_cell_value()` / `_get_int_cell_value()` 工具函数
- `read_bug_list()` 函数(BUG 列表仍为单 Sheet,无需修改)
---
## 三、Phase 2:excel_reader.py 新增测试结果映射
### 修改文件:`backend/app/services/functional/excel_reader.py`
> 注:`normalize_test_result()` 及其 `pass_aliases` 定义位于 `excel_reader.py`(非 config.py),已按实际位置修改。
**改动点**:在 `pass_aliases` 集合中新增 `"功能验证"`
### 当前代码(第 308 行)
```python
pass_aliases = {"通过", "Pass", "PASS", "pass", "成功", "P", "√", "✓", "是", "yes", "Yes", "YES"}
```
### 目标代码
```python
pass_aliases = {"通过", "Pass", "PASS", "pass", "成功", "P", "√", "✓", "是", "yes", "Yes", "YES", "功能验证"}
```
---
## 四、Phase 3:验证
### 验证脚本
```python
from app.services.functional.excel_reader import read_test_cases, normalize_test_result
# 验证 1:多 Sheet 读取
cases = read_test_cases("临时目录/大亚湾一体化会议系统测试用例-260811.xlsx")
assert len(cases) == 277, f"预期 277 条,实际 {len(cases)}"
# 验证 2:测试结果统计
from collections import Counter
result_counter = Counter(c.test_result for c in cases)
print("测试结果分布:", dict(result_counter))
# 验证 3:模块分布
modules = set(c.module for c in cases)
print(f"模块数: {len(modules)}")
print("模块列表:", sorted(modules))
# 验证 4:旧格式兼容
old_cases = read_test_cases("data/input/用例模板.xlsx")
assert len(old_cases) > 0, "旧格式读取失败"
```
### 人工验证点
| 验证项 | 检查方法 | 预期 |
|--------|---------|------|
| 总用例数 | 统计 len(cases) | 277 |
| "功能验证"映射 | 筛选 `test_result="通过"` 的用例 | 包含原"功能验证"的用例 |
| 模块分组 | 统计 module 字段 | 覆盖 3 个 Sheet 的模块 |
| 旧格式回归 | 读取原模板文件 | 数据量不变 |
---
## 五、潜在风险与回退方案
| 风险 | 概率 | 应对 |
|------|------|------|
| 某 Sheet 有额外的合并单元格导致读取异常 | 低 | openpyxl 对合并单元格会返回 None,`_get_cell_value()` 已处理 |
| 某 Sheet 列顺序不一致 | 低 | 检查确认 3 个 Sheet 列结构完全一致 |
| 旧格式文件有不可见 Sheet | 低 | 不影响,旧文件只有 1 个 Sheet |
回退方案:`git checkout -- backend/app/services/functional/excel_reader.py` 恢复单 Sheet 读取。
---
*本文档由 Claude Code 生成,供 prd-code 流程使用。*
\ No newline at end of file
# HANDOFF — 性能测试模块会话交接文档
> **生成时间**: 2026-08-13(晚)
> **当前分支**: `platform-auto-test`
> **最近提交**: `ce2c75de` feat: 功能测试报告新增模板配置功能(多模板管理);`7c1c0488` feat(performance): API接口预设功能
> **会话窗口**: 性能测试 — API 接口预设功能上线 + 新建会议压测任务创建
> **状态**: ✅ 接口预设功能已实现/提交/部署;「新建会议压测」任务已创建(pending,待执行验证 bodyTemplate)
---
## ⚠️ 多窗口并行开发注意
参考 `Docs/多窗口并行开发指南.md`
| 项目 | 本窗口(性能测试) | 其他窗口 |
|------|-------------------|----------|
| 后端端口 | 8001 | 视情况 |
| 前端端口 | 3000 | 视情况 |
| 开发模块 | 性能测试 | 其他模块 |
| 数据库 | 使用 `data/test_platform.db`,注意写入错开 | 同上 |
---
## ⚡ 最新会话更新(2026-08-13 晚)
### A. API 接口预设功能 — 已完成 ✅
**实现内容**(提交 `7c1c0488`,已部署至 5.60):
| 组件 | 文件 | 说明 |
|------|------|------|
| 模型 | `backend/app/models/api_preset.py` | `ApiPreset` 表:name/method/target_url/headers/body/body_template/auth_required/sign_request/account_key/capture_rules |
| Schema | `backend/app/schemas/api_preset.py` | `CaptureRule``json_path` snake_case 字段,无 to_camel) |
| 路由 | `backend/app/routers/performance.py` | `POST/GET/PUT/DELETE /api/performance/presets` |
| 服务 | `backend/app/services/api_preset_service.py` | 预设 CRUD + capture_rules 嵌套更新 |
| 继承 | `backend/app/services/performance_service.py` `_inherit_from_preset()` | 任务传 `preset_id` 时合并预设字段(仅补空,不覆盖) |
| 前端 | 预设管理页 + 任务表单预设下拉 | 选预设自动继承 method/url/headers/body/bodyTemplate/captureRules/auth/sign |
**部署脚本**`deploy_perf_preset.py`(git pull → 上传前端 dist → restart app → 校验迁移字段 `performance_tasks.preset_id`)。
> ⚠️ **数据库现状**:服务器(5.60 MySQL)当前已重置/切换,旧 HANDOFF 记录的 `perf_f27debe671d84b4fb3908af5beac67ed`(新建会议接口压测,1046 请求)**已不在服务器上**。现存仅 2 个任务(见 6.2)。
### B. 「新建会议V3」接口预设 — 已创建 ✅
| 字段 | 值 |
|------|-----|
| ID | `preset_e62140d37c29431284a2bad8fb67528c` |
| method | `PUT` |
| target_url | `https://192.168.5.44/meetingV3/api/message/book` |
| auth_required | `true` |
| sign_request | `true`(创建后补 PUT/签名) |
| account_key | `superadmin` |
### C. 「新建会议压测」任务 — 已创建(pending)⏳
| 字段 | 值 |
|------|-----|
| ID | `perf_5a1a3b4e00e04b2f96713f4108580d2b` |
| preset_id | 继承「新建会议V3」 |
| 模式 | 并发 1 · 持续 15s · ramp_up 0 |
| bodyTemplate | `messageName={__RANDOM_NAME_8__}` / `startTime={__NOW__}` / `endTime={__NOW_+2_H__}` / `topicList={__RANDOM_NAME_4__}` |
| captureRules | `message_ids``$.data.messageId`(multiple=true) |
| 状态 | `pending`**尚未执行**,待验证 bodyTemplate 是否解决重复创建会议的 4xx 问题) |
> **关键修复点**:上次压测 1045/1046 因 body 硬编码重复创建会议返回 4xx。本次用 bodyTemplate 随机化 messageName/topicName + 动态时间,且签名基于解析后的 body。**执行后需重点核对 4xx 是否归零、message_ids 是否被成功捕获**。
### D. 账号映射(已确认)
实际压测登录固定用 **admin@xty / Ubains@13579**(验证码 csba),来自 `_DEFAULT_ACCOUNTS`(superadmin/admin/user 三档全部映射到同一账号),**与预设/任务的 `account_key` 取值无关**。用户已确认此行为正常。
### E. 本次会话待办(交接给下一窗口)
| # | 任务 | 说明 |
|---|------|------|
| 1 | **执行 `perf_5a1a3b4e00e04b2f96713f4108580d2b`** | 验证 bodyTemplate 动态替换 + sign 是否解决 4xx;检查 `message_ids` 捕获 |
| 2 | 若 4xx 仍存在 | 排查 body 结构(PRD 示例 vs 实际接口字段)、签名 body 内容、token 有效性 |
| 3 | 配置断言 | 当前 `assertions=[]`,4xx 会被算"成功"(无断言),建议加 `status_code equals 200` |
| 4 | 创建「修改会议」「取消会议」压测任务 | 取消会议用 `{__REF:perf_5a1a3b4e00e04b2f96713f4108580d2b:message_ids__}` 引用 |
---
## 一、历史任务与完成情况
### 1.1 核心任务:新建会议 API 性能测试
**用户需求**:对被测系统(https://192.168.5.44)的"新建会议"接口进行性能压测。
**接口信息**
- `PUT https://192.168.5.44/meetingV3/api/message/book`
- 需要 `Authorization: Bearer {token}`
- 需要 `X-RANDOM/X-TIMESTAMP/X-SIGN` 签名头
- 请求体为 JSON **数组** `[{...}]`
**已完成**
1.**修复 Pydantic schema body 类型**`body: Optional[Dict[str, Any]]``Optional[Any]`,支持 JSON 数组(涉及 PerformanceTaskCreate、PerformanceTaskUpdate、PerformanceTaskResponse)
2.**修复执行器签名支持**`_build_headers()` 原忽略 `sign_request` 字段,现当 `signRequest=True` 时自动调用 `_generate_sign()` 生成签名头
3.**创建并执行压测任务**
- 任务 ID:`perf_f27debe671d84b4fb3908af5beac67ed`
- 并发 1 / 时长 15s / 预热 5s
- 结果:1046 请求,1046 成功(但 1045 个 4xx,因 body 硬编码重复创建会议)
### 1.2 文档产出
| 文档 | 路径 | 说明 |
|------|------|------|
| PRD 需求文档 | `Docs/PRD/性能测试/需求文档/_PRD_bodyTemplate动态变量替换与响应捕获.md` | bodyTemplate 变量替换 + 响应捕获 + 跨任务引用 |
| 计划执行文档 | `Docs/PRD/性能测试/需求文档/_PRD_bodyTemplate动态变量替换与响应捕获_计划执行.md` | 6 个 Phase 详细执行计划 |
| 接口 Curl 记录 | `Docs/meeting_api_curls.md` | 新建/修改/取消会议三个 API 的 curl 原文 |
### 1.3 其他相关文件
| 文件 | 说明 |
|------|------|
| `backend/capture_meeting_api.py` | Playwright 脚本(已废弃,用户提供 curl 替代) |
| `backend/tmp_create_meeting_task.json` | 创建任务时使用的临时 JSON 文件 |
---
## 二、后端变更摘要
### 2.1 已修改文件(工作区未提交)
| 文件 | 修改内容 |
|------|---------|
| `backend/app/schemas/performance.py` | `body: Optional[Dict[str, Any]]``Optional[Any]`(3 处),增加 `description` 说明"支持 dict 或 list" |
| `backend/app/executors/performance_executor.py` | `_build_headers()` 新增 `sign_request=True` 时自动生成 X-RANDOM/X-TIMESTAMP/X-SIGN 签名头 |
### 2.2 关键代码位置
- **性能测试执行器**`backend/app/executors/performance_executor.py`(817 行)
- `_build_headers()` 约 470-507 行(含新加的 sign_request 逻辑)
- `_send_request()` 约 739-790 行
- `execute()` 入口约 509-549 行
- **性能测试 Schema**`backend/app/schemas/performance.py`(271 行)
- **性能测试模型**`backend/app/models/performance.py`(284 行)
- **签名算法**`backend/app/executors/http_client.py` 约 179-230 行 `_generate_sign()`
- **性能测试路由**`backend/app/routers/performance.py`(297 行)
- **性能测试服务**`backend/app/services/performance_service.py`
### 2.3 服务启动信息
```bash
# 后端
cd backend && uvicorn app.main:app --reload --port 8001
# 前端
cd frontend && npm run dev
```
---
## 三、会议模块 API 信息
### 3.1 三个接口
| 接口 | 方法 | URL | 说明 |
|------|------|-----|------|
| 新建会议 | PUT | `/meetingV3/api/message/book` | 已完成压测 |
| 修改会议 | PUT | `/meetingV3/api/message/update` | 已记录 curl |
| 取消会议 | DELETE | `/meetingV3/api/message/cancel/{mid}` | 已记录 curl |
### 3.2 认证与签名要求
- **认证**`Authorization: Bearer {token}`(通过 `authRequired=true` + `accountKey="superadmin"` 自动获取)
- **签名**`X-RANDOM` + `X-TIMESTAMP` + `X-SIGN`(通过 `signRequest=true` 自动生成,AES-CBC 加密,密钥从 Bearer Token 派生)
- **请求体**:JSON 数组格式(已修复 Pydantic 类型限制,`body: Optional[Any]`
### 3.3 curl 原文位置
`Docs/meeting_api_curls.md` 记录了三个接口的完整 curl 命令(含 Authorization 和签名头)。
---
## 四、已知问题与待办
### 4.1 已发现的问题
| # | 问题 | 影响 | 状态 |
|---|------|------|------|
| 1 | body 硬编码,请求数据重复 | 1045/1046 请求返回 4xx(重复创建会议) | ✅ **已解决**(bodyTemplate 动态变量,见最新会话更新 C) |
| 2 | 错误率统计依赖断言,非 HTTP 状态码 | 虽 4xx 但无断言→全部算成功 | 待配置断言后验证;或考虑改进统计逻辑 |
| 3 | `body_template` 字段已定义但执行器未使用 | 无法动态替换变量 | ✅ **已解决**`template_resolver.py` 已实现,8 种占位符,执行器已集成) |
| 4 | 无 `capture_rules` 字段 | 无法捕获响应中的 messageId | ✅ **已解决**(模型/Schema/执行器 `_capture_response` 已实现) |
### 4.2 待办事项(按优先级)
| 优先级 | 任务 | 对应 Phase | 说明 |
|--------|------|-----------|------|
| ✅ 已完成 | 新增 PerformanceTaskOutput 模型 + capture_rules 字段 | Phase 1 | 模型/Schema 扩展 |
| ✅ 已完成 | 实现 TemplateResolver 解析器 | Phase 2 | 8 种占位符 |
| ✅ 已完成 | 执行器集成模板替换 + 响应捕获 | Phase 3 | `_send_request` + `_capture_response` |
| ✅ 已完成 | 服务/路由:输出保存 + 查询 API | Phase 4 | 跨任务引用(`{__REF:...__}`) |
| ✅ 已完成 | 前端 UI:预设管理 + 任务表单继承 | — | 接口预设功能(`7c1c0488`) |
| P0 | **执行新建会议压测任务,验证全链路** | Phase 6 | `perf_5a1a3b4e00e04b2f96713f4108580d2b`,确认 4xx 归零 + messageIds 捕获 |
| P1 | 集成验证:新建→取消会议全链路 | Phase 6 | 取消会议任务用 REF 引用 message_ids |
| P1 | 改进错误率统计(HTTP 状态码维度) | — | 当前仅依赖断言,4xx 无断言会误判成功 |
### 4.3 实施建议
**Phase 1 实现要点**
- 新建 `models/performance_output.py``PerformanceTaskOutput` 表含 `(task_id, key, value, created_at)`,唯一约束 `(task_id, key)`
- 新建 `schemas/performance_output.py`,定义 `CaptureRule` Schema
- `PerformanceTaskCreate` 增加 `capture_rules: Optional[List[CaptureRule]]``body_template: Optional[Dict[str, Any]]`(后者已有)
- 注册新表到 `database.py`
**Phase 2 实现要点**
- 新建 `executors/template_resolver.py``TemplateResolver`
- 正则 `r"\{__([A-Z_+\d:]+?)__\}"` 匹配占位符
- 支持 `NOW` / `NOW_+N_H` / `NOW_+N_M` / `RANDOM_NAME_N` / `UUID` / `RANDOM_INT_N_M` / `INDEX` / `REF:task_id:key`
- 不修改原模板,返回新容器
**Phase 3 实现要点**
- `_send_request` 中先 `_resolve_body` 再签名
- `_build_headers` 接受 `body_for_sign` 参数
- 每次响应后按 `capture_rules` 提取字段
- `_worker` 循环传递 `request_index`
---
## 五、关键决策记录
| 日期 | 决策 | 理由 |
|------|------|------|
| 2026-08-13 | 使用用户提供的 curl 替代 Playwright 抓包 | 用户明确要求:"或者我把请求给你 请求的curl给你" |
| 2026-08-13 | body 类型改为 `Optional[Any]` 而非新增 `ListBody` | 减少 Schema 复杂度,SQLAlchemy JSON 列和 aiohttp 原生支持 list |
| 2026-08-13 | sign_request 独立于 auth_required | 登录接口签名用空 token,业务接口签名用真实 token,两个开关独立控制 |
| 2026-08-13 | 选择 bodyTemplate 方案而非动态脚本 | 配置化,无需写代码,前端可编辑 |
| 2026-08-13 晚 | 引入「接口预设」抽象 | 重复接口配置(method/url/auth/sign/body)固化为预设,任务继承 + 增量覆盖,避免每次重填 |
| 2026-08-13 晚 | `_DEFAULT_ACCOUNTS` 三档全部映射到 admin@xty | 测试环境仅有单一管理员账号,superadmin/admin/user 统一映射,与预设 account_key 取值解耦 |
| 2026-08-13 晚 | 新建会议压测任务传 `preset_id` 继承 + 显式 `target_url` | schema 把 `target_url` 列为必填,即便 preset_id 继承也必须显式传(校验在前,继承在后) |
| 2026-08-13 晚 | `capture_rules` 用 snake_case `json_path` | 嵌套 `CaptureRule``performance_output.py`)未挂 `to_camel`,顶层任务 schema 虽用 camelCase 别名,但嵌套字段名必须原样 `json_path` |
---
## 六、会话资源
### 6.1 参考文档
| 文档 | 路径 |
|------|------|
| 性能测试 PRD | `Docs/PRD/性能测试/需求文档/_PRD_性能测试模块需求文档.md` |
| 性能测试计划执行 | `Docs/PRD/性能测试/需求文档/_PRD_性能测试模块_计划执行.md` |
| bodyTemplate PRD | `Docs/PRD/性能测试/需求文档/_PRD_bodyTemplate动态变量替换与响应捕获.md` |
| bodyTemplate 计划执行 | `Docs/PRD/性能测试/需求文档/_PRD_bodyTemplate动态变量替换与响应捕获_计划执行.md` |
| 会议 API Curl | `Docs/meeting_api_curls.md` |
| CLAUDE.md | `CLAUDE.md`(项目全局约束、踩坑记录) |
### 6.2 已创建的性能测试任务(5.60 服务器实际现状)
> 服务器 MySQL 库已重置/切换,旧任务 `perf_f27debe671d84b4fb3908af5beac67ed`(1046 请求)已不存在。当前现存:
| 任务 ID | 名称 | 状态 | 说明 |
|---------|------|------|------|
| `perf_5a1a3b4e00e04b2f96713f4108580d2b` | 新建会议压测 | `pending` | **本次会话新建**,PUT meetingV3/book,bodyTemplate+captureRules,待执行验证 |
| `perf_0fbcb792dcc143fd80b42cb405a75176` | 登录接口性能基准测试 | completed | 10983 请求 |
### 6.3 已创建的接口预设
| 预设 ID | 名称 | 方法/URL | 说明 |
|---------|------|----------|------|
| `preset_e62140d37c29431284a2bad8fb67528c` | 新建会议V3 | `PUT https://192.168.5.44/meetingV3/api/message/book` | auth+sign,被「新建会议压测」任务继承 |
### 6.4 用户提到的后续需求
1. ~~实现 bodyTemplate 变量替换 + 响应捕获~~ ✅ 已完成
2. 创建修改会议、取消会议的性能测试任务 ⏳ 待做
3. 取消会议引用新建会议产出的 messageId ⏳ 待做(`{__REF:perf_5a1a3b4e00e04b2f96713f4108580d2b:message_ids__}`
---
*本 HANDOFF 文档由 Claude Code 自动生成,用于会话间交接。*
\ No newline at end of file
# 执行计划 - 修复报告查看TPS趋势和响应时间趋势图表不显示
> **文档类型**: 执行计划文档
> **创建日期**: 2026-08-13
> **作者**: czj
> **关联文档**: `_问题处理_报告查看TPS趋势图表不显示.md`
> **状态**: 待执行
---
## 一、改动总览
| # | 改动项 | 文件 | 说明 |
|---|--------|------|------|
| 1 | 调整 `loadReport()``loading` 状态设置时机 | `frontend/src/views/performance/ReportPanel.vue` | 数据到达后先设 `loading=false` 再初始化图表,确保图表 div 已在 DOM 中 |
---
## 二、详细执行步骤
### Step 1:修复 `loadReport()` 中的 loading 时序
**文件**`frontend/src/views/performance/ReportPanel.vue`(第 247-261 行)
**问题**`loading.value = true` 在 API 调用期间保持,导致 `v-if="loading"` 骨架屏优先渲染,图表 div 不在 DOM 中,`initCharts()` 获取不到 ref。
**修复方案**:在 `report.value = res` 赋值后,立即将 `loading` 设为 `false`,使 Vue 将 DOM 切换为图表区块,再调用 `initCharts()``updateCharts()`
**改动前**
```typescript
async function loadReport() {
if (!taskId.value) return
loading.value = true
try {
const res = await getReport(taskId.value)
report.value = res
await nextTick()
initCharts() // ❌ loading=true,图表 div 不在 DOM 中
updateCharts() // ❌ 操作未初始化的图表实例
} catch (e: any) {
ElMessage.error('加载报告失败: ' + (e.response?.data?.detail || e.message || ''))
} finally {
loading.value = false
}
}
```
**改动后**
```typescript
async function loadReport() {
if (!taskId.value) return
loading.value = true
try {
const res = await getReport(taskId.value)
report.value = res
loading.value = false // ✅ 先让图表 div 渲染到 DOM
await nextTick() // ✅ 等待 Vue DOM 更新
initCharts() // ✅ 此时 ref 有效
updateCharts() // ✅ 图表正确渲染
} catch (e: any) {
ElMessage.error('加载报告失败: ' + (e.response?.data?.detail || e.message || ''))
loading.value = false
}
// 注意:不再有 finally 中的 loading=false
}
```
**具体改动**(第 252 行后插入 `loading.value = false`,移除 `finally` 块中的 `loading.value = false`):
```diff
async function loadReport() {
if (!taskId.value) return
loading.value = true
try {
const res = await getReport(taskId.value)
report.value = res
+ loading.value = false
await nextTick()
initCharts()
updateCharts()
} catch (e: any) {
ElMessage.error('加载报告失败: ' + (e.response?.data?.detail || e.message || ''))
+ loading.value = false
- } finally {
- loading.value = false
}
}
```
### Step 2:验证前端构建
`frontend/` 目录下执行构建验证:
```bash
cd frontend && npm run build
```
预期结果:无 TypeScript 错误,构建成功。
### Step 3:部署到服务器
将前端构建产物上传到 192.168.5.60 并重启容器:
```bash
# 构建前端
cd frontend && npm run build
# 上传 dist 到服务器
scp -r frontend/dist/* ubains@192.168.5.60:/home/ubains/app/frontend/
# 重启容器
ssh ubains@192.168.5.60 "docker restart app"
```
---
## 三、验证步骤
| 步骤 | 操作 | 预期结果 |
|------|------|----------|
| 1 | 访问 http://localhost:3000,进入性能测试 → 报告查看 | 页面正常加载,显示任务选择器 |
| 2 | 选择一个已完成的任务,点击"查看报告" | 骨架屏短暂显示 → 摘要、指标卡片正常显示 |
| 3 | 滚动到趋势图区域 | **TPS 趋势图**显示折线面积图,**响应时间趋势图**显示多条折线 |
| 4 | 点击"刷新报告" | 图表重新加载,保持不变 |
| 5 | 切换任务 | 图表跟随新任务数据更新 |
| 6 | 进入性能测试 → 执行监控 | 实时监控面板图表不受影响(回归测试) |
---
## 四、回滚方案
如果修复后出现异常(如骨架屏闪烁、图表加载失败等),撤销 Step 1 的代码改动即可:
```bash
git checkout -- frontend/src/views/performance/ReportPanel.vue
```
---
*本文档由 Claude Code 生成,遵循项目执行计划文档规范。*
\ No newline at end of file
# 问题处理文档 - 报告查看TPS趋势和响应时间趋势图表不显示
> **文档类型**: 问题处理文档
> **创建日期**: 2026-08-13
> **作者**: czj
> **优先级**: P0
> **状态**: 待修复
---
## 一、问题描述
### 1.1 现象
性能测试任务执行完成后,通过左侧菜单「性能测试 → 报告查看」进入报告面板,摘要信息、核心指标卡片、响应时间摘要、状态码分布均正常显示,但**TPS趋势图和响应时间趋势图两个ECharts图表区域为空白**,没有渲染出任何折线或面积图。
![图表空白](图片占位 - 实际表现为两个图表卡片内无内容)
### 1.2 复现步骤
1. 执行一个性能测试任务(任意模式),等待完成
2. 通过左侧菜单「性能测试 → 报告查看」进入
3. 在任务选择器中选择刚完成的任务 → 点击"查看报告"
4. 观察:摘要和指标正常,但两个趋势图空白
5. 点击"刷新报告"按钮 → 现象不变
### 1.3 影响范围
- 所有已完成/失败的性能测试任务均受影响
- 报告面板的趋势图功能完全不可用
- 用户无法通过趋势图直观观察压测过程中TPS和响应时间的变化趋势
---
## 二、根因分析
### 2.1 代码定位
**文件**`frontend/src/views/performance/ReportPanel.vue`
**关键代码片段**——模板结构(第 41-163 行):
```html
<template v-else>
<!-- 刷新/导出按钮 -->
<el-skeleton v-if="loading" :rows="8" animated />
<template v-else-if="report">
<!-- 摘要信息、指标卡片、状态码表格 -->
<!-- TPS 趋势图 (ref="tpsChartRef") -->
<!-- 响应时间趋势图 (ref="rtChartRef") -->
</template>
<el-empty v-else description="暂无报告数据" />
</template>
```
**关键代码片段**——`loadReport()` 函数(第 247-261 行):
```typescript
async function loadReport() {
if (!taskId.value) return
loading.value = true
try {
const res = await getReport(taskId.value)
report.value = res
await nextTick()
initCharts() // ← 此时 loading 仍为 true,图表 DOM 不存在
updateCharts() // ← 操作未初始化的图表实例
} catch (e: any) {
ElMessage.error('加载报告失败: ' + ...)
} finally {
loading.value = false // ← 图表初始化完成后才隐藏骨架
}
}
```
**`initCharts()` 函数**(第 276-283 行):
```typescript
function initCharts() {
if (tpsChartRef.value && !tpsChart) {
tpsChart = echarts.init(tpsChartRef.value)
}
if (rtChartRef.value && !rtChart) {
rtChart = echarts.init(rtChartRef.value)
}
}
```
### 2.2 根因详解
**Vue 条件渲染冲突**
1. 模板使用 `v-if="loading"`(骨架屏)和 `v-else-if="report"`(图表内容)构成互斥条件分支
2. `loadReport()` 的调用时序:
-`loading.value = true` → 骨架屏渲染
-`report.value = res` → 数据赋值
-`await nextTick()` → 等待 Vue 完成一次 DOM 更新
-**`initCharts()`** → 此时 `loading` 仍为 `true`,所以 DOM 中渲染的是骨架屏,**不是图表区块**
-`updateCharts()` → 操作未初始化的 `tpsChart``rtChart`(均为 `null`
-`finally { loading.value = false }` → 骨架屏隐藏,图表区块渲染,但此时 `initCharts()` 已过
3. 结果:`tpsChartRef.value``rtChartRef.value``undefined``echarts.init(undefined)` 静默失败,图表实例未创建,后续 `setOption()` 无效果
### 2.3 对比分析:MonitorPanel 为何正常
**文件**`frontend/src/views/performance/MonitorPanel.vue`
MonitorPanel 的图表在模板中**不在条件渲染分支内**——只要 `taskId` 存在,图表 div 始终在 DOM 中:
```html
<template v-else>
<!-- TPS 趋势图 -->
<el-card><div ref="tpsChartRef" class="chart" /></el-card>
<!-- 响应时间趋势图 -->
<el-card><div ref="rtChartRef" class="chart" /></el-card>
<!-- 状态码分布 -->
<el-card><div ref="statusChartRef" class="chart" /></el-card>
</template>
```
`initCharts()``onMounted``watch` 中调用,此时图表 div 已在 DOM 中,`ref` 值有效,`echarts.init()` 成功。
### 2.4 数据流验证
已通过实际 API 请求验证后端数据正常:
| 验证项 | 结果 |
|--------|------|
| API 返回快照数量 | 241 条 |
| 数据字段格式 | camelCase(`p50ResponseTime``avgResponseTime``tps` 等) |
| 字段值 | 非零有效值(如 TPS 22~64,响应时间 201~818ms) |
| 前端类型定义 | `PerformanceSnapshot` 接口字段名与 API 返回一致 |
| Pydantic 序列化 | `PerformanceSnapshotResponse` 使用 `populate_by_name=True`+`alias_generator=to_camel`,正确输出 camelCase |
**结论:后端 API 数据完全正确,问题纯属前端渲染时序 Bug。**
---
## 三、修复方案
### 3.1 方案 A(推荐):调整 loading 状态设置时机
**改动最小**:在 `loadReport()` 中,设置 `report.value = res` 后立即将 `loading` 设为 `false`,使图表 div 渲染到 DOM 中,再调用 `initCharts()`
```typescript
async function loadReport() {
if (!taskId.value) return
loading.value = true
try {
const res = await getReport(taskId.value)
report.value = res
loading.value = false // ← 先让图表 div 渲染到 DOM
await nextTick() // ← 等待 DOM 更新
initCharts() // ← 此时 ref 有效
updateCharts() // ← 图表正确渲染
} catch (e: any) {
ElMessage.error('加载报告失败: ' + ...)
loading.value = false
}
// 移除 finally 中的 loading=false
}
```
**优点**:改动仅 1 行,逻辑清晰,不影响骨架屏的加载体验。
**缺点**:无。
### 3.2 方案 B:使用 `watch` 监听 `report` 变化
利用 Vue 的 `watch``report` 值变化后自动初始化图表:
```typescript
watch(report, (newVal) => {
if (newVal) {
nextTick(() => {
initCharts()
updateCharts()
})
}
})
```
**优点**:关注点分离,`loadReport()` 只负责数据加载。
**缺点**:增加额外 watch,需要处理组件卸载时 dispose。
### 3.3 方案选择
**推荐方案 A**,原因:
- 改动最小(1 行代码)
- 修复逻辑直观
- 不引入新的 watch 或生命周期复杂度
- 不影响骨架屏的首次加载体验(API 调用期间骨架屏正常显示)
---
## 四、验收标准
| 测试项 | 预期结果 |
|--------|----------|
| 报告查看 - TPS 趋势图 | 显示折线面积图,x 轴为时间(秒),y 轴为 TPS 值 |
| 报告查看 - 响应时间趋势图 | 显示多条折线(平均/P50/P90/P99),x 轴为时间,y 轴为 ms |
| 报告查看 - 刷新报告 | 点击刷新后图表重新加载,保持显示 |
| 报告查看 - 首次加载 | 骨架屏在 API 调用期间正常显示,数据到达后平滑切换为图表 |
| 报告查看 - 无数据任务 | 显示"暂无报告数据"空状态,图表不渲染 |
| 报告查看 - 无任务 | 显示任务选择器(回归测试) |
| MonitorPanel 回归 | 实时监控面板图表不受影响 |
---
## 五、附录
### 5.1 涉及文件
| 文件 | 说明 |
|------|------|
| `frontend/src/views/performance/ReportPanel.vue` | 需要修改的主文件(第 252 行后增加 `loading.value = false`) |
### 5.2 相关技术栈
- Vue 3 Composition API(`v-if`/`v-else-if` 条件渲染)
- ECharts(`echarts.init()` 需要 DOM 元素已存在)
- Element Plus `el-skeleton`(骨架屏组件)
### 5.3 已知风险
- 无。修复逻辑简单,且已通过 MonitorPanel 的实现验证了正确模式。
---
*本文档由 Claude Code 生成,遵循项目问题处理文档规范。*
\ No newline at end of file
# PRD - 性能测试 bodyTemplate 动态变量替换与响应捕获
> **文档版本**: v1.1
> **创建日期**: 2026-08-13
> **关联模块**: 性能测试
> **优先级**: P1
> **关联需求**: `Docs/PRD/性能测试/需求文档/_PRD_性能测试模块需求文档.md`
---
## 一、背景与目标
### 1.1 背景
当前性能测试模块已具备基本的 HTTP 压测能力(并发/QPS/阶梯模式),但在实际压测场景中暴露出两个关键问题:
1. **请求体硬编码**:body 中的参数字段(如会议名称、开始时间、结束时间)是静态值,每次请求发送完全相同的数据。对于"新建会议"这类接口,第一个请求成功创建会议后,后续请求因重复创建而全部返回 4xx 错误,导致压测结果失真。
2. **响应数据无法复用**:创建会议接口返回的 `messageId`(会议 ID)无法被后续的"取消会议"性能测试任务引用,导致相关接口无法进行有效的自动化压测。
### 1.2 目标
1. **bodyTemplate 动态变量替换**:支持在请求体模板中使用占位符变量(如 `{__NOW__}`),每次请求前自动替换为动态值,确保每次请求的 body 数据唯一有效。
2. **响应捕获与输出复用**:支持从响应体中提取关键字段(如 `messageId`),存储为任务输出,后续任务可引用这些输出作为输入参数。
3. **保持向后兼容**:已有任务(无 bodyTemplate、无响应捕获配置)执行逻辑不变,不受影响。
---
## 二、功能范围
### 2.1 核心功能
| 功能 | 描述 | 优先级 |
|------|------|--------|
| bodyTemplate 变量替换 | 支持 `{__NOW__}` 等占位符在每次请求时动态替换 | P0 |
| 响应字段捕获 | 从响应 JSON 中提取指定字段,存储为任务输出 | P0 |
| 任务输出查询 | 支持查询某次执行产生的输出数据 | P0 |
| 输出跨任务引用 | 一个任务的输出可作为另一个任务的输入变量 | P1 |
| 自定义变量扩展 | 支持用户自定义变量格式和替换规则 | P2 |
### 2.2 支持的占位符变量
| 占位符 | 说明 | 示例值 |
|--------|------|--------|
| `{__NOW__}` | 当前时间,格式 `YYYY-MM-DD HH:mm` | `2026-08-13 14:30` |
| `{__NOW_+N_H__}` | 当前时间 + N 小时 | `{__NOW_+2_H__}``2026-08-13 16:30` |
| `{__NOW_+N_M__}` | 当前时间 + N 分钟 | `{__NOW_+30_M__}``2026-08-13 15:00` |
| `{__RANDOM_NAME_N__}` | 随机名称(N 位字母数字) | `{__RANDOM_NAME_8__}``a3Kf9xQ2` |
| `{__UUID__}` | 随机 UUID(去横线) | `e8d4a2f17c3b4d9e8a5f6b7c8d9e0f1a` |
| `{__RANDOM_INT_N_M__}` | N 到 M 之间的随机整数 | `{__RANDOM_INT_1000_9999__}``5732` |
| `{__INDEX__}` | 当前请求序号(从 0 开始) | `0`, `1`, `2`, ... |
| `{__REF:task_id:key__}` | 引用其他任务的输出 | `{__REF:perf_xxx:message_ids__}` |
### 2.3 响应捕获
响应捕获规则定义在任务配置中,支持从响应 JSON 中按 JSON Path 提取字段:
```json
{
"captureRules": [
{
"key": "message_ids",
"jsonPath": "$.data.messageId",
"multiple": true,
"description": "捕获创建会议返回的 messageId"
}
]
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `key` | string | 输出键名,用于后续引用 |
| `jsonPath` | string | JSON Path 表达式,提取目标字段 |
| `multiple` | bool | 是否允许多个值(true=追加到数组,false=覆盖) |
| `description` | string | 规则描述 |
---
## 三、详细需求
### 3.1 bodyTemplate 变量替换机制
#### 3.1.1 执行流程
```
_send_request() 调用时:
1. 检查 task.body_template 是否存在
2. 若存在,深拷贝 bodyTemplate 作为模板
3. 递归遍历模板中的所有字符串值,匹配占位符模式
4. 对每个匹配的占位符,调用对应生成器生成动态值
5. 替换后的结果作为本次请求的 json_data
6. 若 bodyTemplate 不存在,使用 task.body(原有逻辑)
```
#### 3.1.2 变量解析器设计
```python
class TemplateResolver:
"""
请求体模板变量替换器
支持占位符:
- {__NOW__} → 当前时间 "YYYY-MM-DD HH:mm"
- {__NOW_+N_H__} → 当前时间 + N 小时
- {__NOW_+N_M__} → 当前时间 + N 分钟
- {__RANDOM_NAME_N__} → N 位随机字符串
- {__UUID__} → 随机 UUID(无横线)
- {__RANDOM_INT_N_M__} → N~M 随机整数
- {__INDEX__} → 请求序号
- {__REF:task_id:key__} → 引用其他任务输出
"""
def resolve(self, template: dict, request_index: int) -> dict:
"""递归替换模板中的占位符,返回实际请求体"""
```
#### 3.1.3 签名适配
当使用 bodyTemplate 替换后,每次请求的 body 内容不同,签名计算必须基于替换后的实际 body,而非模板:
```python
# _send_request 中的签名逻辑调整:
json_data = self._resolve_body_template(task, request_index) # 先替换
headers = self._build_headers(task, body_for_sign=json_data) # 用实际 body 签名
```
### 3.2 响应捕获机制
#### 3.2.1 数据模型
```python
class PerformanceTaskOutput(Base):
"""
性能测试任务输出
记录性能测试执行过程中从响应体捕获的字段值,
供其他任务引用(如创建会议后捕获 messageId,用于取消会议)。
"""
__tablename__ = "performance_task_outputs"
id: int # 自增主键
task_id: str # 关联任务 ID
key: str # 输出键名(如 "message_ids")
value: list # 输出值列表(JSON 数组)
created_at: datetime
```
#### 3.2.2 捕获执行流程
```
每次请求响应后:
1. 检查 task.capture_rules 是否存在
2. 对每条 capture rule:
a. 解析响应 JSON
b. 按 jsonPath 提取字段值
c. 若 multiple=true,追加到已有值列表
d. 若 multiple=false,覆盖已有值
3. 压测完成后,将最终输出写入数据库
```
#### 3.2.3 跨任务引用
`{__REF:task_id:key__}` 被引用时:
1. 任务启动时,从 `performance_task_outputs` 表查询 `task_id` 对应的 `key` 的输出
2. 将输出值注入到 bodyTemplate 的解析上下文中
3. 请求时按顺序或随机从值列表中取一个
### 3.3 API 扩展
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/performance/tasks/{id}/outputs` | 获取任务输出列表 |
| GET | `/api/performance/tasks/{id}/outputs/{key}` | 获取指定键的输出值 |
### 3.4 Schema 扩展
#### PerformanceTaskCreate / PerformanceTaskUpdate 新增字段
```python
body_template: Optional[Dict[str, Any]] = Field(None, description="请求体模板(支持变量替换)")
capture_rules: Optional[List[CaptureRule]] = Field(None, description="响应捕获规则列表")
```
#### CaptureRule Schema
```python
class CaptureRule(BaseModel):
"""响应捕获规则"""
key: str = Field(..., description="输出键名")
json_path: str = Field(..., description="JSON Path 表达式")
multiple: bool = Field(True, description="是否允许多值")
description: str = Field("", description="规则描述")
```
### 3.5 前端变更
#### 3.5.1 任务配置页 — bodyTemplate 编辑器
- 新增"请求体模板"编辑区域(与"请求体"互斥,二选一)
- 提供占位符插入按钮:`{__NOW__}``{__NOW_+2_H__}``{__UUID__}``{__RANDOM_NAME_8__}`
- 支持 JSON 语法高亮和校验
#### 3.5.2 任务配置页 — 响应捕获配置
- 新增"响应捕获规则"配置区域
- 支持添加/删除/编辑捕获规则
- 每项配置:key、jsonPath、是否多值
#### 3.5.3 任务详情页 — 输出查看
- 任务执行完成后,显示"执行输出"标签页
- 展示所有捕获的 key-value 列表
- 支持复制值,方便手动粘贴到其他任务配置
---
## 四、非功能需求
### 4.1 性能要求
- 变量替换对单次请求的额外耗时 < 1ms(模板解析后缓存编译结果)
- 大量响应捕获时不显著增加内存占用(限制最大捕获值数量,默认 10000 条)
### 4.2 兼容性
- 已有任务(无 bodyTemplate、无 captureRules)完全不受影响
- bodyTemplate 替换结果不影响数据库存储的原始 body 字段
- 响应捕获不改变现有指标统计逻辑
### 4.3 边界条件
| 场景 | 行为 |
|------|------|
| bodyTemplate 和 body 同时为空 | 发送无 body 请求(GET/DELETE 正常) |
| bodyTemplate 中的占位符无法解析 | 保留原样字符串,不替换 |
| 引用的 REF 任务输出为空 | 使用空字符串,记录警告日志 |
| 响应 JSON 中 jsonPath 不存在 | 跳过该条捕获规则,记录警告 |
| 捕获值超过上限 | 截断并记录警告 |
---
## 五、应用场景示例
### 5.1 新建会议压测
```json
{
"bodyTemplate": {
"messageName": "性能压测会议_{__RANDOM_NAME_8__}",
"startTime": "{__NOW__}",
"endTime": "{__NOW_+2_H__}",
"companyNumber": "cd",
"uid": "402881e7843e5eb101843e6b25960001",
"configValueId": "402881e7843e5eb101843e6b25960001",
"participantList": [
{
"uid": "402881e7843e5eb101843e6b25960001",
"name": "admin",
"department": "技术部",
"companyNumber": "cd"
}
],
"topicList": [
{ "topicName": "性能压测议题_{__RANDOM_NAME_4__}" }
],
"duration": 120
},
"captureRules": [
{
"key": "message_ids",
"jsonPath": "$.data.messageId",
"multiple": true,
"description": "捕获创建会议返回的 messageId"
}
]
}
```
### 5.2 取消会议压测(引用输出)
```json
{
"bodyTemplate": {
"mid": "{__REF:perf_create_xxx:message_ids__}"
},
"captureRules": []
}
```
---
## 六、影响范围
### 6.1 新增文件
| 文件 | 说明 |
|------|------|
| `backend/app/executors/template_resolver.py` | 变量替换解析器 |
| `backend/app/models/performance_output.py` | 任务输出数据库模型 |
| `backend/app/schemas/performance_output.py` | 任务输出 Pydantic Schema |
| `backend/app/routers/performance_output.py` | 任务输出 API 路由 |
### 6.2 修改文件
| 文件 | 变更 |
|------|------|
| `backend/app/executors/performance_executor.py` | `_send_request` 集成模板替换 + 响应捕获 |
| `backend/app/schemas/performance.py` | 新增 `body_template``capture_rules` 字段 |
| `backend/app/models/performance.py` | 新增 `capture_rules` 字段(已有 `body_template`) |
| `backend/app/services/performance_service.py` | 任务执行后保存输出 |
| `backend/app/database.py` | 注册新表 |
| `backend/app/main.py` | 注册新路由 |
| `frontend/src/api/performance.ts` | 新增 API 封装 |
| `frontend/src/types/performance.ts` | 新增类型定义 |
| `frontend/src/views/performance/*.vue` | 配置页 UI 扩展 |
---
## 七、验收标准
| # | 验收项 | 验证方式 |
|---|--------|---------|
| 1 | bodyTemplate 中的 `{__NOW__}` 被替换为当前时间 | 抓包查看实际请求 body |
| 2 | `{__NOW_+2_H__}` 正确替换为当前时间 + 2 小时 | 同上 |
| 3 | `{__RANDOM_NAME_8__}` 每次请求生成不同的 8 位随机字符串 | 连续请求查看 |
| 4 | 无 bodyTemplate 的任务执行逻辑不变 | 对比改造前后结果 |
| 5 | 响应捕获规则正确提取 messageId 并存储 | 查询 outputs API |
| 6 | multiple=true 模式下捕获多个值 | 连续请求后查看 outputs 数组长度 |
| 7 | `{__REF:task_id:key__}` 正确引用其他任务的输出 | 取消会议任务成功使用 messageId |
| 8 | 占位符无法解析时保留原样 | 配置无效占位符验证 |
\ No newline at end of file
# 性能测试 bodyTemplate 动态变量替换与响应捕获 - 计划执行文档
> **文档版本**: v1.1
> **创建日期**: 2026-08-13
> **关联需求**: `_PRD_bodyTemplate动态变量替换与响应捕获.md`(位于 `Docs/PRD/性能测试/需求文档/`)
> **执行策略**: 分阶段迭代,每阶段可独立验证
---
## 一、执行总览
### 1.1 阶段划分
| 阶段 | 名称 | 核心交付 | 预计工期 |
|------|------|---------|---------|
| Phase 1 | Schema 与模型扩展 | PerformanceTask 新增 capture_rules + 新表 PerformanceTaskOutput | 0.5天 |
| Phase 2 | 模板变量解析器 | TemplateResolver 支持 8 种占位符 | 1天 |
| Phase 3 | 执行器集成 | _send_request 集成模板替换 + 响应捕获 + 签名适配 | 1天 |
| Phase 4 | 服务与路由 | 输出保存、输出查询 API、跨任务引用 | 0.5天 |
| Phase 5 | 前端页面 | 配置页 UI + 输出查看 | 1天 |
| Phase 6 | 集成测试与验证 | 新建会议全链路验证 + 取消会议引用验证 | 0.5天 |
### 1.2 依赖关系
```
Phase 1 (模型/Schema) ──→ Phase 4 (服务/路由)
Phase 2 (解析器) ───────→ Phase 3 (执行器集成) ──→ Phase 6 (集成测试)
↑ ↑
Phase 5 (前端页面) ─────────────────┘
```
### 1.3 文件清单
| 文件 | 阶段 | 说明 |
|------|------|------|
| `backend/app/models/performance_output.py` | Phase 1 | PerformanceTaskOutput 新表 |
| `backend/app/schemas/performance_output.py` | Phase 1 | 输出 Pydantic Schema |
| `backend/app/schemas/performance.py` | Phase 1 | 新增 capture_rules 字段 |
| `backend/app/models/performance.py` | Phase 1 | 新增 capture_rules 字段(body_template 已有) |
| `backend/app/executors/template_resolver.py` | Phase 2 | 变量替换解析器 |
| `backend/app/executors/performance_executor.py` | Phase 3 | 集成模板替换 + 响应捕获 |
| `backend/app/services/performance_service.py` | Phase 4 | 输出保存 + 跨任务引用 |
| `backend/app/routers/performance_output.py` | Phase 4 | 输出查询 API |
| `backend/app/database.py` | Phase 4 | 注册新表 |
| `backend/app/main.py` | Phase 4 | 注册新路由 |
| `frontend/src/api/performance.ts` | Phase 5 | 新增 API |
| `frontend/src/types/performance.ts` | Phase 5 | 新增类型 |
| `frontend/src/views/performance/TaskConfig.vue` | Phase 5 | 配置页 UI |
| `frontend/src/views/performance/TaskDetail.vue` | Phase 5 | 输出查看 |
---
## 二、Phase 1: Schema 与模型扩展
### 2.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 1.1 | 新增 PerformanceTaskOutput 模型 | `backend/app/models/performance_output.py` | 输出存储表 |
| 1.2 | 新增输出 Schema | `backend/app/schemas/performance_output.py` | Output/Create/List 响应 |
| 1.3 | PerformanceTask 新增 capture_rules | `backend/app/schemas/performance.py` | Schema + model |
| 1.4 | 注册新表到 database.py | `backend/app/database.py` | 导入新模型 |
### 2.2 详细设计
#### 2.2.1 PerformanceTaskOutput 模型
```python
class PerformanceTaskOutput(Base):
"""性能测试任务输出"""
__tablename__ = "performance_task_outputs"
id: int = mapped_column(Integer, primary_key=True, autoincrement=True)
task_id: str = mapped_column(String(64), index=True) # 关联任务ID
key: str = mapped_column(String(100)) # 输出键名
value: list = mapped_column(JSON, default=list) # 值列表(JSON数组)
created_at: datetime = mapped_column(DateTime, default=datetime.utcnow)
# 唯一约束:(task_id, key) 允许一次执行更新覆盖
```
#### 2.2.2 CaptureRule Schema(加入 performance.py)
```python
class CaptureRule(BaseModel):
"""响应捕获规则"""
key: str = Field(..., description="输出键名")
json_path: str = Field(..., description="JSON Path 表达式,如 $.data.messageId")
multiple: bool = Field(True, description="是否允许多值追加")
description: str = Field("", description="规则描述")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
```
`PerformanceTaskCreate` / `PerformanceTaskUpdate` 增加字段:
```python
capture_rules: Optional[List[CaptureRule]] = Field(None, description="响应捕获规则列表")
body_template: Optional[Dict[str, Any]] = Field(None, description="请求体模板(支持变量替换)")
```
`body` 字段已在前序修复中放宽为 `Optional[Any]`
---
## 三、Phase 2: 模板变量解析器
### 3.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 2.1 | 实现 TemplateResolver 类 | `backend/app/executors/template_resolver.py` | 变量替换核心 |
| 2.2 | 单元测试 | `backend/tests/test_template_resolver.py` | 8 种占位符覆盖 |
### 3.2 详细设计
#### 3.2.1 占位符正则
```python
import re
PATTERN = re.compile(
r"\{__([A-Z_+\d:]+?)__\}"
)
# 占位符格式:
# NOW
# NOW_+2_H
# NOW_+30_M
# RANDOM_NAME_8
# UUID
# RANDOM_INT_1000_9999
# INDEX
# REF:perf_xxx:message_ids
```
#### 3.2.2 解析器实现
```python
class TemplateResolver:
"""请求体模板变量替换器"""
def __init__(self, ref_provider: Optional[Callable[[str, str], list]] = None):
"""
Args:
ref_provider: 跨任务引用取值回调 (task_id, key) -> list
None 时 REF 占位符不替换
"""
self._ref_provider = ref_provider
self._now_offset = 0 # 测试时可注入固定时间
def resolve(self, template, request_index: int = 0):
"""
递归替换模板中的占位符
Args:
template: dict/list/str,支持嵌套
request_index: 当前请求序号(对应 {__INDEX__})
Returns:
替换后的 dict/list/str(不修改原模板,浅拷贝顶层容器)
"""
if isinstance(template, dict):
return {k: self.resolve(v, request_index) for k, v in template.items()}
if isinstance(template, list):
return [self.resolve(v, request_index) for v in template]
if isinstance(template, str):
return self._replace_string(template, request_index)
return template
def _replace_string(self, text: str, request_index: int) -> str:
if "__" not in text:
return text
def _repl(match):
token = match.group(1)
return self._resolve_token(token, request_index)
return PATTERN.sub(_repl, text)
def _resolve_token(self, token: str, request_index: int) -> str:
parts = token.split(":")
kind = parts[0]
if kind == "NOW":
# NOW / NOW_+2_H / NOW_+5_M
if len(parts) > 1:
return self._now_with_offset(parts[1], parts[2] if len(parts) > 2 else "M")
return self._now_str()
if kind == "RANDOM_NAME":
length = int(parts[1]) if len(parts) > 1 else 8
return self._random_string(length)
if kind == "UUID":
return uuid.uuid4().hex
if kind == "RANDOM_INT":
lo, hi = int(parts[1]), int(parts[2])
return str(random.randint(lo, hi))
if kind == "INDEX":
return str(request_index)
if kind == "REF":
# REF:task_id:key
task_id, key = parts[1], parts[2]
if self._ref_provider:
values = self._ref_provider(task_id, key)
if values:
return str(values[request_index % len(values)])
return ""
# 未知占位符 → 保留原样
return f"{{__{token}__}}"
# ---------- 时间相关 ----------
def _now_str(self) -> str:
return (datetime.now() + timedelta(minutes=self._now_offset)).strftime("%Y-%m-%d %H:%M")
def _now_with_offset(self, delta: str, unit: str) -> str:
n = int(delta.lstrip("+"))
base = datetime.now() + timedelta(minutes=self._now_offset)
if unit == "H":
base += timedelta(hours=n)
elif unit == "M":
base += timedelta(minutes=n)
elif unit == "D":
base += timedelta(days=n)
return base.strftime("%Y-%m-%d %H:%M")
def _random_string(self, length: int) -> str:
chars = string.ascii_letters + string.digits
return "".join(random.choice(chars) for _ in range(length))
```
#### 3.2.3 设计要点
- **不修改原模板**:resolve 返回新容器,原始 bodyTemplate 保持数据库存储不变
- **支持嵌套**:dict/list 递归处理,字符串内可包含多个占位符
- **未知占位符保留原样**:避免静默吞掉拼写错误
- **REF 循环取值**:多值输出按 `request_index % len(values)` 轮询使用,保证每次请求用不同的 messageId
- **可注入 now_offset**:便于单元测试验证时间偏移
---
## 四、Phase 3: 执行器集成
### 4.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 3.1 | _send_request 集成模板替换 | `backend/app/executors/performance_executor.py` | 请求体动态化 |
| 3.2 | 签名基于实际 body | 同上 | 替换后的 body 参与签名 |
| 3.3 | 响应捕获回调 | 同上 | 每次响应后提取字段 |
### 4.2 详细设计
#### 4.2.1 _send_request 改造
```python
def _resolve_body(self, task, request_index: int):
"""解析请求体:bodyTemplate 优先,其次 body"""
if getattr(task, "body_template", None):
return self._template_resolver.resolve(task.body_template, request_index)
return task.body if task.method.upper() in ("POST", "PUT", "PATCH") else None
async def _send_request(self, session, task, request_index: int = 0):
start = time.time()
status = 0
body = ""
elapsed_ms = 0.0
try:
# 1. 解析请求体(先于签名)
if self._is_login_target(task):
json_data = self._get_login_body(task)
else:
json_data = self._resolve_body(task, request_index)
# 2. 签名基于实际请求体
headers = self._build_headers(task, body_for_sign=json_data)
async with session.request(
method, url,
headers=headers,
json=json_data,
ssl=False,
timeout=aiohttp.ClientTimeout(total=30),
) as resp:
status = resp.status
body = await resp.text()
elapsed_ms = (time.time() - start) * 1000
# 3. 响应捕获
if getattr(task, "capture_rules", None):
self._capture_response(task.capture_rules, body, resp.status)
...
```
#### 4.2.2 _build_headers 签名适配
原签名逻辑中 `body_for_sign``task.body`,需改为接收实际解析后的 body:
```python
def _build_headers(self, task, body_for_sign=None) -> Dict[str, str]:
...
if task.sign_request and not self._is_login_target(task) and self._token:
if body_for_sign is None:
body_for_sign = task.body if task.method.upper() in ("POST", "PUT", "PATCH") else None
sign_headers = self._get_http_client()._generate_sign(
body_data=body_for_sign, bearer_token=f"Bearer {self._token}"
)
headers.update(sign_headers)
return headers
```
#### 4.2.3 响应捕获实现
```python
def _capture_response(self, capture_rules, body_text, status_code):
"""从响应体中按规则捕获字段"""
if not body_text:
return
try:
data = json.loads(body_text)
except Exception:
return # 非 JSON 响应,跳过捕获
for rule in capture_rules:
key = rule["key"]
path = rule.get("jsonPath", rule.get("json_path", ""))
multiple = rule.get("multiple", True)
values = self._extract_json_path(data, path)
if values:
# 存入内存缓冲区,结束后统一写库
if key not in self._captured:
self._captured[key] = []
if multiple:
self._captured[key].extend(values)
else:
self._captured[key] = [values[-1]]
def _extract_json_path(self, data, path):
"""简化 JSON Path 提取,支持 $.a.b.c 与 $.a[0].b"""
# 实现要点:
# - 以 $ 开头,按 . 分段
# - 支持数组下标 [n]
# - 支持通配 *(提取所有元素)
# - 找不到返回 []
```
#### 4.2.4 请求序号传递
`_worker` 循环中维护 `request_index` 自增计数,传递到 `_send_request`
```python
async def _worker(self, session, task, semaphore):
request_index = 0
while self._running:
async with semaphore:
await self._send_request(session, task, request_index)
request_index += 1
await self._sleep_with_check(...)
```
> 说明:`{__INDEX__}` 为每个 worker 内局部自增;若需全局唯一序号,改为线程安全计数器(见 Phase 6 优化项)。
---
## 五、Phase 4: 服务与路由
### 5.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 4.1 | 执行完成后保存输出 | `backend/app/services/performance_service.py` | 缓冲区 → DB |
| 4.2 | 输出查询路由 | `backend/app/routers/performance_output.py` | GET outputs |
| 4.3 | 注册路由与新表 | `main.py` + `database.py` | 启动加载 |
| 4.4 | 跨任务引用提供者 | IPO 中 executor 的 ref_provider | REF 占位符 |
### 5.2 详细设计
#### 5.2.1 执行完成保存输出
`PerformanceExecutor.execute()` 返回结果中携带 `captured` 字典;`_run_sync` 完成后由 service 写入:
```python
# performance_service.py
async def _save_outputs(self, task_id: str, captured: dict):
"""保存捕获的输出到数据库"""
for key, values in (captured or {}).items():
# upsert: (task_id, key) 存在则覆盖 value,否则插入
existing = await self.db.execute(
select(PerformanceTaskOutput).where(
PerformanceTaskOutput.task_id == task_id,
PerformanceTaskOutput.key == key,
)
)
obj = existing.scalar_one_or_none()
if obj:
obj.value = values
else:
self.db.add(PerformanceTaskOutput(task_id=task_id, key=key, value=values))
await self.db.commit()
```
#### 5.2.2 跨任务引用提供者
```python
# service 中构造 executor 时传入:
def _ref_provider(task_id: str, key: str) -> list:
# 查数据库,返回最新的输出值列表
...
return values or []
executor = PerformanceExecutor(config, ref_provider=self._ref_provider)
```
#### 5.2.3 输出查询 API
| 方法 | 路径 | 响应 |
|------|------|------|
| GET | `/api/performance/tasks/{id}/outputs` | `[{key, value, created_at}]` |
| GET | `/api/performance/tasks/{id}/outputs/{key}` | `{key, value}` |
---
## 六、Phase 5: 前端页面
### 6.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 5.1 | 配置页 bodyTemplate 编辑器 | `TaskConfig.vue` | 占位符按钮 + JSON 校验 |
| 5.2 | 配置页捕获规则编辑器 | `TaskConfig.vue` | 规则增删改 |
| 5.3 | 任务详情输出查看 | `TaskDetail.vue` | 输出标签页 |
### 6.2 详细设计
#### 6.2.1 bodyTemplate 编辑器
- 请求体与请求体模板互斥(radio 切换)
- bodyTemplate 使用 JSON 编辑器(textarea + 校验)
- 提供占位符快捷插入按钮:
`{__NOW__}` `{__NOW_+1_H__}` `{__NOW_+30_M__}` `{__UUID__}` `{__RANDOM_NAME_8__}` `{__RANDOM_INT_1000_9999__}` `{__INDEX__}`
#### 6.2.2 捕获规则编辑器
| 列 | 说明 |
|----|------|
| key | 输出键名 |
| jsonPath | 如 `$.data.messageId` |
| 多值 | switch |
| 描述 | 可选 |
| 操作 | 删除 |
#### 6.2.3 输出查看
- 加载任务后调用 `GET /api/performance/tasks/{id}/outputs`
- 表格展示 key / value(数组展开)/ 更新时间
- 复制按钮
---
## 七、Phase 6: 集成测试与验证
### 7.1 测试用例
| # | 场景 | 步骤 | 预期 |
|---|------|------|------|
| 6.1 | bodyTemplate 基础替换 | 创建任务含 `{__NOW__}`,执行,抓包 | body 中 startTime=当前时间 |
| 6.2 | 时间偏移 | `{__NOW_+2_H__}` | endTime = startTime + 2h |
| 6.3 | 随机名称 | `{__RANDOM_NAME_8__}` | 每次请求名称不同 |
| 6.4 | 无模板兼容 | 旧任务(仅 body) | 行为与改造前一致 |
| 6.5 | 签名正确性 | 有模板 + sign_request=true | 请求返回 2xx(签名通过) |
| 6.6 | 响应捕获 | captureRules 提取 messageId | outputs API 返回 messageId 数组 |
| 6.7 | 跨任务引用 | 取消会议任务 REF 引用 | 使用真实 messageId 成功 |
| 6.8 | 占位符容错 | 无效占位符 | 原样保留,不报错 |
### 7.2 真实场景验证
1. **新建会议任务**:bodyTemplate + captureRules → 执行 → `message_ids` 输出
2. **取消会议任务**`{__REF:xx:message_ids__}` → 执行 → 验证 2xx 成功
3. **前端操作**:配置页创建任务 → 执行 → 输出查看
---
## 八、风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
| JSON Path 实现复杂度 | 影响 Phase 3 工期 | 先支持 `.a.b` / `[n]` / `*` 子集,复杂路径用逗号分隔多个子路径 |
| 签名与请求体不一致 | 每次请求 body 变化导致签名频繁计算 | 签名复用现有 HttpClient._generate_sign,仅输入变化 |
| 捕获值过多内存占用 | 高并发 + 大量捕获 | 缓冲区上限 10000 条,超出丢弃并告警 |
| REF 引用死锁/性能 | 引用任务输出查询频繁 | 每任务启动时一次性加载 REF 值到内存,不每次查询 |
| 前端 JSON 编辑器体验 | 复杂模板编辑不便 | 先用 textarea + 占位符按钮,后续升级 Monaco |
---
## 九、验收标准
见 PRD 第七节。核心验收:
1. `{__NOW__}``{__NOW_+N_H__}``{__RANDOM_NAME_N__}` 替换正确
2. 无 bodyTemplate 任务行为不变
3. 响应捕获的 messageId 可通过 API 查询
4. 取消会议任务引用 messageIds 后压测请求返回成功
5. 签名在动态 body 下依然有效(目标接口返回非 4xx 签名错误)
\ No newline at end of file
# PRD - 性能测试 API 接口预设功能
> **文档版本**: v1.0
> **创建日期**: 2026-08-13
> **关联模块**: 性能测试
> **优先级**: P0
> **关联需求**: `Docs/PRD/性能测试/需求文档/_PRD_性能测试模块需求文档.md`
---
## 一、背景与目标
### 1.1 背景
当前性能测试模块创建任务时,用户需要手动填写以下字段:
- HTTP 方法(GET/POST/PUT/DELETE)
- 目标 URL(完整路径)
- 请求头(JSON 格式)
- 请求体(JSON 格式)
- 请求体模板(bodyTemplate,带占位符)
- 响应捕获规则
- 认证配置(是否需要登录、签名)
这些字段对于同一个 API 接口是固定的,每次新建任务都重复填写既繁琐又容易出错。用户希望将"接口配置"抽象为可复用的**预设**,创建任务时只需选择预设即可自动填充,降低使用门槛。
### 1.2 目标
1. **接口预设管理**:支持用户在界面上增删改查 API 接口预设(方法、URL、请求头、请求体、模板、捕获规则等)
2. **预设选择创建任务**:创建压测任务时,通过下拉框选择预设,选定后自动填充并隐藏接口细节,用户只需配压测参数
3. **保持向后兼容**:已有任务(无预设)不受影响,仍可手动编辑
---
## 二、功能范围
### 2.1 核心功能
| 功能 | 描述 | 优先级 |
|------|------|--------|
| 接口预设 CRUD | 创建/编辑/删除/查询接口预设 | P0 |
| 预设选择创建任务 | 任务创建表单增加预设下拉框 | P0 |
| 预设继承 | 选定预设后自动填充接口字段并隐藏 | P0 |
| 预设列表页 | 独立页面管理所有预设 | P1 |
| 预设导入/导出 | 预设配置的 JSON 导入导出 | P2 |
### 2.2 预设包含的字段
| 字段 | 类型 | 说明 |
|------|------|------|
| 名称 | string | 预设名称,如"新建会议V3" |
| 描述 | string | 可选,说明用途 |
| HTTP 方法 | string | GET/POST/PUT/DELETE |
| 目标 URL | string | 接口完整 URL(含协议和域名) |
| 请求头 | JSON | 自定义请求头,如 Content-Type |
| 请求体 | JSON | 静态请求体(与 bodyTemplate 互斥) |
| 请求体模板 | JSON | 带 `{__XXX__}` 占位符的模板 |
| 响应捕获规则 | array | 捕获响应字段的规则列表 |
| 是否需要登录 | bool | 是否自动获取 Token |
| 是否需要签名 | bool | 是否生成 X-RANDOM/X-TIMESTAMP/X-SIGN |
| 使用账号 | string | 登录使用的账号 key |
---
## 三、详细需求
### 3.1 数据模型
#### 3.1.1 ApiPreset 模型
```python
class ApiPreset(Base):
"""API 接口预设"""
__tablename__ = "api_presets"
id: str # 唯一标识 (preset_xxx)
name: str # 预设名称,如"新建会议V3"
description: str # 描述
# 接口配置
method: str # HTTP 方法
target_url: str # 目标 URL
headers: dict # 请求头
body: Optional[dict|list] # 静态请求体(与 body_template 互斥)
body_template: Optional[dict] # 请求体模板(支持变量替换)
# 认证配置
auth_required: bool # 是否需要登录 Token
sign_request: bool # 是否需要签名
account_key: Optional[str] # 使用账号
# 响应捕获
capture_rules: Optional[list] # 捕获规则列表
created_at: datetime
updated_at: datetime
```
#### 3.1.2 PerformanceTask 模型扩展
`PerformanceTask` 新增字段 `preset_id`,记录创建时使用的预设(可选,向后兼容):
```python
preset_id: Optional[str] # 关联的预设 ID,可为空
```
### 3.2 API 接口
#### 3.2.1 预设管理 API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/performance/presets` | 获取预设列表(分页) |
| POST | `/api/performance/presets` | 创建预设 |
| GET | `/api/performance/presets/{id}` | 获取预设详情 |
| PUT | `/api/performance/presets/{id}` | 更新预设 |
| DELETE | `/api/performance/presets/{id}` | 删除预设 |
#### 3.2.2 任务创建 API 扩展
`POST /api/performance/tasks` 新增可选字段 `preset_id`
`preset_id` 传入时,后端自动从预设继承接口配置(method、target_url、headers、body、body_template、capture_rules、auth_required、sign_request、account_key),前端传的对应字段可覆盖预设值。
### 3.3 前端变更
#### 3.3.1 预设管理页面
- 独立页面 `/performance/presets`,列表展示所有预设
- 每行显示:名称、方法、URL 摘要、更新时间
- 操作:编辑、删除
- 新建/编辑对话框:
- 名称(必填)
- 描述(可选)
- HTTP 方法(下拉选择 GET/POST/PUT/DELETE)
- 目标 URL(必填)
- 请求头(JSON 文本域)
- 请求体 / 请求体模板(互斥,radio 切换,JSON 文本域)
- 响应捕获规则(表格增删改,同 bodyTemplate PRD 设计)
- 认证开关(是否需要登录 + 签名 + 账号选择)
#### 3.3.2 任务创建表单改造
**当前表单**:直接展示 HTTP 方法、目标 URL、请求头、请求体等字段
**改造后表单**
1. 第一行:**预设下拉框**(可选,新增)
- 选项:无预设 + 所有已定义的预设
- 选择预设后,自动填充接口字段并**隐藏**这些字段
- 切换预设自动更新
2. 常规压测参数保持不变:
- 任务名称
- 压测模式(并发/QPS/阶梯)
- 并发数 / QPS / 阶梯配置
- 时长、预热时间
- 断言配置
3. 当选择"无预设"时,展示原有的全部接口字段(完全向后兼容)
#### 3.3.3 预设选择后的只读摘要
选择预设后,在表单中预设下拉框下方显示**只读摘要**,让用户看到当前使用的接口信息:
```
┌─ 接口预设: 新建会议V3 ─────────────────────┐
│ PUT https://192.168.5.44/meetingV3/api/message/book │
│ 需要登录 · 需要签名 · 响应捕获: message_ids │
└──────────────────────────────────────────────┘
```
### 3.4 后端处理逻辑
#### 3.4.1 创建任务时的预设继承
```python
# performance_service.py
async def create_task(self, task_data: PerformanceTaskCreate):
# 如果传入了 preset_id,从预设继承字段
if task_data.preset_id:
preset = await self.db.get(ApiPreset, task_data.preset_id)
if not preset:
raise HTTPException(404, "预设不存在")
# 预设字段作为默认值,任务显式传入的字段覆盖
resolved = {
"method": task_data.method or preset.method,
"target_url": task_data.target_url or preset.target_url,
"headers": task_data.headers or preset.headers,
"body": task_data.body if task_data.body is not None else preset.body,
"body_template": task_data.body_template or preset.body_template,
"capture_rules": task_data.capture_rules or preset.capture_rules,
"auth_required": task_data.auth_required if task_data.auth_required is not None else preset.auth_required,
"sign_request": task_data.sign_request if task_data.sign_request is not None else preset.sign_request,
"account_key": task_data.account_key or preset.account_key,
}
else:
resolved = task_data.dict(exclude={"preset_id"})
# 创建任务...
```
#### 3.4.2 预设更新不影响已有任务
预设更新后,已创建的任务不受影响(任务存储的是当时的值,不是引用)。这是有意为之——历史压测任务的配置不应该被预设变更影响。
---
## 四、非功能需求
### 4.1 兼容性
- 已有任务(无 preset_id)完全不受影响,创建/编辑/执行逻辑不变
- 预设变更不影响已有历史任务
### 4.2 约束
- 预设名称唯一(同一用户不能有两个同名预设)
- 删除预设不影响已有任务(仅关联字段 `preset_id` 置空或保留引用)
- 预设删除时应有二次确认
---
## 五、应用场景示例
### 5.1 场景一:新建会议压测
1. 运维同学在预设管理页面创建"新建会议V3"预设:
- 方法:PUT
- URL:`https://192.168.5.44/meetingV3/api/message/book`
- 请求体模板:`{"messageName": "{__RANDOM_NAME_8__}", ...}`
- 捕获规则:`message_ids → $.data.messageId`
- 需要登录 + 需要签名
2. 测试同学创建压测任务:
- 选择预设:"新建会议V3" → 自动填充,接口字段隐藏
- 设置并发数:50,时长:60s
- 点击执行 → 成功运行
### 5.2 场景二:已有预设更新
1. 接口发生变化(如 URL 路径调整),运维同学更新预设
2. 新建任务时使用更新后的预设,获取最新配置
3. 历史任务保持不变,不受影响
---
## 六、影响范围
### 6.1 新增文件
| 文件 | 说明 |
|------|------|
| `backend/app/models/api_preset.py` | API 预设数据库模型 |
| `backend/app/schemas/api_preset.py` | API 预设 Pydantic Schema |
| `backend/app/routers/api_preset.py` | API 预设 CRUD 路由 |
| `backend/app/services/api_preset_service.py` | API 预设业务逻辑 |
| `frontend/src/views/performance/ApiPresetList.vue` | 预设管理页面 |
| `frontend/src/views/performance/ApiPresetDialog.vue` | 预设编辑对话框 |
### 6.2 修改文件
| 文件 | 变更 |
|------|------|
| `backend/app/models/performance.py` | 新增 `preset_id` 字段 |
| `backend/app/schemas/performance.py` | 新增 `preset_id` 字段 |
| `backend/app/services/performance_service.py` | 创建任务时继承预设字段 |
| `backend/app/database.py` | 注册新表 + 补齐字段 |
| `backend/app/main.py` | 注册新路由 |
| `backend/app/models/__init__.py` | 导出新模型 |
| `frontend/src/views/performance/TaskList.vue` | 任务表单增加预设下拉框 |
| `frontend/src/api/performance.ts` | 新增预设 API 封装 |
| `frontend/src/types/performance.ts` | 新增预设类型 |
| `frontend/src/router/index.ts` | 新增预设管理路由 |
---
## 七、验收标准
| # | 验收项 | 验证方式 |
|---|--------|---------|
| 1 | 创建预设(方法/URL/请求头/请求体模板/认证配置) | 预设列表看到新条目 |
| 2 | 编辑预设 | 修改后保存,内容更新 |
| 3 | 删除预设 | 二次确认后删除,列表消失 |
| 4 | 创建任务时选择预设 | 接口字段自动填充,表单简洁 |
| 5 | 选择预设后隐藏接口字段 | 只显示摘要卡片 |
| 6 | 选择"无预设" | 显示全部接口字段(向后兼容) |
| 7 | 使用预设创建的任务正常执行 | 执行成功,结果正确 |
| 8 | 预设更新不影响历史任务 | 旧任务执行时仍用原配置 |
---
*本文档为接口预设功能的需求定义,后续将输出计划执行文档用于指导开发。*
\ No newline at end of file
# 性能测试 API 接口预设功能 - 计划执行文档
> **文档版本**: v1.0
> **创建日期**: 2026-08-13
> **关联需求**: `_PRD_接口预设功能.md`(位于 `Docs/PRD/性能测试/需求文档/`)
> **执行策略**: 分阶段迭代,每阶段可独立验证
---
## 一、执行总览
### 1.1 阶段划分
| 阶段 | 名称 | 核心交付 | 预计工期 |
|------|------|---------|---------|
| Phase 1 | 数据模型与 Schema | ApiPreset 模型 + Pydantic Schema + 表注册 | 0.5天 |
| Phase 2 | 预设 CRUD 服务与路由 | 预设增删改查 API | 0.5天 |
| Phase 3 | 任务创建继承预设 | PerformanceTask 新增 preset_id + 后端继承逻辑 | 0.5天 |
| Phase 4 | 前端预设管理页面 | 预设列表页 + 编辑对话框 | 1天 |
| Phase 5 | 前端任务表单改造 | 预设下拉框 + 字段隐藏 + 摘要卡片 | 1天 |
### 1.2 依赖关系
```
Phase 1 (模型/Schema) ──→ Phase 2 (CRUD API) ──→ Phase 4 (预设管理页面)
│ ↑
└──→ Phase 3 (任务继承) ──→ Phase 5 (任务表单改造)
```
### 1.3 文件清单
| 文件 | 阶段 | 说明 |
|------|------|------|
| `backend/app/models/api_preset.py` | Phase 1 | ApiPreset 新表 |
| `backend/app/schemas/api_preset.py` | Phase 1 | 预设 Pydantic Schema |
| `backend/app/database.py` | Phase 1 | 注册新表 + 补齐字段 |
| `backend/app/models/__init__.py` | Phase 1 | 导出新模型 |
| `backend/app/services/api_preset_service.py` | Phase 2 | 预设业务逻辑 |
| `backend/app/routers/api_preset.py` | Phase 2 | 预设 CRUD 路由 |
| `backend/app/main.py` | Phase 2 | 注册路由 |
| `backend/app/models/performance.py` | Phase 3 | 新增 preset_id 字段 |
| `backend/app/schemas/performance.py` | Phase 3 | 新增 preset_id 字段 |
| `backend/app/services/performance_service.py` | Phase 3 | 创建任务继承预设 |
| `frontend/src/types/performance.ts` | Phase 4 | 预设类型定义 |
| `frontend/src/api/performance.ts` | Phase 4 | 预设 API 封装 |
| `frontend/src/views/performance/ApiPresetList.vue` | Phase 4 | 预设管理列表页 |
| `frontend/src/views/performance/ApiPresetDialog.vue` | Phase 4 | 预设编辑对话框 |
| `frontend/src/router/index.ts` | Phase 4 | 新增路由 |
| `frontend/src/views/performance/TaskList.vue` | Phase 5 | 任务表单集成预设下拉框 |
---
## 二、Phase 1: 数据模型与 Schema
### 2.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 1.1 | ApiPreset 模型 | `backend/app/models/api_preset.py` | 预设存储表 |
| 1.2 | 预设 Schema | `backend/app/schemas/api_preset.py` | Create/Update/Response |
| 1.3 | 注册新表 | `backend/app/database.py` | 导入 + 注册模型 |
| 1.4 | 模型导出 | `backend/app/models/__init__.py` | 加入 __all__ |
### 2.2 详细设计
#### 2.2.1 ApiPreset 模型
```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:api_preset.py
模块描述:API 接口预设数据库模型 — 存储可复用的压测接口配置
作者:czj
创建日期:2026-08-13
"""
from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Text, JSON, Boolean, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
from app.utils.id_generator import generate_id # 或 uuid 自定义
class ApiPreset(Base):
"""
API 接口预设
存储一组可复用的压测接口配置(方法、URL、请求头、请求体模板、
认证配置、捕获规则),创建性能测试任务时可选择预设自动填充。
"""
__tablename__ = "api_presets"
id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="唯一标识 preset_xxx")
name: Mapped[str] = mapped_column(String(100), unique=True, comment="预设名称")
description: Mapped[str] = mapped_column(Text, default="", comment="描述")
# 接口配置
method: Mapped[str] = mapped_column(String(10), default="GET", comment="HTTP方法")
target_url: Mapped[str] = mapped_column(Text, default="", comment="目标URL")
headers: Mapped[dict] = mapped_column(JSON, default=dict, comment="请求头")
body: Mapped[Optional[Any]] = mapped_column(JSON, nullable=True, comment="静态请求体")
body_template: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="请求体模板")
# 认证配置
auth_required: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否需要登录Token")
sign_request: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否需要签名")
account_key: Mapped[str] = mapped_column(String(100), default="", comment="使用账号")
# 响应捕获
capture_rules: Mapped[Optional[list]] = mapped_column(JSON, nullable=True, comment="捕获规则")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"description": self.description,
"method": self.method,
"target_url": self.target_url,
"headers": self.headers or {},
"body": self.body,
"body_template": self.body_template,
"auth_required": self.auth_required,
"sign_request": self.sign_request,
"account_key": self.account_key,
"capture_rules": self.capture_rules or [],
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
```
#### 2.2.2 预设 Schema
```python
# schemas/api_preset.py
class ApiPresetBase(BaseModel):
"""预设基础字段"""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
name: str = Field(..., min_length=1, max_length=100, description="预设名称")
description: str = Field("", description="描述")
method: str = Field("GET", description="HTTP方法 GET/POST/PUT/DELETE")
target_url: str = Field(..., description="目标接口完整URL")
headers: Optional[Dict[str, str]] = Field({}, description="请求头")
body: Optional[Any] = Field(None, description="静态请求体(dict或list)")
body_template: Optional[Dict[str, Any]] = Field(None, description="请求体模板")
auth_required: bool = Field(False, description="是否需要登录")
sign_request: bool = Field(False, description="是否需要签名")
account_key: Optional[str] = Field(None, description="使用账号")
capture_rules: Optional[List[CaptureRule]] = Field(None, description="响应捕获规则")
class ApiPresetCreate(ApiPresetBase): ...
class ApiPresetUpdate(ApiPresetBase): ...
class ApiPresetResponse(ApiPresetBase):
id: str
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
```
> 注意:`CaptureRule` 已定义于 `app/schemas/performance_output.py`,直接复用。
#### 2.2.3 database.py 注册
```python
# 新表由 create_all 自动创建(模型导入后注册到 Base.metadata)
from app.models import api_preset # noqa: F401
```
---
## 三、Phase 2: 预设 CRUD 服务与路由
### 3.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 2.1 | ApiPresetService | `backend/app/services/api_preset_service.py` | 增删改查 |
| 2.2 | ApiPreset 路由 | `backend/app/routers/api_preset.py` | CRUD 端点 |
| 2.3 | 注册路由 | `backend/app/main.py` | /api/performance/presets |
### 3.2 详细设计
#### 3.2.1 服务层
```python
# services/api_preset_service.py
class ApiPresetService:
def __init__(self, db: AsyncSession):
self.db = db
async def list_presets(self, page, page_size, keyword) -> (list, int):
"""分页查询,支持名称模糊搜索"""
async def get_preset(self, preset_id) -> ApiPreset:
"""详情查询,不存在抛 404"""
async def create_preset(self, data: ApiPresetCreate) -> ApiPreset:
"""创建,名称唯一校验"""
async def update_preset(self, preset_id, data: ApiPresetUpdate) -> ApiPreset:
"""更新,只更新传入的非 None 字段"""
async def delete_preset(self, preset_id) -> None:
"""删除"""
```
#### 3.2.2 路由
```python
# routers/api_preset.py
router = APIRouter() # main.py 注册时 prefix="/api/performance"
@router.get("/presets", response_model=ApiPresetListResponse)
async def list_presets(page=1, page_size=20, keyword=None, db=Depends(get_db)):
"""预设列表(分页 + 搜索)"""
@router.post("/presets", response_model=ApiPresetResponse, status_code=201)
async def create_preset(data: ApiPresetCreate, db=Depends(get_db)):
"""创建预设"""
@router.get("/presets/{preset_id}", response_model=ApiPresetResponse)
async def get_preset(preset_id: str, db=Depends(get_db)):
"""预设详情"""
@router.put("/presets/{preset_id}", response_model=ApiPresetResponse)
async def update_preset(preset_id: str, data: ApiPresetUpdate, db=Depends(get_db)):
"""更新预设"""
@router.delete("/presets/{preset_id}", status_code=204)
async def delete_preset(preset_id: str, db=Depends(get_db)):
"""删除预设"""
```
#### 3.2.3 ID 生成
沿用项目约定 `utils/id_generator.py` 生成 `preset_xxx` 前缀 ID。
---
## 四、Phase 3: 任务创建继承预设
### 4.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 3.1 | PerformanceTask 新增 preset_id | `backend/app/models/performance.py` | 字段 |
| 3.2 | Schema 新增 preset_id | `backend/app/schemas/performance.py` | 字段 |
| 3.3 | 创建任务继承逻辑 | `backend/app/services/performance_service.py` | 预设填充 |
| 3.4 | 旧库补齐字段 | `backend/app/database.py` | columns_to_add |
### 4.2 详细设计
#### 4.2.1 模型与 Schema
```python
# models/performance.py
preset_id: Mapped[Optional[str]] = mapped_column(
String(64), nullable=True, default=None, comment="关联的接口预设ID(可为空)"
)
# schemas/performance.py - 三个 Schema 均增加
preset_id: Optional[str] = Field(None, description="关联的接口预设ID")
```
#### 4.2.2 创建任务继承逻辑
```python
# services/performance_service.py
async def create_task(self, data: PerformanceTaskCreate) -> PerformanceTask:
"""创建性能测试任务,若指定 preset_id 则继承预设配置"""
inherited = {}
if data.preset_id:
preset = await self.db.get(ApiPreset, data.preset_id)
if not preset:
raise HTTPException(status_code=404, detail=f"接口预设不存在: {data.preset_id}")
# 仅当任务显式未传时才使用预设值
inherited = {
"method": preset.method,
"target_url": preset.target_url,
"headers": preset.headers or {},
"body": preset.body,
"body_template": preset.body_template,
"auth_required": preset.auth_required,
"sign_request": preset.sign_request,
"account_key": preset.account_key,
"capture_rules": preset.capture_rules,
}
# 任务显式字段优先,预设字段兜底
params = data.model_dump(exclude_unset=True)
resolved = {**inherited, **params}
task = PerformanceTask(
id=generate_id("perf"),
preset_id=data.preset_id,
**resolved,
)
self.db.add(task)
await self.db.commit()
await self.db.refresh(task)
return task
```
> **关键点**:`data.model_dump(exclude_unset=True)` 只返回前端显式传入的字段。前端选择预设后不传接口字段 → `exclude_unset` 为空 → 全部用预设值;前端传了则覆盖预设。
#### 4.2.3 旧库升级
```python
# database.py columns_to_add 增加
("performance_tasks", "preset_id", "VARCHAR(64) DEFAULT NULL"),
```
---
## 五、Phase 4: 前端预设管理页面
### 5.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 4.1 | 预设类型定义 | `frontend/src/types/performance.ts` | ApiPreset 类型 |
| 4.2 | 预设 API 封装 | `frontend/src/api/performance.ts` | CRUD 函数 |
| 4.3 | 预设列表页 | `frontend/src/views/performance/ApiPresetList.vue` | 列表 + 操作 |
| 4.4 | 预设编辑对话框 | `frontend/src/views/performance/ApiPresetDialog.vue` | 编辑表单 |
| 4.5 | 路由注册 | `frontend/src/router/index.ts` | /performance/presets |
### 5.2 详细设计
#### 5.2.1 类型定义
```typescript
// types/performance.ts
export interface ApiPreset {
id: string
name: string
description: string
method: string
targetUrl: string
headers?: Record<string, string>
body?: any
bodyTemplate?: any
authRequired: boolean
signRequest: boolean
accountKey?: string
captureRules?: CaptureRule[]
createdAt?: string
updatedAt?: string
}
export interface ApiPresetCreate {
name: string
description?: string
method: string
targetUrl: string
headers?: Record<string, string>
body?: any
bodyTemplate?: any
authRequired: boolean
signRequest: boolean
accountKey?: string
captureRules?: CaptureRule[]
}
export interface ApiPresetListResponse {
total: number
items: ApiPreset[]
}
```
#### 5.2.2 API 封装
```typescript
// api/performance.ts
export const listPresets = (params = {}) =>
request.get('/api/performance/presets', { params })
export const createPreset = (data: ApiPresetCreate) =>
request.post('/api/performance/presets', data)
export const updatePreset = (id: string, data: Partial<ApiPresetCreate>) =>
request.put(`/api/performance/presets/${id}`, data)
export const deletePreset = (id: string) =>
request.delete(`/api/performance/presets/${id}`)
```
#### 5.2.3 预设列表页
```
┌─ 接口预设管理 ───────────────────────────────┐
│ [+ 新建预设] [搜索框] │
│ ┌──────────────────────────────────────────┐ │
│ │ 名称 │ 方法 │ URL摘要 │ 更新时间 │ 操作 │
│ │ 新建会议V3 │ PUT │ /meetingV3/ │ 08-13 │ 编辑/删除 │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
- 表格列:名称、方法、URL 摘要(截断显示)、更新时间、操作
- 删除需 `ElMessageBox.confirm` 二次确认,提示"删除预设不影响已有任务"
#### 5.2.4 预设编辑对话框
```
┌─ 编辑接口预设 ─────────────────────────────┐
│ 名称* [新建会议V3 ] │
│ 描述 [会议模块压测用 ] │
│ HTTP方法* [PUT ▾] │
│ 目标URL* [https://192.168.5.44/... ] │
│ 请求头(JSON) [{"Content-Type": "... } ] │
│ 请求体类型 (○ 静态 ● 模板) │
│ 请求体模板 [{ "__NOW__", ... } ] │
│ [插入占位符 ▾] │
│ 认证开关 [需要登录] [需要签名] [账号▾] │
│ 响应捕获规则 [+ 添加规则] │
│ key | jsonPath | 多值 | 描述 | 操作 │
│ ───────────────────────────────────────── │
│ [取消] [保存] │
└───────────────────────────────────────────┘
```
- 表单字段与 bodyTemplate PRD 的配置页设计保持一致(占位符按钮、捕获规则表格)
- HTTP 方法下拉:GET/POST/PUT/DELETE
- 保存时校验:名称、URL 必填;body 或 bodyTemplate 解析为合法 JSON
---
## 六、Phase 5: 前端任务表单改造
### 5.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 5.1 | 表单增加预设下拉框 | `TaskList.vue` | Promise select |
| 5.2 | 预设继承填充 + 字段隐藏 | `TaskList.vue` | 联动逻辑 |
| 5.3 | 只读摘要卡片 | `TaskList.vue` | 接口信息展示 |
### 5.2 详细设计
#### 5.2.1 表单结构变化
**改造前**(现有):
```
名称 | 方法 | URL | 请求头 | 请求体(模板) | 认证 | 压测参数 | 断言
```
**改造后**
```
┌─ 接口配置 ──────────────────────────────┐
│ 接口预设 [无预设 ▾ | 新建会议V3 ▾ ...] │ ← 下拉选择
│ (选择预设后显示摘要卡片,隐藏方法/URL等) │
└────────────────────────────────────────┘
┌─ 压测参数 ──────────────────────────────┐
│ 名称 | 模式 | 并发 | 时长 | QPS/阶梯 | 断言 │
└────────────────────────────────────────┘
```
#### 5.2.2 预设下拉框逻辑
```typescript
// 预设下拉数据
const presets = ref<ApiPreset[]>([])
const selectedPreset = ref<string>('')
// 选择预设后
function onPresetChange(presetId: string) {
if (!presetId) {
// 无预设 → 恢复原表单
showPresetOnly = false
return
}
const preset = presets.value.find(p => p.id === presetId)
// 填充接口字段
form.method = preset.method
form.targetUrl = preset.targetUrl
form.headers = JSON.stringify(preset.headers || {}, null, 2)
if (preset.bodyTemplate) {
form.bodyType = 'template'
form.bodyTemplate = JSON.stringify(preset.bodyTemplate, null, 2)
} else if (preset.body) {
form.bodyType = 'static'
form.body = JSON.stringify(preset.body, null, 2)
}
form.authRequired = preset.authRequired
form.signRequest = preset.signRequest
form.accountKey = preset.accountKey
form.captureRules = preset.captureRules || []
// 隐藏接口字段,显示摘要
showPresetOnly = true
}
```
#### 5.2.3 提交逻辑
```typescript
async function handleSave() {
// 选择预设时提交 preset_id
// 接口字段已填充到 form,仍会提交(后端已有值,覆盖无关紧要)
const payload = {
...formData,
...(selectedPreset.value ? { presetId: selectedPreset.value } : {}),
}
if (editId.value) {
await updateTask(editId.value, payload)
} else {
await createTask(payload)
}
}
```
#### 5.2.4 编辑已有任务
- 编辑任务时若 `task.presetId` 存在,预设下拉框回显该预设
- 用户切换预设 → 重新填充接口字段
- 选择"无预设" → 保留当前已填充的字段不变(用户可以继续微调)
---
## 七、验证方案
### 7.1 后端单元验证
| # | 场景 | 步骤 | 预期 |
|---|------|------|------|
| 1 | 创建预设 | POST /api/performance/presets | 201,返回带 ID 的预设 |
| 2 | 预设列表 | GET /api/performance/presets | 返回创建的预设 |
| 3 | 更新预设 | PUT /api/performance/presets/{id} | 200,字段更新 |
| 4 | 删除预设 | DELETE /api/performance/presets/{id} | 204 |
| 5 | 名称重复 | 创建同名预设 | 409/400 校验错误 |
| 6 | 创建任务继承 | POST /tasks 带 preset_id | 接口字段来自预设 |
| 7 | 任务字段覆盖 | POST /tasks 带 preset_id + 显式 method | method 用显式值 |
| 8 | 无预设兼容 | POST /tasks 不带 preset_id | 行为与原来一致 |
### 7.2 前端验证
| # | 场景 | 步骤 | 预期 |
|---|------|------|------|
| 1 | 预设管理 CRUD | 页面操作 | 列表/编辑/删除正常 |
| 2 | 选择预设建任务 | 下拉选"新建会议V3" | 接口字段自动填充并隐藏 |
| 3 | 摘要卡片 | 选定预设后 | 显示方法+URL+认证摘要 |
| 4 | 无预设 | 下拉选"无预设" | 显示全部接口字段 |
| 5 | 预设建任务执行 | 选预设 → 保存 → 执行 | 压测成功 |
| 6 | 编辑带预设任务 | 打开带 presetId 任务 | 预设回显 |
### 7.3 真实场景(集成)
1. 在预设页创建"新建会议V3"预设(PUT + bodyTemplate + 捕获 messageIds + 登录 + 签名)
2. 新建压测任务 → 选择该预设 → 仅填压测参数 → 保存 → 执行
3. 断言执行成功且 `message_ids` 被捕获查询得到
---
## 八、风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
| 预设字段与任务字段不映射 | 继承逻辑漏字段 | 以 Create/Update Schema 为准逐一核对 |
| 前端隐藏字段后无法微调 | 用户灵活性下降 | 提供"无预设"选项 + 摘要卡片展示全部细节 |
| 预设名称重复歧义 | 同名预设无法区分 | name unique 约束 + 409 错误提示 |
| 预设更新影响历史任务 | 历史压测数据失真 | 任务存储快照值,不引用预设 |
| body 为数组类型 | 静态请求体可能是 list | body 用 Optional[Any],前端 JSON.parse 支持 |
---
## 九、验收标准
见 PRD 第七节。核心验收:
1. 用户可在界面维护接口预设(增删改查)
2. 新建任务时下拉选择预设即可自动填充接口信息,无需手动输入
3. 选择预设后接口字段隐藏,界面简洁
4. 已有任务创建流程不受影响("无预设"选项存在)
5. 使用预设创建的任务可正常执行
---
*本文档供 prd-code skill 执行使用,按 Phase 顺序逐步实现。*
\ No newline at end of file
...@@ -247,3 +247,190 @@ npm run dev # http://localhost:3000 ...@@ -247,3 +247,190 @@ npm run dev # http://localhost:3000
--- ---
*本文档由 Claude Code 生成,记录 V2 部署架构升级的完整进度,供下次会话快速恢复上下文。* *本文档由 Claude Code 生成,记录 V2 部署架构升级的完整进度,供下次会话快速恢复上下文。*
---
## 十一、离线部署包验证进度(2026-08-17 · 5.69 新服务器)
> 本会话工作:**离线 Docker 部署包的制作、加固与端到端验证**,目标服务器为内网新服务器 **192.168.5.69**(KylinOS V10)。V2 部署(上文)在 5.60,本文档后半段是离线包到 5.69 的独立交付物。
### 11.1 离线部署包位置
```
临时目录\部署新服务器\deploy\
├── deploy.sh # 一键部署脚本(v4,LF 行尾)
├── docker-compose.yml # app + mysql 双服务编排(端口变量插值)
├── docker-compose-plugin # Compose v5.3.1 二进制(CLI 插件自动安装用,32MB)
├── .env # MYSQL_ROOT_PASSWORD=Ubains@13579 / MYSQL_USER=platapp / MYSQL_PASSWORD=PlatApp2026
├── init.sql # MySQL 首次初始化(库+用户+权限)
├── plat_base.sql.gz # 基础数据(40 模块/359 用例/0 执行结果),gzip 压缩
├── images/deploy-app.tar # 应用镜像(428MB)
├── images/mysql80.tar # MySQL 8.0 镜像(238MB)
├── backend/ # 后端源码(volume 挂载 /app)
├── frontend/dist/ # 前端构建产物(volume 挂载)
└── 部署说明文档.md
```
> ⚠️ 打包规范:`deploy.sh` **必须 LF 行尾**(Windows 编辑后 `sed -i 's/\r$//' deploy.sh`)。Git Bash 的 `bash -n` 能容忍 CRLF 是假阴性,Linux bash 会报 `set: -:无效选项`。
>
> ⚠️ 目录中还有 `upload_pkg.py` / `upload_backend.py` / `run_deploy.py` 三个**验证用临时工具**(SFTP 上传 + 远程执行),非交付组件,打包分发时可剔除或加标注。
### 11.2 部署脚本 v3/v4 加固内容
| 版本 | 加固点 |
|------|--------|
| v3 | MySQL 半初始化自愈(空密码 root 检测 → ALTER USER 修复 → 幂等建库/建用户/授权);端口冲突自动降级(8081/3307 被占时 ±10 内递增);基础数据幂等导入(modules>0 则跳过,保护现有数据) |
| v4 | 文件统一 LF 行尾;镜像加载后 `docker image inspect` 存在性校验(缺失立即报错,不再被离线 pull 超时误导);复制前校验 `backend/app/main.py` 与 `frontend/dist/index.html`(防 app 因缺代码无限重启);健康检查码空值兜底(修 `000000`) |
### 11.3 本次终验结果(快照恢复后一次通过)
5.69 恢复初始快照(无部署目录、无镜像、无 compose 插件、端口空闲),上传完整包后 `bash deploy.sh` **一次通过,EXIT_CODE=0**:
| 阶段 | 结果 |
|------|------|
| 前置检查(磁盘 30G / 端口 8081+3307 空闲) | ✅ |
| 镜像加载 + 存在性校验 | ✅ |
| backend/frontend 前置校验 | ✅ |
| MySQL 全新初始化(root 密码一次通过,未走自愈) | ✅ |
| 基础数据导入(40 模块/359 用例/0 执行结果) | ✅ |
| 健康检查(2 次重试后 HTTP 200,无异常码) | ✅ |
| 前端 `/` 200、API 文档 `/docs` 200 | ✅ |
| 外部验证 health / modules=40 / cases=359 / executions=0 | ✅ |
| 两容器 `plat-auto-test-app` / `plat-auto-test-mysql` 均 healthy | ✅ |
> 本轮唯一疑点(问题 D):部署前探测 compose 显示 `unknown command`,执行部署时却显示 v5.3.1 已就绪——**未实际触发**自动安装路径(该路径已在 v3 首轮验证过)。下次快照验证以 `ls /usr/libexec/docker/cli-plugins/` 实证为准。
### 11.4 踩坑汇总(新增条目,V2 第五节之上)
| # | 现象 | 根因 | 正确做法 |
|---|------|------|---------|
| 1 | Linux bash 报 `set: -:无效选项` / `\r` 未找到命令 | deploy.sh CRLF 行尾 | 脚本统一 LF,Windows 编辑后 `sed -i 's/\r$//'` |
| 2 | 上传的 700MB 文件"成功"后消失 | KylinOS `/tmp` 是 tmpfs(7.3G)不落盘 | 上传到 `/data`(XFS,32G 空闲) |
| 3 | 远端出现字面文件名 `images\deploy-app.tar` | Windows `os.path.join` 输出反斜杠 | 远程路径统一 `.replace(os.sep, '/')` |
| 4 | 镜像缺失仅警告,2 分钟 pull 超时报"MySQL 等待超时"误导 | compose 离线自动 pull | v4 加载后 `docker image inspect` 立即报错退出 |
| 5 | app 无限重启 `ModuleNotFoundError: No module named 'app'` | volume 挂载宿主机 backend 但代码缺失 | v4 复制前校验 `backend/app/main.py` |
| 6 | 健康检查码 `HTTP 000000` | curl `-w %{http_code}` 失败输出 000 + 拼接叠加 | `|| true` + 空值兜底 `[ -z "$HEALTH" ] && HEALTH="000"` |
| 7 | 上传工具 frontend 第 2 个文件 `FileNotFoundError ENOENT` | `exec_command('mkdir -p')` 异步未等待,put 早于目录创建 | mkdir 后 `channel.recv_exit_status()` 同步等待 |
| 8 | MySQL 半初始化:root 空密码、无库无用户、entrypoint 跳过初始化 | daemon 重启中断首次初始化 | v3 自愈:空密码检测 → ALTER USER → 幂等建库/建用户 |
| 9 | 大文件 SFTP 中途断连 | paramiko 默认窗口小 + 无 keepalive | `transport.set_keepalive(30)` + 调大 window_size + `put(confirm=True)` |
| 10 | `/api/api/cases` 404(复用 V2 已有坑) | axios baseURL 非空 | baseURL 留空,路径写完整 `/api/...` |
### 11.5 服务器 5.69 关键信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 192.168.5.69(KylinOS V10,生产服务器 5.60 的同内网新机) |
| SSH | root / Ubains@123 |
| 部署目录 | `/data/third_party/plat-auto-test/` |
| 上传暂存目录 | `/data/deploy-pkg/`(上传后由脚本拷贝到部署目录) |
| 前端地址 | http://192.168.5.69:8081 |
| API 文档 | http://192.168.5.69:8081/docs |
| 健康检查 | http://192.168.5.69:8081/health |
| MySQL 端口 | 3307(用户 platapp / PlatApp2026,库 plat_auto_test,root Ubains@13579) |
| 旁路容器 | 宿主机已有 unginx/uemqx/uredis/umysql/unacos 等业务容器(MQTT 复用 uemqx:1883) |
| 容器命名 | plat-auto-test-app / plat-auto-test-mysql(网络 plat-auto-test-net) |
### 11.6 问题记录与修复计划文档
完整问题台账(问题 1~11 + 环境问题 A/B/C/D)、修复计划与每轮实施记录见:
`Docs/PRD/部署运维/部署包问题记录与修复计划.md`(已含 7 个章节,最近一节为 2026-08-17 v4 终验记录)。
### 11.7 后续待办
| 优先级 | 待办 | 说明 |
|--------|------|------|
| P0 | 收敛部署包临时工具 | 将 `upload_pkg.py` / `upload_backend.py` / `run_deploy.py` 移出交付目录或加"非交付组件"标注,避免随目录分发混淆 |
| P1 | 下次快照验证时实证 compose 插件自动安装 | 快照恢复后先 `ls /usr/libexec/docker/cli-plugins/` 确认缺失,再跑 deploy.sh 触发安装路径 |
| P2 | 若部署目标为 5.60 生产 | 需注意 5.60 已有 V2 容器(端口 80/3307 被占),deploy.sh 的端口降级逻辑会自动递增到 8082/3308 等 |
---
## 十二、实施记录(2026-08-17 v4 第三次验证 · 快照恢复后再次验证)
### 12.1 验证环境
5.69 再次恢复初始快照后,从零执行完整流程。
**部署前探针确认:**
| 检查项 | 结果 |
|--------|------|
| Docker 版本 | 29.1.3 ✅ |
| docker compose 插件 | v5.3.1 已就绪(`/usr/libexec/docker/cli-plugins/docker-compose` 存在) |
| `/data/third_party/plat-auto-test/` | 不存在 ✅ |
| `/data/deploy-pkg/` | 不存在 ✅ |
| deploy-app:latest 镜像 | 不存在 ✅ |
| mysql:8.0 镜像 | **存在(快照残留)** |
| 端口 8081/3307 | 空闲 ✅ |
| 磁盘空间 | 32G 可用 ✅ |
> 注:快照恢复后 mysql:8.0 镜像残留(上一轮加载的),但数据目录 `/data/third_party/plat-auto-test/mysql/data/` 不存在,所以 MySQL 容器仍会全新初始化。compose 插件也残留(快照未完全重置到裸机状态)。
### 12.2 部署执行结果
**`bash deploy.sh` 执行结果:`EXIT_CODE=0`**
| 阶段 | 结果 | 说明 |
|------|------|------|
| 前置环境检查 | ✅ | 磁盘/端口/Compose 插件 |
| 镜像加载 + 存在性校验 | ✅ | deploy-app.tar 428MB / mysql80.tar 238MB 均加载成功 |
| backend/frontend 前置校验 | ✅ | main.py / index.html 均存在 |
| MySQL 全新初始化 | ✅ | 自愈路径触发(root 空密码初始化 → ALTER USER 修复成功) |
| 基础数据导入 | ⚠️ **报告"失败"但实际成功** | 见下方问题 11 |
| 健康检查 | ✅ | 2 次重试后 HTTP 200 |
| 前端 `/` / API 文档 `/docs` | ✅ | 均 200 |
| 两容器 healthy | ✅ | |
### 12.3 本轮发现的新问题
| # | 问题 | 归属 | 处置 |
|---|------|------|------|
| 11 | 基础数据导入报告"失败"但实际成功——`gzip` 管道 `2>/dev/null` 吞掉 stderr 非致命警告,`\|\|` 误判管道退出码为失败 | 部署包缺陷(v4 漏洞) | 修复 `2>/dev/null` 改为 `2>&1` + `grep -q "ERROR"` 精准判断 |
**根因分析:** deploy.sh 第 271 行:
```bash
gzip -d -c "$BASE_SQL" | docker exec -i plat-auto-test-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" 2>/dev/null || IMPORT_FAIL=1
```
- `mysql` 命令的 stderr 输出非致命信息(如 `[Warning] Using a password on the command line interface can be insecure.`),被 `2>/dev/null` 吞掉
- 管道的退出码来自 `gzip` 或 `docker exec` 的最后一段,在某些 shell 环境下可能返回非零
- 实际数据导入成功(手动验证 `modules=40`),但脚本误判为失败
**修复方案:**
```bash
IMPORT_OUT=$(gzip -d -c "$BASE_SQL" | docker exec -i plat-auto-test-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" 2>&1)
if echo "$IMPORT_OUT" | grep -q "ERROR"; then
IMPORT_FAIL=1
echo " ⚠️ 导入错误: $IMPORT_OUT"
else
echo " ✅ 基础数据导入完成"
IMPORTED=1
fi
```
### 12.4 快照残留说明
本轮快照恢复后,以下资源未被清除(与上一轮终验环境不同):
| 残留项 | 影响 |
|--------|------|
| mysql:8.0 镜像 | 无影响——数据目录为空,MySQL 仍会全新初始化 |
| docker compose 插件 | 正向——跳过自动安装,脚本更快完成 |
| 旁路容器(uemqx/uredis 等 14 个) | 无影响——端口不冲突 |
**结论:** 快照"恢复"不完全等同于裸机,但部署包对部分残留状态有容忍性。compose 插件自动安装路径(v3 设计)仍然**未在本轮触发实证**
### 12.5 问题台账总览
所有问题记录在 `Docs/PRD/部署运维/部署包问题记录与修复计划.md`,当前进度:
| 轮次 | 发现缺陷 | 已修复 | 待修复 |
|------|----------|--------|--------|
| v2 首轮 | 问题 1~7 | 7/7 | 0 |
| v3 修复(端口降级/自愈/幂等) | — | — | — |
| v4 第二轮验证 | 问题 8~11 + A/B/C/D | 11/11 | **0(问题 11 已修复)** |
### 12.6 后续待办更新
| 优先级 | 待办 | 说明 |
|--------|------|------|
| P1 | 收敛部署包临时工具 | 将 `upload_pkg.py` / `upload_backend.py` / `run_deploy.py` 移出交付目录 |
| P2 | 下次快照验证前确认快照范围 | 要求快照恢复至裸机 Docker 状态(无残留镜像/插件),避免"问题 D"疑点反复出现 |
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
> **最后更新**: 2026-08-17 > **最后更新**: 2026-08-17
> **当前分支**: `platform-auto-test` > **当前分支**: `platform-auto-test`
> **开发窗口**: 安全测试模块 > **开发窗口**: 安全测试模块
> **最近提交**: `236b3f3b` feat(security): 安全测试 ERP 配置/上传/任务创建全链路对接 > **最近提交**: `5b5b35de` docs(security): 同步安全测试 HANDOFF 提交状态
> **状态**: ✅ 安全测试 P1 全部完成 + P2 执行验证完成 + 菜单升级 + ERP 配置子菜单 + ERP 上传流程对接完成 + ERP 任务创建对接完成 > **状态**: ✅ 安全测试 P1 全部完成 + P2 执行验证完成 + 菜单升级 + ERP 配置子菜单 + ERP 上传流程对接完成 + ERP 任务创建对接完成,全部**已提交并部署 5.60**
--- ---
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
**背景**:参考 `develop/AuxiliaryTool/ScriptTool/ApiSecurityTest` 工具,其除报告上传外还对接了 **ERP 任务创建(指派跟踪)**,用于形成「扫描 → 修复 → 回归验证」闭环。本次实现将该能力集成到平台安全测试模块。 **背景**:参考 `develop/AuxiliaryTool/ScriptTool/ApiSecurityTest` 工具,其除报告上传外还对接了 **ERP 任务创建(指派跟踪)**,用于形成「扫描 → 修复 → 回归验证」闭环。本次实现将该能力集成到平台安全测试模块。
**流程**:需求文档 → 计划执行文档 → 后端 → 前端 → 构建验证 全链路完成(遵循项目工作流) **流程**:需求文档 → 计划执行文档 → 后端 → 前端 → 构建验证 → Git 提交推送 → 部署 5.60 服务器 全链路完成
**核心成果** **核心成果**
1. ✅ 按规范输出两份文档: 1. ✅ 按规范输出两份文档:
...@@ -88,7 +88,13 @@ ...@@ -88,7 +88,13 @@
3.**前端** 3.**前端**
- `frontend/src/api/security.ts`(新增 `getSecurityTaskPreview` / `createSecurityTask``as any` 返回类型) - `frontend/src/api/security.ts`(新增 `getSecurityTaskPreview` / `createSecurityTask``as any` 返回类型)
- `frontend/src/views/Reports.vue`(安全报告弹窗新增「创建ERP任务」按钮 + 对话框:报告摘要只读折叠区 + 任务配置表单(类型/状态/责任人/创建人/紧急程度/期望工时/截止天数/关联需求)) - `frontend/src/views/Reports.vue`(安全报告弹窗新增「创建ERP任务」按钮 + 对话框:报告摘要只读折叠区 + 任务配置表单(类型/状态/责任人/创建人/紧急程度/期望工时/截止天数/关联需求))
4.**构建验证通过**`npm run build` 无 TS 错误(修复了 `request.get`/`request.post` 返回类型推断问题,统一 `as any`);后端 5 文件语法检查通过;FastAPI 路由注册验证通过 4.**构建验证 + 提交推送**`npm run build` 无 TS 错误;`236b3f3b` 提交推送 origin;`5b5b35de` 补充 HANDOFF 状态同步提交推送
5.**部署至 5.60 服务器**:7 个后端文件上传 + main.py 路由补丁(CRLF 适配) + 前端 dist 上传 + 容器重启
6.**task-preview 接口实测通过**:用服务器真实执行 ID `exec_66d76c33e6184906903ca13d24acf983` 测试,返回正确数据
- `server_ip: 192.168.5.44``task_name: 192.168.5.44接口安全测试及漏洞修复_20260817`
- `level: 5`(最高优先级,因有高危漏洞)
- ERP 基础数据正常:5 种任务类型 + 130 名人员
7.**create-task 待用户在界面实测**(ERP 端生成任务确认)
**默认任务配置**(对齐参考实现 config.yaml): **默认任务配置**(对齐参考实现 config.yaml):
- type_id=3(后端开发)、status_id=1(新建)、level=按漏洞自动计算(高危→5/中危→4/低危→3/信息→2/无→1) - type_id=3(后端开发)、status_id=1(新建)、level=按漏洞自动计算(高危→5/中危→4/低危→3/信息→2/无→1)
...@@ -314,7 +320,7 @@ GET /api/security/executions/{id}/report/download → 下载 .md 文件 ...@@ -314,7 +320,7 @@ GET /api/security/executions/{id}/report/download → 下载 .md 文件
|--------|------|------| |--------|------|------|
| **P2.5** | ✅ ~~前端报告类型标签修复~~ | 报告类型显示"UI自动化"应为"安全测试" | | **P2.5** | ✅ ~~前端报告类型标签修复~~ | 报告类型显示"UI自动化"应为"安全测试" |
| **P3** | ✅ ~~安全报告上传 ERP 流程对接~~ | ERP 配置入口已完成,ERP 上传流程已完成(2026-08-17) | | **P3** | ✅ ~~安全报告上传 ERP 流程对接~~ | ERP 配置入口已完成,ERP 上传流程已完成(2026-08-17) |
| **P3.5** | ✅ ~~安全测试 ERP 任务创建对接~~ | 任务创建预览 + 创建接口 + 前端对话框已完成(2026-08-17),待下次会话实测 | | **P3.5** | ✅ ~~安全测试 ERP 任务创建对接~~ | 任务创建预览 + 创建接口 + 前端对话框已完成(2026-08-17)**已部署 5.60 并实测 task-preview 通过**(真实执行 ID 返回正确的任务名/紧急程度/ERP 基础数据),create-task 待用户在界面实测 |
--- ---
...@@ -398,7 +404,7 @@ PYTHONIOENCODING=utf-8 python scripts/create_security_cases.py ...@@ -398,7 +404,7 @@ PYTHONIOENCODING=utf-8 python scripts/create_security_cases.py
- `frontend/src/api/security.ts`(新增 `getSecurityTaskPreview` + `createSecurityTask` 函数) - `frontend/src/api/security.ts`(新增 `getSecurityTaskPreview` + `createSecurityTask` 函数)
- `frontend/src/views/Reports.vue`(安全报告弹窗新增「创建ERP任务」按钮 + 对话框 UI) - `frontend/src/views/Reports.vue`(安全报告弹窗新增「创建ERP任务」按钮 + 对话框 UI)
- 文档:`Docs/PRD/需求文档/安全测试/` 下两份 ERP 任务创建文档(新增) - 文档:`Docs/PRD/需求文档/安全测试/` 下两份 ERP 任务创建文档(新增)
- **已提交并推送**待下次会话**实测**任务创建端到端流程 - **已提交并推送****已部署至 5.60**,task-preview 接口实测通过(真实执行 ID 返回正确数据),create-task 待用户在界面端到端实测
--- ---
......
...@@ -16,6 +16,7 @@ from pathlib import Path ...@@ -16,6 +16,7 @@ from pathlib import Path
from typing import Optional from typing import Optional
from fastapi import APIRouter, HTTPException, Query, UploadFile, File, Form, Depends from fastapi import APIRouter, HTTPException, Query, UploadFile, File, Form, Depends
from fastapi.responses import FileResponse, HTMLResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论