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

merge: 合并 origin/platform-auto-test 性能测试模块(DB 二进制冲突手工合并)

冲突处理(backend/data/test_platform.db):
- 以远程库为基础:保留 performance_executions 新表、performance_tasks/
  snapshots 新列 schema、device_report_logs 数据
- test_cases/executions/case_results 三表整表覆盖为本地版本
  (远程相对 base 零改动;本地含 315 用例 + 14 条薄弱模块断言对齐
  + R7 执行记录 exec_111da393 31/31 passed)
- 合并后 integrity_check ok,关键数据抽检通过
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
# 执行计划 - 修复执行机资源图表显示太窄
> **关联问题文档**: `_问题处理_执行机资源图表显示太窄.md`
> **创建日期**: 2026-08-21
> **预计总工时**: 0.5 天
> **修复类型**: 前端 UI 布局修复(无后端改动)
---
## 一、执行概述
本计划将「执行机资源图表显示太窄」问题分 2 个 Phase 修复:
1. **Phase 1**:三个页面的资源图表布局改为全宽堆叠(0.5 天)
2. **Phase 2**:构建验证 + 回归 + 文档更新(与 Phase 1 同日完成)
---
## 二、任务分解
### Phase 1:布局修复
**目标**:将资源趋势图从「左右并排(span 12)窄图」改为「上下堆叠(span 24)全宽图」。
| # | 任务 | 关键文件 | 验收标准 |
|---|------|---------|---------|
| 1.1 | MonitorPanel.vue 资源区块布局改造 | `frontend/src/views/performance/MonitorPanel.vue` | CPU&内存 + 网络各占全宽(span 24)上下堆叠,高度 280px |
| 1.2 | BatchMonitor.vue 资源区块布局改造 | `frontend/src/views/performance/BatchMonitor.vue` | 同上 |
| 1.3 | ReportPanel.vue 资源区块布局改造 | `frontend/src/views/performance/ReportPanel.vue` | 同上 |
**验收方式**:浏览器打开监控页/批量监控页/报告页,展开资源区块,确认两图全宽堆叠。
---
### Phase 2:构建验证 + 回归 + 文档
**目标**:验证修改无编译错误、功能不回归,并同步文档。
| # | 任务 | 关键文件 | 验收标准 |
|---|------|---------|---------|
| 2.1 | 前端构建验证 | `frontend/` | `npm run build` 无错误 |
| 2.2 | 功能回归 | MonitorPanel / BatchMonitor / ReportPanel | 折叠展开正常、无数据隐藏正常、图表渲染正常 |
| 2.3 | 更新 HANDOFF 文档 | `HANDOFF_性能测试.md` | 记录本次修复 |
**验收方式**:构建通过,手动回归通过。
---
## 三、验收标准总览
| 验收项 | 对应 Phase |
|--------|-----------|
| 监控页资源图全宽堆叠 | Phase 1 |
| 批量监控页资源图全宽堆叠 | Phase 1 |
| 报告页资源图全宽堆叠 | Phase 1 |
| npm run build 通过 | Phase 2 |
| 功能无回归 | Phase 2 |
---
## 四、关键实现细节
### 4.1 MonitorPanel.vue 修改点
**现状**(第 171-184 行):
```html
<!-- 资源趋势图 -->
<el-row :gutter="12">
<el-col :span="12">
<el-card shadow="never" class="chart-card">
<template #header><span>CPU &amp; 内存趋势 CPU / Memory</span></template>
<div ref="resourceChartRef" class="chart-sm" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never" class="chart-card">
<template #header><span>网络趋势 Network(发送 / 接收 KB/s)</span></template>
<div ref="networkChartRef" class="chart-sm" />
</el-card>
</el-col>
</el-row>
```
**改为**
```html
<!-- 资源趋势图(全宽堆叠) -->
<el-card shadow="never" class="chart-card">
<template #header><span>CPU &amp; 内存趋势 CPU / Memory</span></template>
<div ref="resourceChartRef" class="chart" />
</el-card>
<el-card shadow="never" class="chart-card">
<template #header><span>网络趋势 Network(发送 / 接收 KB/s)</span></template>
<div ref="networkChartRef" class="chart" />
</el-card>
```
> 说明:移除 `el-row/el-col` 包裹即可实现全宽;`chart-sm`(220px)改为 `chart`(280px),与主趋势图高度一致。
### 4.2 BatchMonitor.vue 修改点
同 MonitorPanel.vue(第 249-262 行),结构完全相同,同样改为全宽堆叠 + `chart` 类。
### 4.3 ReportPanel.vue 修改点
**现状**(第 322-335 行):
```html
<el-row :gutter="12">
<el-col :span="12">
<el-card shadow="never" class="chart-card">
<template #header><span>CPU &amp; 内存趋势 CPU / Memory</span></template>
<div ref="resourceChartRef" class="chart" />
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never" class="chart-card">
<template #header><span>网络趋势 Network(发送 / 接收 KB/s)</span></template>
<div ref="networkChartRef" class="chart" />
</el-card>
</el-col>
</el-row>
```
**改为**:移除 `el-row/el-col` 包裹,两个 `el-card` 直接堆叠(`.chart-card` 自带 margin-bottom 12px)。
---
## 五、测试计划
### 手动测试
1. 监控页:展开「执行机资源」折叠面板 → 两图全宽堆叠,高度 280px
2. 批量监控页:同上
3. 报告页:加载含 resourceSummary 的报告 → 资源区块两图全宽堆叠
4. 无 resource 数据的任务 → 资源区块隐藏,不报错
5. 折叠面板默认折叠 → 点击展开正常
### 前端构建
```bash
cd frontend && npm run build
```
---
## 六、风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 修改影响图表初始化逻辑 | 图表无法渲染 | 仅改模板布局与样式类,不动 JS 初始化逻辑 |
| 各页面结构差异 | 遗漏某页 | 三个文件逐一对比确认,全部覆盖 |
| 高度增加页面变长 | 滚动增加 | 可接受,资源区块默认折叠不影响主视图 |
---
## 七、后续工作
- [ ] 实施完成后更新 CLAUDE.md 当前进度
- [ ] 更新 HANDOFF_性能测试.md 交接文档
- [ ] Git 提交(Conventional Commits)
---
## 八、附录
### 修改文件清单
| 文件 | 操作 | 修改点 |
|------|------|--------|
| `frontend/src/views/performance/MonitorPanel.vue` | 修改 | 资源趋势图 span12 并排 → 全宽堆叠;chart-sm → chart |
| `frontend/src/views/performance/BatchMonitor.vue` | 修改 | 同上 |
| `frontend/src/views/performance/ReportPanel.vue` | 修改 | 资源趋势图 span12 并排 → 全宽堆叠 |
| `Docs/PRD/性能测试/问题处理/_问题处理_执行机资源图表显示太窄.md` | 新建 | 问题处理文档(本文档前置) |
---
*本文档为执行机资源图表过窄问题的修复执行计划。*
\ No newline at end of file
# 执行计划:修复会议模板任务 body 字符串存储导致 4XX 响应
> 生成时间:2026-08-21
> 关联文档:`_问题处理_会议模板任务body字符串存储导致4XX响应.md`
> 状态:📋 待执行
---
## 问题概述
通过「会议模板」接口预设(ApiPreset)创建的性能测试任务,执行时**全部请求返回 4XX**
**根因**:前端 `TaskList.vue``handleSave()` 第 769 行 `body: form.body || null` **未做 `JSON.parse()`**(对比 `ApiPresetDialog.vue` 第 295 行正确解析),导致 body 作为**字符串**(且含 Windows CMD `^` 转义残留)存入 MySQL JSON 列。执行时 `_resolve_body()` 返回字符串,`json=str` 被 aiohttp **双重序列化**,API 收到字符串字面量无法反序列化 → 4XX。
---
## 执行步骤
### Step 1: 前端修复 — TaskList.vue 补 JSON.parse
**文件**`frontend/src/views/performance/TaskList.vue``handleSave()`(约第 769 行)
**改动**
```typescript
// 修复前
body: form.body || null,
// 修复后:与 bodyTemplate 保持一致,JSON.stringify 存储的对象需反解析
body: form.body ? JSON.parse(form.body) : null,
```
**验收标准**
- 在 TaskList.vue 新建/编辑任务,body 填写合法 JSON 数组/对象
- 点击保存,`POST /api/performance/tasks` 请求体中 `body`**对象/数组**(而非字符串)
- 执行任务返回 2XX,不再 4XX
### Step 2: 后端防御修复 — 执行器字符串 body 反序列化
**文件**`backend/app/executors/performance_executor.py``_send_request()`(约第 1018 行)
**改动**:在 `session.request(json=...)` 之前,对字符串 body 做防御性 `json.loads`
```python
# 防御:字符串 body 反序列化回 JSON 对象(防止前端直传字符串的历史脏数据)
if isinstance(json_data, str):
try:
json_data = json.loads(json_data)
except (json.JSONDecodeError, TypeError):
logger.warning(f"任务 {task.id} body 不是合法 JSON 字符串,原样发送: {str(json_data)[:80]}")
pass
```
**验收标准**
- 对已存在的脏数据任务(body 为字符串)执行时,先 `json.loads` 再发送,请求体为 JSON 对象
- 非 JSON 字符串(如纯文本)保持原样发送,不抛异常
### Step 3: 数据库脏数据修复
**执行命令**(5.60 MySQL 容器内):
```bash
docker exec -it plat-auto-test-mysql mysql -uroot -p 平台数据库名 -e "
UPDATE performance_tasks
SET body = JSON_REMOVE(body, '$')
WHERE JSON_TYPE(body) = 'STRING'
AND JSON_UNQUOTE(body) LIKE '^[%'
AND id = 'perf_ecbd92aae3b845ab9bd651ca49ca111e';
"
```
> 说明:`JSON_REMOVE(body, '$')` 会去掉字符串外层引号,将 JSON 字符串转回 JSON 对象。执行前先备份或先 SELECT 验证。
**验收标准**
- `SELECT JSON_TYPE(body), body FROM performance_tasks WHERE id='perf_ecbd92...'` 显示 `JSON_TYPE``ARRAY`/`OBJECT`(不再 STRING)
- `^` 字符残留需在 Step 4 的代码层清洗或手动更新
### Step 4: 前端构建
```bash
cd frontend && npm run build
```
**验收标准**:构建成功,无 TypeScript 类型错误。
### Step 5: 部署到 5.60
```bash
scp backend/app/executors/performance_executor.py ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/app/executors/
scp -r frontend/dist ubains@192.168.5.60:/data/third_party/plat-auto-test/frontend/
ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test/deploy && docker compose restart app"
```
**验收标准**
- 容器重启成功(`Container plat-auto-test-app Started`
- `grep -n 'json.loads' backend/app/executors/performance_executor.py` 确认防御代码已部署
- `grep -n 'JSON.parse(form.body)' frontend/dist/assets/index-*.js` 确认前端已部署
### Step 6: 端到端验证
1. 浏览器 **Ctrl+F5** 强刷前端
2. 项目管理 → 新建任务 → body 填写 JSON 数组(模拟会议模板 insertByFkeyId)
3. 保存并执行
4. 监控页确认:**status_2xx > 0,status_4xx = 0**
5. 对问题任务 `perf_ecbd92...` 重新执行,确认 4XX 消失
---
## 改动清单
| 文件 | 类型 | 说明 |
|------|------|------|
| `frontend/src/views/performance/TaskList.vue` | **MODIFY** | `handleSave()` body 加 `JSON.parse()`(1 行) |
| `backend/app/executors/performance_executor.py` | **MODIFY** | `_send_request()` 字符串 body 防御性 `json.loads`(+6 行) |
| `performance_tasks` 表 | **DATA** | 清洗问题任务 body 脏数据(`JSON_REMOVE`) |
| `Docs/.../_问题处理_会议模板任务body字符串存储导致4XX响应.md` | **NEW** | 问题处理文档 |
| `Docs/.../_执行计划_修复会议模板任务body字符串存储导致4XX响应.md` | **NEW** | 本次执行计划 |
**无需改动**
- 后端 API 路由 / schema(`body: Optional[Any]` 本身无问题)
- `ApiPresetDialog.vue`(已有正确 JSON.parse)
- 数据库表结构
---
## 部署后回归
| 验证项 | 预期 |
|--------|------|
| TaskList.vue 新建任务(有 body) | 保存成功,body 为 JSON 对象,执行 2XX |
| TaskList.vue 新建任务(bodyTemplate) | 不受影响,bodyTemplate 正常 |
| ApiPresetDialog 创建预设 | 不受影响,仍正常 |
| 问题任务 perf_ecbd92...(脏数据) | 后端防御解析后正常发送 |
| 旧任务回归(会议 book PUT 等) | 正常执行 |
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 执行计划:修复执行记录双条(僵尸 running 记录)
> 生成时间:2026-08-24
> 关联文档:`_问题处理_执行记录双条僵尸running记录.md`
> 状态:✅ 已完成并验证
---
## 问题概述
`POST /api/performance/tasks/{task_id}/run` 触发执行后,同一任务同一时刻产生**两条**执行记录:API 返回的 `executionId` 永远卡在 `running`/0 请求(僵尸),实际执行数据落在另一条 `completed` 记录上。前端按返回的 `executionId` 跳转监控页 → 无数据。
**根因**`run_task_async()` 创建执行记录 A 并返回其 id;后台 `_run_single_background()` 调用 `run_task()`**未传递该 id**`run_task()` 内部又硬性 `generate_id("exec")` 创建了记录 B。A 无人更新,永远 running。
---
## 执行步骤
### Step 1: `run_task()` 支持复用已有 execution_id
**文件**`backend/app/services/performance_service.py`
**改动**`run_task()`(约 285 行)签名新增可选参数,创建记录处按传入值分支:
```python
async def run_task(
self,
task_id: str,
config_override: Optional[Dict[str, Any]] = None,
progress_callback: Optional[Callable] = None,
triggered_by: str = "manual",
batch_id: Optional[str] = None,
execution_id: Optional[str] = None, # ← 新增:复用已有执行记录
) -> Dict[str, Any]:
```
创建执行记录处:
```python
if not execution_id:
execution_id = generate_id("exec")
execution = PerformanceExecution(
id=execution_id,
...
status="running",
...
)
self.db.add(execution)
await self.db.flush()
else:
# 复用 run_task_async 已创建的记录(刷新状态与起始时间)
execution = await self.db.get(PerformanceExecution, execution_id)
if not execution:
raise ValueError(f"执行记录不存在: {execution_id}")
execution.status = "running"
execution.start_time = datetime.utcnow()
await self.db.flush()
```
**验收标准**
- 不传 `execution_id` 的既有调用(batch_run_tasks / 单测)行为不变
- 传入已存在 id 时不产生新记录
### Step 2: `_run_single_background()` 传递 execution_id
**文件**`backend/app/services/performance_service.py`(约 1080 行)
```python
await service.run_task(
task_id=task_id,
config_override=config_override,
triggered_by=triggered_by,
execution_id=execution_id, # ← 新增
)
```
**验收标准**`run_task_async` → 后台 `run_task` 全链路只存在一条执行记录
### Step 3: 数据清理(5.60 存量僵尸记录)
修复只对新执行生效,**存量僵尸 running 记录需 SQL 清理**
```sql
-- 先查:任务本身已不是 running,但执行记录还是 running 的僵尸
SELECT e.id, e.task_id, e.start_time FROM performance_executions e
JOIN performance_tasks t ON t.id = e.task_id
WHERE e.status = 'running' AND t.status != 'running';
-- 确认后删除(或标记 failed)
DELETE FROM performance_executions WHERE id = 'exec_4e4eb234101b42a4a216df44f6705ba8';
```
**验收标准**`GET /executions?task_id=perf_0fbcb7...` 仅剩 completed 记录
### Step 4: 部署 + 端到端验证
```bash
scp backend/app/services/performance_service.py ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/app/services/
ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test/deploy && docker compose restart app"
# 重跑任务验证只产生一条执行记录,且 API 返回的 executionId 就是最终记录
```
**验收标准**
- run 返回的 `executionId` 对应记录在执行结束后变为 `completed` 且有统计数据
- 执行历史下拉无重复/僵尸条目
---
## 改动清单
| 文件 | 类型 | 说明 |
|------|------|------|
| `backend/app/services/performance_service.py` | **MODIFY** | `run_task()` +`execution_id` 参数(复用分支);`_run_single_background()` 透传 |
| 5.60 MySQL `performance_executions` | **DATA** | 删除存量僵尸 running 记录(`exec_4e4e...`) |
| `Docs/.../_问题处理_执行记录双条僵尸running记录.md` | **NEW** | 问题处理文档 |
| `Docs/.../_执行计划_修复执行记录双条僵尸running.md` | **NEW** | 本执行计划 |
**无需改动**
- `routers/performance.py`(run 端点签名不变,仍走 `run_task_async`
- 前端(返回契约不变)
- `PerformanceExecution` 模型
---
## 验证结果
| 验证项 | 结果 | 说明 |
|--------|------|------|
| 本地语法检查 | ✅ | 模块导入无错误 |
| 5.60 重跑任务单记录 | ✅ | run 返回 id == 最终 completed 记录 id |
| 存量僵尸清理 | ✅ | `exec_4e4e...` 已删除 |
| 兼容性(batch 批量路径) | ✅ | batch_run_tasks 未传 execution_id,行为不变 |
---
## 教训
1. "前台建记录返回 id + 后台另起会话执行"的模式里,**后台必须透传 id 复用记录**,否则必然双条。
2. 修 bug 时顺手写清理 SQL:代码修复只管增量,存量脏数据要一并处理,否则前端历史列表长期脏。
3. API 返回的标识符要与最终落库实体一一对应——返回前先问"这个 id 后面会不会被更新"。
---
*本文档由 Claude Code 生成*
# 执行计划:修复批量监控页任务状态进入即显示「已完成」
> 生成时间:2026-08-21
> 关联文档:`_问题处理_批量监控页任务状态残留completed.md`
> 状态:✅ 已完成并部署
---
## 问题概述
点击「执行全部」跳转到 `BatchMonitor.vue`,任务列表**一进来就显示「已完成」**(带 TPS/响应时间数据),但顶部进度条显示「执行中」。
**根因**`create_batch_and_start()` 创建 batch 记录后、后台任务启动前,**没有将本次任务的状态重置为 pending**。前端在后台任务开始执行前的窗口期查询 `get_batch_details()`,读到的是上一次执行残留的 `completed` 状态。
**附带发现**`_run_batch_background()` 漏传 `batch_id`,导致后台创建第二个 batch 记录。
---
## 执行步骤
### Step 1: 后端 — 前置重置任务状态
**文件**`backend/app/services/performance_service.py``create_batch_and_start()`
**改动**:在 batch 记录提交后、后台任务启动前,重置任务状态为 pending:
```python
self.db.add(batch_record)
# 将批次内所有任务状态重置为 pending(避免读到上一次执行的残留 completed/failed 状态)
for task_id in task_ids:
task = await self.db.get(PerformanceTask, task_id)
if task and task.status in ("completed", "failed", "cancelled", "pending"):
task.status = "pending"
await self.db.commit()
```
**注意**:跳过 `running` 状态的任务,避免干扰其他正在执行的批量。
**验收标准**
- 点击「执行全部」后立即查询 batch-details,所有任务状态为 `pending`
- 正在 `running` 的任务不会被重置
### Step 2: 后端 — 修复漏传 batch_id
**文件**`backend/app/services/performance_service.py``_run_batch_background()`
**改动**
```python
await service.batch_run_tasks(
task_ids=task_ids,
project_id=project_id,
batch_id=batch_id, # 修复前:漏传,导致创建第二个 batch 记录
)
```
**验收标准**:每次批量执行只创建 1 条 batch 记录
### Step 3: 构建前端
```bash
cd frontend && npm run build
```
**验收标准**:构建成功,无 TypeScript 错误(本次前端无代码改动,仅重新构建部署)
### Step 4: 部署到 5.60
```bash
scp backend/app/services/performance_service.py ubains@192.168.5.60:.../backend/app/services/
scp -r frontend/dist ubains@192.168.5.60:.../frontend/
ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test/deploy && docker compose restart app"
```
**验收标准**:容器重启成功,`grep -n 'task.status = .pending'` 确认修复代码已部署
### Step 5: 补充文档
- `Docs/PRD/性能测试/问题处理/_问题处理_批量监控页任务状态残留completed.md`(NEW)
- `Docs/PRD/性能测试/问题处理/_执行计划_修复批量监控页任务状态残留completed.md`(NEW)
---
## 改动清单
| 文件 | 类型 | 说明 |
|------|------|------|
| `backend/app/services/performance_service.py` | **MODIFY** | `create_batch_and_start()` 前置重置任务状态为 pending(+4 行) |
| `backend/app/services/performance_service.py` | **MODIFY** | `_run_batch_background()` 补传 `batch_id`(1 行) |
| `Docs/.../_问题处理_批量监控页任务状态残留completed.md` | **NEW** | 问题处理文档 |
| `Docs/.../_执行计划_修复批量监控页任务状态残留completed.md` | **NEW** | 本次执行计划 |
**无需改动**
- 前端代码(`BatchMonitor.vue` 逻辑本身正确,只是读到的数据有问题)
- 数据库结构 / schema
---
## 验证计划
1. 浏览器 **Ctrl+F5** 强刷
2. 进入项目管理 → 点击「执行全部」
3. 确认页面跳转到批量监控页后:
- 任务列表显示「等待中」(pending)而非「已完成」
- 进度条显示「执行中」,进度 0/N
4. 观察任务逐个流转:等待中 → 运行中 → 已完成
5. 确认全部完成后进度条 100%、显示「查看合并报告」
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 执行计划:修复批量详情 API 500 错误(actual_concurrency 字段缺失)
> 生成时间:2026-08-21
> 关联文档:`_问题处理_批量详情API500_actual_concurrency字段缺失.md`
> 状态:✅ 已完成并验证
---
## 问题概述
批量执行启动后,前端 `BatchMonitor.vue` 调用 `GET /api/performance/batch-details/{batch_id}` 返回 **500**,页面无法展示任务列表和实时监控。
**根因**`get_batch_details()` 访问 ORM 对象不存在的属性 `task.actual_concurrency`,抛出 `AttributeError``PerformanceTask` 模型只有 `concurrency`(配置并发数),没有 `actual_concurrency` 字段。
---
## 执行步骤
### Step 1: 本地定位并修复
**文件**`backend/app/services/performance_service.py`
**改动**`get_batch_details()` 中(约第 1648 行)
```python
# 修复前(报错)
"actual_concurrency": task.actual_concurrency,
# 修复后
"actual_concurrency": task.concurrency,
```
**验收标准**
- 本地运行 `python -c "from app.services.performance_service import PerformanceService; ..."` 无 AttributeError
- schema 与前端类型无需改动(`BatchTaskItem.actual_concurrency` 仅是响应字段名,本身正确)
### Step 2: 上传修复文件到 5.60 服务器
```bash
scp app/services/performance_service.py ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/app/services/performance_service.py
```
**验收标准**:上传成功,无报错
### Step 3: 重启后端容器
```bash
ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test/deploy && docker compose restart app"
```
**验收标准**`Container plat-auto-test-app Started`,无启动异常
### Step 4: 验证 API
```bash
# 用数据库中的真实 batch_id 验证
curl http://localhost/api/performance/batch-details/batch_xxx
# 不存在的 batch_id 验证 404 语义
curl http://localhost/api/performance/batch-details/nonexistent-id-123
```
**验收标准**
- 真实批次返回 200,`tasks[]` 内包含 `actualConcurrency` 字段
- 不存在批次返回 `{"detail":"批量执行记录不存在"}`
### Step 5: 补充文档
**文件**
- `Docs/PRD/性能测试/问题处理/_问题处理_批量详情API500_actual_concurrency字段缺失.md`(NEW)
- `Docs/PRD/性能测试/问题处理/_执行计划_修复批量详情API500_actual_concurrency字段缺失.md`(NEW)
**验收标准**:问题处理 + 执行计划文档配对齐全
---
## 改动清单
| 文件 | 类型 | 说明 |
|------|------|------|
| `backend/app/services/performance_service.py` | **MODIFY** | `get_batch_details()``task.actual_concurrency``task.concurrency`(1 行) |
| `Docs/.../_问题处理_批量详情API500_actual_concurrency字段缺失.md` | **NEW** | 问题处理文档(根因分析 + 修复 + 验证) |
| `Docs/.../_执行计划_修复批量详情API500_actual_concurrency字段缺失.md` | **NEW** | 本次执行计划 |
**无需改动**
- `schemas/performance.py``BatchTaskItem.actual_concurrency` 是正确响应字段名)
- `types/performance.ts``actualConcurrency` 前端类型正确)
- 前端代码(无前端问题)
---
## 验证结果
| 验证项 | 结果 | 说明 |
|--------|------|------|
| 容器重启 | ✅ | `docker compose restart app` 成功 |
| 真实批次详情 | ✅ 200 | 返回 projectName/status/tasks,任务含 actualConcurrency=10 |
| 不存在批次 | ✅ 404 语义 | `{"detail":"批量执行记录不存在"}` |
| 前端页面 | 待用户复验 | 强刷后进入批量监控页确认任务列表正常展示 |
---
## 教训
1. service 层访问 ORM 属性前,先核对 `models/*.py` 字段定义,不要凭 schema 或前端类型猜测字段来源。
2. 500 定位最快路径:容器日志搜 `AttributeError`,直接给出行号和属性名。
3. 修复闭环:改代码 → 上传 → 重启 → 真实数据调接口确认 200,缺一不可。
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 执行计划:修复捕获输出查询 500(大 JSON 列排序爆内存)
> 生成时间:2026-08-24
> 关联文档:`_问题处理_捕获输出查询500与大JSON列排序爆内存.md`
> 状态:✅ 已完成并验证
---
## 问题概述
`GET /api/performance/outputs``GET /api/performance/tasks/{task_id}/outputs` 在查询含大 JSON `value` 列(1.28MB,4033 个 token)的记录时,`ORDER BY created_at` 导致 MySQL filesort 将整行载入 sort buffer,报 `Out of sort memory`(1038)→ 接口 500。
**根因**:SQLAlchemy `select(Model).order_by(...)` 会 SELECT 全部列(含大 `value` 列)参与排序,行宽超过 `sort_buffer_size`
---
## 执行步骤
### Step 1: 修改列表端点为两段式查询
**文件**`backend/app/routers/performance_output.py`
**改动**`list_outputs()``get_task_outputs()` 两个端点,将
```python
query.order_by(desc(created_at)).offset(...).limit(...) # 整行进 sort buffer
result.scalars().all()
```
改为两段式:
```python
# 1) 轻量排序:只取 id 列(value 大列不进 sort buffer)
id_rows = await db.execute(
select(PerformanceTaskOutput.id)
.where(同过滤条件)
.order_by(desc(PerformanceTaskOutput.created_at))
.offset((page - 1) * page_size)
.limit(page_size)
)
ids = [r[0] for r in id_rows]
# 2) 无任务时直接返回空列表;否则按 id 回查整行
items = []
if ids:
rows = await db.execute(
select(PerformanceTaskOutput).where(PerformanceTaskOutput.id.in_(ids))
)
# 按第一段排序还原分页顺序
by_id = {o.id: o for o in rows.scalars().all()}
items = [by_id[i] for i in ids if i in by_id]
```
**验收标准**
- 生成 SQL 的 ORDER BY 子查询只含 `id` 列(可开启 SQL echo 日志确认)
- 返回结果顺序、分页与修复前一致
- 本地 SQLite 跑通(无 task_id 过滤 / 有过滤 / key 过滤 / 空 ids 四种场景)
### Step 2: 本地回归验证
**验收标准**
- `python -c "import app.routers.performance_output"` 无语法错误
- 本地起服务后 `GET /api/performance/outputs` 返回 200
### Step 3: 上传到 5.60 并重启
```bash
scp backend/app/routers/performance_output.py ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/app/routers/performance_output.py
ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test/deploy && docker compose restart app"
```
**验收标准**:容器正常启动,`/health` 200
### Step 4: 服务器真实大列数据验证
```bash
# 原复现路径(修复前 500)
curl "http://192.168.5.60/api/performance/outputs?task_id=perf_0fbcb792dcc143fd80b42cb405a75176"
curl "http://192.168.5.60/api/performance/tasks/perf_0fbcb792dcc143fd80b42cb405a75176/outputs"
```
**验收标准**
- 两个接口均 200
- 返回 `items[0].value` 含 4033 个 token(数据完整)
---
## 改动清单
| 文件 | 类型 | 说明 |
|------|------|------|
| `backend/app/routers/performance_output.py` | **MODIFY** | `list_outputs` / `get_task_outputs` 两段式查询(轻量列排序 + id 回查) |
| `Docs/.../_问题处理_捕获输出查询500与大JSON列排序爆内存.md` | **NEW** | 问题处理文档 |
| `Docs/.../_执行计划_修复捕获输出查询500与大JSON列排序.md` | **NEW** | 本执行计划 |
**无需改动**
- `models/performance_output.py`(表结构不变)
- `schemas/performance_output.py`(响应结构不变)
- 前端代码(接口契约不变)
- MySQL 配置(不调 sort_buffer_size,治本不治标)
---
## 验证结果
| 验证项 | 结果 | 说明 |
|--------|------|------|
| 本地语法/导入检查 | ✅ | 模块导入无错误 |
| 5.60 `GET /outputs?task_id=...` | ✅ 200 | 原 500,返回 1 行 tokens 输出 |
| 5.60 `GET /tasks/{id}/outputs` | ✅ 200 | 原 500 |
| 大列数据完整 | ✅ | value 数组 4033 个 JWT token |
---
## 教训
1. 含 JSON 大列的表做 `ORDER BY` 时,先 SELECT 轻量列拿 id,再按 id 回查整行——排序成本与行宽解耦。
2. 本地 SQLite 不会暴露 MySQL filesort 的内存限制,列表接口上线前要想"最大那一行有多大"。
3. 修复闭环:改代码 → 上传 → 重启 → 用触发过 500 的真实数据原路径复测。
---
*本文档由 Claude Code 生成*
# 执行计划:修复监控无数据 / 报告 MySQL 缺失 / 请求详情无数据
## 一、目标
解决 192.168.5.60 部署后用户反馈的 3 个问题:
1. 监控界面未连接 / 无数据
2. 报告页 MySQL 监控数据缺失
3. 请求详情采集无数据
**验收标准(必须真实执行自测通过)**:重新触发一次任务执行,快照/报告/请求明细链路全部有数据。
## 二、排查步骤(已完成)
| # | 排查项 | 方法 | 结论 |
|---|--------|------|------|
| 1 | 执行记录是否带 `request_detail_enabled` | 读 `run_task()` / `run_task_async()` 两条创建路径 | `run_task_async()` 漏字段(线上路径) |
| 2 | `_copy_result_to_execution()` 是否覆盖 flag | 读函数体(行 1489-1538) | 不涉及,排除 |
| 3 | 监控「未连接」是否异常 | 读 MonitorPanel.vue 状态机 | 完成任务后 WebSocket 断开属正常,切回放模式 |
| 4 | 报告 MySQL 缺失是后端还是前端 | 读报告 API + ReportPanel.vue 渲染条件 | 后端数据在,前端按 `mysql.available` 条件渲染 |
| 5 | 请求明细 API 读哪个 flag | 读 `get_request_details()`(行 898-1038) | 读执行记录 flag,bug 时恒 off |
## 三、代码修改(已完成)
### 3.1 后端根因修复
`backend/app/services/performance_service.py` 行 ~1206,`run_task_async()` 创建执行记录补字段:
```python
request_detail_enabled=getattr(task, "request_detail_enabled", "off") or "off",
```
### 3.2 前端
- `MonitorPanel.vue`:任务结束回看时直接进入「回放模式」而非停留在「未连接」
- `RequestDetailPanel.vue`:flag 误置为 off 但确实有数据时,仍强制展示明细
- `ReportPanel.vue`:核对 `targetResourceSummary.mysql.available` 渲染条件
## 四、部署(已完成)
```bash
# 前端构建 → dist
cd frontend && npm run build
# 打包发送 5.60(scp + tar 方式)
# 覆盖容器内 /app/frontend/dist 与 /app/backend/app/services/performance_service.py
docker compose restart app (容器 plat-auto-test-app,健康)
```
## 五、自测(已完成,全部通过)
执行 `backend/scripts/probe_e2e_rerun.py`(容器内跑):
| 校验项 | 预期 | 实测 |
|--------|------|------|
| 执行状态 | completed | ✅ 8s 完成 |
| totalRequests / successCount | 200 / 200 | ✅ |
| 请求明细 total / truncated / enabled | 200 / False / on | ✅ **on(修复生效)** |
| 快照 resource / targetResource | 存在 | ✅ mysql.available=true |
| 报告 targetResourceSummary.mysql | 存在 | ✅ available=true |
## 六、验收清单(用户侧)
- [x] 监控界面:重新执行新任务,执行中可见实时曲线;完成后回看显示「回放模式」且有历史数据
- [x] 报告页:新增「MySQL 监控」区块(线程/慢查询/缓冲池命中率等)
- [x] 请求详情:重新执行后报告 → 请求详情有 200 条明细,可点开看请求/响应报文
- [ ] 旧执行记录(历史数据)无法回溯修复,需重新执行(已告知)
---
*计划执行维护:czj · 2026-08-25*
\ No newline at end of file
# 问题处理:会议模板新增任务 body 字符串存储导致 4XX 响应
> 处理时间:2026-08-21
> 现象报告人:用户(执行会议模板新增任务时)
> 关联文档:`_执行计划_修复会议模板任务body字符串存储导致4XX响应.md`(修复计划)
> 状态:🔍 根因已确定,待修复
---
## 1. 问题现象
用户在项目管理页面,通过「会议模板」接口预设(ApiPreset)创建性能测试任务后,执行该任务时**全部请求返回 4XX 状态码**
**问题任务**
| 字段 | 值 |
|------|-----|
| 任务 ID | `perf_ecbd92aae3b845ab9bd651ca49ca111e` |
| 接口预设 | `preset_meeting_insertByFkeyId`(POST /meetingV3/api/template/insertByFkeyId) |
| 请求数 | 16,215 次 |
| 状态码分布 | 全部 4XX(status_4xx=16215) |
| 断言 | 无(assertions=[]),所以 fail_count=0 |
| 并发/时长 | 10 并发,持续 60s |
---
## 2. 初步定位
### 2.1 容器日志确认
查询 5.60 服务器容器日志:
```bash
docker logs plat-auto-test-app 2>&1 | grep -E '4[0-9][0-9]|status_4|4XX|status.*4' | tail -20
```
日志显示:
- 登录成功(Token 获取正常)
- 任务执行正常完成,无异常堆栈
- 只有 count 数据展示 4XX
### 2.2 数据库查询
```sql
-- 查询问题任务 body 原始存储
SELECT id, name, body, body_template, method, target_url
FROM performance_tasks
WHERE id = 'perf_ecbd92aae3b845ab9bd651ca49ca111e';
```
**结果**`body` 列存储的内容为 **JSON 字符串**(含 `^` 转义字符),而非 JSON 对象/数组。
---
## 3. 根因分析
### 3.1 直接原因
**`_resolve_body()` 返回字符串 → `json=str` 双重序列化 → API 收到字符串字面量 → 反序列化失败 → 400/422**
执行引擎 `_send_request()` 的核心调用链:
```
_resolve_body(task, index) → 返回字符串 "[...]"
session.request(method, url, json=字符串) → aiohttp 内部 json.dumps(字符串)
→ 实际发送 HTTP body: "\"[...]\"" (带引号的字符串字面量)
→ API 期望 JSON 对象/数组,收到字符串 → 反序列化失败 → 4XX
```
### 3.2 前端根因:TaskList.vue 缺少 JSON.parse()
**问题代码**`frontend/src/views/performance/TaskList.vue` 第 769 行):
```typescript
const data: any = {
// ...
body: form.body || null, // ❌ 未 JSON.parse,直接发送原始字符串
bodyTemplate: form.bodyTemplate
? (typeof form.bodyTemplate === 'string' ? JSON.parse(form.bodyTemplate) : form.bodyTemplate)
: null, // ✅ bodyTemplate 正确解析了
// ...
}
```
**对比正确代码**`frontend/src/views/performance/ApiPresetDialog.vue` 第 295 行):
```typescript
data.body = JSON.parse(form.body) // ✅ 正确解析 JSON 字符串为对象
```
`bodyTemplate``JSON.parse()` 处理,但 `body` 没有——这是**不一致的编码遗漏**
### 3.3 数据流追踪
```
用户输入 JSON 字符串 body → 前端 form.body 为字符串
→ handleSave(): body: form.body || null → 未 JSON.parse
→ POST /api/performance/tasks → body 字段为字符串
→ Pydantic 接收字符串 → SQLAlchemy JSON 列存储字符串
→ MySQL 存储为 JSON 字符串字面量(带外层引号)
→ 执行时 _resolve_body() 返回字符串
→ json=字符串 → aiohttp 双重序列化
→ API 收到字符串字面量 → 4XX
```
### 3.4 `^` 字符的来源
问题任务 body 中包含 `^` 字符(如 `^[{^\"messageName^\":...`),这是**Windows CMD 环境的转义字符残留**
- `^` 在 CMD 中用于转义 `&` `|` `<` `>` 等特殊字符
- 用户可能从 CMD 窗口复制粘贴 curl 命令或 body 内容时引入了 `^`
另一个任务 `perf_82d72c5c17f94f9ebba1f899b41bc018` 的 URL 中也存在 `^&`(如 `pageNum=1^&pageSize=10`),验证了这一点。
### 3.5 为什么旧任务(8月13日前)没出问题
旧任务(如会议 book PUT)虽然 body 也是字符串,但那时**成功是因为 body 中的 `{__NOW__}` 占位符被正常替换**,且旧版本的执行引擎可能对字符串 body 做了额外处理。当前版本代码中,`_resolve_body()` 对字符串 body 不做二次解析,直接返回字符串,导致 `json=str` 双重序列化。
### 3.6 为什么没有断言失败
任务 `assertions=[]`(无断言),所有响应(包括 4XX)均被视为「成功响应」,所以 `fail_count=0`。如果任务配置了断言(如 `status == 200`),4XX 会被标记为失败,问题会更早暴露。
---
## 4. 影响范围
### 4.1 受影响的任务
通过 **TaskList.vue 页面创建**(非 ApiPresetDialog)且**使用 body** 的任务均受影响。具体判断标准:
| 条件 | 受影响 |
|------|--------|
| TaskList.vue 创建 + 有 body | ✅ 是 |
| ApiPresetDialog 创建(预设管理) | ❌ 否(有 JSON.parse) |
| GET 请求(无 body) | ❌ 否 |
| body 为 null/空 | ❌ 否 |
| 只使用 bodyTemplate | ❌ 否(bodyTemplate 有 JSON.parse) |
### 4.2 已确认受影响的任务
| 任务 ID | 名称 | URL | 问题 |
|---------|------|-----|------|
| `perf_ecbd92aae3b845ab9bd651ca49ca111e` | 会议模板新增 | POST insertByFkeyId | body 为字符串,含 `^` 字符,全部 4XX |
| `perf_82d72c5c17f94f9ebba1f899b41bc018` | 会议模板列表查询 | GET list(含 `^&`) | URL 含 `^&`,但 GET 请求无 body |
### 4.3 不涉及的范围
- **ApiPresetDialog 创建的预设**:不涉及
- **bodyTemplate 字段**:不涉及(前端已正确 JSON.parse)
- **GET 请求**:不涉及(`_resolve_body` 对 GET 返回 None)
- **后端执行引擎**:逻辑本身无 bug,只是对字符串 body 缺少防御性处理
---
## 5. 修复方案
### 5.1 前端修复(根因修复)
**`frontend/src/views/performance/TaskList.vue`**`handleSave()` 第 769 行:
```typescript
// 修复前
body: form.body || null,
// 修复后
body: form.body ? JSON.parse(form.body) : null,
```
### 5.2 后端防御性修复
**`backend/app/executors/performance_executor.py`**`_send_request()` 第 1018-1020 行:
```python
# 修复前:直接 json=json_data
async with session.request(method, url, headers=headers, json=json_data, ...)
# 修复后:如果 json_data 是字符串,尝试反序列化
if isinstance(json_data, str):
try:
json_data = json.loads(json_data)
except (json.JSONDecodeError, TypeError):
pass # 非 JSON 字符串,保持原样
async with session.request(method, url, headers=headers, json=json_data, ...)
```
### 5.3 数据库脏数据修复
```sql
-- 清理 body 中被错误存储为字符串的 JSON(去除 ^ 转义字符并重新解析)
UPDATE performance_tasks
SET body = JSON_REMOVE(body, '$')
WHERE JSON_TYPE(body) = 'STRING'
AND JSON_UNQUOTE(body) LIKE '^[%'
AND id = 'perf_ecbd92aae3b845ab9bd651ca49ca111e';
```
---
## 6. 经验总结
1. **三端 JSON 字段一致性**`body` 字段贯穿前端(表单输入)→ 后端(Pydantic schema)→ 数据库(JSON 列),前端提交时必须确保 `JSON.parse()`,否则整个链路断裂。
2. **编辑/创建页面代码对齐**`TaskList.vue``ApiPresetDialog.vue` 都涉及 body 字段的编辑提交,但一处有 JSON.parse 一处没有——**同类型字段的提交逻辑应统一处理,避免复制遗漏**
3. **Windows CMD `^` 转义**:涉及 Windows CMD 环境的数据输入,需提示用户不要从 CMD 窗口直接复制粘贴包含 `^` 的内容。
4. **断言配置的重要性**:无断言的任务即使全部 4XX 也不会标记为失败,建议在创建任务时默认添加 `status == 200` 断言。
5. **执行引擎防御性编程**`json=str` 应该做 `json.loads(str)` 回退,防止前端错误传播到 API 请求。
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 问题处理 - 执行机资源图表显示太窄
> **文档类型**: 问题处理
> **创建日期**: 2026-08-21
> **模块**: 性能测试 - 执行机资源监控 UI
> **优先级**: P1
> **关联文档**: `_PRD_执行机资源监控.md`、`_PRD_执行机资源监控_计划执行.md`
---
## 一、问题描述
### 1.1 现象
压测任务执行中,在**实时监控页(MonitorPanel)****批量监控页(BatchMonitor)****报告页(ReportPanel)** 打开「执行机资源 Resource Monitor」区块时,**资源趋势图显示过窄**
- CPU & 内存趋势图与网络趋势图**并排显示**(各占 50% 宽度 `el-col :span="12"`),每个图仅约页面一半宽
- 资源曲线在窄图中难以辨认趋势细节(CPU 波动、内存爬升被横向压缩)
- 与上方 TPS / 响应时间图(独占全宽)对比明显失衡
### 1.2 影响
| 影响项 | 说明 |
|--------|------|
| 可读性 | 曲线被横向压缩,波动细节难以观察,影响瓶颈定位分析 |
| 一致性 | 其他趋势图均为全宽,资源图半宽显示风格不统一 |
| 操作体验 | 用户需反复缩放/悬停才能读取数据,交互成本高 |
### 1.3 截图对比(示意)
```
现状(半宽并排) 期望(全宽堆叠)
┌────────────────────┐ ┌──────────────────────────┐
│ CPU&内存 │ 网络 │ │ CPU & 内存趋势 │
│ (窄) │ (窄) │ │ (全宽) │
└────────────────────┘ ├──────────────────────────┤
│ 网络趋势(全宽) │
└──────────────────────────┘
```
---
## 二、问题定位
### 2.1 根因分析
资源趋势图在两个页面中均使用 `el-col :span="12"` 左右并排布局,导致每个图表只占半屏宽度:
| 文件 | 位置 | 问题代码 |
|------|------|---------|
| `frontend/src/views/performance/MonitorPanel.vue` | 第 171-184 行 | `<el-col :span="12">` × 2,resourceChartRef / networkChartRef 并排 |
| `frontend/src/views/performance/BatchMonitor.vue` | 第 249-262 行 | `<el-col :span="12">` × 2,与 MonitorPanel 相同的并排结构 |
| `frontend/src/views/performance/ReportPanel.vue` | 第 322-335 行 | `<el-col :span="12">` × 2,resourceChartRef / networkChartRef 并排 |
`el-col :span="12"` 表示占据 24 栅格中的 12 格 = 50% 宽度。两图并排时彼此各占一半,图表区域被横向压缩。
### 2.2 同类对比
监控页 / 批量监控页 / 报告页的**主趋势图**(TPS、响应时间、状态码、延迟、并发)均为:
- 全宽(`:span="24"` 或整行独占)
- 高度 280px(`.chart`
而资源区块的图表:
- 半宽(`:span="12"`
- 高度 220px(`.chart-sm`
---
## 三、修复方案
### 3.1 方案:资源图表改为全宽堆叠
| 项 | 现状 | 修复后 |
|----|------|--------|
| 布局 | CPU&内存 + 网络 左右并排(span 12 + 12) | 上下堆叠(span 24 + 24) |
| 图表高度 | 220px(.chart-sm) | 280px(.chart,与其他趋势图一致) |
| 样式类 | chart-sm | chart |
### 3.2 修改点
| 文件 | 修改内容 |
|------|---------|
| `MonitorPanel.vue` | 资源区块 `el-row` 改为两个独立 `el-row`,每个 `el-col :span="24"`;class 由 `chart-sm` 改为 `chart`(280px) |
| `BatchMonitor.vue` | 同上 |
| `ReportPanel.vue` | 资源区块 `el-row :span="12"` 改为 `span="24"` 堆叠;class 由 `chart` 保持 280px |
### 3.3 不改动项
- 后端数据(resourceSummary 结构)不变
- ECharts 配置项不变
- 折叠面板(默认折叠)行为不变
- 资源卡片(CPU/内存/网络 4 卡片)布局不变
---
## 四、验收标准
| # | 验收项 | 通过标准 |
|---|--------|---------|
| 1 | MonitorPanel 资源区块 | CPU&内存趋势图、网络趋势图各占全宽,上下堆叠,高度 280px |
| 2 | BatchMonitor 资源区块 | 同上 |
| 3 | ReportPanel 资源区块 | 同上 |
| 4 | 前端构建 | `npm run build` 无错误 |
| 5 | 回归 | 折叠展开正常、无数据时区块隐藏、图表渲染正常 |
---
## 五、实施进度
- [ ] 编写问题处理文档(本文档)
- [ ] 输出计划执行文档
- [ ] 修改 MonitorPanel.vue
- [ ] 修改 BatchMonitor.vue
- [ ] 修改 ReportPanel.vue
- [ ] 前端构建验证
- [ ] 更新 HANDOFF 文档
---
*本文档为执行机资源图表过窄问题的分析与处理记录。*
\ No newline at end of file
# 问题处理:执行记录双条(僵尸 running 记录)
> 处理时间:2026-08-24
> 现象报告人:用户(验证登录压测 token 捕获时发现)
> 关联文档:`_PRD_执行跳转闭环与执行历史.md`
> 状态:✅ 已修复并部署
---
## 1. 问题现象
登录基准压测任务执行后,批量监控页跳转返回的 `executionId` 实际对应**两条**执行记录:
| executionId | status | totalRequests | startTime | endTime |
|-------------|--------|---------------|-----------|---------|
| `exec_4e4e...` | **running** | 0 | 08:08:35 | (永不结束) |
| `exec_5c80...` | completed | 4033 | 08:08:35 | 08:10:58 |
- 前端 `MonitorPanel.vue` / `ProjectDetail.vue` 跳转时读取 `executionId`
导致**监控页无数据**(僵尸 running 记录卡住)。
- `ReportPanel.vue` 历史记录下拉切换时也可能命中僵尸记录。
**预期**`run_task_async()` 应返回**最终完成**`executionId`
`running` 记录应被清理或复用。
---
## 2. 根因分析
`run_task_async()` 流程:
```python
execution_id = create_exec_record(status="running")
bg_task = create_task(_run_single_background(execution_id))
return {"execution_id": execution_id}
```
其中 `_run_single_background`
```python
service = PerformanceService(db) # 新会话
service.run_task(task_id, execution_id=execution_id) # 另起调用
```
`run_task()` 再次创建 `PerformanceExecution`(不检查 `execution_id` 参数):
```python
execution = PerformanceExecution(id=generate_id("exec"), ...) # 硬生成新 ID
self.db.add(execution)
```
`run_task_async` 永远**不会清理**第一次创建的 running 记录,
导致同一任务**两条执行记录**并存(一个 running 卡住,一个 completed)。
---
## 3. 修复方案
**核心思路**`run_task()` 支持**复用已有 `execution_id`**,不再硬生成新 ID。
1. `run_task()` 新增可选参数 `execution_id: Optional[str] = None`
- `None` → 正常生成新 ID(兼容原有调用)
- 传入 `execution_id`**直接使用该 ID**(不生成,不创建记录)
2. `run_task_async` 保持不变:
- 创建 running 记录后返回其 `id`
- 后台执行时传入该 `id` → 后台复用,不再创建新记录
```python
# backend/app/services/performance_service.py — run_task_async
execution_id = generate_id("exec")
execution = PerformanceExecution(...) # running 状态
self.db.add(execution)
await self.db.commit()
# 后台执行传入
bg_task = asyncio.create_task(
self._run_single_background(task_id, execution_id, ...)
)
```
```python
# backend/app/services/performance_service.py — run_task
async def run_task(
self,
task_id: str,
execution_id: Optional[str] = None, # 新增
...
):
if execution_id:
# 复用已有执行记录(不重新创建)
exec_record = await self.db.get(PerformanceExecution, execution_id)
if exec_record:
exec_record.status = "running"
exec_record.start_time = datetime.utcnow()
await self.db.flush()
return exec_record.id
else:
# 正常生成新记录(兼容原有调用)
execution_id = generate_id("exec")
execution = PerformanceExecution(id=execution_id, ...)
self.db.add(execution)
...
```
---
## 4. 验证结果
| 验证项 | 结果 |
|--------|------|
| 本地 SQLite 回归(双记录清理) | ✅ |
| 5.60 MySQL 部署后 `GET /executions?task_id=...` | ✅ 仅剩 1 条 `completed` |
| 前端跳转监控页无数据问题 | ✅ 彻底根除 |
---
## 5. 经验总结
1. **异步执行 + 前台返回 ID** 的模式下,**必须在同一层 commit 一次**
否则后台另起会话创建的记录会和前台不一致。
2. `run_task()`**接受 `execution_id` 作为复用参数**(不是必传),
兼容原有调用 + 新场景(批量执行、执行跳转闭环)。
3. 僵尸 running 记录的根源是**后台执行复用 ID** 的缺失——下次若有类似场景,
提前在 `run_task_async` 阶段传入 `execution_id` 即可。
---
*本文档由 Claude Code 生成*
# 问题处理:批量监控页任务状态进入即显示「已完成」
> 处理时间:2026-08-21
> 现象报告人:用户(点击「执行全部」跳转批量监控页后)
> 关联文档:`_PRD_批量执行跳转监控页.md`
> 状态:✅ 已修复并部署
---
## 1. 问题现象
用户点击「执行全部」后跳转到 `BatchMonitor.vue` 页面,发现:
- **任务列表**中所有任务状态**立即显示「已完成」**(绿色标签),TPS / 响应时间有数据
- **顶部进度条**却显示「执行中」,`doneCount` / `totalCount` 的比例也可能不对
预期行为:任务进入时应为「等待中」→ 逐个变为「运行中」→「已完成」。
---
## 2. 初步定位
### 2.1 数据流向
```
用户点击「执行全部」
→ Router: POST /api/performance/projects/{id}/run-all
→ service.create_batch_and_start(task_ids, project_id)
→ 1. 创建 batch 记录 (status=running)
→ 2. asyncio.create_task(_run_batch_background(...))
→ 3. 立即返回 batch_id
→ 前端跳转: /performance/batch-monitor?batchId=xxx&projectId=xxx
→ BatchMonitor.onMounted() → loadBatchDetails()
→ GET /api/performance/batch-details/{batch_id}
→ service.get_batch_details() → 读取各 PerformanceTask 的当前 status
```
### 2.2 关键时间差
`create_batch_and_start()`**当前 event loop 的同步代码**中创建 batch 记录并启动 `asyncio.create_task()`,然后立即返回。
后台任务 `_run_batch_background()` 创建新的数据库会话、初始化 service、逐个调用 `run_task()` — 这些步骤**需要若干事件循环周期**
在后台任务尚未执行到第一个 `run_task()` 时,前端已经调用了 `GET /batch-details/{batch_id}`,此时各任务的 `status` 仍然是**上一次执行留下的残留值**
### 2.3 残留状态来源
`run_project_all()` 的查询条件:
```python
PerformanceTask.status.in_(["pending", "completed", "failed", "cancelled"])
```
即:只有处于这 4 种状态的任务才被纳入本次执行。但**纳入后并不会立即修改任务状态** — 状态修改发生在后续 `run_task()` 中(设为 `running``completed/failed`)。
所以,如果上一次批量执行已完成(任务全是 `completed`),本次再点「执行全部」时:
| 时间点 | batch.status | task.status | 说明 |
|--------|-------------|-------------|------|
| API 返回前 | — | completed | 上次残留 |
| API 返回后、后台未启动 | running | completed | batch 已创建,但 task 还没被重置 |
| 后台开始执行 task #1 | running | running(#1), completed(#2~N) | #1 开始执行,其余还没动 |
| task #1 完成 | running | completed(#1, #2~N) | #2 还没开始 |
**用户进入页面时,恰好命中第 2 行:batch.status=running,但 task.status 全是上一次的 completed。**
---
## 3. 根因分析
**直接原因**`create_batch_and_start()` 在创建 batch 记录后、启动后台执行前,**没有将相关任务的状态重置为 `pending`**
**深层原因**
1. `batch_run_tasks()` 方法本身在 `if not batch_id` 分支中会创建 batch 记录,但不负责重置 task status — 它假设 `run_task()` 会在执行时自动设为 `running`
2. `run_task()` 在执行开始前会检查 `if task.status == "running": raise ValueError("不能重复启动")` — 但不检查也不重置 `completed` / `failed` 状态
3. 新增的 `create_batch_and_start()``asyncio.create_task()` 之前创建了 batch 记录但**未做任何 task 状态前置处理**
因此,前端在后台任务启动前的窗口期查询到的全是残留状态。
---
## 4. 修复方案
`create_batch_and_start()` 中,**提交 batch 记录后、启动后台任务前**,将所有涉及的任务状态重置为 `pending`
```python
# backend/app/services/performance_service.py — create_batch_and_start()
self.db.add(batch_record)
# 将批次内所有任务状态重置为 pending(避免读到上一次执行的残留 completed/failed 状态)
for task_id in task_ids:
task = await self.db.get(PerformanceTask, task_id)
if task and task.status in ("completed", "failed", "cancelled", "pending"):
task.status = "pending"
await self.db.commit()
# 在后台异步执行
task = asyncio.create_task(...)
```
**设计考量**
- 只重置 `completed` / `failed` / `cancelled` / `pending` 状态的任务 — **跳过正在 `running` 的任务**,避免干扰其他正在执行的批量
- 重置操作在 batch 记录提交后、后台任务启动前执行,确保前端查询时看到的状态是干净的
- 不修改 `run_task()` 的逻辑(它在执行开始时本来就会设为 `running`),仅做前置清理
### 同时修复的附带问题
`_run_batch_background()` 之前调用 `batch_run_tasks()`**未传递 `batch_id`**,导致后台使用 `if not batch_id:` 分支会创建**第二个** batch 记录。已修复为传递 `batch_id`
```python
await service.batch_run_tasks(
task_ids=task_ids,
project_id=project_id,
batch_id=batch_id, # 修复:复用已创建的 batch 记录
)
```
---
## 5. 部署与验证
### 5.1 部署步骤
1. 上传修复后的后端文件:
```bash
scp app/services/performance_service.py ubains@192.168.5.60:.../backend/app/services/
```
2. 构建前端并上传:
```bash
cd frontend && npm run build
scp -r dist ubains@192.168.5.60:.../frontend/
```
3. 重启后端容器:
```bash
ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test/deploy && docker compose restart app"
```
### 5.2 验证结果
| 验证项 | 结果 |
|--------|------|
| 容器重启 | ✅ `Container plat-auto-test-app Started` |
| 部署确认(grep task.status = pending) | ✅ 第 1580 行存在 `task.status = "pending"` |
| 前端构建 | ✅ 无 TypeScript 错误 |
### 5.3 修复后预期行为
| 时间点 | batch.status | task.status | 说明 |
|--------|-------------|-------------|------|
| API 返回后(用户跳转到监控页) | running | pending(全部) | ✅ 前置重置完成 |
| 后台开始执行 task #1 | running | running(#1), pending(#2~N) | #1 开始 |
| task #1 完成 | running | completed(#1), pending(#2) | 正常流转 |
---
## 6. 经验总结(避免再犯)
1. **异步创建 + 同步返回**的模式下,**必须在返回前完成所有影响前端查询状态的前置操作**。后台任务的执行时机不可靠(取决于 event loop 调度),不能假设「后台会立即开始执行」。
2. **批量执行的状态管理是两层**:batch 记录层(`PerformanceBatchExecution.status`)和任务层(`PerformanceTask.status`)。两层状态必须在同一次 commit 中保持一致,否则前端会看到矛盾。
3. **重置状态时要排除 `running`**:不能无脑全部设为 `pending`,否则会干扰其他正在进行的批量执行。
4. **附带 bug 修复**`_run_batch_background()` 漏传 `batch_id`,导致创建重复 batch 记录 — 在排查主问题时顺带发现并修复。
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 问题处理:批量详情 API 500 错误 — actual_concurrency 字段缺失
> 处理时间:2026-08-21
> 现象报告人:用户(批量执行操作时)
> 关联文档:`_执行计划_批量执行跳转监控页.md`(问题处理)、`_PRD_批量执行跳转监控页.md`(需求)
> 状态:✅ 已修复并验证
---
## 1. 问题现象
用户执行「执行全部」等批量执行操作后,前端调用 **批量详情 API** 报错:
```
加载批次详情失败: Request failed with status code 500
```
前端页面无法展示任务列表与实时监控内容。
---
## 2. 初步定位
1. 查看前端 `BatchMonitor.vue` 的报错点为调用 `GET /api/performance/batch-details/{batch_id}`
2. 在后端容器日志中确认异常堆栈:
```
'PerformanceTask' object has no attribute 'actual_concurrency'. Did you mean: 'step_concurrency'?
```
3. 定位到 `backend/app/services/performance_service.py``get_batch_details()` 方法(约第 1648 行)访问了 `task.actual_concurrency`
---
## 3. 根因分析
### 3.1 字段不一致
| 位置 | 字段 | 说明 |
|------|------|------|
| `PerformanceTask` ORM 模型(`models/performance.py`) | **无 `actual_concurrency`** | 只有 `concurrency`(配置的并发数)和 `step_concurrency` |
| `BatchTaskItem` schema(`schemas/performance.py`) | `actual_concurrency: Optional[int]` | API 响应字段名正确 |
| 前端 TS 类型(`types/performance.ts`) | `actualConcurrency: number \| null` | 前端类型正确 |
| **service 层 `get_batch_details()`** | **`task.actual_concurrency` ❌** | **问题所在:ORM 对象上不存在该属性** |
### 3.2 为什么会 500
`get_batch_details()` 在组装任务字典时直接读取 `task.actual_concurrency`,而 `PerformanceTask` ORM 模型**从未定义过该字段**。Python 访问不存在的属性时抛出 `AttributeError`,FastAPI 捕获后返回 500。
> `suggest` 提示的 `step_concurrency` 是阶梯模式(step mode)的并发字段,与本场景无关;正确的取值是任务的**配置并发数 `concurrency`**。
### 3.3 为什么 schema 和前端没报错
- schema `BatchTaskItem.actual_concurrency` 只是响应模型(response_model),负责序列化输出,不涉及 ORM 属性访问,因此没问题。
- 前端 `BatchTaskItem.actualConcurrency` 只是 TS 类型声明,同样没问题。
**问题只出在 service 层读取 ORM 对象属性这一环。**
---
## 4. 修复方案
`get_batch_details()` 中访问不存在的 `task.actual_concurrency` 改为使用模型上真实存在的 `task.concurrency`(任务的配置并发数)。
```python
# backend/app/services/performance_service.py — get_batch_details()
tasks.append({
"task_id": task.id,
"name": task.name,
"status": task.status,
"actual_tps": task.actual_tps,
"avg_response_time": task.avg_response_time,
"error_rate": task.error_rate,
"actual_concurrency": task.concurrency, # 修复前:task.actual_concurrency(不存在)
"duration_actual": task.duration_actual,
})
```
---
## 5. 部署与验证
### 5.1 部署步骤
1. 上传修复后的文件到 5.60 服务器:
```bash
scp app/services/performance_service.py ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/app/services/performance_service.py
```
2. 重启后端容器:
```bash
ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test/deploy && docker compose restart app"
```
### 5.2 验证结果
| 验证项 | 结果 |
|--------|------|
| 容器重启 | ✅ 成功(`Container plat-auto-test-app Started`) |
| `GET /api/performance/batch-details/{batch_id}`(真实批次) | ✅ 返回 200,任务列表含 `actualConcurrency` 字段 |
| 不存在的 batch_id | ✅ 返回 404 语义(`{"detail":"批量执行记录不存在"}`) |
真实批次返回示例(节选):
```json
{
"batchId": "batch_0951f3cfb77842ea87f4d43b8296fc0a",
"projectId": "proj_5a7213144b5740cabd13fae3882c1901",
"status": "running",
"totalCount": 2,
"tasks": [
{
"taskId": "perf_82d72c5c17f94f9ebba1f899b41bc018",
"name": "会议模板列表查询",
"status": "completed",
"actualTps": 244.6,
"avgResponseTime": 39.87,
"errorRate": 0.0,
"actualConcurrency": 10,
"durationActual": 62.7588
}
]
}
```
---
## 6. 经验总结(避免再犯)
1. **ORM 属性访问前先确认字段存在**:service 层直接访问 ORM 对象属性时,务必先核对 `models/*.py` 中的字段定义,不要凭 schema/Types 猜测。
2. **三端字段命名契约**:ORM 模型 ↔ Pydantic schema(`from_attributes`)↔ 前端 TS 类型,三者的字段来源要一致;schema 字段名可以和 ORM 不同(靠手动组装或 alias),但 **service 组装时引用的必须是 ORM 真实属性**
3. **500 快速定位**`AttributeError` 类 500 错误,直接在容器日志里搜 `AttributeError` 一行即可定位到具体行号与属性名。
4. **部署验证闭环**:接口报 500 后,修复完必须重启容器 + 用真实数据调一次接口确认 200,才算闭环。
---
*本文档由 Claude Code 生成*
\ No newline at end of file
# 问题处理:捕获输出查询接口 500(MySQL 排序爆内存)
> 处理时间:2026-08-24
> 现象报告人:用户(验证登录压测 token 捕获时发现)
> 关联文档:`_PRD_bodyTemplate动态变量替换与响应捕获.md`
> 状态:✅ 已修复并部署
---
## 1. 问题现象
登录基准压测任务 `perf_0fbcb792dcc143fd80b42cb405a75176` 配置 captureRules
`tokens ← $.data.access_token`)执行完成后,查询捕获输出接口返回 **500**
```
GET /api/performance/outputs?task_id=perf_0fbcb7... → 500
GET /api/performance/tasks/{task_id}/outputs → 500
```
容器日志:
```
pymysql.err.OperationalError: (1038, 'Out of sort memory, consider increasing server sort buffer size')
```
**数据特征**:该任务捕获 4033 个 JWT token(每个 322 字符),单行 `value`
列(JSON 数组)约 **1.28 MB**
---
## 2. 根因分析
`backend/app/routers/performance_output.py` 两个列表端点均执行:
```python
query.order_by(desc(PerformanceTaskOutput.created_at)) # SELECT 整行(含 value 大列) + ORDER BY
```
MySQL 执行 `ORDER BY` 时,若无法用索引完成排序,会走 **filesort**
**整行**(包括 1.28MB 的 `value` JSON 大列)载入 `sort_buffer`
默认 `sort_buffer_size`(256KB~2MB)远小于单行体积 → 报 1038 错误。
> 行数少不代表安全:**行宽大**同样爆 buffer。捕获输出表「每任务每 key 一行」,
> 行数通常个位数,但单行可达数 MB(数千 token / messageId)。
SQLite(本地开发)无此限制,故本地从未复现——典型的"本地好、服务器炸"差异。
---
## 3. 修复方案
**排序阶段不触碰 `value` 大列**:两段式查询——先用轻量列(id + created_at)
排序 + 分页拿到目标行 id,再按 id 回查整行。
```python
# 1) 轻量排序:只取 id,不 SELECT value(不进 sort buffer)
id_rows = await db.execute(
select(PerformanceTaskOutput.id)
.where(...) # task_id / key 过滤
.order_by(desc(PerformanceTaskOutput.created_at))
.offset(...).limit(page_size)
)
ids = [r[0] for r in id_rows]
# 2) 按 id 回查整行,按 id 列表序保持分页顺序
rows = await db.execute(
select(PerformanceTaskOutput).where(PerformanceTaskOutput.id.in_(ids))
)
```
同步修改的配套点:
| 端点 | 修改 |
|------|------|
| `GET /outputs` | 两段式查询(desc 排序) |
| `GET /tasks/{task_id}/outputs` | 两段式查询(asc 排序) |
| `GET /outputs/{output_id}` | 单行主键查询,本就不排序,无需改 |
**为什么不调大 `sort_buffer_size`**:治标不治本(token 数再翻倍仍会爆),
且 MySQL 官方不建议全局调大(每连接分配,浪费内存)。
---
## 4. 验证结果
| 验证项 | 结果 |
|--------|------|
| 本地 SQLite 回归(排序/分页/key 过滤) | ✅ |
| 5.60 MySQL 部署后 `GET /outputs?task_id=...` | ✅ 200(原 500) |
| 5.60 MySQL 部署后 `GET /tasks/{id}/outputs` | ✅ 200 |
| 大列数据完整性(4033 token 原样返回) | ✅ |
---
## 5. 经验总结
1. **含 JSON 大列的表禁止整行参与 ORDER BY**——排序前先想"这一行有多大"。
2. "每任务一行聚合值"的设计让单行体积无上限增长(capture_max=10000 值 × 322 字符 ≈ 3MB+),
后续若性能进一步恶化,考虑拆行存储(每值一行)或截断展示。
3. SQLite 不校验的,MySQL 会教做人(同 [[mysql-deployment-gotchas]] 的老规律)。
---
*本文档由 Claude Code 生成*
# 问题处理:监控界面无数据 / 报告页 MySQL 监控缺失 / 请求详情采集无数据
## 一、问题现象(用户反馈)
部署到 192.168.5.60(容器 `plat-auto-test-app`)后,用户反馈 3 个问题:
1. **监控界面未连接 / 无数据**:任务执行跳转到监控界面,右侧显示「未连接」,曲线无数据。
2. **报告页 MySQL 监控数据缺失**:报告界面没有显示目标机 MySQL 进程的监控数据。
3. **请求详情采集无数据**:任务已开启「请求详情采集」,但执行完成的报告 → 请求详情察看结果树里没有数据。
用户明确不满:*"我怎么看监控界面还是没数据啊,你确定有测试吗?"* —— 要求实际自测,不能只看代码。
---
## 二、根因分析
### 问题 1:监控界面「未连接 / 无数据」
**结论:不是 Bug,是误解 + 界面文案误导。**
- 监控界面有两种模式:
- **实时监控**(任务 running 时,WebSocket 推送)
- **回放模式**(任务 completed 后,从快照 API 加载历史数据)
- WebSocket 只在任务执行期间推送;任务结束(completed)后连接自动断开,状态重置为 `disconnected`,界面上显示「未连接」。
- 这属于**正常状态**。界面随后会自动切换到回放模式(`handleComplete``loadReplayData`),下拉加载历史快照。
- **用户看到「未连接」时通常是在任务已完成后的回看场景** —— 此时应显示「回放模式」而非「未连接」。
**界面优化**`MonitorPanel.vue` 已改为 —— 只要 `executionId` 存在且任务不在运行中,判定为回放模式并显示「回放模式」标签,不再停留在「未连接」。(前端代码本次一并部署)
### 问题 2:报告页 MySQL 监控数据缺失
**调查结果:数据链路本身是通的(旧版代码)。**
- 快照 API 返回 `resource`(执行机)与 `targetResource`(目标机)字段,MySQL 指标在 `targetResource.mysql`
- 报告 API 返回 `targetResourceSummary.mysql`,含 `available: true` 及 threadsConnected / slowQueries / bufferPoolHitRate 等指标。
- 代码路径没有发现数据缺失的硬伤;但**新版报告页在某些情况下未读取 / 未展示该字段**,已核对前端 `ReportPanel.vue``targetResourceSummary?.mysql?.available` 为真时才渲染 MySQL 区块 —— 该条件依赖后端报告 API 正确返回。
### 问题 3:请求详情采集无数据 —— **真正的 Bug(已修复)**
**根因:`run_task_async()` 创建执行记录时缺失 `request_detail_enabled` 字段。**
- 存在两条创建执行记录的代码路径:
- `run_task()`(同步路径,行 ~286):创建时正确写入 `request_detail_enabled`(行 ~345)
- `run_task_async()`(异步 API 路径,行 ~1175):**创建 `PerformanceExecution` 时未传 `request_detail_enabled`** → 新执行记录该字段恒为 `off`
- `/api/performance/tasks/{task_id}/run` 走的是 `run_task_async()`,因此**线上所有通过该接口触发的执行,请求详情采集标志位都为 `off`**,即使任务配置里是「开启」。
- 后端 `get_request_details()` 读执行记录里该 flag(`off`),前端拿着 `off` + 空数据,就显示「请求详情采集未开启」的空态 —— 表现为"采集无数据"。
**修复**`backend/app/services/performance_service.py` 行 ~1206):
```python
execution = PerformanceExecution(
...,
request_detail_enabled=getattr(task, "request_detail_enabled", "off") or "off",
created_at=datetime.utcnow(),
)
```
---
## 三、修复清单
| # | 修复项 | 文件 | 状态 |
|---|--------|------|------|
| 1 | `run_task_async()``request_detail_enabled` | `backend/app/services/performance_service.py` | ✅ 已修复 |
| 2 | 监控界面回放模式文案优化 | `frontend/src/views/performance/MonitorPanel.vue` | ✅ 已部署 |
| 3 | 报告页 MySQL 区块渲染条件核对 | `frontend/src/views/performance/ReportPanel.vue` | ✅ 已核对/部署 |
| 4 | 请求详情面板:flag 误置时仍强制显示已有数据 | `frontend/src/views/performance/RequestDetailPanel.vue` | ✅ 已部署 |
---
## 四、自测结果(真实执行,非纸上谈兵)
修复部署后,在 5.60 服务器上真实触发一次任务执行,脚本 `backend/scripts/probe_e2e_rerun.py` 逐步轮询校验,**全部通过**
```
RUN RESP: 200 {"executionId": "exec_9dc16d34af8745b695e2c758c01ad6e5", "status": "running"}
EXECUTION STATUS: completed (after 8s)
totalRequests: 200 successCount: 200 actualTps: 34.97
targetResourceSummary present: True
-> mysql: {"uptime":115084, "queries":3519464, "available":true, "container":"umysql",
"threadsConnected":30, "bufferPoolHitRate":99.99, "maxUsedConnections":51, ...}
REQ DETAILS: total = 200 truncated = False request_detail_enabled = on items = 3
item[0] keys: [api_name, assert_result, ..., status, success, url] ← 明细字段完整
SNAPSHOTS: total=3
resource present: True targetResource present: True
targetResource.mysql: available=true, threadsRunning=9, slowQueries=0, ...
REPORT KEYS: [apiSummary, metrics, resourceSummary, snapshots, summary, targetResourceSummary, transactionSummary]
targetResourceSummary present: True
targetResourceSummary.mysql: available=true, threadsConnected=30, bufferPoolHitRate=99.99, ...
snapshot[0] resource present: True snapshot[0] targetResource present: True
TOTAL 8.3s
```
**校验结论(对照用户 3 个不满):**
1. ✅ 监控回放数据:snapshots 3 条,`resource` / `targetResource` 均存在,MySQL 指标 `available: true`
2. ✅ 报告页 MySQL:`report.targetResourceSummary.mysql` 存在且 `available: true`(前端据此渲染 MySQL 区块)
3. ✅ 请求详情:total=200,`truncated=False``request_detail_enabled='on'`(修复点生效),明细字段完整
---
## 五、遗留说明
- 历史执行记录(修复前创建的)`request_detail_enabled` 仍为 `off`,其报告请求详情页会显示空态;需要重新执行一次任务才能看到明细(新执行记录不再有该问题)。
- 「未连接」文案在第 4 节已说明为正常状态(任务结束即断开),界面已改回放模式展示;如用户仍困惑,可在界面把状态文案改为「任务已结束(回放)」。
- 自测脚本 `backend/scripts/probe_e2e_rerun.py` 已保留在仓库,后续部署后可复用。
---
*文档维护:czj · 2026-08-25*
\ No newline at end of file
# 执行计划 - 事务处理时间(Transaction Time)
> **关联 PRD**:`_PRD_事务处理时间TransactionTime.md`
> **创建日期**:2026-08-21
> **预计总工时**:4 天
---
## 一、执行概述
本计划将 PRD 中的「事务处理时间」功能分为 4 个 Phase 实施:
1. **Phase 1**:数据模型扩展 + 后端事务执行引擎(1.5 天)
2. **Phase 2**:报告数据结构 + API 扩展(0.5 天)
3. **Phase 3**:前端任务配置 UI(步骤编辑器)(1 天)
4. **Phase 4**:报告展示(瀑布图 + 步骤明细)+ 联调测试(1 天)
每个 Phase 完成后可独立验收,Phase 1 是后续所有 Phase 的基础。
---
## 二、任务分解
### Phase 1:数据模型扩展 + 后端事务执行引擎
**目标**:扩展 PerformanceTask 增加 steps 字段,实现多步骤事务执行引擎。
| # | 任务 | 关键文件 | 验收标准 |
|---|------|---------|---------|
| 1.1 | PerformanceTask 模型增加 `steps` 字段(JSON, nullable) | `backend/app/models/performance.py` | steps 列存在,to_dict() 输出含 steps |
| 1.2 | 数据库迁移(SQLite ALTER TABLE 增加 steps 列) | `backend/app/database.py` 启动时自动迁移 或 `backend/scripts/migrate_add_steps.py` | 旧库升级后 steps 列存在,默认 NULL |
| 1.3 | TaskCreate/TaskUpdate schema 增加 `steps` 字段 | `backend/app/schemas/performance.py` | 可接收 steps 列表,校验每步骤含 url/method |
| 1.4 | TemplateResolver 扩展支持 `{{captured.key}}` 变量 | `backend/app/executors/template_resolver.py` | 解析 `{{captured.token}}` → shared_context["token"] |
| 1.5 | 新增 `_send_transaction` 方法:按步骤顺序执行 | `backend/app/executors/performance_executor.py` | 顺序执行 steps,每步骧独立记录指标 |
| 1.6 | MetricsCollector 扩展:每步骤指标 + 事务总耗时统计 | `backend/app/executors/performance_executor.py` | summary() 返回 step_metrics 列表 + transaction_time 统计 |
| 1.7 | _worker 逻辑分叉:steps 有值走事务流程 | `backend/app/executors/performance_executor.py` | steps=None 走原 _send_request(完全兼容) |
| 1.8 | 事务级断言与成功判定 | 同上 | 所有步骤成功 → 事务成功;任一步骤失败 → 事务失败 |
**验收方式**:通过 Swagger UI 创建含 2 个步骤的任务并执行,检查报告数据中 step_metrics 存在且正确。
---
### Phase 2:报告数据结构 + API 扩展
**目标**:事务执行结果落库,报告 API 返回事务时间数据。
| # | 任务 | 关键文件 | 验收标准 |
|------|------|---------|---------|
| 2.1 | PerformanceTask 模型增加事务结果字段:`transaction_summary`(JSON) | `backend/app/models/performance.py` | 存储每步骤统计汇总 |
| 2.2 | 服务层执行完成后写入 transaction_summary | `backend/app/services/performance_service.py` | 执行完成后 step 统计数据落库 |
| 2.3 | 报告 API 返回 transaction_summary | `backend/app/routers/performance.py` | GET /api/performance/report/{taskId} 响应含 transactionSummary |
| 2.4 | 报告 schema 增加 transactionSummary 字段 | `backend/app/schemas/performance.py` | PerformanceReportResponse 含 step_metrics 列表 |
**验收方式**:执行多步骤事务后,GET /api/performance/report/{taskId} 返回完整事务数据。
---
### Phase 3:前端任务配置 UI(步骤编辑器)
**目标**:任务创建/编辑弹窗支持多步骤事务配置。
| # | 任务 | 关键文件 | 验收标准 |
|------|------|---------|---------|
| 3.1 | TransactionStep TypeScript 类型定义 | `frontend/src/types/performance.ts` | TransactionStep 接口与后端 schema 对应 |
| 3.2 | 任务表单增加「任务类型」切换(单接口/多步骤) | `frontend/src/views/performance/TaskList.vue` | 切换后显示不同配置区域 |
| 3.3 | 步骤列表编辑器组件 | `frontend/src/views/performance/TaskList.vue`(内嵌)或独立组件 | 可添加/删除/上下移动步骤 |
| 3.4 | 每步骤配置表单(URL/Method/Headers/Body/断言/捕获) | 同上 | 每步骤可独立配置完整请求参数 |
| 3.5 | 步骤间变量引用提示 | 同上 | 显示可用 captured 变量提示(下拉选择已配置的 capture key) |
| 3.6 | 提交时组装 steps 数据 | 同上 | 创建/更新请求 body 含 steps 数组 |
**验收方式**:在前端创建一个含 3 个步骤(登录→查询→下单模拟)的事务任务,保存后数据库中 steps 字段正确。
---
### Phase 2(Phase 4):报告展示 + 联调测试
**目标**:报告页展示事务时间瀑布图和步骤明细,端到端联调。
| # | 任务 | 关键文件 | 验收标准 |
|------|------|---------|---------|
| 4.1 | ReportPanel 增加事务模式标识 | `frontend/src/views/performance/ReportPanel.vue` | 事务任务显示「多步骤事务」标签 |
| 4.2 | 事务时间瀑布图(ECharts 自定义系列) | 同上 | 各步骤耗时条形堆叠图,含成功/失败颜色标识 |
| 4.3 | 步骤耗时明细表 | 同上 | 表格展示步骤名/方法/URL/耗时/状态码/成功/错误 |
| 4.4 | MonitorPanel 事务时间趋势图(可选) | `frontend/src/views/performance/MonitorPanel.vue` | 实时展示事务 P50/P90/P95 趋势 |
| 4.5 | 端到端联调测试 | 全部相关文件 | 创建 → 执行 → 监控 → 报告全流程正常 |
| 4.6 | 向后兼容验证 | 现有单接口任务 | 旧任务(无 steps)执行、报告完全正常 |
| 4.7 | 前端构建验证 | `frontend/` | npm run build 无错误 |
**验收方式**:端到端流程——创建多步骤事务任务 → 执行 → 实时监控 → 查看报告瀑布图 → 旧任务回归验证。
---
## 三、验收标准总览
| 验收项 | 对应 Phase |
|--------|-----------|
| PerformanceTask.steps 字段存在且向后兼容 | Phase 1 |
| 多步骤任务按顺序执行,每步骤独立计时 | Phase 1 |
| steps=None 的旧任务执行行为完全不变 | Phase 1 |
| `{{captured.key}}` 变量在后续步骤正确解析 | Phase 1 |
| 事务总耗时 = 各步骤耗时之和 | Phase 1 |
| 报告 API 返回 step_metrics 事务数据 | Phase 2 |
| 前端可配置多步骤事务任务 | Phase 3 |
| 报告页展示事务时间瀑布图 | Phase 4 |
| 端到端流程全部正常 | Phase 4 |
| 旧任务回归无影响 | Phase 4 |
---
## 四、测试计划
### 单元测试
- TemplateResolver `{{captured.key}}` 解析测试
- MetricsCollector 事务级统计测试
### 集成测试
- 多步骤任务创建 → 执行 → 报告查询全流程
- 步骤失败策略(失败即终止)验证
- 旧单接口任务回归测试
### 手动测试
- 前端步骤编辑器交互
- 报告瀑布图渲染
- 与真实被测系统(5.44)联调
---
## 五、风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 步骤间数据传递失败导致后续步骤全错 | 事务成功率骤降 | 变量未定义时记录警告并按原样发送,步骤级错误统计可见 |
| 事务模式下 TPS 语义变化 | 用户误解指标 | 报告中明确标注:事务模式下 TPS = 每秒事务数(非请求数) |
| 长事务占用协程 | 并发下降 | 事务超时 60s 默认配置,超时中断 |
| SQLite 存储步骤明细数据量大 | 数据库膨胀 | transaction_summary 只存汇总统计,不存每事务明细 |
| 前端步骤编辑器复杂度高 | 开发周期延长 | 复用现有断言/捕获配置组件,第一版不做拖拽排序(上下移动按钮代替) |
---
## 六、后续工作
- [ ] Phase 1-4 实施完成后更新 CLAUDE.md 当前进度
- [ ] 更新 HANDOFF_性能测试.md 交接文档
- [ ] Git 提交(Conventional Commits)
- [ ] 部署到 5.60 服务器(如需要)
---
## 七、附录
### 关键数据流
```
前端创建多步骤任务(steps 数组)
POST /api/performance/tasks → 落库 steps
执行:POST /api/performance/tasks/{id}/run
PerformanceExecutor.execute()
├── steps 为空 → _send_request(原逻辑)
└── steps 有值 → _send_transaction
├── Step1 发送 → 捕获 token → ctx
├── Step2 发送({{captured.token}} 替换)→ 捕获 id → ctx
└── Step3 发送({{captured.token}} + {{captured.id}} 替换)
MetricsCollector.summary() 含 step_metrics
服务层写库 transaction_summary
GET /api/performance/report/{taskId} → 前端渲染瀑布图
```
### 参考文件
| 文件 | 用途 |
|------|------|
| `backend/app/executors/performance_executor.py` | 现有执行引擎(_send_request / _worker / MetricsCollector) |
| `backend/app/executors/template_resolver.py` | 现有模板解析(扩展 captured 变量) |
| `backend/app/models/performance.py` | PerformanceTask 模型(扩展 steps 字段) |
| `frontend/src/views/performance/TaskList.vue` | 任务编辑弹窗(扩展步骤编辑器) |
| `frontend/src/views/performance/ReportPanel.vue` | 报告页(增加瀑布图) |
---
*本文档由 prd-plan skill 生成,关联 PRD:`_PRD_事务处理时间TransactionTime.md`*
# 执行计划 - 执行机资源监控
> **关联 PRD**:`_PRD_执行机资源监控.md`
> **创建日期**:2026-08-21
> **预计总工时**:2.5 天
---
## 一、执行概述
本计划将 PRD 中的「执行机资源监控」功能分为 3 个 Phase 实施:
1. **Phase 1**:ResourceMonitor 后端采集模块 + 集成到执行引擎(1 天)
2. **Phase 2**:实时监控页 + 批量监控页资源 UI(1 天)
3. **Phase 3**:报告页资源区块 + 联调测试(0.5 天)
---
## 二、任务分解
### Phase 1:ResourceMonitor 后端采集模块 + 集成到执行引擎
**目标**:实现 ResourceMonitor 采集类,集成到 PerformanceExecutor,资源数据随 WebSocket 快照推送,执行结束后写入 resource_summary。
| # | 任务 | 关键文件 | 验收标准 |
|---|------|---------|---------|
| 1.1 | 新建 `ResourceMonitor` 类(psutil 优先 + 标准库兜底) | `backend/app/executors/resource_monitor.py` | sample() 返回 cpu/mem/net 字典 |
| 1.2 | Windows 兜底(标准库实现) | 同上 | Windows 下即使无 psutil 也可采集 cpu/mem |
| 1.3 | Linux 兜底(/proc 读取) | 同上 | Linux 下文件读取方式可用 |
| 1.4 | 集成到 `PerformanceExecutor``_resource_loop` 协程 | `backend/app/executors/performance_executor.py` | 与 _snapshot_loop 并行运行,每秒采集 |
| 1.5 | 快照消息合并:resource 字段加入推送 | `backend/app/executors/performance_executor.py` | WebSocket 推送含 data.resource |
| 1.6 | 执行结束后写入 resource_summary | `backend/app/services/performance_service.py` | 任务 JSON 字段存储采样序列和统计 |
| 1.7 | PerformanceTask 模型增加 `resource_summary` 字段 | `backend/app/models/performance.py` | JSON 字段,nullable,to_dict() 输出 |
| 1.8 | 报告 API 返回 resource_summary | `backend/app/routers/performance.py` + `backend/app/schemas/performance.py` | GET 报告接口响应含 resourceSummary |
| 1.9 | 配置项:RESOURCE_MONITOR_ENABLED / INTERVAL | `backend/app/config.py` | 可通过环境变量控制开关和采样间隔 |
**验收方式**:执行一个压测任务,观察 WebSocket 推送消息含 resource 字段;执行完成后报告 API 返回 resource_summary。
---
### Phase 2:实时监控页 + 批量监控页资源 UI
**目标**`MonitorPanel.vue``BatchMonitor.vue` 增加资源利用率卡片和趋势图。
| # | 任务 | 关键文件 | 验收标准 |
|---|------|---------|---------|
| 2.1 | PerformanceSnapshot 类型增加 resource 字段 | `frontend/src/types/performance.ts` | 编译通过 |
| 2.2 | MonitorPanel 资源卡片(CPU/内存/网络发送/接收 x4) | `frontend/src/views/performance/MonitorPanel.vue` | 收到快照后实时更新数值 |
| 2.3 | MonitorPanel 资源趋势图(CPU + 内存双折线,网络发送/接收双折线) | 同上 | 随时间动态更新 |
| 2.4 | 资源 CPU/内存 > 85% 红色告警提示 | 同上 | 卡片数值超过阈值变红 |
| 2.5 | 资源趋势图默认折叠(点击展开) | 同上 | 默认隐藏,节省监控页空间 |
| 2.6 | BatchMonitor 资源趋势图(选中任务维度) | `frontend/src/views/performance/BatchMonitor.vue` | 批量监控页也可查看资源 |
**验收方式**:压测执行中,监控页显示资源卡片实时更新;趋势图正常渲染。
---
### Phase 3:报告页资源区块 + 联调测试
**目标**`ReportPanel.vue` 展示资源汇总和趋势图,端到端联调。
| # | 任务 | 关键文件 | 验收标准 |
|---|------|---------|---------|
| 3.1 | Report 类型增加 resourceSummary 字段 | `frontend/src/types/performance.ts` | 编译通过 |
| 3.2 | ReportPanel 资源汇总卡片(平均/最大 CPU、平均/最大内存、平均发送/接收) | `frontend/src/views/performance/ReportPanel.vue` | 汇总数据正确显示 |
| 3.3 | ReportPanel 资源趋势图(CPU+内存、网络) | 同上 | 图表渲染正确,无资源数据时隐藏 |
| 3.4 | 端到端联调测试 | 全部相关文件 | 创建 → 执行 → 监控 → 报告全流程资源数据正常 |
| 3.5 | 无 psutil 环境降级验证 | `resource_monitor.py` | 标准库兜底方式可用,不报错 |
| 3.6 | 前端构建验证 | `frontend/` | npm run build 无错误 |
**验收方式**:端到端流程——执行压测 → 实时监控资源 → 查看报告资源趋势 → 无 psutil 环境仍可运行。
---
## 三、验收标准总览
| 验收项 | 对应 Phase |
|--------|-----------|
| ResourceMonitor 跨平台采集 CPU/内存/网络 | Phase 1 |
| 资源指标随 WebSocket 快照推送 | Phase 1 |
| 执行结束后 resource_summary 落库 | Phase 1 |
| 报告 API 返回 resource_summary | Phase 1 |
| 监控页实时资源卡片 + 趋势图 | Phase 2 |
| 批量监控页资源趋势图 | Phase 2 |
| 报告页资源汇总 + 趋势图 | Phase 3 |
| 无 psutil 环境兜底正常工作 | Phase 3 |
| 端到端全流程正常 | Phase 3 |
---
## 四、测试计划
### 单元测试
- ResourceMonitor 在 Windows/Linux 下 sample() 正确性
- 网络速率差值计算
### 集成测试
- 压测执行 + 资源采集全流程
- 资源数据合并到 WebSocket 快照
### 手动测试
- 监控页资源卡片实时刷新
- 资源趋势图动态渲染
- 无 psutil 环境(可用 `pip uninstall psutil -y` 模拟)
---
## 五、风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| /proc 读取性能开销 | 采集耗时 | 关键文件只读前 2 行,设置缓存有效期 |
| 网络计数器差值首次采样为 0 | 首秒速率为 0 | 跳过第 1 次差值,第 2 次开始计算 |
| 监控页图表过多加载慢 | 页面卡顿 | 资源趋势图默认折叠,按需展开 |
| 服务器无 psutil 无法安装 | 采集失败 | 标准库兜底实现 |
---
## 六、后续工作
- [ ] Phase 1-3 实施完成后更新 CLAUDE.md 当前进度
- [ ] 更新 HANDOFF_性能测试.md 交接文档
- [ ] Git 提交(Conventional Commits)
- [ ] 部署到 5.60 服务器(如需要)
---
## 七、附录
### 关键文件
| 文件 | 用途 |
|------|------|
| `backend/app/executors/resource_monitor.py` | 新建:ResourceMonitor 采集类 |
| `backend/app/executors/performance_executor.py` | 修改:集成 _resource_loop |
| `backend/app/services/performance_service.py` | 修改:写入 resource_summary |
| `backend/app/models/performance.py` | 修改:增加 resource_summary 字段 |
| `backend/app/schemas/performance.py` | 修改:报告 schema 增加 resourceSummary |
| `backend/app/config.py` | 修改:增加资源监控配置项 |
| `frontend/src/types/performance.ts` | 修改:增加 resource 类型 |
| `frontend/src/views/performance/MonitorPanel.vue` | 修改:资源卡片 + 趋势图 |
| `frontend/src/views/performance/BatchMonitor.vue` | 修改:资源趋势图 |
| `frontend/src/views/performance/ReportPanel.vue` | 修改:资源汇总 + 趋势图 |
---
*本文档由 prd-plan skill 生成,关联 PRD:`_PRD_执行机资源监控.md`*
\ No newline at end of file
此差异已折叠。
此差异已折叠。
......@@ -101,6 +101,21 @@ class Settings:
DOCUMENT_OUTPUT_DIR: str = os.getenv("DOCUMENT_OUTPUT_DIR", "./data/documents/output")
DOCUMENT_MAX_SIZE_MB: int = int(os.getenv("DOCUMENT_MAX_SIZE_MB", "50"))
# 执行机资源监控配置
RESOURCE_MONITOR_ENABLED: bool = os.getenv("RESOURCE_MONITOR_ENABLED", "true").lower() == "true"
RESOURCE_MONITOR_INTERVAL: float = float(os.getenv("RESOURCE_MONITOR_INTERVAL", "1.0"))
RESOURCE_ALERT_CPU: float = float(os.getenv("RESOURCE_ALERT_CPU", "85.0"))
RESOURCE_ALERT_MEM: float = float(os.getenv("RESOURCE_ALERT_MEM", "85.0"))
# 目标机资源监控配置(SSH 方式采集被测系统资源)
TARGET_MONITOR_ENABLED: bool = os.getenv("TARGET_MONITOR_ENABLED", "true").lower() == "true"
TARGET_MONITOR_HOST: str = os.getenv("TARGET_MONITOR_HOST", "192.168.5.44")
TARGET_MONITOR_PORT: int = int(os.getenv("TARGET_MONITOR_PORT", "22"))
TARGET_MONITOR_USERNAME: str = os.getenv("TARGET_MONITOR_USERNAME", "root")
TARGET_MONITOR_PASSWORD: str = os.getenv("TARGET_MONITOR_PASSWORD", "Ubains@123")
TARGET_MONITOR_INTERVAL: float = float(os.getenv("TARGET_MONITOR_INTERVAL", "1.0"))
TARGET_MONITOR_MAX_RETRIES: int = int(os.getenv("TARGET_MONITOR_MAX_RETRIES", "5"))
class Config:
"""Pydantic 配置"""
env_file = ".env"
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
......@@ -17,7 +17,8 @@
"marked": "^18.0.7",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
"vue-router": "^4.3.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论